您的位置:首页 > 其它

VS2005(vs2008,vs2010)使用map文件查找程序崩溃原因

2013-10-24 10:11 537 查看
一般程序崩溃可以通过debug,找到程序在那一行代码崩溃了,最近编一个多线程的程序,都不知道在那发生错误,多线程并发,又不好单行调试,终于找到一个比较好的方法来找原因,通过生成map文件,由于2005取消map文件生成行号信息(vc6.0下是可以生成行号信息的,不知道microsoft怎么想的,在2005上取消了),只能定位在那个函数发生崩溃。这里可以通过生成cod文件,即机器码这一文件,具体定位在那一行崩溃。

首先配置vc2005生成map文件和cod文件:

(1).map文件:property->Configuration Properties->Linker->Debugging 中的Generate Map File选择Yes(/MAP);

(2).cod文件:property->Configuration Properties->C/C++->output Files中Assembler OutPut中选择Assembly,Maching Code and Source(/FAcs),生成机器,源代码。

上面所说的 property 是“项目”菜单下的 property,而非“工具”菜单下的 property。(转者注)

简单例子:C++代码
#include "stdafx.h"

void errorFun(int * p)
{
*p=1;
}

int  _tmain(int argc, _TCHAR* argv[])
{
int * p=NULL;
errorFun(p);

return 0;
}

在errorFun中函数中,*p=1这一行出错,由于p没有申请空间,运行时出错,弹出

Unhandled exception at 0x004113b1 in testError.exe: 0xC0000005: Access violation writing location 0x00000000.

在0x004113b1程序发生崩溃。

具体步骤:

(1)debug文件下打开map文件,定位崩溃函数.

map文件开头是一些链接信息,然后我们要找函数和实始地址信息。地址是函始的开始地址

Address       Publics by Value       Rva+Base    Lib:Object

0000:00000000    ___safe_se_handler_count   00000000    <absolute>

0000:00000000    ___safe_se_handler_table   00000000    <absolute>

0000:00000000    ___ImageBase         00400000    <linker-defined>

0001:00000000    __enc$textbss$begin      00401000     <linker-defined>

0001:00010000    __enc$textbss$end       00411000     <linker-defined>

0002:00000390    ?errorFun@@YAXPAH@Z    00411390 f    testError.obj

0002:000003d0    _wmain            004113d0 f    testError.obj

0002:00000430    __RTC_InitBase        00411430 f   MSVCRTD:init.obj

0002:00000470    __RTC_Shutdown        00411470 f   MSVCRTD:init.obj

0002:00000490    __RTC_CheckEsp        00411490 f   MSVCRTD:stack.obj

0002:000004c0    @_RTC_CheckStackVars@8    004114c0 f   MSVCRTD:stack.obj

0002:00000540    @_RTC_AllocaHelper@12     00411540 f    MSVCRTD:stack.obj

....

程序崩溃地址0x004113b1,我们找到第一个比这个地址大的004113d0,前一个是00411390,地址是函数的开始地址,所以发生的崩溃的的函数是errorFun,这个函数的初始地址00411390.

(2)找出具体崩溃行号.

由(2)可知,发生错误函数是errorFun,在testError.obj,打开testError.cod文件,找到errorFun函数生成的机器码.

?errorFun@@YAXPAH@Z PROC ; errorFun, COMDAT

; 7 : {

00000 55 push ebp

000018b ec mov ebp, esp

0000381 ec c0 00 00

00 sub esp, 192 ; 000000c0H

00009 53 push ebx

0000a 56 push esi

0000b 57 push edi

0000c 8d bd 40 ff ff

ff lea edi, DWORD PTR [ebp-192]

00012 b9 30 00 00 00 mov ecx, 48 ; 00000030H

00017 b8 cc cc cc cc mov eax, -858993460 ; ccccccccH

0001c f3 ab rep stosd

; 8 : *p=1;

0001e 8b 45 08 mov eax, DWORD PTR _p$[ebp]

00021 c7 00 0100 00

00 mov DWORD PTR [eax], 1

; 9 : }

