(转载)
如何使用MAP文件找到程序崩溃的原因
作者 Wouter Dhondt 翻译 冯亦成(fengyc@pset.suntec.net)
[译 者] 在我们调试程序的时候,习惯于不停的Step in, Step in...可是如果我们发现Debug版的exe可以完全正常运行,而Release版却经常莫名其妙Crash。那该怎么办??没有关系,这篇文章就是 帮你解决这个问题的:) 当然,你如果希望全面提高你的Debug能力,不妨去读一下John Robbins的"Debugging Applications"一书,不过你要读英文版,中文版翻译的太烂了!
导言
编写整洁的程序是一回事。而当用户通知你的程序崩溃了,你知道在增加程序新属性之前最好先修正这些错误,如果你足够幸运的话,用户会给你提供一个崩溃地址,要解决这个问题还是要有很长的路要走。有了崩溃地址,你怎么确定到底是在什么出了错呢?
创建MAP文件
首 先,你需要MAP文件。如果你没有MAP文件,那你几乎是不可能通过崩溃地址找到你程序出错的具体代码行。那么先让我教你怎么创建合适的MAP文件。为此 我们创建了一个新工程(MAPFILE):我在VC++6.0中创建了应用Win32 Application选项的新工程,并且选择'typical "Hello Word!" application',这样可以使得生成的MAP文件能够满足我下边解说的需要。
当生成新工程后,我们调整release版的工程设置信息。在C/C++属性页,设置Debug Info的值为"Line Numbers Only"。
很 多人都忘了这一步,但是如果你想得到合适的MAP文件,你就需要设置这个选项,这不会对你的release程序造成任何影响。下一步是Link属性页,你 需要选择"Generate mapfile"选项。在Project Options编辑框中输入/MAPINFO:LINES和/MAPINFO:EXPORTS开关。
现在你可以编译和链接你的工程了,链接之后,你可以在你的中间目录中找到.map文件(和exe文件在一起)。
阅读MAP文件
在上面这些无趣的工作之后,接下去就是很有趣的部分:怎么读MAP文件。我们通过一个崩溃实例来介绍怎么读MAP文件。那么我们先得让程序崩溃,于是我在InitInstance()函数的最后增加了下边的两行代码:
char* pEmpty = NULL;
*pEmpty = 'x'; // 第119行
我 相信你能够找到其它代码使得你的程序崩溃。现在重新编译且链接工程。如果你运行你的程序,程序将崩溃,并且得到怎样的消息:'The instruction at "0x004011a1" referenced memory at "0x00000000"。0x00000000内存不能写。
现在,可以用Notepad或者类似的编辑工具打开MAP文件。MAP文件如下所示:
在MAP文件的头部包含了模块名称,表示工程链接时刻的时间戳,以及首选加载地址(一般是0x00400000,除非是dll)。文件头之后就是一些section信息,是由链接程序把各种OBJ和LIB文件的section信息组织起来的。
MAPFILE
Timestamp is 3df6394d (Tue Dec 10 19:58:21 2002)
Preferred load address is 00400000
Start Length Name Class
0001:00000000 000038feH .text CODE
0002:00000000 000000f4H .idata$5 DATA
0002:000000f8 00000394H .rdata DATA
0002:0000048c 00000028H .idata$2 DATA
0002:000004b4 00000014H .idata$3 DATA
0002:000004c8 000000f4H .idata$4 DATA
0002:000005bc 0000040aH .idata$6 DATA
0002:000009c6 00000000H .edata DATA
0003:00000000 00000004H .CRT$XCA DATA
0003:00000004 00000004H .CRT$XCZ DATA
0003:00000008 00000004H .CRT$XIA DATA
0003:0000000c 00000004H .CRT$XIC DATA
0003:00000010 00000004H .CRT$XIZ DATA
0003:00000014 00000004H .CRT$XPA DATA
0003:00000018 00000004H .CRT$XPZ DATA
0003:0000001c 00000004H .CRT$XTA DATA
0003:00000020 00000004H .CRT$XTZ DATA
0003:00000030 00002490H .data DATA
0003:000024c0 000005fcH .bss DATA
0004:00000000 00000250H .rsrc$01 DATA
0004:00000250 00000720H .rsrc$02 DATA
在 section信息之后,你看到的时公共函数信息。注意下边的"public"部分,如果你有静态C函数,那它不会在"public"部分出现。幸运的 是,在line numbers部分仍会反映静态函数的信息。"public"函数信息的最重要的部分是函数名称和Rva+Base栏的信息,Rva+Base信息是函数 的起始地址。
Address Publics by Value Rva+Base Lib:Object
0001:00000000 _WinMain@16 00401000 f MAPFILE.obj
0001:000000c0 ?MyRegisterClass@@YAGPAUHINSTANCE__@@@Z 004010c0 f MAPFILE.obj
0001:00000150 ?InitInstance@@YAHPAUHINSTANCE__@@H@Z 00401150 f MAPFILE.obj
0001:000001b0 ?WndProc@@YGJPAUHWND__@@IIJ@Z 004011b0 f MAPFILE.obj
0001:00000310 ?About@@YGJPAUHWND__@@IIJ@Z 00401310 f MAPFILE.obj
0001:00000350 _WinMainCRTStartup 00401350 f LIBC:wincrt0.obj
0001:00000446 __amsg_exit 00401446 f LIBC:wincrt0.obj
0001:0000048f __cinit 0040148f f LIBC:crt0dat.obj
0001:000004bc _exit 004014bc f LIBC:crt0dat.obj
0001:000004cd __exit 004014cd f LIBC:crt0dat.obj
0001:00000591 __XcptFilter 00401591 f LIBC:winxfltr.obj
0001:00000715 __wincmdln 00401715 f LIBC:wincmdln.obj
//SNIPPED FOR BETTER READING
0003:00002ab4 __FPinit 00408ab4 <common>
0003:00002ab8 __acmdln 00408ab8 <common>
entry point at 0001:00000350
Static symbols
0001:000035d0 LeadUp1 004045d0 f LIBC:memmove.obj
0001:000035fc LeadUp2 004045fc f LIBC:memmove.obj
//SNIPPED FOR BETTER READING
0001:00000577 __initterm 00401577 f LIBC:crt0dat.obj
0001:0000046b _fast_error_exit 0040146b f LIBC:wincrt0.obj
Public 函数部分之后是line信息部分(你设置了Link属性页中使用了/MAPINFO:LINES并且在C/C++属性页中选择"Line numbers")。在这之后就是export信息了,只要你的程序有输出函数并且在link属性页包含了/MAPINFO:EXPORTS,你就可得到 export信息。
Line numbers for .\Release\MAPFILE.obj(F:\MAPFILE\MAPFILE.cpp) segment .text
24 0001:00000000 30 0001:00000004 31 0001:0000001b 32 0001:00000027
35 0001:0000002d 53 0001:00000041 40 0001:00000047 43 0001:00000050
45 0001:00000077 47 0001:00000088 48 0001:0000008f 52 0001:000000ad
53 0001:000000b3 71 0001:000000c0 80 0001:000000c3 81 0001:000000c8
82 0001:000000ff 86 0001:00000114 88 0001:00000135 89 0001:00000145
102 0001:00000150 108 0001:00000155 110 0001:00000188 122 0001:0000018d
115 0001:0000018e 116 0001:0000019a 119 0001:000001a1 121 0001:000001a8
122 0001:000001ae 135 0001:000001b0 143 0001:000001cc 172 0001:000001ee
175 0001:0000020d 149 0001:00000216 157 0001:0000022c 175 0001:00000248
154 0001:00000251 174 0001:0000025f 175 0001:00000261 151 0001:0000026a
174 0001:00000287 175 0001:00000289 161 0001:00000294 164 0001:000002a8
165 0001:000002b6 166 0001:000002d8 174 0001:000002e7 175 0001:000002e9
169 0001:000002f2 174 0001:000002fa 175 0001:000002fc 179 0001:00000310
186 0001:0000031e 193 0001:0000032e 194 0001:00000330 188 0001:00000333
183 0001:00000344 194 0001:00000349
现 在我们来定位代码中哪里发生崩溃。首先我们先要确定是哪个函数包含了崩溃地址。浏览"Rva+Base"栏,查找到第一个地址比崩溃地址大的函数,那么该 函数的上一个函数就是发生崩溃的函数了。在我们的例子中,崩溃地址是0x004011a1,这个地址位于0x00401150 和 0x004011b0之间,这样我们就知道崩溃函数是?InitInstance@@YAHPAUHINSTANCE__@@H@Z。任何以问号开头的函 数名都是C++ decorated name。你可以把C++ decorated name作为命令行参数传递给Platform SDK的UNDNAME.EXE程序,就可以得到原始函数名称了。在大多数情况下你不需要这样做,通过观察C++ decorated name我们就可以知道原始函数名称(这里,函数名称就是InitInstance())
以上是bug跟踪的重要一步。但是我们可以做得更好:我们可以找到是哪一行代码导致崩溃!我们需要做一些基本的十六进制计算,因此需要一个计算器。首先计算下边的值:崩溃地址 – 首选加载地址 - 0x1000。
地 址就是相对于第一个code section的偏移量,因此需要做这样的计算。减去首选加载地址可以得到相对于文件起始位置的偏移量(逻辑上),但是为什么还要减去0x1000? 由于Line numbers中的地址是相对于code section的起始位置的偏移量。二进制代码的第一部分是Portable Executable (PE),这部分有0x1000字节长度。因此,在我们的例子中,崩溃地址(即相对于code section的偏移量)应该为0x004011a1 - 0x00400000 - 0x1000 = 0x1a1
现在我们查 看MAP文件的line information section部分,每一行都是像30 0001:00000004这个样子。第一个数字是行数,第二个数字是这一行代码相对于code section的偏移值。如果我们要找崩溃代码的行数,我们只要使用与刚才定位崩溃函数相同的方式:找到第一个比我们计算得到的崩溃地址大的偏移量,那么 发生崩溃的就是上一个偏移。在我们的例子中,0x1a1 在0x1a8 之前,那我们就可以确定崩溃发生在MAPFILE.CPP文件的119行。
保持对MAP文件的跟踪
每 个发布版本都有自己相应的MAP文件。在发布exe文件时包含MAP文件不是一个坏主意。这样,你可以确保当前exe拥有合适的MAP文件。你可以让系统 里的每个exe都有相应的MAP文件,但是我们都知道这样最后可能导致一些问题,MAP文件中不包含任何你要让用户知道的信息,对用户来说一点用处都没 有。不过,当发生程序崩溃后,如果你没有了MAP文件,你至少可以让用户提供MAP文件。
致谢
感谢John Robbins的"Debugging Applications"一书提供的帮助。