Function calls, part 2 (stack and calling conventions)
У частині першій ви познайомилися з основами механізму виклику функції, який був розглянутий з точки зору генерованого коду. При цьому ви дізналися про двох регістрах, які використовуються процесорами сімейства х86, - про регістрі EIP (вказівник на інструкцію) і регістрі ESP (покажчик на стек). У другій частині ви дізнаєтеся трохи більше про стек і його важливої ролі в роботі механізму виклику функцій. Крім того, ви познайомитеся з двома найбільш популярними угодами про виклики [calling conventions] в Windows / C програмуванні.
Стек - це область пам'яті (в межах відведених процесу чотирьох гігабайт), в якій потік може зберігати дані, необхідні йому для виконання. Зокрема в стеці можуть зберігатися локальні змінні, використовувані вашим кодом, тимчасові змінні, використовувані компілятором, аргументи функцій і т.д. Поведінка стека нагадує поведінку колоди карт [stack of cards], звідси він отримав свою назву. Це означає, що коли ви кладете в стек об'єкти, вони завжди виявляються на його вершині, а коли ви видаляєте об'єкт з стека, ви завжди видаляєте самий верхній об'єкт. У технічної термінології подібний метод доступу до даних називається LIFO (Last In First Out - останнім прийшов, першим вийшов).
Як ми з'ясували в частині першій, система надає стек кожному потоку. За замовчуванням розмір стека дорівнює 1 Мб, але він може бути замінений значенням, що містяться в заголовку образу процесу [the process 'image header value]. Розмір стека також можна задати при виклику функцій CreateThread () або _beginthreadex ().
Процесор завжди повинен знати, де знаходиться вершина стека. Її розташування вказує регістр ESP. Значення регістра EIP не можна змінювати явно. Значення регістра ESP не тільки може бути змінений процесором неявним чином, але також його можна явно змінити за допомогою інструкцій.
Яким чином параметри передаються функції?
Яким чином функція отримує параметри?
Таким чином, при вході в функцію код має все, що йому потрібно для виконання.
Угоди про виклики [Calling conventions]
Тепер буде доречно розглянути угоди про виклики. Угода про виклики - це протокол для передачі аргументів функцій. Іншими словами, це домовленість між викликає і викликається кодом. Розглянуте нами в темах "Яким чином параметри передаються функції?" і "Яким чином функція отримує параметри?" - і є цей протокол, його саме загальний опис. Однак якщо ви маєте справу з програмами Microsoft, то тут є додаткові угоди. Найбільш корисні з них:
__cdecl
__stdcalll
thiscall
У цій частині статті ми детально розглянемо механізми угод __cdecl і __stdcall і дізнаємося, як виглядає стек і скомпільований код в кожному з цих випадків.
Детальний опис протоколу __cdecl можна знайти тут. Особливо важливі такі моменти:
* Порядок передачі аргументів: справа наліво
* Відповідальність за цілісність стека: викликає функція повинна видалити аргументи з стека
Порядок передачі аргументів в протоколі __cdecl
Порядок передачі аргументів описує спосіб, яким аргументи кладуться в стек викликає кодом. У разі протоколу __cdecl мова йде про порядок "справа наліво". Тобто останній аргумент кладеться в стек в першу чергу, за ним кладеться передостанній аргумент, і так далі, поки всі аргументи не виявляться в стеці. Як тільки це буде зроблено, виконується інструкція call, що викликає функцію.
* Зберіть проект (пункт меню Build solution).
* Натисніть F5 для запуску програми під відладчиком. Виконання програми зупиниться на 13-му рядку.
* Натисніть Alt + 5. З'явиться вікно, яке відображає вміст регістрів [Registers Window].
* Натисніть Alt + 6. З'явиться вікно, яке відображає вміст пам'яті [Memory Watch Window].
* Перейдіть на рядок 13, викличте контекстне меню і виберіть Go To Disassembly.
* У дизассемблера знову викличте контекстне меню і переконайтеся, що відзначені наступні пункти:
- Show Address
- Show Source Code
- Show Code Bytes
Вікно дизассемблера має виглядати наступним чином:
Відповідальність за цілісність стека в протоколі __cdecl
* Натисніть F5, щоб запустити програму під отладчиком. Виконання програми зупиниться на 13-му рядку.
* Натисніть Alt + 5. З'явиться вікно, яке відображає вміст регістрів [Registers Window].
* Натисніть Alt + 6. З'явиться вікно, яке відображає вміст пам'яті [Memory Watch Window].
* Перейдіть на рядок 13, викличте контекстне меню і виберіть Go To Disassembly.
* У дизассемблера знову викличте контекстне меню і переконайтеся, що відзначені наступні пункти:
- Show Address
- Show Source Code
- Show Code Bytes
Вікно дизассемблера має виглядати наступним чином:
* Запам'ятайте, яке значення знаходиться в регістрі ESP зараз (ви ще не поклали аргументи в стек). На нашому малюнку це значення дорівнює 0x12FF64. Перше, що повинно статися відразу після виконання інструкції call, - це відновлення в ESP того значення, яке ви запам'ятали. Натисніть F10, щоб пропустити блок, що містить дві інструкції push та інструкцію call. Потім подивіться на значення ESP.
* У нашому прикладі в регістрі ESP буде знаходитися число 0x12FF5C. Як бачите, вона не дорівнює початкового значенням. Отже, необхідно відновлювати цілісність стека. Тепер подивіться на рядок коду, на якій в даний момент зупинилося виконання. це рядок
При виконанні цієї дії ми отримаємо 12FF5Ch + 8h = 12FF64h - потрібне нам число. Таким чином, цей рядок відновлює цілісність стека, оскільки ESP повертається до свого початкового значення. Зверніть увагу, що цей рядок належить зухвалому коду, а не викликається. Саме це в протоколі __cdecl називається "Зухвала опцію, щоб видалити аргументи з стека [Calling function pops the arguments from the stack]". Хоча при цьому не використовуються безпосередньо інструкції pop, все одно результат виходить той же - покажчик ESP отримує колишнє значення.
Висновки по протоколу __cdecl
* Відповідальність викликає коду за цілісність стека означає, що якщо викликає код в різних місцях викликав 100 функцій, використовуючи протокол __cdecl, то він повинен для кожного з цих викликів виконати додатковий код, що забезпечує цілісність стека, навіть якщо викликалася весь час одна і та ж функція . Таким чином, обсяг генерованого коду може збільшитися.
* Оскільки відповідальність за цілісність стека лежить на яскравому коді, протокол __cdecl дозволяє створювати список аргументів змінної довжини. При виконанні функції зі змінним числом аргументів тільки викликає код знає, скільки параметрів було їй передано. Отже, протокол __cdecl дуже підходить для такої ситуації.