Послідовність виконання функцій | Блог по Windows

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

portcls! PcHandlePropertyWithTable + 0x1b

Очевидно, що наведений всього-лише загальний вид запису, але нам цікава сьогодні, в першу чергу, сама структура цього запису. У наведеному вище форматі відладчик виводить дані про викликаються в ході виконання коду функції. Формат запису в стек викликів наступний:

Як можна побачити з малюнку, формат запису такий: імя_модуля! Імя_функциі + зсув, де:

  • імя_модуля – ім’я бібліотеки (виконуваного файлу), що містить функцію;
  • імя_функциі – найменування (ім’я) функції, в складі якої розміщується інструкція, що викликає вищу (наступну по стеку викликів) функцію;
  • зміщення – кількість байт (в шістнадцятковій системі числення) від адреси точки входу функції до початку інструкції, наступної за інструкцією, що викликала вищу (по стеку викликів) функцію. Зсув вираховується від стартового адреси функції (від першого її байта).

У ряді ситуацій (при відсутності символів) ім’я функції може бути відсутнім, тоді адреса відображається не цілком коректно: імя_модуля + зміщення або імя_модуля! Імя_бліжайшей_определенной_функціі + зсув.
За великим рахунком, саме сукупність подібних записів і становить собою стек викликів.

Стек викликів (call stack) – це структура даних, що зберігає інформацію про підпрограма (процедурах, функціях) виконується додатки (програми).

За ідеєю, з назви випливає, що стек викликів повинен являти собою тільки частина (окремі осередки) традиційного стека потоку, що зберігає виключно адреси повернення (інформацію для повернення) управління до вищої (що викликає, викликала) підпрограму. Проте, оскільки в сучасному програмуванні на мовах високого рівня підпрограми (функції) оперують стековими фреймами, то мається на увазі, що стек викликів зберігає інформацію про підпрограма (локальні дані, параметри, значення регістрів і ПРЧ.).
Тобто стек викликів це не весь стек (потоку), оскільки в останньому можуть зберігатися “сторонні” дані (локальні змінні функцій, значення регістрів і ПРЧ.), А виключно інформація, що відноситься до викликуваним підпрограм (функцій). Ці значення формують свого роду послідовність (ланцюжок) вкладених викликів підпрограм, яка є досить важливою частиною при дослідженні процесу виконання коду потоку (при різного роду збої), оскільки по ланцюжку викликів можна визначити як саме код поводився протягом усього часу виконання (до моменту збою / контрольованого зупинки) і навіть визначити області коду, де імовірно сталася помилка. Тому, для реверсивного інженера надзвичайно важливо якомога докладніше вивчити послідовність виконання функцій при виконанні коду.

Послідовність виконання функцій – формована на основі стека викликів послідовність викликаються кодом програми функцій, утворює список.

Послідовність виконання функцій, як уже говорилося, можна спостерігати в стеці викликів. Давайте подивимося, як саме стек викликів виглядає в отладчике Windbg, для цього в програмі є клас команд k * (наприклад: knL). Ось типовий приклад:

Child RetAddr fffff880021ab748fffff80002cedd42KeBugCheckExfffff880021ab750fffff80002ca065fFNODOBFMstring’0x3132afffff880021ab830fffff80002c868e9MiIssueHardFault0x28bfffff880021ab900fffff80002c773eeMmAccessFault0x1399fffff880021aba60fffff96000212cd0KiPageFault0x16efffff880021abbf8fffff8000301b4fb32UserPowerInfoCalloutfffff880021abc00fffff80003021a52PopNotifyConsoleUserPresentfffff880021abc70fffff80002c82265PopUserPresentSetWorkerfffff880021abcb0fffff80002f10f06ExpWorkerThread0x111fffff880021abd40fffff80002c6a686PspSystemThreadStartupfffff880021abd800000000000000000KiStartSystemThread

Чим вам не послідовність? Правда читається вона досить своєрідно: від низу до верху. Пояснюється це принципами роботи самого стека: останнім прийшов, першим вийшов першим прийшов (Last In First Out), або інакше першим прийшов, останнім вийшов. Тобто, найперший виклик процедури поміщає в стек адреса повернення першим (на саму вершину стека), потім наступний другим і так далі. У підсумку перша адреса виявляється як би “на дні” стека. Стек зростає в бік зменшення адрес. Таким чином, з часом в стеці виходить така собі своєрідна “вежа” з знаходяться один над одним адрес повернення (можна відстежити по стовпцю Child-SP).