00027 5f pop edi

00028 5e pop esi

00029 5b pop ebx

0002a 8b e5 mov esp, ebp

0002c 5d pop ebp

0002d c3 ret 0

(说明: 7,8,9是表示在源代码的行号。

00000 55 push ebp,000000是相对偏移地地,55是机器码号,push ebp,000000是汇编码。)

通过(2)我们计算相对偏移地址,即崩溃地址-函数起始地址,0x004113b1-0x00411390=0x21(16进制的计数)。找到0x21这一行对应的机器码是 00021 c7 00 0100 00,向上看它是由第8行*p=1;生成的汇编码,由此可见是这一行程序发生崩溃。

结束语:当然这只是一个简单的例子,实际上一运行便知道是这一行出错,但是对于一个比较大的工程,特别是在多线程并发情况下,要找出那一行出错比较困难,便可以使用map和cod文件找到程序崩溃原因。

补充:

读了老罗的“仅通过崩溃地址找出源代码的出错行”(下称"罗文")一文后,感觉该文还是可以学到不少东西的。不过文中尚存在有些说法不妥,以及有些操作太繁琐的地方 。为此,本人在学习了此文后,在多次实验实践基础上,把该文中的一些内容进行补充与改进,希望对大家调试程序,尤其是release版本的程序有帮助 。欢迎各位朋友批评指正。
一、该方法适用的范围
在windows程序中造成程序崩溃的原因很多,而文中所述的方法仅适用与:由一条语句当即引起的程序崩溃。如原文中举的除数为零的崩溃例子。而笔者在实际工作中碰到更多的情况是:指针指向一非法地址 ,然后对指针的内容进行了,读或写的操作。例如:

1.
void
Crash1()


2.
{


3.
char
*
p =(
char
*)100;


4.
*p=100;


5.
}


这些原因造成的崩溃,无论是debug版本,还是release版本的程序,使用该方法都可找到造成崩溃的函数或子程序中的语句行,具体方法的下面还会补充说明。 另外,实践中另一种常见的造成程序崩溃的原因:函数或子程序中局部变量数组越界付值,造成函数或子程序返回地址遭覆盖,从而造成函数或子程序返回时崩溃。例如:

01.
#include


02.
void
Crash2();


03.
int
main(
int
argc,
char
*
argv[])


04.
{


05.
Crash2();


06.
return
0;


07.
}


08.


09.
void
Crash2()


10.
{


11.
char
p[1];


12.
strcpy
(p,
"0123456789"
);


13.
}


在vc中编译运行此程序的release版本,会跳出如下的出错提示框。



图一 上面例子运行结果

这里显示的崩溃地址为:0x34333231。这种由前面语句造成的崩溃根源,在后续程序中方才显现出来的情况,显然用该文所述的方法就无能为力了。不过在此例中多少还有些蛛丝马迹可寻找到崩溃的原因:函数Crash2中的局部数组p只有一个字节大小 ,显然拷贝"0123456789"这个字符串会把超出长度的字符串拷贝到数组p的后面,即*(p+1)=''1'',*(p+2)=''2'',*(p+3)=''3'',*(p+4)=4。。。。。。而字符''1''的ASC码的值为0x31,''2''为0x32,''3''为0x33,''4''为0x34。。。。。,由于intel的cpu中int型数据是低字节保存在低地址中
,所以保存字符串''1234''的内存,显示为一个4字节的int型数时就是0x34333231。显然拷贝"0123456789"这个字符串时,"1234"这几个字符把函数Crash2的返回地址给覆盖 ,从而造成程序崩溃。对于类似的这种造成程序崩溃的错误朋友们还有其他方法排错的话,欢迎一起交流讨论。

二、设置编译产生map文件的方法

