Чому рекурсивний виклик викликає StackOverflow на різній глибині стека?


74

Я намагався з'ясувати, як обробляються зворотні виклики компілятором 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 на будь-якому з:

  • `` Оптимізувати збірку '' ВИМКНЕНО (налагодження або випуск)
  • Ціль: x86
  • Ціль: AnyCPU + "Віддати перевагу 32 біту" (це нове у VS 2012, і я вперше це бачив. Докладніше тут .)
  • Деякі, здавалося б, нешкідливі гілки в коді (див. Коментар "else" гілки).

З іншого боку , з допомогою «Оптимізувати збірки» ON + (Target = x64 або AnyCPU з «Воліють 32bit» OFF (на 64 - бітних CPU)), 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потрапляння?

ОНОВЛЕННЯ

Ганс Пасант порушує проблему 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 або The Enhanced Mitigation Experience Toolkit . Це дозволяє встановити прапор ASLR (і багато іншого) на рівні системи або процесу.
(Існує також загальносистемна альтернатива злому реєстру, яку я не пробував)

Графічний інтерфейс EMET

Для того, щоб перевірити ефективність інструменту, я також виявив, що Провідник процесів належним чином повідомляє статус прапора ASLR на сторінці "Властивості" процесу. Ніколи цього не бачив до сьогодні :)

введіть тут опис зображення

Теоретично EMET може (повторно) встановити прапор ASLR для одного процесу. На практиці це, здається, нічого не змінило (див. Зображення вище).

Однак я вимкнув ASLR для всієї системи, і (одна перезавантаження пізніше), нарешті, міг переконатися, що справді виняток SO тепер завжди відбувається на одній глибині стека.

БОНУС

Пов’язані з ASLR, у попередніх новинах: Як Chrome отримав pwned


1
Я відредагував вашу назву. Будь ласка, див. " Чи повинні запитання включати" теги "у свої заголовки? ", Де консенсус "ні, вони не повинні".
Джон Сондерс,

FYI: щойно пробували без Randomі лише друку sz. Те саме трапляється.
Хуліан Урбано,

Цікаво, що це за методика з’ясувати, чи JIT вбудував виклик методу чи ні.
Крістіан Діаконеску

1
@CristiDiaconescu Приєднайте налагоджувач у візуальній студії після того, як JIT скомпілював би код (за допомогою випадаючого списку Debug->Attach to processабо введення Debugger.Attach()коду), а потім перейдіть до випадаючого меню, Debug->Windows->Disassemblyщоб побачити машинний код, створений JIT. Пам'ятайте, що JIT компілює код по-різному, якщо у вас приєднаний відладчик чи ні, тому не забудьте запустити його без приєднаного відладчика.
Скотт Чемберлен,

12
+1 за публікацію запитання, яке насправді є темою для StackOverflow. Смішно, скільки людей публікують запитання, які взагалі не стосуються переповнення стека!
Ben Lee

Відповіді:


51

Я думаю, що це може бути 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 байт вашого початкового стека.


2
Ніколи раніше не чув про ASLR. Поки що +1 - я люблю, коли дізнаюся нові речі. Буде тестувати завтра.
Крістіан Діаконеску

1
@Hans: Дослідження Symantec точно говорить, що стек компенсується випадковою кількістю, до половини сторінки, тому ефективно зменшується розмір.
fejesjoco

Гаразд, мені більше подобається ваше пояснення.
Ганс Пассант

-3

Змінити r.Next()на r.Next(10). StackOverflowExceptions повинні відбуватися в однаковій глибині.

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

Те саме стосується, якщо ви використовуєте r.Next(100, 1000)


Ні, він зупиняється на різній глибині. Навіть якщо ви взагалі видаляєте випадкові випадки.
Хуліан Урбано,

Це працює для мене (як у режимах DEBUG, так і RELEASE). (XP SP3 - VS 2K8 - .NET 3.5)
Ахмед КРАЄМ

Хоча це має сенс, це не для мене (Win7 64 SP1, VS 2010, .net 4.5)
Хуліан Урбано,

4
Це не випадково в XP, оскільки він не має ASLR.
fejesjoco

1
@AhmedKRAIEM, ти впевнений, це теж не працює у тебе із випадковим насінням? Передбачувані різниці довжини рядка не повинні впливати на стек. Зрештою, стек просто тримає (однакову кількість - і розмір -) покажчиків на рядки, виділені в купі. (Крім того, це спрацювало для мене в обидві сторони після вимкнення ASLR)
Крістіан Діаконеску
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.