Стек викликів починається, традиційно, знизу, і (спрощено) читається як: “ця функція викликає функцію, яка розміщується вище за списком (знаходиться над). Та, в свою чергу, викликає вищу функцію, і так далі”. Це досить важливо для розуміння того, що насправді відбувається в стеці викликів.

У блоці можна спостерігати стовпець з ім’ям Call site. Це позначення можна перевести як Область викликів, і воно відображає символічне, осмислене ім’я, відповідне адресою повернення (зазначеного в колонці RetAddr), сформований з використанням (якщо доступні) символів для представлення більш осмисленого виведення. Кожен запис області викликів містить в собі найменування модуля, функції та зміщення, для спрощеної ідентифікації як самої викликає функції, так і входить до її складу інструкції. Грубо кажучи вона являє собою точний покажчик (адреса) на інструкцію всередині функції. Для кращого розуміння доречно провести провести аналогію з поштовою адресою: ім’я модуля це місто, ім’я функції – вулиця, і зміщення – це адреса будинку. Ця абстракція помітно спрощує розуміння принципів адресації тієї чи іншої інструкції в адресному просторі досліджуваного процесу.
Тепер давайте повернемося до базових принципів. В операційній системі присутнє таке поняття як потік виконання, в якому виконуються різноманітні операції, або, кажучи простими словами, код.

Потік виконання (нитка, thread) – хід обробки інформації (виконання набору команд), що розділяє інструкції процесу (код) і його контекст (регістри, змінні, покажчики). Даний механізм забезпечує паралельне виконання (на процесорі) різних завдань.

У кожному потоці є початкова (головна, стартова) функція, код якої розміщується з точки входу, і починає виконуватися при старті потоку (додатки). Ви думаєте, що цієї головною функцією все в програмі і обмежується, тобто додаток складається з єдиної функції? Ні ж, не існує такої однієї єдиної, “чарівної” функції, яка виконала б всю необхідну будь-якого додатка роботу (виняток становлять лише вкрай прості програми). Очевидно, що для виконання навіть найпростіших завдань цієї функції необхідна допомога безлічі інших внутрішніх або зовнішніх (системних) функцій. З цього виходить що:

систему можна представити у вигляді величезної кількості функцій, які виконують завдання різного ступеня складності.

Для того, що б не породжувати хаос, набори згаданих функцій класифіковані в різноманітні модулі (файли), звані бібліотеками та систематизовані за родом діяльності. І ось ці то самі функції постійно викликаються виконуваних в даний момент на процесорі кодом.
Наприклад, ваша програма хоче вивести на робочий стіл вікно. Уже представили наскільки складна ця задача на рівні системи і на скільки подзадач вона “підрозділяється”? Ви думали, що викликається одна єдина функція, яка спокійно намалює вікно і поверне управління? Не тут то було!! Подібне завдання розбивається на безліч складових (подзадач). Відповідно, наша початкова функція викликає іншу функцію що б зробити якусь частину загальної роботи, наприклад вказати бібліотеці DirectX отрисовать вікно. Функція DirectX отримує управління і в ході своєї роботи звертається до будь-якої іншої функції, потім та, в свою чергу, звертається до наступної, що б зробити вже необхідну саме їй на довільному етапі виконання, роботу і так далі і так далі. Виходить така своєрідна “матрьошка” з вкладених (або викликають) один в одного функцій.

Суть потоку виконання і полягає в тому, як саме завдання фрагментируется на складові і проходить через весь процес подій.

Однак, давайте знову подивимося на стек викликів, який ми навели вище. Потрібно пам’ятати, що:

Кожна функція, зазначена в стеці викликів, викликає функцію, яка розміщена над нею в стеку.

Але тут не все так просто, як здається, треба розуміти як саме вона це робить. Диявол, як завжди, криється в деталях. Реалізує вона це зі зміщення, яке зазначено в стовпці Call site (виклики), і не з адреси, який вказаний в стовпці RetAddr (адреса повернення). Насправді, нижчестоящих функція просто викликає “вищу”, а це означає, що управління передається в самий початок, тобто на найперший байт, що викликається. Це регламентує базовий принцип виклику функцій (або підпрограм), який споконвіку застосовується в програмуванні. Тепер зрозуміло, чому зміщення, зазначені відразу за ім’ям модуля / функції в списку викликів (стовпець Call site), можуть ввести в оману?
Тому:

  • зміщення від початку модуля / функції в списку викликів (стовпець Call site) показує виключно зміщення (в байтах, від початку) інструкції, з якої продовжиться виконання, якщо вищестояща функція поверне це саме управління назад.
  • адресаповернення (стовпець RetAddr) показує адресу, куди повернеться управління в нижчу функцію, коли поточна функція відпрацює і поверне управління.

Повторення – мати навчання, закріпимо:

