Я пытался выяснить практические вопросы о том, как хвостовые вызовы обрабатываются компилятором С#.
(Ответ: Это не так. Но 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 (и многое другое) на системном уровне или на уровне процесса.
(Существует также общесистемный, альтернативный способ взлома реестра, который я не пробовал)
Чтобы проверить эффективность инструмента, я также обнаружил, что Process Explorer должным образом сообщает о статусе флага ASLR в поле" Свойства страницы. Никогда не видел этого до сегодняшнего дня:)
Теоретически EMET может (повторно) установить флаг ASLR для одного процесса. На практике это ничего не меняет (см. Выше изображение).
Однако я отключил ASLR для всей системы и (одна перезагрузка позже), я мог бы наконец убедиться, что действительно, исключение SO теперь всегда происходит с той же глубиной стека.
BONUS
Связанный с ASLR, в старых новостях: Как Chrome получил pwned