简介

编写整洁的应用程序是一回事。但是当用户告诉你你的软件已经崩溃时,你知道在添加其他功能之前最好先解决这个问题。如果你够幸运的话,用户会有一个崩溃地址。这将大大有助于解决这个问题。但是你怎么能用这个崩溃地址来判断出了什么问题呢?

创建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 .\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++修饰的名称。要转换名称,请将其作为命令行参数传递给平台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及以后的版本,只能借助其他手段来获取代码行了。

保留Map文件

每个版本都有自己的映射文件。在EXE发行版中包含映射文件并不是一个坏主意。这样,您就可以确定这个EXE的映射文件是正确的。你可以在你的系统中保留每个带有EXE的映射文件,但是我们都知道这可能会给以后带来一些麻烦。映射文件不包含任何您不希望用户看到的信息(除非可能是类和函数名?)。用户将无法使用它,但如果您自己没有副本,至少可以请求映射文件。

标签: none

添加新评论