четверг, 13 января 2011 г.

Ручная расшифровка параметров брошеных C++ исключений (0xE06D7363)

Иногда проблемы приходится исследовать в практически “спартанских” условиях (чаще всего тогда, когда в вашу систему входят компоненты от третьесторонних поставщиков, которые не удосуживаются снабдить вас отладочными символами - мотивация этой глупости при этом может быть абсолютно любой и к делу не относится).

Тем не менее, некоторое количество информации можно получить, анализируя стек по export’ам соответствующих DLL, бывает, что проблемы “deadlock’ами” можно выявить просто анализируя память, после того как выяснилось, что поток “застрял” на бесконечном ожидании критической секции, и т.д.

Готовые “рецепты” и команды не работают при этом из-за отсутствия правильных символов, но что-то можно делать и вручную, например, получить дополнительную информацию о том, какое исключение было выброшено...

Дальнейшее обсуждение относится только к текущей реализации и только для случая 32-битных программ.

Не существует никакой гарантии, что этот метод будет продолжать работать в будущем, так что не надо написать код, который полагается на него. Это просто прием отладки.

Когда генерируется C++ исключение, за возникает системное исключение с кодом 0xE06D7363, после которого на стеке следуют три (возможно четыре) параметра.
* Параметр 0 - некоторая внутренняя инфрамация, которая для нас ценности не представляет.
* Параметр 1 - своего сорта указатель на объект сгенерированного исключения.
* Параметр 2 - указатель на информацию, которая описывает объект исключния (она-то нам и интересна).
* Параметр 3 - HINSTANCE из DLL, вызвавший исключение (настоящее время только на 64-битных Windows - не наш случай).

Дальнейший рецепт прост как “2+2=4” (что, кстати, является хорошей перевернутой мнемоникой для запоминаня смещений):

1) Возьмите Параметр 2 и перейти к четвертому DWORD, рассматривая его как указатель на другую область памяти.
2) Затем перейдите на второй DWORD и тоже рассматривайте его как указатель.
3) Затем перейдите на второй DWORD и рассматривать его как указатель на финальную область памяти.

Первые два DWORD’а для нас интереса не представляют, а вот дальше начинается декорированное имя класса, по которому сгенерировано исключение.

Проиллюстрирую это широко известной ASCII-картинкой (кажется, когда-то я ее с MSDN стащил, указатели промаркированы звездочками, поля, значения которых неизвестны или неважны - тильдами):

EXCEPTION_RECORD
+----------+
| E06D7363 |
+----------+
| ~~~ |
+----------+
|* ~~~ |
+----------+
|* ~~~ |
+----------+
| 3 or 4 |
+----------+
|* ~~~ |
+----------+
|*Object |
+----------+ +---+
|* ------> |~~~|
+----------+ +---+
|*HINSTANCE| |~~~|
+----------+ +---+
|~~~|
+---+ +---+
| -----> |~~~|
+---+ +---+ +---+
| -----> |~~~|
+---+ +---+ +----------+
| -----> |* ~~~ |
+---+ +----------+
|* ~~~ |
+----------+
|Class name|
+----------+

Напишем простую тестовую программу:
#include <exception>
struct XXXException: public std::exception
{
char* what() { return "exception"; }
};

int main(int argc, char* argv[])
{
throw XXXException();
return 0;
}
После ее запуска мы остановимся в отладчике по необработанному исключению:
0:000> k
ChildEBP RetAddr
0012fdac 00412609 kernel32!RaiseException+0x53
0012fdec 00411c5a ThrowTest!_CxxThrowException+0x39
0012fedc 00412840 ThrowTest!main+0x3a [d:\projects\throwtest\throwtest\throwtest.cpp @ 18]
0012ffc0 7c817077 ThrowTest!mainCRTStartup+0x170 [f:\vs70builds\6030\vc\crtbld\crt\src\crt0.c @ 259]
0012fff0 00000000 kernel32!BaseProcessStart+0x23
Теперь посмотрим содержимое памяти на стеке:
0:000> dd esp
0012fd58 00425158 e06d7363 00000001 00000000
0012fd68 7c812afb 00000003 19930520 0012fe0c
0012fd78 004282b0 cccccccc cccccccc cccccccc
0012fd88 cccccccc cccccccc cccccccc cccccccc
0012fd98 cccccccc cccccccc cccccccc cccccccc
0012fda8 cccccccc 0012fdec 00412609 e06d7363
0012fdb8 00000001 00000003 0012fde0 0012fedc
0012fdc8 00000040 e06d7363 00000001 00000000
Первым идет адрес возврата, а дальше картинка идентична описанной.

