首先配置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.定位