Некоторое время назад мы опубликовали в OpenSource небольшую библиотечку скриптов для популярного отладчика Windbg. Они предназначены для автоматизации ряда рутинных задач, возникающих при анализе причин падения программ как при отладке вживую, так и при работе с дампами памяти. Мы запланировали несколько постов, объясняющих, как эти скрипты использовать. Вот первый пост из данного цикла. Он демонстрирует использование команды !exccandidates на синтетическом примере.
Иногда при разборе дампа возникает подозрение, что случившееся исключение тут уже не первое, но при этом прямых ссылок на него нигде не сохранилось. Таких ситуаций не так уж и мало: например, повторные исключения могут возникнуть в процессе обработки исключения или при раскрутке стека после исключения; они могут быть спровоцированы состоянием переменных после первого исключения; наконец, такое часто происходит при использовании сторонних библиотек, которые применяют другую модель обработки ошибок. Во всех этих случаях можно попытаться найти на стеке записи от предыдущих исключений. Гарантии, что они остались в целости и сохранности, конечно, нет, но во многих случаях это удается.
Сам по себе трюк с поиском исключений достаточно широко известен — нужно поискать какие-то редко меняющиеся части структуры CONTEXT. Обычно ищут флаги (хотя они-то как раз могут быть изменены) или сегментные регистры, что, как мне кажется, надежнее. Команда !exccandidates автоматизирует данный процесс и обрезает лишнее, оставляя только те исключения, которые произошли в выбранном потоке.
Для примера возьмем вот такую небольшую программку:
#include <exception>
class A
{
public:
A()
{
throw(std::exception("It's too cold!"));
}
};
class B
{
public:
bool Do()
{
try
{
A a;
return true;
}
catch (const std::exception&)
{
return false;
}
}
};
class Log
{
public:
void Notify(const char* msg)
{
message = msg;
}
private:
const char* message;
};
class C
{
public:
C()
{
B b;
if (!b.Do())
reinterpret_cast<Log*>(1)->Notify("I don't know what happened!"); // Simulating tracer lifetime error
}
};
int main()
{
C c;
return 0;
}
Она имитирует ситуацию, когда после ошибки обнаруживается, что у нас есть проблема с временем жизни трейсера. Естественно, проблему с трейсером надо чинить, но чтобы два раза не обуваться, почему бы не попытаться заодно выяснить, в чем заключалась первичная ошибка.
При запуске этой программы под Windbg выполнение останавливается на ожидаемом AV. Для удобства find_exception.cpp можно скачать вот отсюда.
Вот стек, который мы видим:
>.ecxr; k
# ChildEBP RetAddr
00 (Inline) -------- find_exception!Log::Notify [e:\Work\Perforce\core_tech_from_p4\_articles_\WinDbg public scripts\find_exception\find_exception.cpp @ 34]
01 (Inline) -------- find_exception!C::{ctor}+0x9 [e:\Work\Perforce\core_tech_from_p4\_articles_\WinDbg public scripts\find_exception\find_exception.cpp @ 48]
02 006ffbf4 00cd1387 find_exception!main+0x9 [e:\Work\Perforce\core_tech_from_p4\_articles_\WinDbg public scripts\find_exception\find_exception.cpp @ 54]
03 (Inline) -------- find_exception!invoke_main+0x1c [d:\agent\_work\57\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 78]
04 006ffc3c 7749fa29 find_exception!__scrt_common_main_seh+0xfa [d:\agent\_work\57\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
05 006ffc4c 778a76b4 kernel32!BaseThreadInitThunk+0x19
06 006ffca8 778a7684 ntdll!__RtlUserThreadStart+0x2f
07 006ffcb8 00000000 ntdll!_RtlUserThreadStart+0x1b
Естественно, никаких указаний на исходное исключение тут нет. Но мы можем их поискать:
>!exccandidates
Searching for exception candidate in current thread
Stack top: 0x600000
Found exception 0
.exr 0x6ff5f0 - exception code 0xe06d7363
.cxr 0x6ff640
@$exccandidates() : 0x1
Отлично, нашлось плюсовое исключение — 0xe06d7363. Посмотрим его, для чего воспользуемся командами, которые нам любезно подсказывает скрипт:
>.exr 0x6ff5f0
ExceptionAddress: 7773a8b2 (KERNELBASE!RaiseException+0x00000062)
ExceptionCode: e06d7363 (C++ EH exception)
ExceptionFlags: 00000001
NumberParameters: 3
Parameter[0]: 19930520
Parameter[1]: 006ffbbc
Parameter[2]: 00cd2724
pExceptionObject: 006ffbbc
_s_ThrowInfo : 00cd2724
Type : class std::exception
И стек:
>.cxr 0x6ff640; k
*** Stack trace for last set context - .thread/.cxr resets it
# ChildEBP RetAddr
00 006ffb7c 711f7a86 KERNELBASE!RaiseException+0x62
01 006ffbac 00cd110d VCRUNTIME140!_CxxThrowException+0x66 [d:\agent\_work\9\s\src\vctools\crt\vcruntime\src\eh\throw.cpp @ 74]
02 006ffbc8 00cd1145 find_exception!A::A+0x1d [e:\Work\Perforce\core_tech_from_p4\_articles_\WinDbg public scripts\find_exception\find_exception.cpp @ 8]
03 006ffbf0 00cd1175 find_exception!B::Do+0x35 [e:\Work\Perforce\core_tech_from_p4\_articles_\WinDbg public scripts\find_exception\find_exception.cpp @ 24]
04 (Inline) -------- find_exception!C::{ctor}+0x5 [e:\Work\Perforce\core_tech_from_p4\_articles_\WinDbg public scripts\find_exception\find_exception.cpp @ 47]
05 006ffbf4 00cd1387 find_exception!main+0x5 [e:\Work\Perforce\core_tech_from_p4\_articles_\WinDbg public scripts\find_exception\find_exception.cpp @ 54]
06 (Inline) -------- find_exception!invoke_main+0x1c [d:\agent\_work\57\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 78]
07 006ffc3c 7749fa29 find_exception!__scrt_common_main_seh+0xfa [d:\agent\_work\57\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
08 006ffc4c 778a76b4 kernel32!BaseThreadInitThunk+0x19
09 006ffca8 778a7684 ntdll!__RtlUserThreadStart+0x2f
0a 006ffcb8 00000000 ntdll!_RtlUserThreadStart+0x1b
Вот таким образом иногда удается заглянуть чуть дальше в поисках причин падения.