Найменування модуля, функції та інструкції в стовпці RetAddr (наприклад: nt! KiPageFault + 0x16e), Вказує на команду, яка почне виконуватися в разі повернення управління з вищестоящої функції.

Такі собі своєрідні перехресні посилання на сусіда в ланцюжку викликів. Так, в цьому і полягає деяка неінтуітівнимі списку, і не все так просто, як хотілося б, але з часом ви усвідомлюєте, що все побудовано виконано логічно. І ось ще, що означає вираз “функція повернула управління”? Це означає рівно те, що функція виконала очікувану від неї роботу, і потім управління повернулося (частіше при допомогою підкласу команд ret) в функцію нижче за списком в стеці викликів, яка і продовжила виконання, і так далі.

приклад

Давайте розглянемо конкретний приклад. Я візьму довільний файл дампа пам’яті, створений при виникненні критичної системної помилки (BSOD). Відкрию його в відладчик Windbg, введу команду knL і отримаю такий ось висновок:

Child RetAddr fffff880021ab748fffff80002cedd42KeBugCheckExfffff880021ab750fffff80002ca065fFNODOBFMstring’0x3132afffff880021ab830fffff80002c868e9MiIssueHardFault0x28bfffff880021ab900fffff80002c773eeMmAccessFault0x1399fffff880021aba60fffff96000212cd0KiPageFault0x16efffff880021abbf8fffff8000301b4fb32UserPowerInfoCalloutfffff880021abc00fffff80003021a52PopNotifyConsoleUserPresentfffff880021abc70fffff80002c82265PopUserPresentSetWorkerfffff880021abcb0fffff80002f10f06ExpWorkerThread0x111fffff880021abd40fffff80002c6a686PspSystemThreadStartupfffff880021abd800000000000000000KiStartSystemThread

Розгляд потоку виконання ми почнемо, традиційно, знижу вгору. Системний потік стартує з функції nt! KiStartSystemThread, бачите її в самому низу стека викликів? Ця функція ядра і призначена для створення потоку і ініціювання його виконання. Потім, в ході виконання стартовою функції nt! KiStartSystemThread, їй раптом знадобилося викликати функцію, що знаходиться в стеку викликів вище за списком. Це теж ядерна функція, що має ім’я nt! PspSystemThreadStartup. Чесно кажучи, не сильно я поки розбираюся в функціональні особливості ініціалізації системних потоків, але можу припустити, що вона теж виконує якісь підготовчі дії для створення і виконання потоку. Потім вже функція nt! PspSystemThreadStartup і викликає функцію nt! ExpWorkerThread, ну і так далі за списком.
Тепер відступимо 3 стекових фрейму від початку (знизу) списку і зупинимося на розгляді функції nt! PopUserPresentSetWorker. Для початку виведемо на екран її вміст в дизасемблювати вигляді. З цією метою я відкрию вікно дизассемблирования і просто вставлю назва функції, ну або введу команду:

uf nt! PopUserPresentSetWorker

PopUserPresentSetWorkerfffff800030219f03 fffff800030219f24883ec30 fffff800030219f6 fffff800030219f8381d43d2dfffPopPowerSettingValuesfffff80002e1ec41fffff800030219fe PopUserPresentSetWorkerfffff80003021a4bfffff80003021a008d4301 fffff80003021a034c8d053ed2dfff8PopPowerSettingValuesfffff80002e1ec48fffff80003021a0a488d150f8eceffPopAwayModeUserPresenceDpcfffff80002d0a820fffff80003021a11870531d2dfffdwordPopPowerSettingValuesfffff80002e1ec48fffff80003021a17488d0dc2c2e0ffPopAwayModeUserPresenceDpcObjectfffff80002e2dce0fffff80003021a1e81523 KeInitializeDpcfffff80002c56cf4fffff80003021a2348894c2420 qwordfffff80003021a28488d0d91d0dfffPopAwayModeUserPresenceTimerfffff80002e1eac0fffff80003021a2f4533c9 99fffff80003021a324533c0 88fffff80003021a3548c7c2803c36fe0FFFFFFFFFE363C80hfffff80003021a3c8416 KiSetTimerExfffff8000 2c83890fffff80003021a41980000000 fffff80003021a46869940 PopSetNotificationWorkfffff80002c2aeb4fffff80003021a4b fffff80003021a4d839affff PopNotifyConsoleUserPresentfffff8000301b490fffff80003021a52448bdb 11fffff80003021a55 fffff80003021a5744871d52c2e0ff11dwordPopUserPresentSetStatusfffff80002e2dcb0fffff80003021a5e00111cmpxchgdwordPopPowerSettingValuesfffff80002e1ec44fffff80003021a66 PopUserPresentSetWorkerfffff80003021a79

