Навіщо це потрібно
Підміняти стек має сенс коли:
- Ви викликаєте функцію, яка вимагає великого стека, і ви не хочете, щоб вона користувалася поточним.
- Ви викликаєте функцію, що вимагає великого стека, коли ваш власний стек практично заповнений (наприклад, в тому ж самому обробнику переповнення стека).
На 32-х розрядних Intel-сумісних процесорах для додатків призначеного для користувача режиму розмір сторінки дорівнює 4096 байт.
Після короткого розбору, ми з'ясували, що обраний метод (підхід) вірний, а от реалізація була помилковою. Я провів пару днів за отладчиком, і дещо з'ясував, щодо того, як сама система організовує стек. Результати представлені вашій увазі.
трохи теорії
команда PUSH
команда POP
команда RETN
Діє також як і pop, однак операндом для неї неявно служить регістр eip - extended instruction pointer (покажчик команд). За допомогою цієї команди ви можете змінювати вміст покажчика команд, хоча явних інструкцій для його зміни немає.
Розмір стека
Як і у всього хорошого в цьому світі, у стека є межа або розмір. Тобто ви не можете нескінченно поміщати в нього дані. Як тільки він досягне свого ліміту, система згенерує виняток EXCEPTION_STACK_OVERFLOW. Трохи пізніше ми розглянемо всі подробиці цієї операції. За замовчуванням, розмір стека дорівнює одному мегабайту. Це значення можна змінити за допомогою опції линкера «/ STACK». Для кожного потоку система організовує свій стек.
організація стека
У WindowsXP, за допомогою прапора STACK_SIZE_PARAM_IS_A_RESERVATION, ви можете вказати, що резервуються розмір стека.
Для останньої з переданих спочатку станиць встановлюється прапор PAGE_GUARD. У міру розростання дерева викликів система передає все більше сторінок стека фізичної пам'яті. Остання сторінка «звичайного» стека ніколи не передається і завжди залишається зарезервованої.
Робота зі стеком
Остання передана сторінка стека завжди має встановлений прапор PAGE_GUARD.
Для повністю заповненого стека це не так. Остання передана сторінка має такий же атрибут, як і всі інші передані сторінки стека, а саме PAGE_READWRITE.
Коли відбувається звернення до цієї сторінки, система генерує виняток EXCEPTION_GUARD_PAGE.
Якщо воно виникло при зверненні до сторінок стека, то система обробляє його сама. У всіх інших випадках виключення передається з додатком. Я не знаю, на якому рівні система визначає, що це виняток стека, але знаю, на основі яких даних вона це робить. Про це трохи пізніше.
Оброблювач виключення EXCEPTION_GUARD_PAGE знімає атрибут PAGE_GUARD зі сторінки, на якій сталася помилка, і намагається передати наступну сторінку. Якщо наступна сторінка є останньою, то вона не передається і обробник генерує EXCEPTION_STACK_OVERFLOW. Якщо ж вона не остання - сторінка передається з прапорами PAGE_GUARD і PAGE_READWRITE, і стає черговий сторожовий сторінкою стека потоку.
Таким чином, система повинна знати, як мінімум три речі про стек як про структуру:
Насправді система використовує наступну структуру для управління стеком:
Не знаю точно, але можу припустити, що перші два поля є застарілими і ігноруються. По крайней мере, в [2] і в kernel32.dll при створенні потоку вони обнуляються.
Інформаційний блок потоку
TIB - це структура, яка перебуває на самому початку іншої структури - TEB. TEB - Thread Environment Block (блок оточення потоку). Повний опис TEB ми розглядати не будемо, а ось структуру TIB привести можна. Вона документована в NTDDK. Також її можна знайти в заголовки winnt.h.
Дуже проста і важлива функція, яка повертає за посиланням TIB для поточного потоку.
Тепер, озброївшись теорією, спробуємо реалізувати свій стек, вірніше сказати, спробуємо підмінити поточної стек потоку на свій власний.
підміна стека
І так, стек - досить складна структура і обмежитися одним оновленням регістра esp ми не можемо. Що потрібно для заміни стека?
Для відновлення стека необхідно:
- Відновити старі значення полів StackBase і StackLimit структури NT_TIB.
- Відновити регістр esp.
- Звільнити зайняту під стек пам'ять.
Всю роботу по заміні та відновленню стека виконують дві функції:
Початковий розмір стека встановлюється рівним семи сторінках, остання з яких - сторожова.
Ліміт стека встановлюється на сторінку, що передує сторожовий.
Збереження попереднього стану і зміна полів TIB
Тут наводиться уривок з функції SetNewStack. Вся вона буде розглянута далі.
Перші два рядки зберігають попередні значення полів StackLimit і StackBase. Потім вони встановлюються новими значеннями. Зараз фактично структура TIB знаходиться не в узгодженому стані, так як сам регістр esp ще не оновлено. При виникненні будь-якої ситуації, де системі буде потрібно інформація про стек, може статися все що завгодно. Відзначимо, що перемикання контексту не є такою операцією.
Так як функція SetNewStack приймає два параметри, стек виглядає наступним чином:
Зміна регістра esp і вихід з функції SetNewStack
Якщо ви вже забули, щодо зміщення 0x4 в TIB знаходиться база стека (вже нова). Це значення ми і заносимо в регістри ebp і esp. Наступні дві команди призначені для повернення з процедури.
Повний текст функції SetNewStack
Так як доступ до вхідних параметрів здійснюється через регістр ebp, ми повинні відповідним чином його налаштувати. Першою командою в регістр ebx перший параметр. Потім ми зберігаємо значення регістра ebp і, потім, встановлюємо його значенням з регістра esp за вирахуванням чотирьох. Навіщо? Справа в тому, що компілятор генерує стандартні пролог і епілог для функції, який виглядає наступним чином:
Стек після прологу виглядає так:
Для того щоб отримати доступ до першого параметру, потрібно використовувати регістр ebp, збільшений на 8. Але ми в своїй функції не поміщали ebp в стек, так що [ebp + 8] звернеться до другого параметру. Для того щоб виправити цю ситуацію, ми і коригуємо ebp.