Частина 15: Стек
Для повного змісту змісту всіх уроків, будь ласка, натисніть нижче, оскільки це надасть вам короткий зміст кожного уроку, а також теми, які будуть обговорені. https://github.com/mytechnotalent/Reverse-Engineering-Tutorial
Функції є найбільш фундаментальним елементом в розробці програмного забезпечення. Функція дозволяє організувати код у логічному порядку для виконання певної задачі. Не дуже важливо, чи ви розумієте, як працюють функції на цій стадії, важливо лише те, щоб ви розуміли: якщо ми починаємо вивчати розробку, ми хочемо мінімізувати дублікацію, використовуючи функції, які можна викликати кілька разів, а не дублікувати код, займаючи надмірну кількість пам'яті.
Покажчик стека — це регістр, який містить верхню частину стека. Покажчик стека містить найменшу адресу, скажімо, наприклад, 0x00001000, так що будь-яка адреса, менша за 0x00001000, вважається сміттям, а будь-яка адреса, більша за 0x00001000, вважається дійсною.
Зазначена адреса є випадковою і не є абсолютною, де ви знайдете покажчик стека від програми до програми, оскільки вона буде змінюватися. Давайте подивимося, як виглядає стек з абстрактної точки зору:
Вище діаграма - це те, чого я хочу, щоб ви зберігали чітко в своїй голові, оскільки це те, що насправді відбувається в пам'яті. Наступна серія діаграм буде показувати протилежність того, що показано вище.
Ви побачите, що стек зростає вгору в нижчих діаграмах, проте насправді він зростає вниз від вищої пам'яті до нижчої пам'яті.
У наведеному нижче прикладі addMe, покажчик стека (ESP), при перевірці в пам'яті на точці зупинки в головній функції, показує адрес 0xffffd050. Коли програма викликає функцію addMe з main, ESP тепер дорівнює 0xffffd030, що є НИЖЧИМ значенням в пам'яті. Тому стек зростає ВНИЗ, незважаючи на те, що на діаграмі він вказує вгору. Просто майте на увазі, що коли стрілки нижче вказують вгору, вони насправді вказують на нижчі адреси пам'яті.
Дно стека є найбільшою дійсною адресою стека і розташоване в більшій області адреси або у верхній частині моделі пам'яті. Це може звучати заплутано, оскільки дно стека знаходиться вище в пам'яті. Стек зростає вниз у пам'яті, і дуже важливо, щоб ви це зрозуміли, поки ми будемо рухатися далі.
Межа стека — це найменша дійсна адреса стека. Якщо покажчик стека стає меншим за цю адресу, відбувається переповнення стека, що може пошкодити програму і дозволити зловмиснику взяти контроль над системою. Шкідливе ПЗ намагається скористатися переповненням стека. На сьогоднішній день у сучасних ОС вбудовані засоби захисту, які намагаються запобігти цьому.
Існує дві операції зі стеком: push і pop. Ви можете додати один або кілька регістрів, встановивши покажчик стека на менше значення. Зазвичай це робиться шляхом віднімання чотирикратного числа регістрів, які потрібно додати до стека, і копіювання регістрів до стека.
Ви можете вивести один або кілька регістрів, скопіювавши дані зі стека в регістри, а потім додавши значення до покажчика стека. Зазвичай це робиться шляхом додавання чотирикратного значення кількості регістрів, які потрібно вивести зі стека.
Давайте розглянемо, як стек використовується для реалізації функцій. Для кожного виклику функції існує розділ стека, зарезервований для цієї функції. Він називається стековою рамкою.
Ми бачимо дві функції. Перша з них — unreachableFunction
, дія якої ніколи не виконуватиметься за звичайних обставин, і також бачимо main
яка завжди буде першою функцією записаною на стек. Коли ми запустимо цю програму, стек буде виглядати так:
Ми бачимо стек-фрейм для int main(void)
вище. Він також називається записом активації. Стек-фрейм існує завжди, коли функція запущена, але ще не завершена. Наприклад, всередині тіла int main(void)
є виклик int addMe(int a, int b)
, який приймає два аргументи a і b. У int main(void)
повинен бути код на асемблері, щоб помістити аргументи для int addMe(int a, int b)
у стек. Давайте розглянемо деякий код.
Коли ми скомпілюємо і запустимо цю програму, ми побачимо значення 5, яке буде виведено так:
Дуже просто, int main(void)
викликає int addMe(int a, int b)
спочатку, і буде розміщений на стеку так:
Ви можете побачити, що після розміщення аргументів у стеку розмір стек-фрейму для int main(void)
збільшився. Ми також зарезервували місце для return
, яке обчислюється функцією int addMe(int a, int b)
, і коли функція повертається, значення return
в int main(void)
відновлюється, і виконання продовжується в int main(void)
до її завершення.
Як тільки ми отримаємо інструкції для int addMe(int a, int b)
, функція може потребувати локальних змінних, тому їй потрібно звільнити місце на стеку, що виглядатиме так:
int addMe(int a, int b)
може отримувати аргументи, передані їй з int main(void)
, оскільки код в int main(void)
розміщує аргументи саме так, як очікує цього int addMe(int a, int b)
.
FP — це покажчик кадру, який вказує на місце, де знаходився покажчик стека безпосередньо перед тим, як int addMe(int a, int b)
перемістив покажчик стека або SP для власних локальних змінних int addMe(int a, int b)
.
Використання покажчика кадру є необхідним, коли функція може переміщати покажчик стека кілька разів протягом виконання функції. Ідея полягає в тому, щоб зберегти покажчик кадру фіксованим протягом усього часу існування стек-фрейму int addMe(int a, int b)
. Тим часом покажчик стека може змінювати значення.
Ми можемо використовувати покажчик кадру для обчислення місць в пам'яті як для аргументів, так і для локальних змінних. Оскільки він не переміщується, обчислення для цих місць повинні бути фіксованим зміщенням від покажчика кадру.
Коли настає час виходу з int addMe(int a, int b)
, покажчик стека встановлюється на місце, де знаходиться покажчик кадру, який виштовхує стековий кадр int addMe(int a, int b)
.
Підсумовуючи, стек — це спеціальна область пам'яті, яка зберігає тимчасові змінні, створені кожною функцією, включаючи main. Стек — це LIFO (last in, first out), тобто структура даних «останній увійшов, перший вийшов», яка ретельно управляється та оптимізується процесором. Кожного разу, коли функція оголошує нову змінну, вона додається до стека. Кожного разу, коли функція виходить, усі змінні, додані до стека цією функцією, звільняються або видаляються. Після звільнення змінної стека ця область пам'яті стає доступною для інших змінних стека.
Перевага стека для зберігання змінних полягає в тому, що пам'ять управляється за вас. Вам не потрібно вручну виділяти пам'ять або звільняти її. Процесор дуже ефективно та швиденько управляє, а також організовує пам'ять стека.
Дуже важливо розуміти, що коли функція завершується, всі її змінні вилучаються зі стека і втрачаються назавжди. Змінні стека є локальними. Стек збільшується та зменшується, коли функції додають та вилучають локальні змінні.
Я бачу, як у вас голова йде обертом. Майте на увазі, що ці теми є складними і будуть розвиватися в майбутніх туторіалах. Ми розглядали багато складних тем, таких як регістри, пам'ять, а тепер і стек, і це може бути надто складно. Якщо у вас виникнуть питання, будь ласка, залишайте коментарі нижче, і я допоможу вам краще зрозуміти цю структуру.
У наступному підручнику ми обговоримо, що таке купа.