(转载)

如何使用MAP文件找到程序崩溃的原因


作者 Wouter Dhondt 翻译 冯亦成(fengyc@pset.suntec.net)

[译 者] 在我们调试程序的时候,习惯于不停的Step in, Step in...可是如果我们发现Debug版的exe可以完全正常运行,而Release版却经常莫名其妙Crash。那该怎么办??没有关系,这篇文章就是 帮你解决这个问题的:) 当然,你如果希望全面提高你的Debug能力,不妨去读一下John Robbins的"Debugging Applications"一书,不过你要读英文版,中文版翻译的太烂了!

导言

    编写整洁的程序是一回事。而当用户通知你的程序崩溃了,你知道在增加程序新属性之前最好先修正这些错误,如果你足够幸运的话,用户会给你提供一个崩溃地址,要解决这个问题还是要有很长的路要走。有了崩溃地址,你怎么确定到底是在什么出了错呢?

创建MAP文件

    首 先,你需要MAP文件。如果你没有MAP文件,那你几乎是不可能通过崩溃地址找到你程序出错的具体代码行。那么先让我教你怎么创建合适的MAP文件。为此 我们创建了一个新工程(MAPFILE):我在VC++6.0中创建了应用Win32 Application选项的新工程,并且选择'typical "Hello Word!" application',这样可以使得生成的MAP文件能够满足我下边解说的需要。

    当生成新工程后,我们调整release版的工程设置信息。在C/C++属性页,设置Debug Info的值为"Line Numbers Only"。
http://www.codeproject.com/debug/mapfile/mapc.jpg

    很 多人都忘了这一步,但是如果你想得到合适的MAP文件,你就需要设置这个选项,这不会对你的release程序造成任何影响。下一步是Link属性页,你 需要选择"Generate mapfile"选项。在Project Options编辑框中输入/MAPINFO:LINES和/MAPINFO:EXPORTS开关。
http://www.codeproject.com/debug/mapfile/maplink.jpg

    现在你可以编译和链接你的工程了,链接之后,你可以在你的中间目录中找到.map文件(和exe文件在一起)。

阅读MAP文件

    在上面这些无趣的工作之后,接下去就是很有趣的部分:怎么读MAP文件。我们通过一个崩溃实例来介绍怎么读MAP文件。那么我们先得让程序崩溃,于是我在InitInstance()函数的最后增加了下边的两行代码:

    char* pEmpty = NULL;
    *pEmpty = 'x';    // 第119行

    我 相信你能够找到其它代码使得你的程序崩溃。现在重新编译且链接工程。如果你运行你的程序,程序将崩溃,并且得到怎样的消息:'The instruction at "0x004011a1" referenced memory at "0x00000000"。0x00000000内存不能写。

    现在,可以用Notepad或者类似的编辑工具打开MAP文件。MAP文件如下所示:

    在MAP文件的头部包含了模块名称,表示工程链接时刻的时间戳,以及首选加载地址(一般是0x00400000,除非是dll)。文件头之后就是一些section信息,是由链接程序把各种OBJ和LIB文件的section信息组织起来的。

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

    在 section信息之后,你看到的时公共函数信息。注意下边的"public"部分,如果你有静态C函数,那它不会在"public"部分出现。幸运的 是,在line numbers部分仍会反映静态函数的信息。"public"函数信息的最重要的部分是函数名称和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 函数部分之后是line信息部分(你设置了Link属性页中使用了/MAPINFO:LINES并且在C/C++属性页中选择"Line numbers")。在这之后就是export信息了,只要你的程序有输出函数并且在link属性页包含了/MAPINFO:EXPORTS,你就可得到 export信息。

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++ decorated name。你可以把C++ decorated name作为命令行参数传递给Platform SDK的UNDNAME.EXE程序,就可以得到原始函数名称了。在大多数情况下你不需要这样做,通过观察C++ decorated name我们就可以知道原始函数名称(这里,函数名称就是InitInstance())

    以上是bug跟踪的重要一步。但是我们可以做得更好:我们可以找到是哪一行代码导致崩溃!我们需要做一些基本的十六进制计算,因此需要一个计算器。首先计算下边的值:崩溃地址 – 首选加载地址 - 0x1000。

    地 址就是相对于第一个code section的偏移量,因此需要做这样的计算。减去首选加载地址可以得到相对于文件起始位置的偏移量(逻辑上),但是为什么还要减去0x1000? 由于Line numbers中的地址是相对于code section的起始位置的偏移量。二进制代码的第一部分是Portable Executable (PE),这部分有0x1000字节长度。因此,在我们的例子中,崩溃地址(即相对于code section的偏移量)应该为0x004011a1 - 0x00400000 - 0x1000 = 0x1a1

    现在我们查 看MAP文件的line information section部分,每一行都是像30 0001:00000004这个样子。第一个数字是行数,第二个数字是这一行代码相对于code section的偏移值。如果我们要找崩溃代码的行数,我们只要使用与刚才定位崩溃函数相同的方式:找到第一个比我们计算得到的崩溃地址大的偏移量,那么 发生崩溃的就是上一个偏移。在我们的例子中,0x1a1 在0x1a8 之前,那我们就可以确定崩溃发生在MAPFILE.CPP文件的119行。


保持对MAP文件的跟踪

    每 个发布版本都有自己相应的MAP文件。在发布exe文件时包含MAP文件不是一个坏主意。这样,你可以确保当前exe拥有合适的MAP文件。你可以让系统 里的每个exe都有相应的MAP文件,但是我们都知道这样最后可能导致一些问题,MAP文件中不包含任何你要让用户知道的信息,对用户来说一点用处都没 有。不过,当发生程序崩溃后,如果你没有了MAP文件,你至少可以让用户提供MAP文件。

致谢

    感谢John Robbins的"Debugging Applications"一书提供的帮助。
 
posted on 2009-04-10 06:43  大熊猫  阅读(2277)  评论(0编辑  收藏  举报