用SetUnhandledExceptionFilter自定义错误处理, 在自定义处理中记录(或显示)错误地址(在我的Win7X64下,只是跳出崩溃对话框,看不出是错误地址是啥).
然后再使用MAP和COD文件,定位具体的源代码位置, 错误可以具体定位到源代码行.
实验环境: win7X64Sp1 + Microsoft Visual C++ 2008 Professional 9.0.21022.8 RTM.
Demo工程直接在: http://www.codeproject.com/Articles/154686/SetUnhandledExceptionFilter-and-the-C-C-Runtime-Li 上的Demo中修改的, 定位的错误是"构造函数和析构函数中不能直接或间接调用纯虚函数". 这错误比除零错要难找些, 不容易直接能定位到源代码行数.
/* * @file crash_hook.cpp * @brief 实验: parse crash info by map and cod file * 用 SetUnhandledExceptionFilter 自定义错误处理, 打印出出错地址 * 用map文件和Cod文件定位具体的源代码错误行数 */ /** * ApiHook 和 SetUnhandledExceptionFilter 来自CodeProject * original url from : http://www.codeproject.com/Articles/154686/SetUnhandledExceptionFilter-and-the-C-C-Runtime-Li * <<SetUnhandledExceptionFilter and the C/C++ Runtime Library>> */ // (c) 2011 Cristian Adam #include <iostream> #include <ctime> #include <vector> #include <windows.h> #include <tchar.h> #include "APIHook.h" bool showCrashDialog = TRUE; /**< 是否显示OS崩溃对话框 */ LONG WINAPI RedirectedSetUnhandledExceptionFilter(EXCEPTION_POINTERS * lpException) { // When the CRT calls SetUnhandledExceptionFilter with NULL parameter // our handler will not get removed. if(lpException) { /** 在vsIde中, 进不了这里, lpException恒为NULL */ _tprintf(_T("error on address 0x%p\n"), lpException->ExceptionRecord->ExceptionAddress); } /** 在vsIde中返回 */ return showCrashDialog ? EXCEPTION_CONTINUE_SEARCH : EXCEPTION_EXECUTE_HANDLER; } LONG WINAPI OurSetUnhandledExceptionFilter(EXCEPTION_POINTERS * lpException) { _tprintf(_T(">> OurSetUnhandledExceptionFilter\n")); if(lpException) { _tprintf(_T("error on address 0x%p\n"), lpException->ExceptionRecord->ExceptionAddress); } return showCrashDialog ? EXCEPTION_CONTINUE_SEARCH : EXCEPTION_EXECUTE_HANDLER; } void MemoryAccessCrash() { std::cout << "Normal null pointer crash" << std::endl; char *p = 0; *p = 5; } void OutOfBoundsVectorCrash() { std::cout << "std::vector out of bounds crash!" << std::endl; std::vector<int> v; v[0] = 5; } void AbortCrash() { std::cout << "Calling Abort" << std::endl; abort(); } void VirtualFunctionCallCrash() { _tprintf(_T("VirtualFunctionCallCrash's address is 0x%p\n"), VirtualFunctionCallCrash); struct B { B() { std::cout << "Pure Virtual Function Call crash!" << std::endl; Bar(); /**< 构造函数和析构函数中不能直接或间接调用纯虚函数 */ } virtual void Foo() = 0; void Bar() { Foo(); } }; struct D: public B { virtual void Foo() { } }; B* b = new D;/**< 执行这行报错 */ // Just to silence the warning C4101: 'VirtualFunctionCallCrash::B::Foo' : unreferenced local variable b->Foo(); _tprintf(_T("<< VirtualFunctionCallCrash")); } int MyMakeCrash(int iOption); int main(int argc, char **argv) { int iOption = 0; ::SetUnhandledExceptionFilter(OurSetUnhandledExceptionFilter); CAPIHook apiHook("kernel32.dll", "SetUnhandledExceptionFilter", (PROC)RedirectedSetUnhandledExceptionFilter); switch(argc) { case 2: { iOption = atoi(argv[1]); _tprintf(_T("parameter is [%d]\n"), iOption); MyMakeCrash(iOption); } break; default: { _tprintf(_T("parameter number is [%d], please input paramter at lease one \n"), argc); } break; } return 0; } int MyMakeCrash(int iOption) { switch(iOption) { case 0: MemoryAccessCrash(); break; case 1: OutOfBoundsVectorCrash(); break; case 2: AbortCrash(); break; case 3: VirtualFunctionCallCrash(); break; } return iOption; }我是在Release版带调试符号的版本上调试的.
设置工程的为Release版带调试符号 + MAP和COD文件:
程序编译后生成EXE, MAP, COD文件
程序运行后直接报错, 没有程序报错的地址, 如果能看到错误地址, 就不用SetUnhandledExceptionFilter来处理.
报错地址0x000000013F171E68的bit16~bit31的2个字节每次运行都是变化的, 需要在MAP文件中寻找的地址是: 0x0000000100001E68.
查看MAP文件, 找到0x0000000100001E68的上下边界地址, 正好能夹住报错地址的函数名称.
可以看到: VirtualFunctionCallCrash包含出错的地址, 如下:
0001:00000d30 ?AbortCrash@@YAXXZ 0000000140001d30 f crash_hook.obj 0001:00000d70 ?VirtualFunctionCallCrash@@YAXXZ 0000000140001d70 f crash_hook.obj 0001:00000e70 ??1_Container_base_aux@std@@QEAA@XZ 0000000140001e70 f crash_hook.obj计算报错行数离函数VirtualFunctionCallCrash入口的偏移: ErrOffset = 0x0000000100001E68 - 0x0000000100001D70 = 0xF8
在COD文件中找到VirtualFunctionCallCrash对应的机器码和源码的列表, 因为这个函数中有一个结构定义, 不容易看. 我在函数的入口和出口处打印了2句. 我就找我打印的语句之间的代码, 找不到偏移接近0xF8的语句, 函数最后一行偏移为0x007c. 这个可能是虚函数搞的, 以后再分析.
; 74 : { $LN6: 00000 48 83 ec 58 sub rsp, 88 ; 00000058H 00004 48 c7 44 24 38 fe ff ff ff mov QWORD PTR $T85408[rsp], -2 ; 75 : _tprintf(_T("VirtualFunctionCallCrash's address is 0x%p\n"), VirtualFunctionCallCrash); 0000d 48 8d 15 00 00 00 00 lea rdx, OFFSET FLAT:?VirtualFunctionCallCrash@@YAXXZ ; VirtualFunctionCallCrash 00014 48 8d 0d 00 00 00 00 lea rcx, OFFSET FLAT:$SG79383 0001b ff 15 00 00 00 00 call QWORD PTR __imp_wprintf ; 76 : struct B ; 77 : { ; 78 : B() ; 79 : { ; 80 : std::cout << "Pure Virtual Function Call crash!" << std::endl; ; 81 : Bar(); /**< 构造函数和析构函数中不能直接或间接调用纯虚函数 */ ; 82 : } ; 83 : ; 84 : virtual void Foo() = 0; ; 85 : ; 86 : void Bar() ; 87 : { ; 88 : Foo(); ; 89 : } ; 90 : }; ; 91 : ; 92 : struct D: public B ; 93 : { ; 94 : virtual void Foo() ; 95 : { ; 96 : } ; 97 : }; ; 98 : ; 99 : B* b = new D;/**< 执行这行报错 */ 00021 b9 08 00 00 00 mov ecx, 8 00026 e8 00 00 00 00 call ??2@YAPEAX_K@Z ; operator new 0002b 48 89 44 24 30 mov QWORD PTR $T85400[rsp], rax 00030 48 83 7c 24 30 00 cmp QWORD PTR $T85400[rsp], 0 00036 74 11 je SHORT $LN3@VirtualFun 00038 48 8b 4c 24 30 mov rcx, QWORD PTR $T85400[rsp] 0003d e8 00 00 00 00 call ??0D@?2??VirtualFunctionCallCrash@@YAXXZ@QEAA@XZ 00042 48 89 44 24 40 mov QWORD PTR tv76[rsp], rax 00047 eb 09 jmp SHORT $LN4@VirtualFun $LN3@VirtualFun: 00049 48 c7 44 24 40 00 00 00 00 mov QWORD PTR tv76[rsp], 0 $LN4@VirtualFun: 00052 48 8b 44 24 40 mov rax, QWORD PTR tv76[rsp] 00057 48 89 44 24 28 mov QWORD PTR $T85399[rsp], rax 0005c 48 8b 44 24 28 mov rax, QWORD PTR $T85399[rsp] 00061 48 89 44 24 20 mov QWORD PTR b$[rsp], rax ; 100 : // Just to silence the warning C4101: 'VirtualFunctionCallCrash::B::Foo' : unreferenced local variable ; 101 : b->Foo(); 00066 48 8b 44 24 20 mov rax, QWORD PTR b$[rsp] 0006b 48 8b 00 mov rax, QWORD PTR [rax] 0006e 48 8b 4c 24 20 mov rcx, QWORD PTR b$[rsp] 00073 ff 10 call QWORD PTR [rax] ; 102 : _tprintf(_T("<< VirtualFunctionCallCrash")); 00075 48 8d 0d 00 00 00 00 lea rcx, OFFSET FLAT:$SG79411 0007c ff 15 00 00 00 00 call QWORD PTR __imp_wprintf ; 103 : }
我只能推测是函数靠后的代码出错, 如果是在实际工程中, 我就直接在这函数中打调试日志了~.
以前只遇到过, 函数中构造函数和析构函数中调用虚函数运行不正常的情况。
但是像这种构造函数中调用纯虚函数挂掉的BUG, 还是第一次见到. 惭愧, 最后还是用单步看到99行报错~~
不过还是可以看到, 只要能从Windows崩溃的转储信息或自定义错误处理看到发生错误的函数, 排错的范围就大大缩小了.
<2012_0128>
只看Map文件和COD文件, 无法准确定位故障发生地点. 甚至连工程中的函数名都确认不了。 这个demo中模拟的bug,有的是在stl中发生的. 只看COD文件,看不出来故障点.
CodeProject上有个XCrashReport, 可以转储崩溃信息到DUMP文件, 然后用WinDbg查看DUMP文件,查找到堆栈调用链上的工程内部地址。这样才能确定问题.
现在的想法是用APIHook自定义错误处理函数,在自定义错误处理函数中,自己DUMP出报错时的DUMP文件。然后用WinDbg查找出函数调用链上位于工程中的代码行数,至少要定位到哪个函数报错,然后加入调试日志来确定问题。
安装了X64版的WinDbg, 打开dump文件后, 使用命令: !analyze -v
看到了windbg对程序中自己保存的miniDump文件的分析结果:
STACK_TEXT:
00000000`0020fb40 00000000`691052f5 : 00000000`00000000 00000000`00000000 00000000`00000000 00000001`3f0d20b0 : msvcr90!invalid_parameter+0x70
00000000`0020fb80 00000001`3f0d2115 : 00000000`0020fc10 00000000`0020fbc0 ffffffff`fffffffe 00000000`00000000 : msvcr90!invalid_parameter_noinfo+0x19
00000000`0020fbc0 00000000`0020fc10 : 00000000`0020fbc0 ffffffff`fffffffe 00000000`00000000 00000000`00000000 : crash_hook+0x2115
00000000`0020fbc8 00000000`0020fbc0 : ffffffff`fffffffe 00000000`00000000 00000000`00000000 00000001`3f0d1dc7 : 0x20fc10
00000000`0020fbd0 ffffffff`fffffffe : 00000000`00000000 00000000`00000000 00000001`3f0d1dc7 00000000`0020fc10 : 0x20fbc0
00000000`0020fbd8 00000000`00000000 : 00000000`00000000 00000001`3f0d1dc7 00000000`0020fc10 00000000`00000000 : 0xffffffff`fffffffe
因为还没有安装调试符号库, 所以没有看到调用链上的函数名称.
已经看到工程中崩溃的位置是: crash_hook+0x2115.
但是无法将 crash_hook+0x2115 和MAP和COD文件中的源码行数相对应.