experiment: parse crash info on vs2008

用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文件:

experiment: parse crash info on vs2008_第1张图片

experiment: parse crash info on vs2008_第2张图片

experiment: parse crash info on vs2008_第3张图片

experiment: parse crash info on vs2008_第4张图片

experiment: parse crash info on vs2008_第5张图片

程序编译后生成EXE, MAP, COD文件

experiment: parse crash info on vs2008_第6张图片

程序运行后直接报错, 没有程序报错的地址, 如果能看到错误地址, 就不用SetUnhandledExceptionFilter来处理.

experiment: parse crash info on vs2008_第7张图片

报错地址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文件中的源码行数相对应.








你可能感兴趣的:(experiment: parse crash info on vs2008)