Теперь пытаемся пройти дальше:
0:000> dd poi(esp + 8*4)
004282b0 00000000 00411505 00000000 004282a0
004282c0 00000000 00000000 00000000 00000000
004282d0 00000000 00000000 00000000 00000000
004282e0 00000000 00000000 00000000 00000000
004282f0 00000000 00000000 00000000 00000000
00428300 00000000 00000000 00000000 00000000
00428310 00000000 00000000 00000000 00000000
00428320 00000000 00000000 00000000 00000000
Нас интересует 004282a0.
0:000> dd  poi(poi(esp + 8*4) + 3*4)
004282a0 00000002 00428280 00428260 00000000
004282b0 00000000 00411505 00000000 004282a0
004282c0 00000000 00000000 00000000 00000000
004282d0 00000000 00000000 00000000 00000000
004282e0 00000000 00000000 00000000 00000000
004282f0 00000000 00000000 00000000 00000000
00428300 00000000 00000000 00000000 00000000
00428310 00000000 00000000 00000000 00000000
Дальше, думаю понятно...
0:000> db poi(poi( poi(poi(esp + 8*4) + 3*4) + 1*4) + 1*4) + 2*4
00429d64 2e 3f 41 55 58 58 58 45-78 63 65 70 74 69 6f 6e .?AUXXXException
00429d74 40 40 00 00 00 00 00 00-30 51 42 00 00 00 00 00 @@......0QB.....
00429d84 2e 3f 41 56 62 61 64 5f-63 61 73 74 40 40 00 00 .?AVbad_cast@@..
00429d94 00 00 00 00 30 51 42 00-00 00 00 00 2e 3f 41 56 ....0QB......?AV
00429da4 62 61 64 5f 74 79 70 65-69 64 40 40 00 00 00 00 bad_typeid@@....
00429db4 00 00 00 00 30 51 42 00-00 00 00 00 2e 3f 41 56 ....0QB......?AV
00429dc4 5f 5f 6e 6f 6e 5f 72 74-74 69 5f 6f 62 6a 65 63 __non_rtti_objec
00429dd4 74 40 40 00 00 00 00 00-00 00 00 00 30 51 42 00 t@@.........0QB.
XXXException - что нам и требуется!

Резюмируем - по адресу "poi(poi( poi(poi(esp + 20) + c) + 4) + 4) + 8" находится начало декорированного имени исключения,а по нему довольно часто становится хоть-что-то понятно.

Еще одним применением подобной техники может служить отлов возникновения конкретного исключения с помощью точки останова в kernel32!RaiseException и сценария, который автоматически определит имя исключения, сравнит его с заданным паттерном и, в случае совпадения, остановит выполнение программы.

Теоретически, сделать это несложно, выполним пару предварительных тестов:
0:000> as /ma ${/v:ExcName} poi(poi( poi(poi(@esp + 20) + c) + 4) + 4) + 8
0:000> .echo ${ExcName}
.?AUXXXException@@
Алиас нам нужен для дальнейшего использования в выражении $spat, которое допускает только строки.

Теперь проверим условие с помощью:
0:000> j $spat("${ExcName}", "*XXXException*") '.echo got it' '.echo skip it'
got it
Сработало, теперь можно записать сценарий:

* c:\temp\script.txt
as /ma ${/v:ExcName} poi(poi( poi(poi(@esp + 20) + c) + 4) + 4) + 8
j $spat("${ExcName}", "*XXXException*") '.echo got it' 'gn'
И, наконец, ставим точку останова:
bp kernel32!RaiseException "$$< c:\\temp\\script.txt"
BTW, скажу откровенно, что когда речь заходит об "ловле" конкретного исключения, я бы уже предпочел иметь дело с отладчиком от Visual Studio...

Комментариев нет: