65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 MAP 文件查找崩溃信息

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (135投票s)

2003年1月6日

CPOL

5分钟阅读

viewsIcon

673772

使用 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。它介于 0x004011500x004011b0 之间,因此我们知道崩溃函数是 ?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。第一个数字是行号,第二个数字是距发生该行的代码节开头的偏移量。如果我们想查找我们的行号,只需执行与查找函数相同的事情:确定第一个大于我们刚刚计算出的偏移量的出现项。崩溃发生在前面的条目中。在我们的示例中:0x1a10x1a8 之前。因此,我们的崩溃发生在 MAPFILE.CPP第 119 行

跟踪 MAP 文件

每个发布版本都有其自己的 MAP 文件。将 MAP 文件包含在 EXE 分发版中并非坏事。这样,您可以确定您拥有此 EXE 的正确 MAP 文件。您可以在您的系统上将每个 MAP 文件与每个 EXE 文件一起保存,但我们都知道这以后可能会带来一些麻烦。MAP 文件不包含您不希望用户看到的信息(除非可能是类名和函数名?)。用户将无法使用它,但至少如果您自己没有副本,您可以要求提供 MAP 文件。

致谢

  • John Robbins 的“调试应用程序”一书
© . All rights reserved.