简介
编写整洁的应用程序是一回事。但是当用户告诉你你的软件已经崩溃时,你知道在添加其他功能之前最好先解决这个问题。如果你够幸运的话,用户会有一个崩溃地址。这将大大有助于解决这个问题。但是你怎么能用这个崩溃地址来判断出了什么问题呢?
创建Map文件
首先,你需要一个Map文件。如果没有,使用崩溃地址几乎不可能找到应用程序崩溃的位置。首先,我将向您展示如何创建一个好的Map文件。为此,我将创建一个新项目(MAPFILE)。你也可以这样做,或者调整你自己的项目。我在VC++ 6.0中使用Win32应用程序选项创建一个新项目,选择“典型的”Hello World!应用程序'。
而vs2013里如下,从vs2005开始不在支持/MAPINFO:LINES
现在,您可以编译并链接您的项目了。链接后,您将在中间目录中找到一个.map文件(连同您的EXE文件)。
读取Map文件
我们将使用一个崩溃示例来实现这一点。所以首先:如何崩溃你的应用程序。我在InitInstance()函数的末尾添加了这两行:
char* pEmpty = NULL;
*pEmpty = 'x'; // This is line 119
我相信你能找到其他会使你的申请失败的指令。现在重新编译并链接。如果启动应用程序,它将崩溃,您将收到如下消息:“0x004011a1处的指令”引用了“0x00000000”处的内存。无法“写入”内存。
现在,是时候用记事本或类似的东西打开Map文件了。您的Map文件将如下所示:Map文件的顶部包含模块名、指示项目链接的时间戳和首选加载地址(除非使用DLL,否则可能是0x00400000)。头之后是显示链接器从各种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 function信息。注意“public”部分。如果您有静态声明的C函数,它们不会显示在映射文件中。幸运的是,行号仍将反映静态函数。public function信息的重要组成部分是函数名和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 function部分后面是行信息(如果您在链接选项卡中使用了/MAPINFO:LINES,在C/C++选项卡中选择了“行号”,Vs2005开始没有了)。之后,如果项目包含导出的函数,并且在“链接”选项卡中包含/MAPINFO:EXPORTS,则将获得导出信息。
Line numbers for .ReleaseMAPFILE.obj(F:MAPFILEMAPFILE.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++修饰的名称。要转换名称,请将其作为命令行参数传递给平台SDK程序UNDNAME.EXE(在bin dir中)。大多数时候,您不需要这样做,因为您可以通过查看它来了解它(这里是MAPFILE.obj中的InitInstance())。
这是追踪错误的一大步。但情况变得更好了:我们可以查出车祸发生在哪条线上!我们需要做一些基本的十六进制数学,所以没有计算器就做不到的人:现在是使用它的时候了。第一步是以下计算:crash_address-preferred_load_address-0x1000。
地址是从第一个代码段开始的偏移量,因此我们需要进行此计算。减去首选加载地址是合乎逻辑的,但为什么我们需要再减去一个0x1000?崩溃地址是代码段开头的偏移量,但二进制文件的第一部分不是代码段!二进制文件的第一部分是可移植可执行文件(PE),长度为0x1000字节。谜团解开了。在我们的示例中,这是:0x004011a1-0x00400000-0x1000=0x1a1
现在是时候查看Map文件的行信息部分了。这些行如下所示:30 0001:00000004。第一个数字是行号,第二个数字是该行所在代码段开头的偏移量。如果我们想查找行号,我们只需做与函数相同的事情:确定第一次出现的偏移量大于我们刚刚计算的偏移量。崩溃发生在前面的条目中。在我们的示例中:0x1a1在0x1a8之前。所以我们的崩溃发生在MAPFILE.CPP的119行。
如果我们是VS2005及以后的版本,只能借助其他手段来获取代码行了。