У блоці коду вище маркером я помітив інструкцію, куди посилається покажчик nt! PopUserPresentSetWorker + 0x62. Ця інструкція відстоїть на 0x62 (62 в шістнадцятковій, або 98 в десяткового) байт від початку функції nt! PopUserPresentSetWorker. Давайте подивимося на інструкцію, яка передує тій, яку ми тільки що розглянули. Це інструкція виклику функції nt! PopNotifyConsoleUserPresent (рядок 22), яку ви бачите в загальному списку викликів наступної (вище за списком). Так що ж відбувається? При виклику nt! PopUserPresentSetWorker вона отримує управління, виконується до певного моменту, потім в якийсь момент часу заявляє, що “мені необхідно викликати nt! PopNotifyConsoleUserPresent тому як мені потрібно за допомогою неї щось зробити”, і, відповідно, викликає її. Тепер подивимося на список виклику. Невже наша функція передає керування за адресою nt! PopNotifyConsoleUserPresent + 0x6b, як у нас позначено в стеці викликів? Ні !! Замість цього, вона передає управління строго в початок функції PopNotifyConsoleUserPresent, а саме за адресою fffff800`0301b490. Давайте в цьому переконаємося. Дізассембліруем код за вказаною тільки що адресою:

PopNotifyConsoleUserPresentfffff8000301b49048895c2408 qwordfffff8000301b495 fffff8000301b4964883ec60 fffff8000301b49a48833d8e39e0ff00qwordPopWin32InfoCalloutfffff80002e1ee30fffff8000301b4a2408af9 fffff8000301b4a5 PopNotifyConsoleUserPresentfffff8000301b510fffff8000301b4a748b8d802000080f7ffff0FFFFF780000002D8hfffff8000301b4b1 dwordfffff8000301b4b383f9ff 0FFFFFFFFhfffff8000301b4b6 PopNotifyConsoleUserPresentfffff8000301b510fffff8000301b4b88263 MmGetSessionByIdfffff80002c4db6cfffff8000301b4bd488bd8 fffff8000301b4c04885c0 fffff8000301b4c3 PopNotifyConsoleUserPresentfffff8000301b510

Ось вам і вагомий доказ. Адреса fffff800`0301b490 відповідає першій інструкції функції PopNotifyConsoleUserPresent, але ніяк не тому, що у нас зазначено в стеці викликів: nt! PopNotifyConsoleUserPresent + 0x6b, і немає тут ніякого зсуву, оскільки по зміщенню 0x6b від початку функції знаходиться зовсім інша інструкція. Що у нас тут відбувається, функція PopNotifyConsoleUserPresent робить якусь свою роботу, поки вона, в свою чергу, не захоче викликати іншу, котре було потрібне їй для якихось там своїх потреб, функцію. Давайте перевіримо, куди веде покажчик nt! PopNotifyConsoleUserPresent + 0x6b, розташований в стеку викликів в стовпці Call site:

PopNotifyConsoleUserPresentfffff8000301b4d6488364242800qwordfffff8000301b4dc8364242000 dwordfffff8000301b4e1901000000 fffff8000301b4e64c8d4c2478 9fffff8000301b4eb448bc1 8fffff8000301b4ee fffff8000301b4f040887c2478 fffff8000301b4f51535390qwordPopWin32InfoCalloutfffff80002e1ee30fffff8000301b4fb488d542430 fffff8000301b500488bcb fffff8000301b50384263 MmDetachSessionfffff80002c4dbdcfffff8000301b508488bcb fffff8000301b50b820726 ObfDereferenceObjectfffff80002c82730

Ну, зміщення 6b вказує на якусь інструкцію. А що стоїть безпосередньо перед нею? А тут знову йде інструкція виклику наступної функції. Таким чином, далі ми потрапляємо на чергову функцію в стеці з ім’ям nt! PopWin32InfoCallout.

Тут не все так очевидно, як хотілося б, справа в тому, що функція то в дизасемблювати блоці називається nt! PopWin32InfoCallout, а в списку викликів вже іменується як win32k! UserPowerInfoCallout (до того ж без зміщення інструкції). Швидше за все це пояснюється тим, що nt! PopWin32InfoCallout є “переходником”.

Що ж відбувається, коли остання функція win32k! UserPowerInfoCallout повертає управління? Ось тоді управління повертається за адресою nt! PopNotifyConsoleUserPresent + 0x6b, потім повертається до nt! PopUserPresentSetWorker + 0x62 і так далі назад по стеку.

Ссылка на основную публикацию