该文中产生map文件的方法是手工添加编译参数来产生map文件。其实在vc6的IDE中有产生map文件的配置选项的。操作如下:先点击菜单"Project"->"Settings。。。",弹出的属性页中选中"Link"页 ,确保在"category"中选中"General",最后选中"Generate mapfile"的可选项。若要在在map文件中显示Line numbers的信息的话 ,还需在project options 中加入/mapinfo:lines 。Line numbers信息对于"罗文"所用的方法来定位出错源代码行很重要
,但笔者后面会介绍更加好的方法来定位出错代码行,那种方法不需要Line numbers信息。



图二 设置产生MAP文件

三、定位崩溃语句位置的方法

"罗文"所述的定位方法中,找到产生崩溃的函数位置的方法是正确的,即在map文件列出的每个函数的起始地址中,最近的且不大于崩溃地址的地址即为包含崩溃语句的函数的地址 。但之后的再进一步的定位出错语句行的方法不是最妥当,因为那种方法前提是,假设基地址的值是 0x00400000 ,以及一般的 PE 文件的代码段都是从 0x1000偏移开始的 。虽然这种情况很普遍,但在vc中还是可以基地址设置为其他数,比如设置为0x00500000,这时仍旧套用

1.
崩溃行偏移
= 崩溃地址 - 0x00400000 - 0x1000


的公式显然无法找到崩溃行偏移。 其实上述公式若改为

1.
崩溃行偏移
= 崩溃地址 - 崩溃函数绝对地址 + 函数相对偏移


即可通用了。仍以"罗文"中的例子为例:"罗文"中提到的在其崩溃程序的对应map文件中,崩溃函数的编译结果为

1.
0001:00000020
?Crash@@YAXXZ 00401020 f CrashDemo。obj


对与上述结果,在使用我的公式时 ,"崩溃函数绝对地址"指00401020, 函数相对偏移指 00000020, 当崩溃地址= 0x0040104a时, 则 崩溃行偏移 = 崩溃地址 - 崩溃函数起始地址+ 函数相对偏移 = 0x0040104a - 0x00401020 + 0x00000020= 0x4a,结果与"罗文"计算结果相同 。但这个公式更通用。

四、更好的定位崩溃语句位置的方法。

其实除了依靠map文件中的Line numbers信息最终定位出错语句行外,在vc6中我们还可以通过编译程序产生的对应的汇编语句,二进制码,以及对应c/c++语句为一体的"cod"文件来定位出错语句行 。先介绍一下产生这种包含了三种信息的"cod"文件的设置方法:先点击菜单"Project"->"Settings。。。",弹出的属性页中选中"C/C++"页 ,然后在"Category"中选则"Listing Files",再在"Listing file type"的组合框中选择"Assembly,Machine
code, and source"。接下去再通过一个具体的例子来说明这种方法的具体操作。



图三 设置产生"cod"文件

准备步骤1)产生崩溃的程序如下:

01.
01
//****************************************************************


02.
02
//文件名称:crash。cpp


03.
03
//作用:
演示通过崩溃地址找出源代码的出错行新方法


04.
04
//作者:
伟功通信 roc


05.
05
//日期:
2005-5-16


06.
06
//****************************************************************


07.
07
void
Crash1();


08.
08
int
main(
int
argc,
char
*
argv[])


09.
09
{


10.
10
Crash1();


11.
11
return
0;


12.
12
}


13.
13


14.
14
void
Crash1()


15.
15
{


16.
16
char
*
p =(
char
*)100;


17.
17
*p=100;


18.
18
}


准备步骤2)按本文所述设置产生map文件(不需要产生Line numbers信息)。

准备步骤3)按本文所述设置产生cod文件。

准备步骤4)编译。这里以debug版本为例(若是release版本需要将编译选项改为不进行任何优化的选项,否则上述代码会因为优化时看作废代码而不被编译,从而看不到崩溃的结果),编译后产生一个"exe"文件 ,一个"map"文件,一个"cod"文件。

运行此程序,产生如下如下崩溃提示:



