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

一般程序崩潰可以通過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 
  00001 8b ec   mov  ebp, esp 
  00003 81 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 01 00 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 01 00 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 USE32 PUBLIC ''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 USE32 PUBLIC ''CODE''
023._TEXT   ENDS
024.;   COMDAT ?Crash1@@YAXXZ
025._TEXT   SEGMENT PARA USE32 PUBLIC ''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.定位


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章