Виключення Windows x64. Як це працює. Частина 1

Раніше ми обговорювали прикладне застосування механізму обробки виключень поза середовища Windows. Тепер ми більш детально розглянемо, як це працює в Windows x64. Матеріал буде описаний послідовно, починаючи з самих основ. Тому багато чого може виявитися вам знайомим, і в цьому випадку такі моменти можна просто пропустити.

Реалізація механізму знаходиться в папці exceptions сховища git за адресою.

1. Функція, її пролог, тіло, епілог і кадр функції
Будь-яка функція має пролог, тіло і епілог. Детальніше зупинимося на пролозі та епілозі, т. к. з самим тілом ніяких питань не виникає, оскільки саме заради нього все і затівається.

У пролозі функції розташовується код, який виконує попередні дії, які необхідні перед роботою тіла функції. У них входить збереження регістрів загального призначення, значення яких могли бути встановлені викликає функцією, виділення пам'яті в стеку для локальних змінних функції, встановлення покажчика кадру (frame pointer) і збереження XMM регістрів процесора. У пролозі встановлені суворі правила по відношенню до дій, які він може виконувати, та їх послідовності. Спочатку, якщо потрібно, пролог зберігає перші 4 параметра в області регістрових параметрів (більш докладно про цю області та всьому, що з нею пов'язано, буде написано в розділі 3), потім заштовхуються регістри загального призначення, виділяється пам'ять в стеку, опціонально встановлюється вказівник кадру функції і зберігаються XMM регістри процесора. Будь-яку з перерахованих дій може бути відсутнім, але описаний порядок виконання строго дотримується. Такі суворі правила дозволяють аналізувати дії епілогу за його програмного коду, про що більш докладно буде розказано нижче. Малюнок 1 ілюструє пролог функції, яка зберігає перші 4 переданих параметра, зберігає три регістра загального призначення, виділяє пам'ять і зберігає XMM регістр.


Малюнок 1

Також слід зазначити, що збереження регістрів загального призначення може виконуватися і після виділення пам'яті в стеку (а також і після встановлення кадру функції, якщо таке має місце), але в такому разі виконується воно за допомогою попадання, а звичайною записом в пам'ять, як це показано в попередньому прикладі. Також слід зазначити, що у дуже рідкісних випадках допускається виділення 8 байт в стеку перед заталкиванием регістрів загального призначення в пролозі функції. Такий пролог найбільш характерний для обробників винятків, для яких процесор не заштовхує в стек код помилки, і таке 8-ми байтне виділення дозволяє його симулювати.

Збережені регістри загального призначення, виділена пам'ять в стеку і збережені регістри XMM всі разом формують так званий кадр (frame) функції, який є у кожної викликаної функції. Нижче, на рисунку 2, представлений стек, що складається з трьох кадрів. Перший кадр — це кадр функції, в контексті якої відбулося виключення. Для стислості на рисунку відображена тільки та область кадру, яка заталкивается процесором в момент виключення. Другий кадр — це кадр обробника виключення, який складається з порожнього коду помилки (нехай в даному прикладі виняток було викликано діленням на нуль, яке не заштовхує в стек код помилки, і наш обробник, як і обробник Windows, для однаковості формує порожній код), збережених регістрів RAX, RCX, RDX, R8, R9, R10, R11, збережених регістрів XMM0, XMM1, XMM2, XMM3, XMM4, XMM5 і адреси повернення. Ці збережені регістри загального призначення і XMM регістри перераховані неспроста, про це ми ще поговоримо в розділі 3. Третій кадр — це кадр функції, яку викликав обробник винятку. Її кадр складається з збережених регістрів RBP, RBX, XMM і виділеного простору для локальних змінних функції. Стрілкою вказано напрямок росту стека.


Малюнок 2