图四 上面例子运行结果

排错步骤1)定位崩溃函数。可以查询map文件获得。我的机器编译产生的map文件的部分如下:

01.
Crash


02.


03.
Timestamp
is 42881a01(Mon May 16 11:56:49 2005)


04.


05.
Preferred
load address is 00400000


06.


07.
Start
Length Name Class


08.
0001:00000000
0000ddf1H .text CODE


09.
0001:0000ddf1
0001000fH .textbss CODE


10.
0002:00000000
00001346H .rdata DATA


11.
0002:00001346
00000000H .edata DATA


12.
0003:00000000
00000104H .CRT$XCA DATA


13.
0003:00000104
00000104H .CRT$XCZ DATA


14.
0003:00000208
00000104H .CRT$XIA DATA


15.
0003:0000030c
00000109H .CRT$XIC DATA


16.
0003:00000418
00000104H .CRT$XIZ DATA


17.
0003:0000051c
00000104H .CRT$XPA DATA


18.
0003:00000620
00000104H .CRT$XPX DATA


19.
0003:00000724
00000104H .CRT$XPZ DATA


20.
0003:00000828
00000104H .CRT$XTA DATA


21.
0003:0000092c
00000104H .CRT$XTZ DATA


22.
0003:00000a30
00000b93H .data DATA


23.
0003:000015c4
00001974H .bss DATA


24.
0004:00000000
00000014H .idata$2 DATA


25.
0004:00000014
00000014H .idata$3 DATA


26.
0004:00000028
00000110H .idata$4 DATA


27.
0004:00000138
00000110H .idata$5 DATA


28.
0004:00000248
000004afH .idata$6 DATA


29.


30.
Address
Publics by Value Rva+Base Lib:Object


31.


32.
0001:00000020
_main 00401020 f Crash.obj


33.
0001:00000060
?Crash1@@YAXXZ 00401060 f Crash.obj


34.
0001:000000a0
__chkesp 004010a0 f LIBCD:chkesp.obj


35.
0001:000000e0
_mainCRTStartup 004010e0 f LIBCD:crt0.obj


36.
0001:00000210
__amsg_exit 00401210 f LIBCD:crt0.obj


37.
0001:00000270
__CrtDbgBreak 00401270 f LIBCD:dbgrpt.obj


38.
...


对于崩溃地址0x00401082而言,小于此地址中最接近的地址(Rva+Base中的地址)为00401060,其对应的函数名为?Crash1@@YAXXZ,由于所有以问号开头的函数名称都是 C++ 修饰的名称 ,"@@YAXXZ"则为区别重载函数而加的后缀,所以?Crash1@@YAXXZ就是我们的源程序中,Crash1() 这个函数。

排错步骤2)定位出错行。打开编译生成的"cod"文件,我机器上生成的文件内容如下:

001.
TITLE
E:\Crash\Crash。cpp


002.
.386P


003.
include
listing.inc


004.
if
@Version
gt 510


005.
.model
FLAT


006.
else


007.
_TEXT
SEGMENT PARA USE32PUBLIC
''
CODE
''


008.
_TEXT
ENDS


009.
_DATA
SEGMENT
DWORD
USE32
PUBLIC
''
DATA
''


010.
_DATA
ENDS


011.
CONST
SEGMENT
DWORD
USE32
PUBLIC
''
CONST
''


012.
CONST
ENDS


013.
_BSS
SEGMENT
DWORD
USE32
PUBLIC
''
BSS
''


014.
_BSS
ENDS


015.
$$SYMBOLS
SEGMENT
BYTE
USE32
''
DEBSYM
''


016.
$$SYMBOLS
ENDS


017.
$$TYPES
SEGMENT
BYTE
USE32
''
DEBTYP
''


018.
$$TYPES
ENDS


019.
_TLS
SEGMENT
DWORD
USE32
PUBLIC
''
TLS
''


020.
_TLS
ENDS


021.
;
COMDAT _main


