對“僅通過崩潰地址找出原始碼的出錯行”一文的補充與改進
作者:上海偉功通訊 roc
下載原始碼
讀了老羅的“僅通過崩潰地址找出原始碼的出錯行”(下稱"羅文")一文後,感覺該文還是可以學到不少東西的。不過文中尚存在有些說法不妥,以及有些操作太繁瑣的地方 。為此,本人在學習了此文後,在多次實驗實踐基礎上,把該文中的一些內容進行補充與改進,希望對大家偵錯工具,尤其是release版本的程式有協助 。歡迎各位朋友批評指正。
一、該方法適用的範圍
在windows程式中造成程式崩潰的原因很多,而文中所述的方法僅適用與:由一條語句當即引起的程式崩潰。如原文中舉的除數為零的崩潰例子。而筆者在實際工作中碰到更多的情況是:指標指向一非法地址 ,然後對指標的內容進行了,讀或寫的操作。例如:
void Crash1(){ char * p =(char*)100; *p=100;}
這些原因造成的崩潰,無論是debug版本,還是release版本的程式,使用該方法都可找到造成崩潰的函數或子程式中的語句行,具體方法的下面還會補充說明。 另外,實踐中另一種常見的造成程式崩潰的原因:函數或子程式中局部變數數組越界付值,造成函數或子程式返回地址遭覆蓋,從而造成函數或子程式返回時崩潰。例如:
#include void Crash2();int main(int argc,char* argv[]){Crash2();return 0;}void Crash2(){char p[1];strcpy(p,"0123456789");}
在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 ,以及一般的 可攜式執行檔的程式碼片段都是從 0x1000位移開始的 。雖然這種情況很普遍,但在vc中還是可以基地址設定為其他數,比如設定為0x00500000,這時仍舊套用
崩潰行位移 = 崩潰地址 - 0x00400000 - 0x1000
的公式顯然無法找到崩潰行位移。 其實上述公式若改為
崩潰行位移 = 崩潰地址 - 崩潰函數絕對位址 + 函數相對位移
即可通用了。仍以"羅文"中的例子為例:"羅文"中提到的在其崩潰程式的對應map檔案中,崩潰函數的編譯結果為
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 //****************************************************************02 //檔案名稱:crash。cpp03 //作用: 示範通過崩潰地址找出原始碼的出錯行新方法04 //作者: 偉功通訊 roc05 //日期: 2005-5-1606//****************************************************************07 void Crash1();08 int main(int argc,char* argv[])09 {10Crash1();11return 0;12 }1314 void Crash1()15 {16 char * p =(char*)100;17 *p=100;18 }
準備步驟2)按本文所述設定產生map檔案(不需要產生Line numbers資訊)。
準備步驟3)按本文所述設定產生cod檔案。
準備步驟4)編譯。這裡以debug版本為例(若是release版本需要將編譯選項改為不進行任何最佳化的選項,否則上述代碼會因為最佳化時看作廢代碼而不被編譯,從而看不到崩潰的結果),編譯後產生一個"exe"檔案 ,一個"map"檔案,一個"cod"檔案。
運行此程式,產生如下如下崩潰提示:
圖四 上面例子運行結果
排錯步驟1)定位崩潰函數。可以查詢map檔案獲得。我的機器編譯產生的map檔案的部分如下:
Crash Timestamp is 42881a01 (Mon May 16 11:56:49 2005) Preferred load address is 00400000 Start Length Name Class0001:00000000 0000ddf1H .text CODE0001:0000ddf1 0001000fH .textbss CODE0002:00000000 00001346H .rdata DATA0002:00001346 00000000H .edata DATA0003:00000000 00000104H .CRT$XCA DATA0003:00000104 00000104H .CRT$XCZ DATA0003:00000208 00000104H .CRT$XIA DATA0003:0000030c 00000109H .CRT$XIC DATA0003:00000418 00000104H .CRT$XIZ DATA0003:0000051c 00000104H .CRT$XPA DATA0003:00000620 00000104H .CRT$XPX DATA0003:00000724 00000104H .CRT$XPZ DATA0003:00000828 00000104H .CRT$XTA DATA0003:0000092c 00000104H .CRT$XTZ DATA0003:00000a30 00000b93H .data DATA0003:000015c4 00001974H .bss DATA0004:00000000 00000014H .idata$2 DATA0004:00000014 00000014H .idata$3 DATA0004:00000028 00000110H .idata$4 DATA0004:00000138 00000110H .idata$5 DATA0004:00000248 000004afH .idata$6 DATAAddress Publics by Value Rva+Base Lib:Object0001:00000020 _main 00401020 f Crash.obj0001:00000060 ?Crash1@@YAXXZ 00401060 f Crash.obj0001:000000a0 __chkesp 004010a0 f LIBCD:chkesp.obj0001:000000e0 _mainCRTStartup 004010e0 f LIBCD:crt0.obj0001:00000210 __amsg_exit 00401210 f LIBCD:crt0.obj0001:00000270 __CrtDbgBreak 00401270 f LIBCD:dbgrpt.obj...
對於崩潰地址0x00401082而言,小於此地址中最接近的地址(Rva+Base中的地址)為00401060,其對應的函數名為?Crash1@@YAXXZ,由於所有以問號開頭的函數名稱都是 C++ 修飾的名稱 ,"@@YAXXZ"則為區別重載函數而加的尾碼,所以?Crash1@@YAXXZ就是我們的來源程式中,Crash1() 這個函數。
排錯步驟2)定位出錯行。開啟編譯產生的"cod"檔案,我機器上產生的檔案內容如下:
TITLEE:/Crash/Crash。cpp.386Pinclude listing.incif @Version gt 510.model FLATelse_TEXTSEGMENT PARA USE32 PUBLIC ''CODE''_TEXTENDS_DATASEGMENT DWORD USE32 PUBLIC ''DATA''_DATAENDSCONSTSEGMENT DWORD USE32 PUBLIC ''CONST''CONSTENDS_BSSSEGMENT DWORD USE32 PUBLIC ''BSS''_BSSENDS$SYMBOLSSEGMENT BYTE USE32 ''DEBSYM''$SYMBOLSENDS$TYPESSEGMENT BYTE USE32 ''DEBTYP''$TYPESENDS_TLSSEGMENT DWORD USE32 PUBLIC ''TLS''_TLSENDS;COMDAT _main_TEXTSEGMENT PARA USE32 PUBLIC ''CODE''_TEXTENDS;COMDAT ?Crash1@@YAXXZ_TEXTSEGMENT PARA USE32 PUBLIC ''CODE''_TEXTENDSFLATGROUP _DATA, CONST, _BSSASSUMECS: FLAT, DS: FLAT, SS: FLATendifPUBLIC?Crash1@@YAXXZ; Crash1PUBLIC_mainEXTRN__chkesp:NEAR;COMDAT _main_TEXTSEGMENT_mainPROC NEAR; COMDAT; 9 : { 0000055 push ebp 000018b ec mov ebp, esp 0000383 ec 40 sub esp, 64; 00000040H 0000653 push ebx 0000756 push esi 0000857 push edi 000098d 7d c0 lea edi, DWORD PTR [ebp-64] 0000cb9 10 00 00 00 mov ecx, 16; 00000010H 00011b8 cc cc cc cc mov eax, -858993460; ccccccccH 00016f3 ab rep stosd; 10 : Crash1(); 00018e8 00 00 00 00 call ?Crash1@@YAXXZ; Crash1; 11 : return 0; 0001d33 c0 xor eax, eax; 12 : } 0001f5f pop edi 000205e pop esi 000215b pop ebx 0002283 c4 40 add esp, 64; 00000040H 000253b ec cmp ebp, esp 00027e8 00 00 00 00 call __chkesp 0002c8b e5 mov esp, ebp 0002e5d pop ebp 0002fc3 ret 0_mainENDP_TEXTENDS;COMDAT ?Crash1@@YAXXZ_TEXTSEGMENT_p$ = -4?Crash1@@YAXXZ PROC NEAR; Crash1, COMDAT; 15 : { 0000055 push ebp 000018b ec mov ebp, esp 0000383 ec 44 sub esp, 68; 00000044H 0000653 push ebx 0000756 push esi 0000857 push edi 000098d 7d bc lea edi, DWORD PTR [ebp-68] 0000cb9 11 00 00 00 mov ecx, 17; 00000011H 00011b8 cc cc cc cc mov eax, -858993460; ccccccccH 00016f3 ab rep stosd; 16 : char * p =(char*)100; 00018c7 45 fc 64 0000 00 mov DWORD PTR _p$[ebp], 100; 00000064H; 17 : *p=100; 0001f8b 45 fc mov eax, DWORD PTR _p$[ebp] 00022c6 00 64 mov BYTE PTR [eax], 100; 00000064H; 18 : } 000255f pop edi 000265e pop esi 000275b pop ebx 000288b e5 mov esp, ebp 0002a5d pop ebp 0002bc3 ret 0?Crash1@@YAXXZ ENDP; Crash1_TEXTENDSEND
其中
?Crash1@@YAXXZ PROC NEAR; Crash1, COMDAT
為Crash1彙編代碼的起始行。產生崩潰的代碼便在其後的某個位置。接下去的一行為:
; 15 : {
冒號後的"{"表示源檔案中的語句,冒號前的"15"表示該語句在源檔案中的行數。 這之後顯示該語句彙編後的位移地址,二進位碼,彙編代碼。如
0000055 push ebp
其中"0000"表示相對於函數開始地址後的位移,"55"為編譯後的機器代碼," push ebp"為彙編代碼。從"cod"檔案中我們可以看出,一條(c/c++)語句通常需要編譯成數條彙編語句 。此外有些彙編語句太長則會分兩行顯示如:
00018c7 45 fc 64 0000 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),所以崩潰地址相對函數起始地址的位移就很容易計算了:
崩潰位移地址 = 崩潰語句地址 - 崩潰函數的起始地址 = 0x00401082 - 0x00401060 = 0x22。
第二步,計算出錯的彙編語句在cod檔案中的相對位移。我們可以看到函數Crash1()在cod檔案中的相對位移地址為0000,則
崩潰語句在cod檔案中的相對位移 = 崩潰函數在cod檔案中相對位移 + 崩潰位移地址 = 0x0000 + 0x22 = 0x22
第三步,我們看Crash1函數位移0x22除的代碼是什麼?結果如下
00022c6 00 64 mov BYTE PTR [eax], 100; 00000064H
這句彙編語句表示將100這個數儲存到寄存器eax所指的記憶體單元中去,儲存空間大小為1個位元組(byte)。程式正是執行這條命令時產生了崩潰,顯然這裡eax中的為一個非法地址 ,所以程式崩潰了!
第四步,再查看該彙編語句在其前面幾行的其對應的原始碼,結果如下:
; 17 : *p=100;
其中17表示該語句位於源檔案中第17行,而“*p=100;”這正是源檔案中產生崩潰的語句。
至此我們僅從崩潰地址就尋找出了造成崩潰的原始碼語句和該語句所在源檔案中的確切位置,甚至尋找到了造成崩潰的編譯後的確切彙編代碼!
怎麼樣,是不是感覺更爽啊?
五、小節
1、新方法同樣要注意可以適用的範圍,即程式由一條語句當即引起的崩潰。另外我不知道除了VC6外,是否還有其他的編譯器能夠產生類似的"cod"檔案。
2、我們可以通過比較 新方法產生的debug和releae版本的"cod"檔案,尋找那些僅release版本(或debug版本)有另一個版本沒有的bug(或其他性狀)。例如"羅文"中所舉的那個用例 ,只要開啟release版本的"cod"檔案,就明白了為啥debug版本會產生崩潰而release版本卻沒有:原來release版本中產生崩潰的語句其實根本都沒有編譯 。同樣本例中的release版本要看到崩潰的效果,需要將編譯選項改為為不最佳化的配置。
原文:http://www.vckbase.com/document/viewdoc/?id=1473