寻找Release版程发生异常退出的地方比Debug版麻烦得多。发生异常的时候windows通常会弹出一个错误对话框,点击详细信息,我们能获得出错的地址和大概的出错信息,然后可以用以下办法分析我们的程序。
一. 用MAP文件定位异常代码位置。
1. 如何生成map文件
打开“Project →Project Settings”,选择 C/C++ 选项卡,在“Debug Info”栏选择“Line Numbers Only”(或者在最下面的 Project Options 里面输入:/Zd),然后要选择 Link 选项卡,选中“Generate mapfile”复选框,并再次编辑 Project Options,输入:/mapinfo:lines,以便在 MAP 文件中加入行信息。然后编译工程则可以在输出目录得到同名的.map文件。
2. 使用map文件定位发生异常的代码行
编译得到的map文件可以用文本方式打开,大致是这样的格式:(括号内是PomeloWu填加的注释)
|
在获得程序异常的地址以后,首先通过函数信息部分定位出错的OBJ和函数。做法是用获得的异常地址与Rva+Base栏地址进行比较(Rva,偏移地址;Base,基址)。找到最后一个比获得的异常地址小的那个函数,那就是出错的函数。
之后,用获得的异常地址减去该函数的Rva+Base,就得到了异常行代码相对于函数起始地址的偏移。在“Line number for”部分找到相对应的模块,并把其后的行号信息与上面减得的偏移量对比,找到最接近的一个,前面的行号大致就是目标行了。
二. 获得错误的详细信息。
实际上,光靠Windows的错误消息对话框提供的信息量是很有限的,用自己写的exception filter可以获得更多的错误信息。用SetUnhandledExceptionFilter设定自定义错误处理回调函数替换Win32默认的top-level exception filter:
2 SetUnhandledExceptionFilter的函数原型:
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter( LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter // exception filter function );2 SetUnhandledExceptionFilter返回当前的exception filter。应当保存这个函数指针并在不再需要使用自定义错误处理函数的时候当作参数再次调用SetUnhandledExceptionFilter。
2 lpTopLevelExceptionFilter 是自定义的exception filter函数指针,如果传入NULL值则指定UnhandledExceptionFilter来负责异常处理。lpTopLevelExceptionFilter其函数原型应该是与UnhandledExceptionFilter同型:
LONG WINAPI UnhandledExceptionFilter( STRUCT _EXCEPTION_POINTERS *ExceptionInfo // address of // exception info );2 lpTopLevelExceptionFilter的返回值应该是下面3种之一:
EXCEPTION_EXECUTE_HANDLER = 1
EXCEPTION_CONTINUE_EXECUTION = -1
这两个返回值都应该由调用UnhandledExceptionFilter后返回。
EXCEPTION_EXECUTE_HANDLER表示进程结束
EXCEPTION_CONTINUE_EXECUTION表示处理异常之后继续执行
EXCEPTION_CONTINUE_SEARCH = 0
进行系统通常的异常处理(错误消息对话框)
2 lpTopLevelExceptionFilter的唯一的参数是_EXCEPTION_POINTERS结构指针。
typedef struct _EXCEPTION_POINTERS { // exp PEXCEPTION_RECORD ExceptionRecord; PCONTEXT ContextRecord; } EXCEPTION_POINTERS;其中PCONTEXT是一个指向进程上下文结构的指针,保存了各个寄存器在异常发生的时候的值,详细信息参考《Windows核心编程》。
ExceptionRecord则指向另一个结构体EXCEPTION_RECORD:
typedef struct _EXCEPTION_RECORD { // exr DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];} EXCEPTION_RECORD;
DWORD ExceptionCode;
异常代码,指出异常原因。常见异常代码有:
EXCEPTION_ACCESS_VIOLATION = C0000005h
读写内存冲突
EXCEPTION_INT_DIVIDE_BY_ZERO = C0000094h
除0错误
EXCEPTION_STACK_OVERFLOW = C00000FDh
堆栈溢出或者越界
EXCEPTION_GUARD_PAGE = 80000001h
由Virtual Alloc建立起来的属性页冲突
EXCEPTION_NONCONTINUABLE_EXCEPTION = C0000025h
不可持续异常,程序无法恢复执行,异常处理例程不应处理这个异常
EXCEPTION_INVALID_DISPOSITION = C0000026h
在异常处理过程中系统使用的代码
EXCEPTION_BREAKPOINT = 80000003h
调试时中断(INT 3)
EXCEPTION_SINGLE_STEP = 80000004h
单步调试状态(INT 1)DWORD ExceptionFlags;
异常标志
0,表示可修复异常
EXCEPTION_NONCONTINUABLE = 1,表示不可修复异常。在不可修复异常后尝试继续执行会导致EXCEPTION_NONCONTINUABLE_EXCEPTION = C0000025H异常。struct _EXCEPTION_RECORD *ExceptionRecord;
当异常处理程序中发生异常时,此字段被填充,否则为NULLPVOID ExceptionAddress;
发生异常的地址(EIP)DWORD NumberParameters;
规定与异常相关的参数数量(0-15),是ExceptionInformation数组中元素个数。DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
异常描述信息,大多数异常都未定义此数组,仅有EXCEPTION_ACCESS_VIOLATION异常的描述信息:
ExceptionInformation[0],描述导致异常的操作类型
= 0 读异常
= 1 写异常
ExceptionInformation[1],发生读写异常的内存地址
也就是说,只要注册了自己写的这个exception filter,一旦发生异常,进入这个exception filter,从参数我们就能获得各种需要的信息了。而这个exception filter需要做的就是保存这些信息,然后将异常处理的事情交还给系统就行了:
// in the beginning
// Install the unhandled exception filter function
g_previousFilter = SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
// exception filter
LONG WINAPI MyUnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo)
{
WriteLogFile(pExceptionInfo); // 写入文件
if ( g_previousFilter )
return g_previousFilter( pExceptionInfo );
else
return EXCEPTION_CONTINUE_SEARCH;
}
三. 使用COD文件精确分析异常原因
说精确分析多少有点言过其实,发生异常的情况各不相同,分析真正原因很可能是一件极其复杂的事情。不过用COD文件能比MAP文件更精确地定位产生异常的位置。结合汇编代码和自定义的exception filter获得的错误情报寄存器状态等各种信息,找到异常发生的直接原因是很容易的。
1. 如何生成cod文件
仍然是打开“Project →Project Settings”,选择 C/C++ 选项卡,在“Category”栏选择“Listing Files”然后在Listing file type栏选择“Assembly with Machine Code”。重新编译工程后则可以在输出目录看到与每一个.cpp文件同名的.cod文件。
2. Cod文件的使用
首先还是利用map文件用获得的程序异常地址通过函数信息部分定位出错的OBJ和函数,并同样记录偏移地址(用获得的异常地址减去该函数的Rva+Base的差值)。然后,在相应的cod文件中(而不是在map文件后面的行号信息部分)来查找出错的函数,找到如下的格式:
|
找到出错的函数以后,再用偏移地址就能找到准确的异常发生的地方。然后通过源程序、汇编码即可进行更详尽的分析了。
CodeProject文章:http://www.codeproject.com/KB/debug/postmortemdebug_standalone1.aspx
文章2:http://hi.baidu.com/0x9000/item/7b355010f4be8c413a176e81
如何去做崩溃后的定位是一个有效的方法。
1. 可以先利用连接器的配置,产生 map 文件:
在 VC Project Setting 对话框中打开 Generate mapfile,然后在Project Options对话框中键入“/mapinfo:lines /mapinfo:exports”,这样在 ./release 目录下就有 .pdb 文件了。
然后在配合 这个方法 就可以获取源码行数了。
2. 可以产生一下 mfc42.dll 的 pdb 文件:
1). cd C:\Program Files\Microsoft Visual Studio\VC98\MFC\SRC
2). ..\..\bin\vcvars32.bat
设置一下 vc 的编译环境
3). nmake /f mfcdll.mak libname=mfc42 debug=0 编译的中间文件在 $DLL.W。
4). 将产生的 mfc42.dll 放到待调试程序的相同路径下,开始调试。
后者在调试 mfc 程序的时候非常有效,通过调用堆栈即可获悉一些很重要的信息。
文章2:http://www.cppblog.com/woaidongmao/archive/2009/10/19/98929.html
我们来演示一下如何制造一起崩溃事件:
我把这个试验的源代码列出来:
const int x =10000;
int main(int argc, char* argv[])
{
int *y=0;
y=(int*)&x;
*y=10;
return 0;
}
我们用Microsoft Visual C++ 6.0(SP5)编译出一个Debug版本的EXE。双击运行它。在Windows 2000 Server下,你将会得到这样一个对话框:
标题:“Pointer.exe ? 应用程序错误”;
正文:“”0x00401279”指令引用的”0x0043101c”内存。该内存不能为”written”。
要终止程序,请单击”确定”。
要调试程序,请单击”取消”。”
知道了这些信息后,如何找到错误发生策源地呢?
请记住这个地址“0x00401279”,它是崩溃发生地。
如何找到崩溃的源头:
有两种情况:
μ 一是我们拥有源代码,可以现场调试;
μ 二是现场绝对不可以安装VC,无法调试,但是我们有它的MAP文件。
第一种情况,有源代码,这被叫做“事后调试”:
首先我们用VC IDE装载这个工程,按F11执行它,切换至反汇编窗口(Disassembly)。
按下Ctrl+G热键。
你就会得到一个“Go To”的窗口。默认选择是“Address”。在“Enter address expression”编辑框中输入崩溃发生地0x00401279。然后点击“Go To”按钮。你就来到了这个地方:
00401279 mov dword ptr [eax],0Ah
好了,我们看到了发生崩溃时执行的是这行反汇编代码,但是为什么会崩溃呢?
我们在这里设置一个断点,按F5来到这里。
在Watch窗口中键入“@EAX”察看EAX寄存器,得到的数值是“0xcccccccc”。显然这是因为向一个空指针指向的地址复制一个数据,从而造成了崩溃。
好了,针对这个问题,你已经调试成功了。
还有一个问题,对于Release版本的EXE,也可以这么调试吗?
当然可以。同样是这个例子,运行它的Release版本,得到的崩溃地址是0x0040108a。
我们在VC中装载这个工程的Release版本,按F11运行它。
来到它的反汇编代码的0x004018a处,我们看到:
0040108A mov dword ptr ds:[40B0D0h],0Ah
第二种情况,有映射文件Pointer.map:
值得注意的是,如果你只在VC Project Setting对话框中打开Generate mapfile,还是不够的。因为你一定还要输出程序代码地址和源代码行号!!这非常的重要!
要得到这些信息,请在Project Options对话框中键入“/mapinfo:lines /mapinfo:exports”。请你一定要养成这种习惯!因为这不是默认设置。
我们得到的map文件大致如下,我删节了大多数输出:
Pointer
(应用程序名)
Timestamp is 3d4407a7 (Sun Jul 28 23:03:03 2002)
(时间戳)
Preferred load address is 00400000
(最佳装载基地址。非常重要的一个数据。不过一般都是这个数。)
Address Publics by Value Rva+Base Lib:Object
0001:00000250 _main 00401250 f Pointer.obj
(_main的虚地址)
Line numbers for .\Debug\Pointer.obj(E:\ Pointer\Pointer.cpp) segment .text
12 0001:00000250 14 0001:00000268 15 0001:0000026f 16 0001:00000276
18 0001:0000027f 20 0001:00000291 23 0001:000002a4 24 0001:000002a6
(这就是我们的Pointer.cpp所对应的程序代码行号和相对虚拟地址的对应表)
我们可以从中看到,最佳装载基地址是0x00400000,_main的虚地址是0x00401250,而0001:00000250又是什么意思呢?
0x00000250就是_main的相对虚拟地址(RVA)。
0x00010000就是PE头文件的大小,一般都是这个数。
所以虚地址就是这么算出来的:
0x00401250 = 0x00400000 + 0x00010000 + 0x00000250
虚地址 = 最佳装载基地址 + PE头文件的大小 + 相对虚拟地址(RVA)
通过_main的RVA的计算,我们也就知道了怎么计算崩溃地址0x00401279的RVA,是0x00000279,对吧?
然后,在这个MAP映射文件的“Line numbers for .\Debug\Pointer.obj(E:\ Pointer\Pointer.cpp) segment .text”这个行号段中查找这个地址。如你所看到的,只有16行对应的00000276和18行对应的0000027F,没有00000279呀?
没有17行的对应关系,说明17行是空行。
那么00000279就一定是16行的了!这样你不用看那个程序员的代码,就可以通知他:崩溃发生在你的Pointer.cpp的第16行了!很酷吧!