022.
_TEXT
SEGMENT PARA USE32PUBLIC
''
CODE
''


023.
_TEXT
ENDS


024.
;
COMDAT ?Crash1@@YAXXZ


025.
_TEXT
SEGMENT PARA USE32PUBLIC
''
CODE
''


026.
_TEXT
ENDS


027.
FLAT
GROUP _DATA, CONST, _BSS


028.
ASSUME
CS: FLAT, DS: FLAT, SS: FLAT


029.
endif


030.
PUBLIC
?Crash1@@YAXXZ                  ; Crash1


031.
PUBLIC
_main


032.
EXTRN
__chkesp:NEAR


033.
;
COMDAT _main


034.
_TEXT
SEGMENT


035.
_main
PROC NEAR                   ; COMDAT


036.


037.
;
9    : {


038.


039.
00000
55       push    ebp


040.
00001
8b ec        mov     ebp, esp


041.
00003
83 ec 40     sub     esp, 64            ; 00000040H


042.
00006
53       push    ebx


043.
00007
56       push    esi


044.
00008
57       push    edi


045.
00009
8d 7d c0     lea     edi,
DWORD
PTR
[ebp-64]


046.
0000c
b9 10 00 00 00 mov ecx, 16 ; 00000010H


047.
00011
b8 cc cc cc cc   mov     eax, -858993460        ; ccccccccH


048.
00016
f3 ab        rep stosd


049.


050.
;
10   :    Crash1();


051.


052.
00018
e8 00 00 00 00   call    ?Crash1@@YAXXZ     ; Crash1


053.


054.
;
11 :
return
0;


055.


056.
0001d
33 c0        xor     eax, eax


057.


058.
;
12   : }


059.


060.
0001f
5f       pop     edi


061.
00020
5e       pop     esi


062.
00021
5b       pop     ebx


063.
00022
83 c4 40     add     esp, 64            ; 00000040H


064.
00025
3b ec        cmp     ebp, esp


065.
00027
e8 00 00 00 00   call    __chkesp


066.
0002c
8b e5        mov     esp, ebp


067.
0002e
5d       pop     ebp


068.
0002f
c3       ret     0


069.
_main
ENDP


070.
_TEXT
ENDS


071.
;
COMDAT ?Crash1@@YAXXZ


072.
_TEXT
SEGMENT


073.
_p$
= -4


074.
?Crash1@@YAXXZ
PROC NEAR                ; Crash1, COMDAT


075.


076.
;
15   : {


077.


078.
00000
55       push    ebp


079.
00001
8b ec        mov     ebp, esp


080.
00003
83 ec 44     sub     esp, 68            ; 00000044H


081.
00006
53       push    ebx


082.
00007
56       push    esi


083.
00008
57       push    edi


084.
00009
8d 7d bc     lea     edi,
DWORD
PTR
[ebp-68]


085.
0000c
b9 11 00 00 00   mov     ecx, 17            ; 00000011H


086.
00011
b8 cc cc cc cc   mov     eax, -858993460        ; ccccccccH


087.
00016
f3 ab        rep stosd


088.


089.
;
16 :
char
*
p =(
char
*)100;


090.


091.
00018
c7 45 fc 64 00


092.
00
00        mov
DWORD
PTR
_p$[ebp], 100    ; 00000064H


093.


094.
;
17   :  *p=100;


095.


096.
0001f
8b 45 fc     mov     eax,
DWORD
PTR
_p$[ebp]


097.
00022
c6 00 64     mov
BYTE
PTR
[eax], 100    ; 00000064H


098.


099.
;
18   : }


100.


101.
00025
5f       pop     edi


102.
00026
5e       pop     esi


103.
00027
5b       pop     ebx


104.
00028
8b e5        mov     esp, ebp


105.
0002a
5d       pop     ebp


106.
0002b
c3       ret     0


107.
?Crash1@@YAXXZ
ENDP                 ; Crash1


