"Раскрутка" стека
Далеко не во всех случаях принудительный выход из функции оказывается возможным. Ряд критических сбоев затрагивает не одну, а сразу несколько вложенных функций, и тогда для реанимации программы мы должны совершить глубокий откат назад, продолжив выполнение программы с того места, где бы ее работоспособности ничто не угрожало. Точная глубина отката подбирается экспериментально и обычно составляет три–— пять ступеней. Имейте ввиду, что если вложенные функции модифицируют глобальные данные (например данные кучи), то попытка отката может привести к полному краху отлаживаемой программы, поэтому требуемую глубину отката желательно угадать с первого раза, придерживаясь правила: "лучше перебрать, чем недобрать". С другой стороны, чрезмерно глубокий откат ведет к потере всех не сохраненных данных…
Процедура отката состоит из трех шагов: а)
q построения дерева вызовов;
q б) определения координат стекового фрейма для каждого из них; в)
q восстановления регистрового контекста материнской функции.
Хороший отладчик все это сделает за нас, и вам останется лишь записать в регистры EIP и ESP соответствующие значения. К сожалению, отладчик Microsoft Visual Studio Debugger к хорошим отладчикам не относится. Он довольно посредственно трассирует стек, пропуская FPO-функции (Frame Point Omission — – функции с оптимизированным фреймом) и не сообщает координат стекового фрейма, "благодаря" чему самую трудоемкую часть работы нам приходится выполнять самостоятельно.
Впрочем, даже такой стек вызовов все же лучше, чем совсем ничего. Раскручивая его вручную мы будем отталкиваться от того, что координаты фрейма естественным образом определяются по адресу возврата. Допустим, содержимое окна "Call Stacks"
выглядит так как это показано в листинге 3.8.:
Листинг 3.8. Содержимое окна Call Stacks отладчика Microsoft Visual Studio Debugger
TESTCEDIT! 00401362()
MFC42! 6c2922ae()
MFC42! 6c298fc5()
MFC42! 6c292976()
MFC42! 6c291dcc()
MFC42! 6c291cea()
MFC42! 6c291c73()
MFC42! 6c291bfb()
MFC42! 6c291bba()
Листинг 8 содержимое окна Call Stacks отладчика Microsoft Visual Studio Debugger
Попробуем найти в стеке адреса 6C2922AEh и 6C298FC5h, соответствующие двум последним ступеням исполнения. Нажимаем <ATL>+<-6> для перехода в окно дампа и, воспользовавшись "горячей" комбинацией клавиш <Ctr> +<l-G> в качестве базового адреса отображения, выбираем "ESP". Прокручивая окно дампа вниз, мы обнаруживаем оба адреса возврата (в приведенном далеениже листинге 3.9 они выделены рамкой).:
Листинг 3.9. Содержимое стека после "раскрутки"
0012F488 0012FA64 0012FA64 004012FF ß 0040136F:ret 8 первый адрес возврата
0012F494 00000000 00000064 00403458 ß 00401328:pop esi
0012F4A0 FFFFFFFF 0012F4C4 6C291CEA
0012F4AC 00000019 00000000 6C32FAF0
0012F4B8 0012F4C0 0012FA64 01100059
0012F4C4 00320774 002F5788 00000000
0012F4D0 00320701 77E16383 004C1E20
0012F4DC 00320774 002F5788 00000000
0012F4E8 000003E8 0012FA64 004F8CD8
0012F4F4 0012F4DC 002F5788 0012F560
0012F500 77E61D49 6C2923D8 00403458 ß 0040132C:ret;
0012F50C 00000111 0012F540 6C2922AE ß6C29237E:pop ebx/pop ebp/ret 1Ch
0012F518 0012FA64 000003E8 00000000
0012F518 0012FA64 000003E8 00000000
0012F524 004012F0 00000000 0000000C
0012F530 00000000 00000000 0012FA64
0012F53C 000003E8 0012F564 6C298FC5
0012F548 000003E8 00000000 00000000
0012F554 00000000 000003E8 0012FA64
Листинг 9 содержимое стека после раскрутки
Ячейки памяти, лежащие выше адресов возврата, представляют собой значения регистров, сохраненные в стеке при входе в функцию и восстанавливаемые при ее завершении. Ячейки памяти, лежащие ниже адресов возврата, "оккупированы" аргументами функции (если, конечно, у функции есть аргументы), или же принадлежат локальным переменным материнской функции, если дочерняя функция не принимает никаких аргументов.
Возвращаясь к листингу 3.5, отметим, что два двойных слова, лежащие на верхушке стека, соответствуют машинным командам POP EDI и POP ESI, а следующий за ними адрес –— 4012FFh –— это тот самый адрес, управление которому передается командой 40136Fh:RET 8. Для продолжения раскрутки стека мы должны дизассемблировать код по этому адресу (листинг 3.1:0).
Листинг 3.10. Дизассемблерный листинг праматеринской функции ("бабушки")
004012FA call 00401350
004012FF cmp eax,0FFh
00401302 je 0040132D
00401304 push eax
00401305 lea eax,[esp+8]
00401309 push 405054h
0040130E push eax
0040130F call dword ptr ds:[4033B4h]
00401315 add esp,0Ch
00401318 lea ecx,[esp+4]
0040131C push 0
0040131E push 0
00401320 push ecx
00401321 mov ecx,esi
00401323 call 00401BC4
00401328 pop esi
00401329 add esp,64h
0040132C ret ; SS:[ESP] = 6C2923D8
Листинг 10 дизассемблерный листинг праматеринской функции ("бабушки")
Прокручивая экран вниз, мы замечаем инструкцию ADD ESP, 64, закрывающую текущий кадр стека. Еще восемь байт снимает инструкция 40136Fh:RET 8 и четыре байта оттягивает на себя 401328:POP ESI. Таким образом, позиция адреса возврата в стеке равна: current_ESP + 64h + 8 + 4 == 70h. Спускаемся на 70h байт ниже и видим адрес возврата из праматеринской функции (листинг3.11).:
Листинг 3.12. Адрес возврата из праматеринской функции
0012F500 77E61D49 6C2923D8 00403458 ß
00401328:POP ESI/ret;
Листинг 11 адрес возврата из праматеринской функции
Первое двойное слово –— это значение регистра ESI, который нам предстоит вручную восстановить; второе –— адрес возврата из функции. Нажатием <Ctrl>+<-G>, "0x6C2923D8" мы продолжаем "раскручивать" стек (листинг 3.12).:
Листинг 3.12. Дизассемблерный листинг прапраматеринской функции
6C2923D8 jmp 6C29237B
…
6C29237B mov eax,ebx
6C29237D pop esi
6C29237E pop ebx
6C29237F pop ebp
6C292380 ret 1Ch
Листинг 12 дизассемблерный листинг пра-праматеринской функции
Вот мы и добрались до восстановления регистров! Сместившись на одно двойное слово вправо (оно только что было вытолкнуто из стека командой RET), переходим в окно "Registers" и восстанавливаем регистры ESI, EBX, EBP, извлекая сохраненные значения из стека (листинг 3.13).:
Листинг 3.13. Содержимое регистров, ранее сохраненных в стеке вместе с адресом возврата
0012F500 77E61D49 6C2923D8 00403458
ß
6C29237D:pop esi
0012F50C 00000111 0012F540 6C2922AE ß6C29237E:pop ebx/pop ebp/ret 1Ch
Листинг 13 содержимое регистров, ранее сохраненных в стеке вместе с адресом возврата
Как вариант можно переместить регистр EIP на адрес 6C29237Dh, а регистр ESP на адрес 12F508h, после чего нажать на клавишу <F5> для продолжения выполнения программы. И этот прием действительно срабатывает! Причем, реанимированная программа уже "не ругается" на ошибку последней операции (как это было при восстановлении путем принудительного выхода из функции), а просто ее не выполняет. Красота!