使用map文件快速定位程序崩溃代码行

作为程序员,平时最担心见到的事情就是程序发生了崩溃,无论是指针越界还是非法操作,都将给我们的应用系统造成巨大的损失。但在一个大型系统的测试过程中,初期出现程序崩溃似乎成了不可避免的事。其实测试中出现程序崩溃并不可怕,反而是测试的成功。我们更为关心的是程序中的哪一行导致了系统崩溃,这样我们才能有针对性的进行改正。

vc中,我们可以利用出现程序崩溃时vc的自动跳转,定位到出错代码行。但在大量的压力测试时,尤其是多线程测试时,同时出现几十个错,这时vc本身的出错跳转往往会失灵。

  • 在这里我们介绍一种辅助查找程序崩溃代码行的好方法,它的核心就是利用编译时生成map文件中的信息来定位代码行。

下面就开始我们的介绍。

首先我们必须生成程序的map文件。那么什么是 map 文件呢?简单地讲, map 文件是程序的全局符号、源文件和代码行号信息的唯一的文本表示方法,是整个程序工程信息的静态文本。它可以在任何地方、任何时候使用,不需要有额外的程序进行支持,仅仅通过一个文本阅读工具如ultra edit就可以打开了。而且,这是唯一能找出程序崩溃代码行的救星。

那么我们应该如何生成 map文件呢?在 vc 中,我们可以按下 alt+f7,打开“project settings”选项页,选择c/c++ 选项卡,并在最下面的 project options 里面输入:/zd ,然后要选择 link 选项卡,选中“generate mapfile”复选框,并在最下面的 project options 里面输入:/mapinfo:lines,表示生成 map 文件时,加入行信息。最后按下 f7 来编译生成 exe 可执行文件和 map 文件,此时可以在工程的debug目录下找到刚刚生成的map文件,文件名为“工程名.map”。

通过上面的步骤,已经得到了 map 文件,那么我们该如何利用它呢?让我们从一个简单的实例入手,一步一步演示使用map文件定位程序崩溃行的过程。 首先假设我们的vc工程中有下面这个文件:

//*****************************************************

//******************************************************

#include "stdafx.h"

int crashtest(int a,int b)

{

int c;

c = a/b;

return c;

}

void main(void)

{

int a = 30;

int b = 0;

int ret;

printf("let's begin crash test...\n");

ret = crashtest(a,b);

}

很显然本程序有“除0错误”,在 debug 方式下编译,运行时会产生“非法操作”。我们记录下产生崩溃的地址——在我的机器上是 0x0040102f 。这个在不同的机器上可能地址不同,但记下这个地址我们下面将要使用。

我们打开它的 map 文件:(这里列出我们比较关心的内容,其他的就略过了)

abort(工程名)

timestamp is 3ef16533 (thu jun 19 15:24:35 2003)

preferred load address is 00400000

start    length    name        class

0001:00000000 0001081dh .text        code

0002:00000000 000013bah .rdata        data

0002:000013ba 00000000h .edata        data

0003:00000000 00000104h .crt$xca       data

0003:00000104 00000104h .crt$xcz       data

0003:00000208 00000104h .crt$xia       data

0003:0000030c 00000109h .crt$xic       data

0003:00000418 00000104h .crt$xiz       data

0003:0000051c 00000104h .crt$xpa       data

0003:00000620 00000104h .crt$xpx       data

0003:00000724 00000104h .crt$xpz       data

0003:00000828  00000104h .crt$xta       data

0003:0000092c  00000104h .crt$xtz       data

0003:00000a30  00003236h .data        data

0003:00003c68  000019c8h .bss        data

0004:00000000  00000014h .idata$2       data

0004:00000014  00000014h .idata$3       data

0004:00000028  00000120h .idata$       data

0004:00000148  00000120h .idata$5        data

0004:00000268  000004f4h .idata$6       data

address publics by value rva+base lib:object

0001:00000020 ?crashtest@@yahhh@z 00401020 f main.obj

0001:0000003c _main 0040103c f main.obj

0001:000000b0 _printf 004010b0 f libcd:printf.obj