108.
_TEXT
ENDS


109.
END


其中

1.
?Crash1@@YAXXZ
PROC NEAR                ; Crash1, COMDAT


为Crash1汇编代码的起始行。产生崩溃的代码便在其后的某个位置。接下去的一行为:

1.
;
15   : {


冒号后的"{"表示源文件中的语句,冒号前的"15"表示该语句在源文件中的行数。这之后显示该语句汇编后的偏移地址,二进制码,汇编代码。如

1.
00000
55       push    ebp


其中"0000"表示相对于函数开始地址后的偏移,"55"为编译后的机器代码," push ebp"为汇编代码。从"cod"文件中我们可以看出,一条(c/c++)语句通常需要编译成数条汇编语句 。此外有些汇编语句太长则会分两行显示如:

1.
00018
c7 45 fc 64 00


2.
00
00        mov
DWORD
PTR
_p$[ebp], 100    ; 00000064H


其中"0018"表示相对偏移,在debug版本中,这个数据为相对于函数起始地址的偏移(此时每个函数第一条语句相对偏移为0000);release版本中为相对于代码段第一条语句的偏移(即代码段第一条语句相对偏移为0000,而以后的每个函数第一条语句相对偏移就不为0000了)。"c7 45 fc 64 00 00 00 "为编译后的机器代码 ,"mov DWORD PTR _p$[ebp], 100"为汇编代码, 汇编语言中";"后的内容为注释,所以";00000064H",是个注释这里用来说明100转换成16进制时为"00000064H"。

接下去,我们开始来定位产生崩溃的语句。

第一步,计算崩溃地址相对于崩溃函数的偏移,在本例中已经知道了崩溃语句的地址(0x00401082),和对应函数的起始地址(0x00401060),所以崩溃地址相对函数起始地址的偏移就很容易计算了:

1.
崩溃偏移地址
= 崩溃语句地址 - 崩溃函数的起始地址 = 0x00401082 - 0x00401060 = 0x22。


第二步,计算出错的汇编语句在cod文件中的相对偏移。我们可以看到函数Crash1()在cod文件中的相对偏移地址为0000,则

1.
崩溃语句在cod文件中的相对偏移
=  崩溃函数在cod文件中相对偏移 + 崩溃偏移地址 = 0x0000 + 0x22 = 0x22


第三步,我们看Crash1函数偏移0x22除的代码是什么?结果如下

1.
00022
c6 00 64     mov
BYTE
PTR
[eax], 100    ; 00000064H


这句汇编语句表示将100这个数保存到寄存器eax所指的内存单元中去,保存空间大小为1个字节(byte)。程序正是执行这条命令时产生了崩溃,显然这里eax中的为一个非法地址 ,所以程序崩溃了!

第四步,再查看该汇编语句在其前面几行的其对应的源代码,结果如下:

1.
;
17   :  *p=100;


其中17表示该语句位于源文件中第17行,而“*p=100;”这正是源文件中产生崩溃的语句。

至此我们仅从崩溃地址就查找出了造成崩溃的源代码语句和该语句所在源文件中的确切位置,甚至查找到了造成崩溃的编译后的确切汇编代码!

怎么样,是不是感觉更爽啊?

五、小节

1、新方法同样要注意可以适用的范围,即程序由一条语句当即引起的崩溃。另外我不知道除了VC6外,是否还有其他的编译器能够产生类似的"cod"文件。

2、我们可以通过比较 新方法产生的debug和releae版本的"cod"文件,查找那些仅release版本(或debug版本)有另一个版本没有的bug(或其他性状)。例如"罗文"中所举的那个用例 ,只要打开release版本的"cod"文件,就明白了为啥debug版本会产生崩溃而release版本却没有:原来release版本中产生崩溃的语句其实根本都没有编译 。同样本例中的release版本要看到崩溃的效果,需要将编译选项改为为不优化的配置。

一个示例:

1.设置





2.重新编译

3.定位



















内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: