使用 MAP 文件查找崩溃信息






4.94/5 (135投票s)
使用 MAP 文件查找崩溃信息:如何创建和读取文件。
引言
编写出色的应用程序是一回事。但当用户告知您的软件崩溃时,您就知道最好在添加其他功能之前修复它。如果幸运的话,用户会提供一个崩溃地址。这对于解决问题大有帮助。但是,您如何使用此崩溃地址来确定出了什么问题呢?
创建 MAP 文件
首先,您需要一个 MAP 文件。如果您没有,使用崩溃地址几乎不可能找到您的应用程序崩溃的位置。因此,我将首先向您展示如何创建良好的 MAP 文件。为此,我将创建一个新项目 (MAPFILE
)。您可以这样做,或者调整您自己的项目。我使用 VC++ 6.0 中的 Win32 应用程序选项创建了一个新项目,选择了“典型‘Hello World!
’应用程序”,以使 MAP 文件的大小保持合理以供解释。
创建后,我们需要调整发布版本的项目设置。在 C/C++ 选项卡中,为调试信息选择“仅行号”。

许多人会忘记这一点,但如果您想获得一个好的 MAP 文件,则需要此选项。这不会以任何方式影响您的发布版本。接下来是链接选项卡。在这里,您需要选择“生成映射文件”选项。此外,在项目选项编辑框中键入开关 /MAPINFO:LINES
和 /MAPINFO:EXPORTS
。

现在,您可以编译和链接项目了。链接后,您将在中间目录中找到一个 .map 文件(与您的 EXE 文件一起)。
读取 MAP 文件
经过所有这些枯燥的工作后,现在到了有趣的部分:如何读取 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
函数信息。请注意“public
”部分。如果您声明了 static
的 C 函数,它们将不会出现在 MAP 文件中。幸运的是,行号仍然会反映 static
函数。public
函数信息的重要部分是函数名称以及 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
函数部分之后是行信息(如果您在链接选项卡中使用了 /MAPINFO:LINES
并在 C/C++ 选项卡中选择了“行号”,则会获得此信息)。之后,如果您的项目包含导出的函数并且您在链接选项卡中包含了 /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
现在我们将查找崩溃发生的位置。首先,我们将确定哪个函数包含崩溃地址。查看“Rva+Base
”列,并查找第一个地址大于崩溃地址的函数。MAP 文件中的前一项是发生崩溃的函数。在我们的示例中,崩溃地址为 0x004011a1
。它介于 0x00401150
和 0x004011b0
之间,因此我们知道崩溃函数是 ?InitInstance@@YAHPAUHINSTANCE__@@H@Z
。以问号开头的任何函数名都是 C++ 装饰名称。要翻译该名称,请将其作为命令行参数传递给 Platform SDK 程序 UNDNAME.EXE(位于 bin 目录中)。您大多数时候都不需要这样做,因为您可能仅通过查看名称就能猜出来(此处: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 行
。
跟踪 MAP 文件
每个发布版本都有其自己的 MAP 文件。将 MAP 文件包含在 EXE 分发版中并非坏事。这样,您可以确定您拥有此 EXE 的正确 MAP 文件。您可以在您的系统上将每个 MAP 文件与每个 EXE 文件一起保存,但我们都知道这以后可能会带来一些麻烦。MAP 文件不包含您不希望用户看到的信息(除非可能是类名和函数名?)。用户将无法使用它,但至少如果您自己没有副本,您可以要求提供 MAP 文件。
致谢
- John Robbins 的“调试应用程序”一书