Почему рекурсивный вызов вызывает 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

Ответы на вопрос(2)

Ваш ответ на вопрос