Почему рекурсивный вызов вызывает StackOverflow на разных глубинах стека?


Я пытался выяснить, как хвостовые вызовы обрабатываются компилятором C#.

(ответ: это не так. но 64bit JIT (s) будет делать 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();
    }

прямо по сигналу, программа заканчивается с таким исключением на любом оф:

  • 'оптимизировать сборку' выкл (отладка или выпуск)
  • цель: x86
  • Target: AnyCPU + "Prefer 32 bit" (это новое в VS 2012 и в первый раз, когда я его увидел. здесь.)
  • некоторая, казалось бы, безобидная ветвь в коде (см. прокомментированную ветвь "else").

и наоборот, используя "оптимизировать сборку" на + (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 без отладчика. Когда программа выходит из строя, я подключаю отладчик и проверяю значение счетчика.

и еще не то же самое на несколько рейсов. (Или мой тест ошибочен.)

обновление: закрытие

как предложил fejesjoco, я заглянул в ASLR (рандомизация макета адресного пространства).

это метод безопасности, который затрудняет атаки переполнения буфера, чтобы найти точное местоположение (например) конкретных системных вызовов, путем рандомизации различных вещей в адресном пространстве процесса, включая позицию стека и, видимо, его размер.

теория звучит хорошо. Давайте воплотим это в жизнь!

чтобы проверить это, я использовал инструмент Microsoft конкретные задачи: EMET или расширенный набор инструментов для смягчения последствий. Это позволяет установить флаг ASLR (и многое другое) на уровне системы или процесса.
(Существует также общесистемная альтернатива взлома реестра, которую я не пробовал)

для проверки эффективность инструмента, я также обнаружил, что Process Explorer должным образом сообщает о состоянии флага ASLR на странице "свойства" процесса. Никогда не видел этого до сегодняшнего дня:)

теоретически EMET может (re)установить флаг ASLR для одного процесса. На практике это, похоже, ничего не изменило (см. изображение выше).

тем не менее, я отключил ASLR для всей системы и (одна перезагрузка позже) я мог бы, наконец, проверить, что действительно так исключение теперь всегда происходит на той же глубине стека.

бонус

ASLR-related, в старых новостях:как хром получил pwned

2 73

2 ответа:

Я думаю, что это может быть ASLR на работе. Вы можете отключить DEP, чтобы проверить эту теорию.

смотрите здесь для служебного класса C#, чтобы проверить информацию о памяти:https://stackoverflow.com/a/8716410/552139

кстати, с помощью этого инструмента, я обнаружил, что разница между максимальным и минимальным размером стека составляет около 2 Кб, который полстраницы. Это странно.

обновление: хорошо, теперь я знаю, что я прав. Я проследил за теорией полстраницы, и нашел этот документ, который рассматривает реализацию ASLR в Windows: http://www.symantec.com/avcenter/reference/Address_Space_Layout_Randomization.pdf

цитата:

Как только стек был помещен, начальный указатель стека далее рандомизированная случайной декрементной величиной. Начальное смещение равно некоторые до половины страницы (2048 байт)

и это ответ на ваш вопрос. ASLR забирает между 0 и 2048 байт вашего начального стека случайным образом.

изменить r.Next() до r.Next(10). StackOverflowExceptions должно происходить на той же глубине.

генерируемые строки должны потреблять одинаковую память, потому что они имеют одинаковый размер. r.Next(10).ToString().Length == 1всегда. r.Next().ToString().Length переменной.

то же самое применяется, если вы используете r.Next(100, 1000)