介绍
开发优秀的程序是一个重要事。但是当一个用户向你报告你的程序有一个Crash,你知道最好的在新增其它Feature之前先修改这个问题。如果你足够的幸运的话,同时用户提供了crash地址信息。解决这个问题有很多的方法。但是,你如何使用这个crash地址来定位程序什么地方出错了。
创建MAP文件
第一步,你需要一个MAP文件。假如你没有,这几乎不可能使用这个crash地址找到应用程序crash位置。首先,我将用一个例子来描述如何创建一个有用的MAP文件。为了这个,我创建了一个新工程叫MAPFILE。你可以用同样的方法来设置你的自己的工程。我用VC6创建了一个Win32应用程序,为了解释MAP文件,选择了“Hello World!”程序。
一旦创建工程,我们先要更改release版本工程设置。在C/C++标题中选择"Line Number Only“。
为了生成调试信息。很多人忘记这个,但是你假如需要一个好的MAP文件就需要设置这个选项。这不会影响你的release版本。不一步是修改LINK标题中的选项,在这里你需要设置"Generate mapfile"选项。同时,在Project选项中输入/MAPINFO:LINES和/MAPINFO:EXPORTS。
现在,你可以编译和连接你的程序了。编译完成后,你会在你的intermediate目录找到一个MAP文件(就是EXE目录)
分析MAP文件
做完这个无趣的工作,现在到了关键地步:如何分析这个MAP文件。我们将用这个做一个crash例子。首先要做的就是:如何把你的应用程序crash掉。我在InitInstance()函数结尾地方加入两行代码。
char* pEmpty = NULL;
*pEmpty = 'x'; // 这里是第119行
我相信你还有很多方法可以使你的程序crash掉。重新编译你的程序。运行程序,crash同时会弹出一个消息对话框:'The instruction at "0x004011a1" referenced memory at "0x00000000". The memory could not be "Written".' 。
现在,你用记事本或其它工具打开这个MAP文件。你的MAP文件看起来象下面这样:
MAP文件的开头包含模块的名字,工程连接的时间,和首选装载的地址(大概是0x00400000除非你正在使用DLL)。接下来就是片段信息,这些信息显示不同OBJ和LIB文件的连接信息。
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
在片段信息后面是一些公共函数信息。注意这个”public“部分。假如你有以static声名的C函数,它们不会显示在这个MAP文件中。幸运的是,这个行数仍然反映这些static函数。公共函数信息最重要的部分是函数名和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
接着公共函数信息后面的是行信息(你能够看到这个就是因为你在link标题中使用/MAPINFO:LINES,并且在C/C++标题中选择了"Line numbers")。在这个之后,是你工程的导出函数信息如何你导出了一些函数(如果你在Link标题中包含了/MAPINFO:EXPORTS)
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
现在我们来查找crash的位置。首先,我们先找到引起crash的函数。查看"Rva+Base" 栏,并找到比crash地址大的函数。MAP文件中在这个函数前面的入口就是发生crash的函数。在我们例子中,crash地址是0x004011a1。在0x00401150和0x004011b0之间,因此我们知道发生crash函数是?InitInstance@@YAHPAUHINSTANCE__@@。H@Z函数名开头是一个C++修饰名“?”号。如果要转化这个名字,把这个名字做为命令行参数传给Platform SDK的UNDNAME.EXE (在bin目录)。很多时候你并不需要这个,一看就可以指出这个函数正确的名字(在这里是:InitInstance()函数)。
最重要部分是问题的定位。但是我们可以得到更精确的位置:我们能够找到是那一行代码发生crash问题!在这里我们需要一些基本的十六进制算术知识,在这个世界上人们还不能离开数学:现在是时候用它了。
第一步采用下面的计算公式:crash_address - preferred_load_address - 0x1000这是代码段开头的偏移地址,因此我们需要做这个计算。得到的值就是逻辑首选的装载地址,但是为什么我们需要减去这个0x1000?由于这个crash地址是从代码段开始的偏移量,但是生成的二进制文件的第一部分并不是代码段!第一部分的二进制是Portable Executable (PE)信息头,大小就是0x1000字节长。神秘的解决方案。在我们的例子中,它结果就是:0x004011a1 - 0x00400000 - 0x1000 = 0x1a1
现在是时候研究MAP文件中的行信息部分。这些行象这个: 30 0001:00000004。第一个数字是行数,第二个数字是一个偏移量,这个偏移量就是当前代码行到代码段开始的偏移量。假如我们需要我们的行数,我们必须对这个函数做同样的事情:确定我们刚才计算更大的偏移量。这个crash发生在前面的入口。在我们的例子中:0x1a1比0x1a8少。因此我们crash发生在MAPFILE.CPP中第119行。
深入分析MAP文件
每个release版本工程都有自己的MAP文件。在发布的EXE文件包含MAP文件并不是一件坏主意。这样,你就能够拥有这个EXE正确版本的MAP文件。你能够在你的机器保留每个EXE文件的MAP文件,但是我们也知道这样做可能在后来带来一些问题。这个MAP文件没有包含任何信息你不想用户去看(除非可能的类和函数名字?)。一个用户可能对它没有任何用,但是至少你能要求有这个MAP文件,假如你没有这个文件。
原文地址:Finding crash information using the MAP file
http://www.codeproject.com/debug/mapfile.asp