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

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

(Ответ: Это не так. Но 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 + "Предпочитаете 32 бит" (это новое в VS 2012 и в первый раз я его видел. Подробнее здесь.)
  • Некоторая, казалось бы, безобидная ветвь в коде (см. прокомментированную ветку else).

И наоборот, используя "Оптимизировать сборку" ON + (Target = x64 или AnyCPU с "Предпочтительным 32bit" OFF (на 64-битном процессоре)), TCE происходит, и счетчик продолжает вращаться навсегда (нормально, он, возможно, вращается каждый раз его значение переполняется).

Но я заметил поведение, которое я не могу объяснить в случае StackOverflowException: он никогда (?) не происходит с точно такой же глубиной стека. Вот выходы нескольких 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?

UPDATE

Hans Passant поднимает вопрос о Console.WriteLine касании P/Invoke, interop и, возможно, недетерминированной блокировки.

Поэтому я упростил код:

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 (и многое другое) на системном уровне или на уровне процесса.
(Существует также общесистемный, альтернативный способ взлома реестра, который я не пробовал)

EMET GUI

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

enter image description here

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

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

BONUS

Связанный с ASLR, в старых новостях: Как Chrome получил pwned

Ответ 1

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

См. здесь для служебного класса С# для проверки информации о памяти: fooobar.com/questions/32442/...

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

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

Цитата:

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

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

Ответ 2

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

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

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