Почему рекурсивный вызов вызывает StackOverflow на разных глубинах стека?
Я пытался понять, как хвостовые вызовы обрабатываются компилятором C #.
(Ответ:Они не. Но64-битный JIT БУДЕТ делать TCE (устранение хвостовых вызовов).Ограничения применяются.)
Поэтому я написал небольшой тест с использованием рекурсивного вызова, который печатает, сколько раз он вызывается доStackOverflowException
убивает процесс.
class Program
{
static void Main(string[] args)
{
Rec();
}
static int sz = 0;
static Random r = new Random();
static void Rec()
{
sz++;
//uncomment for faster, more imprecise runs
//if (sz % 100 == 0)
{
//some code to keep this method from being inlined
var zz = r.Next();
Console.Write("{0} Random: {1}\r", sz, zz);
}
//uncommenting this stops TCE from happening
//else
//{
// Console.Write("{0}\r", sz);
//}
Rec();
}
Прямо по сигналу, программа заканчивается SO Exception на любом из:
«Оптимизировать сборку» выключено (отладка или выпуск)Цель: x86Цель: AnyCPU + «Prefer 32 bit» (это впервые в VS 2012, и я впервые это увидел.Больше здесь.)Некоторая, казалось бы, безобидная ветка в коде (см. Прокомментированную ветку 'else').И наоборот, используя «Optimize build» ON + (Target = x64 или AnyCPU с «Prefer 32bit» OFF (на 64-битном процессоре)), происходит TCE, и счетчик продолжает вращаться вечно (хорошо, возможно, он вращаетсявниз каждый раз, когда его значение переполняется).
Но я заметил поведение, которое не могу объяснить вStackOverflowException
case: это никогда (?) не происходит вименно так одинаковая глубина стека. Вот результаты нескольких 32-разрядных прогонов, Release build:
51600 Random: 1778264579
Process is terminated due to StackOverflowException.
51599 Random: 1515673450
Process is terminated due to StackOverflowException.
51602 Random: 1567871768
Process is terminated due to StackOverflowException.
51535 Random: 2760045665
Process is terminated due to StackOverflowException.
И отладка сборки:
28641 Random: 4435795885
Process is terminated due to StackOverflowException.
28641 Random: 4873901326 //never say never
Process is terminated due to StackOverflowException.
28623 Random: 7255802746
Process is terminated due to StackOverflowException.
28669 Random: 1613806023
Process is terminated due to StackOverflowException.
Размер стека постоянен (по умолчанию 1 МБ). Размеры стековых кадров постоянны.
Итак, что может объяснить (иногда нетривиальное) изменение глубины стека, когдаStackOverflowException
хиты?
Ханс Пассант поднимает вопрос оConsole.WriteLine
касание P / Invoke, взаимодействия и, возможно, недетерминированной блокировки.
Поэтому я упростил код до этого:
class Program
{
static void Main(string[] args)
{
Rec();
}
static int sz = 0;
static void Rec()
{
sz++;
Rec();
}
}
Я запустил его в Release / 32bit / Optimization ON без отладчика. Когда программа падает, я присоединяю отладчик и проверяю значение счетчика.
И этоВсе еще не то же самое на нескольких пробегах. (Или мой тест некорректен.)
ОБНОВЛЕНИЕ: ЗакрытиеПо предложению fejesjoco, я изучил ASLR (рандомизация расположения адресного пространства).
Это метод безопасности, который затрудняет атакам переполнения буфера точное определение местоположения (например, конкретных системных вызовов) путем случайного выбора различных вещей в адресном пространстве процесса, включая позицию стека и, по-видимому, его размер.
Теория звучит хорошо. Давайте применим это на практике!
Чтобы проверить это, я использовал инструмент Microsoft, специфичный для этой задачи:EMET или расширенный инструментарий по смягчению последствий, Это позволяет установить флаг ASLR (и многое другое) на уровне системы или процесса.
(Существует такжеобщесистемная альтернатива взлому реестра что я не пробовал)
Чтобы проверить эффективность инструмента, я также обнаружил, чтоProcess Explorer должным образом сообщает о состоянии флага ASLR на странице «Свойства» процесса. Никогда такого не видел до сегодняшнего дня :)
Теоретически, EMET может (повторно) установить флаг ASLR для одного процесса. На практике это ничего не меняет (см. Изображение выше).
Однако я отключил ASLR для всей системы и (одна перезагрузка позже) наконец смог убедиться, что действительно, исключение SO теперь всегда происходит на одной и той же глубине стека.
БОНУССвязанные с ASLR, в более старых новостях:Как Chrome получил pwned