Функція може мати вказівник кадру. У такому разі доступ до кадру виконується через цей вказівник. В першу чергу це потрібно в тих випадках, коли в процесі виконання функції виділяється простір в стеку може динамічно змінюватися (тобто виділення пам'яті в стеку додатково виконується в тілі функції, а не в пролозі). А оскільки це тягне за собою зміну покажчика стека, то він не буде вказувати на кадр функції. У разі якщо функція не має покажчика кадру, вона не може динамічно виділяти пам'ять в стеку, отже, покажчик стека статичним і є також покажчиком кадру функції. Рисунок 3 ілюструє такий пролог. Після збереження всіх регістрів і виділення пам'яті, в тілі функції викликається функція, яка RAX повертає розмір структури, цей розмір виділяється в стеку і далі покажчик стека використовується як покажчик буфера, в яку зчитуються дані.


Малюнок 3

Якщо пролог виділяє область стеку розміром, що перевищує одну сторінку (тобто більше 4Кб), тоді ймовірно, що таке виділення буде охоплювати більше однієї віртуальної сторінки пам'яті і, отже, таке виділення повинно бути перевірено перед фактичним його виконанням. З цією метою пролог функції викликає спеціальну функцію, що виконує дану перевірку. Ім'я функції _chkstk. Також ця функція не змінює значень регістрів, в яких передаються параметри (про цих регістрах детально буде написано в розділі 3). На малюнку 4 зображено приклад прологу функції, який виділяє 4Кб пам'яті в стеку.


Малюнок 4

Епілог виконує протилежні по відношенню до прологу дії: відновлює XMM регістри та регістри загального призначення, які були збережені після виділення пам'яті в стеку, звільняє пам'ять в стеку (а якщо використовувався вказівник кадру, то і динамічно виділену в тому числі), виштовхує регістри загального призначення, виконує повернення в викликає функцію або передає керування на початок поточної функції, або іншої функції. На малюнку 5 зображено епілог, відповідний прологу з прикладу на малюнку 1. З малюнка видно, що виконуються дії, протилежні діям прологу. Також зверніть увагу на той факт, що передані параметри не відновлюються, пояснення цьому ви знайдете в розділі 3.


Малюнок 5

У епілогу, як і у прологу, є суворі правила щодо використовуваних інструкцій процесора. Якщо функція не використовувала вказівник кадру, то пам'ять в стеку, як відображено в попередньому прикладі, звільняється посредствам add rsp, константа інструкції, а якщо використовувала, то посредствам lea rsp, [вказівник кадру + константа]. Потім слідують інструкції виштовхування регістрів загального призначення з стека, інструкція повернення або інструкція безумовного переходу на іншу функцію або на початок поточної функції. На рисунку 6 зображений епілог, відповідний прологу з прикладу на малюнку 3. Зверніть увагу на те, що замість інструкції ret використовується jmp для виклику іншої функції.


Малюнок 6

Що ж стосується інструкцій переходу, то тільки обмежений набір з них допускається. Незважаючи на те, що епілог спочатку відновлює XMM регістри та регістри загального призначення, початком епілогу, при розкручуванні вважається звільнення пам'яті з стека через add rsp, константа lea rsp, [вказівник кадру + константа] інструкції. Пояснення цьому буде дано в третій частині цієї статті, а перші відомості про розкрутку будуть приведені в наступній частині цієї статті.

Все вищеописане щодо епілогу справедливо для функцій, версія структури UNWIND_INFO яких дорівнює 1 (докладно про UNWIND_INFO буде написано в наступній частині цієї статті). Виконував процесор епілог функції в момент переривання/виключення, визначається за кодом самої функції. Це можливо, оскільки, як вже було неодноразово відзначено, на дії прологу і епілогу накладено строгий порядок дій, а на епілог ще й обмеження, що стосуються використовуваних їм інструкцій процесора. Структури UNWIND_INFO версії 2 можуть також описувати розташування епілогу функції. Про це ми більш детально поговоримо в наступній частині цієї статті, тут варто лише згадати, що эпилоги функцій, які описуються структурами UNWIND_INFO версії 2, можуть після виштовхування регістрів загального призначення звільняти 8 байт з стека, про яких ми вже говорили під час обговорення прологу. Таке ж звільнення 8 байт з стека після виштовхування регістрів загального призначення не очікується від эпилогов функцій, які описуються структурами UNWIND_INFO версії 1. Отже, в існуючих Windows-реалізаціях перевірка наявності цього звільнення в програмному коді епілогу функцій, які описуються структурами UNWIND_INFO версії 1, не виконується. В прикладеній до статті реалізації цього механізму така перевірка також не виконується.

Як мінімум, функція має один епілог.

2. Типи функцій
Є два типи функцій: кадрові функції (frame function) і прості функції (leaf function).

Кадрові функції — це ті, які мають свій кадр в стеку і вони не мають ніяких обмежень в частині їх дій. Вони можуть викликати інші функції, виділяти пам'ять в стеку, зберігати і використовувати будь-які регістри процесора. Якщо функція не викликає інших функцій, то її стек не має обмежень у вирівнюванні, якщо викликає, то стек повинен бути вирівняний по 16-байтного кордоні. Також у кадрової функції є відповідні записи з розкручування її кадру (про це ми поговоримо докладніше в наступній частині цієї статті).

Прості функції — це ті функції, які не мають свого кадру в стеку, тому вони не можуть виконувати все те, що можуть кадрові функції, в тому числі використовувати будь-які регістри процесора. Оскільки проста функція не може викликати інших функцій, вона не вирівнює свій стек. Також у простої функції немає записів по розкрутці, оскільки вона не має кадру.

3. Угода про виклики
Перші 4 параметри передаються функції через регістри. Якщо їх більше, то інші передаються через стек. Також, викликає функцією для перших 4 параметрів виділяється область стеку, звана областю регістрових параметрів (register parameters area або home location). Викликана функція може використовувати цю область для збереження параметрів, як це робив пролог з малюнка 1, або в будь-яких інших цілях. Навіть якщо функція приймає менше 4 параметрів або не приймає їх взагалі, область регістрових параметрів завжди виділяється в стеку. Параметри, що передаються через стек, розташовуються в області, званої областю стекових параметрів (stack parameters area). Ця область, на відміну від області регістрових параметрів може бути відсутнім, а її розмір дорівнює розміру всіх параметрів, які вона включає. Один параметр в області регістрових і стекових параметрів завжди займає 8 байт. Якщо ж розмір параметра більше 8, то замість нього передається вказівник на нього. Якщо ж розмір параметра менше 8 байт, то старші невикористовувані байти у відповідних областях ігноруються. Нижче на малюнку 7 зображено виклики двох функцій, одна з яких приймає 6 параметрів, а інша 1, ліворуч і праворуч від стрілки напрямку росту стека відповідно.


Малюнок 7

На дні стека завжди розташовується область регістрових параметрів, вище якої слід область стекових параметрів. У разі виклику функції адреса повернення буде розташовуватися відразу нижче області регістрових параметрів. У розділі 2 було згадано, що якщо функція викликає інші функції, то її стек повинен бути вирівняний по 16-байтного кордоні. На цій 16-байтного кордоні завжди починається область регістрових параметрів.

Перші 4 параметри передаються через регістри RCX, RDX, R8 і R9, якщо це ціле число або користувальницький тип, розмір якого 1, 2, 4 або 8 байт. В іншому випадку передається покажчик на відповідний параметр. Для рядків і масивів завжди передається їх покажчик. Якщо параметр є числом з плаваючою точкою, то для її передачі використовуються XMM0, XMM1, XMM2, XMM3 регістри за умови, що розмір параметра не перевищує 8 байт, інакше передається покажчик на нього. Якщо передається покажчик на параметр замість самого параметра, то сам параметр розміщується в тимчасовій пам'яті на 16-байтного кордоні. На рисунку 8 наведено приклади передачі параметрів функції.


Малюнок 8

Коли використовується XMM для передачі параметра, використовується той XMM регістр, який за номером відповідає одному з регістрів RCX, RDX, R8 або R9. Наприклад, на малюнку 8, параметр 3 функції func1 несе в собі число з плаваючою точкою, в цьому випадку буде використовуватися XMM2 регістр. Якщо б цей параметр був би цілим числом, як у функції func2, тоді використовувався б регістр R8.

Функція повертає результат через RAX або XMM0. Числа з плаваючою точкою і вектора розміром до 16 байт (наприклад, _m128) повертаються в XMM0. Цілі числа і користувальницькі типи, розмір яких 1, 2, 4 або 8 байт, повертаються в RAX. Якщо обчислене значення менше 8 байт, то старші невикористовувані байти не визначені. У всіх інших випадках перший параметр функції є покажчиком на область, куди повертається значення, а в RAX повертається цей вказівник. Також слід зазначити, що в такому випадку передаються параметри зсуваються на один параметр вправо, тобто перший параметр буде передаватися не в RCX, а в RDX регістрі, а 4-й параметр буде передаватися не в R9, а в стеку. На малюнку 9 представлені приклади повернення результату.


Малюнок 9

C++ компілятор накладає додаткові обмеження на власні типи. Якщо результат повертається не статичною функцією (яка є членом класу, структури тощо), або сам тип має конструктор, деструктор, оператор присвоювання, приватні або захищені нестатичные члени, нестатичные члени типу посилання, успадкованого батьків, віртуальні функції або члени, що містять будь-що з перерахованого, то результат повертається не в RAX, а в область пам'яті, покажчик на яку передано в першому параметрі.

Регістри RBX, RBP, RDI, RSI, RSP, R12, R13, R14 і R15 вважаються постійними (nonvolatile або callee-saved), тобто викликається функція повинна зберігати їх перед використанням і відновлювати перед поверненням, а викликає функція може покладатися на значення цих регістрів після виклику функцій.

Регістри RAX, RCX, RDX, R8, R9, R10 і R11 вважаються непостійними (volatile або caller-saved), тобто викликається функція не повинна зберігати їх перед використанням, а викликає функція не повинна покладатися на значення цих регістрів після виклику функцій. З цієї причини епілог, зображений на рисунку 5, відповідний прологу з прикладу на рисунку 1, не відновлює регістри RCX, RDX, R8, R9, збережені прологом. І з цієї ж причини обробник винятку, згаданий в розділі 1, зберігає тільки їх, оскільки ці регістри не відновлюються викликаються функціями перед поверненням.

Подібно регістрів загального призначення, регістри XMM0 — XMM5 вважаються непостійними, а регістри XMM6 — XMM15 постійними.

Висновок
У цій частині статті ми розібрали базові поняття, визначення і процеси, які, на перший погляд, хоч і не мають явного відношення до обговорюваної теми, але, тим не менш, знання і розуміння яких необхідно для подальшого розгляду матеріалу, т.оскільки це є основною, на якій будується обговорюється механізм. Продовження статті буде присвячено опису тих областей PE образу, які використовуються в процесі обробки винятків.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.