0001:00000130 chkesp 00401130 f libcd:chkesp.obj amsg_exit 004012a0 f libcd:crt0.obj

0001:00000170 _maincrtstartup 00401170 f libcd:crt0.obj

0001:000002a0

0001:00000300 stbuf 00401300 f libcd:_sftbuf.obj ftbuf 00401460 f libcd:_sftbuf.obj

0001:00000460

0001:00000520 output 00401520 f libcd:output.obj _initstdio 004023c0 f libcd:_file.obj

0001:000013c0

0001:000014f0 _endstdio 004024f0 f libcd:_file.obj crtdbgbreak 00402510 f libcd:dbgrpt.obj

0001:00001510

0001:00001520 crtsetreportmode 00402520 f libcd:dbgrpt.obj crtsetreportfile 00402580 f libcd:dbgrpt.obj

0001:00001580

0001:00001600 crtsetreporthook 00402600 f libcd:dbgrpt.obj crtdbgreport 00402620 f libcd:dbgrpt.obj

0001:00001620

如果仔细浏览 rva+base 这栏,我们可以发现第一个比崩溃地址 0x0040102f 大的函数地址是 0x0040103c ,所以在 0x0040103c 这个地址之前的那个入口就是产生崩溃的函数,也就是这行:

0001:00000020 ?crashtest@@yahhh@z 00401020 f main.obj

因此,发生崩溃的函数就是 ?crashtest@@yahhh@z,所有以问号开头的函数名称都是 c++ 修饰的名称。所以在我们的源程序中,这个发生崩溃的函数就是 crashtest ()!

现在我们便轻而易举地知道了发生崩溃的函数名称。把它记下来,然后我们将要直接定位发生崩溃的代码行了。我们注意 map 文件的最后部分——代码行信息(line numbers information),它是以这样的形式显示的:

line numbers for .\debug\main.obj(d:\我的工作\技术\出异常例子abort\main.cpp) segment .text

12 0001:00000020 14 0001:0000002b 15 0001:00000035 16 0001:00000038

19 0001:0000003c 20 0001:00000057 21 0001:0000005e 23 0001:00000065

24 0001:00000072 25 0001:00000085

第一个数字代表在源代码中的代码行号,第二个数是该代码行在所属的代码段中的偏移量。如果要查找代码行号,需要使用下面的公式做一些十六进制的减法运算:

崩溃行偏移 = 崩溃地址(crash address)-基地址(imagebase address)- 0x1000

  • 为什么要这样做呢?因为我们得到的崩溃地址都是由偏移地址(rva)+ 基地址(base)得来的,所以在计算行号的时候要把基地址减去。一般情况下,基地址的值是 0x00400000 。另外,由于一般的 pe 文件的代码段都是从 0x1000 偏移开始的,所以也必须减去 0x1000 。

所以我们的:崩溃行偏移 = 0x0040102f - 0x00400000 - 0x1000 = 0x2f 我们在map 文件的中的代码行信息里查找不超过计算结果0x2f,但却最接近的数。发现是 main.cpp 文件中的:

14 0001:0000002b

也就意味着在源代码中的第 14 行!让我们来看看源代码,注意注释行和空行也要计算在内,程序的第14行为:

c = a/b;

果然就是第 14 行啊,它发生了“除0异常”!

方法已经介绍完了,从今以后,我们就可以精确地定位到源代码中的崩溃行,而且只要编译器可以生成 map 文件,无论在win平台还是unix平台,本方法都是适用的。

本文我们只是列举了一个非常简单的“除0异常”例子,使用map文件的效力或许还不十分明显。但相信在我们的大型应用系统调试中,使用map文件的辅助 方法来快速定位发生程序崩溃的函数以及代码行,将会为我们的程序调试工作节省大量时间和精力,提高我们的调试质量。我们甚至可以要求远地用户直接提供程序崩溃的地址,然后就可以在自己机器上利用map文件静态地找到出错的那行,并在程序中进行相应修正了。

**********************************************************************************

这种方式有时候不一定准确的定位到出错的行,可能上下偏移一两行,但是能快速的定位出错的位置。

再加以输出日志方式定位,就能很快找出问题。不错的方法哦!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值