當(dāng)開發(fā)的軟件發(fā)布以后,在客戶那運(yùn)行時(shí)可能會(huì)因?yàn)楦鞣N原因?qū)е鲁绦蛲顺?。這種情況很尷尬,很明顯我們無法在客戶機(jī)器上裝個(gè)Visual
Studio調(diào)試,所以必須有機(jī)制來收集出錯(cuò)的信息。軟件本身的運(yùn)行日志能提供部分信息,但是可能還不夠。Windows系統(tǒng)為此提供了解決方
案:Dr.Watson工具。Dr.Watson也算是一個(gè)小巧的調(diào)試器,32位的版本名字是drwtsn32.exe??捎糜诋?dāng)系統(tǒng)中有進(jìn)程發(fā)生異常崩
潰時(shí)采集信息。下面結(jié)合一個(gè)例子看一下其用法。
先把測(cè)試代碼貼一下:
- #include <stdio.h>
- #include <process.h>
- #include <windows.h>
- const int cnThreadnum = 4;
- UINT WINAPI Worker(LPVOID lpParam)
- {
- srand((DWORD)lpParam);
- DWORD dwTid = GetCurrentThreadId();
- int k=100;
- while(k--)
- {
- printf("tid[%u] tmp = %d\n", dwTid, RAND_MAX/(rand()%cnThreadnum));
- Sleep(10);
- }
- return 0;
- }
- int main(int argc, char* argv[])
- {
- HANDLE hThd[cnThreadnum] = {0};
- for(DWORD i=0; i<cnThreadnum; i++)
- {
- hThd[i] = (HANDLE)_beginthreadex(NULL, 0, Worker, (LPVOID)i, 0, NULL);
- }
- printf("Wait...\n");
- WaitForMultipleObjects(cnThreadnum, hThd, TRUE, INFINITE);
- for(int k=0; k<cnThreadnum; k++)
- {
- CloseHandle(hThd[k]);
- }
- printf("Finish!\n");
- system("pause");
- return 0;
- }
程序已開始就啟動(dòng)了4個(gè)線程,然后
主線程等待所有的線程結(jié)束。線程函數(shù)中可能會(huì)因?yàn)镽AND_MAX/(rand()%10)導(dǎo)致出現(xiàn)除0的錯(cuò)誤。這里注意,編譯的時(shí)候選擇Link頁,
把"Generate mapfile"前面勾上。一般來說我就這樣用了。但是還可以讓map信息更詳細(xì)一些,在最下面的"Project
Options"的最后面手工敲入:"/mapinfo:lines",注意,跟前面內(nèi)容要用空格隔開。然后再編譯。
因?yàn)槭且菔綝r.Waston,所以我們?cè)诿钚邢扔?strong>drwtsn32 -i的命令注冊(cè)Dr.Watson為默認(rèn)調(diào)試器(下次啟動(dòng)VC6的時(shí)候,在菜單option-->debug里面確認(rèn)just-in-time debugging選中,就會(huì)把VC6恢復(fù)為默認(rèn)調(diào)試器的)。注冊(cè)完以后,我們看看注冊(cè)表是否滿足需要了。

看看注冊(cè)表中
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows
NT\CurrentVersion\AeDebug,其中的Auto鍵的值如果是0的話,程序出錯(cuò)后還會(huì)彈出錯(cuò)誤提示。我們需要的是出錯(cuò)后安靜的處理
掉,客戶知道了多不好~所以,改成1。下面的Debugger已經(jīng)是drwtsn32了,很好。最后的UserDebuggerHotKey是調(diào)試器的熱
鍵,我們不需要。接下來,還是在命令行敲Drwtsn32,回車,終于看到Drwtsn32的廬山真面目了:

上面是日志文件和dump文件的保存路徑。這次我們要看的是日志,dump
文件下次再說。符號(hào)表我一般都勾上了,有的話就更好不是~
重點(diǎn)是下面的應(yīng)用程序錯(cuò)誤(&R)這里,如果日志中有信息,就會(huì)在下面一條一條以紀(jì)錄的形式顯示出來。如果你的Drwatson中確實(shí)有日志的
話,可以點(diǎn)清除把以前的都刪掉。既然已經(jīng)見過面了,那就把它關(guān)掉吧,需要時(shí)再打開,反正它自己也不會(huì)實(shí)施刷新。
好,現(xiàn)在一切就緒了!運(yùn)行一下程序~ 只見嗖的一下,程序就停了。趕緊再把Dr.Watson請(qǐng)出來看看~

這次我們看到應(yīng)用程序錯(cuò)誤那里有了一條記錄,把它選中變藍(lán)后點(diǎn)上面的查看按
鈕。于是又出來一個(gè)對(duì)話框,顯示“發(fā)生應(yīng)用程序意外錯(cuò)誤:”云云,內(nèi)容很長,信息相當(dāng)多。嗯,很好,很強(qiáng)大,這就是我們要的。考慮到這個(gè)框小了點(diǎn),可以
Ctrl+Shift+END全選后copy出來,用記事本看哈。在記事本中查找"錯(cuò)誤 ->",然后就會(huì)看到個(gè)這:
錯(cuò)誤 ->00401087 f7f9 idiv eax,ecx
沒錯(cuò),這就是錯(cuò)誤的位置。仔細(xì)看看上下文,分析分析。由于是一個(gè)多線程的程
序,所以這里把出錯(cuò)的線程單獨(dú)剝離出來。如下所示,每個(gè)線程的信息分為幾個(gè)部分:一開始是錯(cuò)誤發(fā)生時(shí)寄存器的信息;然后是當(dāng)前發(fā)生錯(cuò)誤的指令的前后各10
條指令(什么是10條,不是20條或者2條?因?yàn)镈r.Watson里面默認(rèn)設(shè)置的就是10條呀~,你剛才又沒改);接下來是堆棧反向跟蹤信息,也就是函數(shù)的逆向調(diào)用序列;最后是原始堆棧信息,可以獲得參數(shù),返回地址,局部變量之類的信息。
- *----> 線程 ID 0x2714 的狀態(tài)轉(zhuǎn)儲(chǔ) <----*
- eax=00007fff ebx=003724b8 ecx=00000000 edx=00000000 esi=0063ff2c edi=0063ff80
- eip=00401087 esp=0063ff2c ebp=0063ff80 iopl=0 nv up ei pl zr na pe nc
- cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010246
- 函數(shù): lesson_2!Worker
- 0040106b 7441 je lesson_2!Worker+0x8e (004010ae)
- 0040106d e86e020000 call lesson_2!rand (004012e0)
- 00401072 8bc8 mov ecx,eax
- 00401074 81e103000080 and ecx,80000003h
- 0040107a 7905 jns lesson_2!Worker+0x61 (00401081)
- 0040107c 49 dec ecx
- 0040107d 83c9fc or ecx,0FFFFFFFCh
- 00401080 41 inc ecx
- 00401081 b8ff7f0000 mov eax,7FFFh
- 00401086 99 cdq
- 錯(cuò)誤 ->00401087 f7f9 idiv eax,ecx
- 00401089 50 push eax
- 0040108a 8b55fc mov edx,dword ptr [ebp-4]
- 0040108d 52 push edx
- 0040108e 681cd04200 push offset lesson_2!`string' (0042d01c)
- 00401093 e898010000 call lesson_2!printf (00401230)
- 00401098 83c40c add esp,0Ch
- 0040109b 8bf4 mov esi,esp
- 0040109d 6a0a push 0Ah
- 0040109f ff15d8714300 call dword ptr [lesson_2!_imp__Sleep (004371d8)]
- 004010a5 3bf4 cmp esi,esp
- *----> 堆棧反向跟蹤 <---*
- ChildEBP RetAddr Args to Child
- 0063ff80 004015e2 00000001 00000000 00000000 lesson_2!Worker+0x67
- 0063ffb8 7c824829 003724b8 00000000 00000000 lesson_2!_threadstartex+0xb2
- WARNING: Stack unwind information not available. Following frames may be wrong.
- 0063ffec 00000000 00401530 003724b8 00000000 kernel32!GetModuleHandleA+0xdf
- *----> 原始堆棧轉(zhuǎn)儲(chǔ) <----*
- 000000000063ff2c 00 00 00 00 00 00 00 00 - b8 24 37 00 cc cc cc cc .........$7.....
- 000000000063ff3c cc cc cc cc cc cc cc cc - cc cc cc cc cc cc cc cc ................
- 000000000063ff4c cc cc cc cc cc cc cc cc - cc cc cc cc cc cc cc cc ................
- 000000000063ff5c cc cc cc cc cc cc cc cc - cc cc cc cc cc cc cc cc ................
- 000000000063ff6c cc cc cc cc cc cc cc cc - cc cc cc cc 06 00 00 00 ................
- 000000000063ff7c 14 27 00 00 b8 ff 63 00 - e2 15 40 00 01 00 00 00 .'....c...@.....
- 000000000063ff8c 00 00 00 00 00 00 00 00 - b8 24 37 00 94 00 00 c0 .........$7.....
- 000000000063ff9c 00 00 00 00 8c ff 63 00 - 60 fb 63 00 dc ff 63 00 ......c.`.c...c.
- 000000000063ffac b4 65 40 00 b0 d1 42 00 - 00 00 00 00 ec ff 63 00 .e@...B.......c.
- 000000000063ffbc 29 48 82 7c b8 24 37 00 - 00 00 00 00 00 00 00 00 )H.|.$7.........
- 000000000063ffcc b8 24 37 00 94 00 00 c0 - c4 ff 63 00 60 fb 63 00 .$7.......c.`.c.
- 000000000063ffdc ff ff ff ff 60 1a 82 7c - 30 48 82 7c 00 00 00 00 ....`..|0H.|....
- 000000000063ffec 00 00 00 00 00 00 00 00 - 30 15 40 00 b8 24 37 00 ........0.@..$7.
- 000000000063fffc 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ................
- 000000000064000c 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ................
- 000000000064001c 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ................
- 000000000064002c 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ................
- 000000000064003c 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ................
- 000000000064004c 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ................
- 000000000064005c 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ................
盡管這里可以簡單的利用錯(cuò)誤處指出的地址0x401087直接定位,那是因?yàn)檫@是很簡單的情況。所以我決定還是簡單講一下在實(shí)際的復(fù)雜得多的環(huán)境中如何分析這些數(shù)據(jù)。
1.定位出錯(cuò)的模塊。
我們看到錯(cuò)誤的地址是0x401087,應(yīng)該先確認(rèn)這個(gè)地址是在哪個(gè)模塊中。在錯(cuò)誤日志中查找“模塊清單”,能找到以下內(nèi)容:
- *----> 模塊清單 <----*
- 0000000000400000 - 000000000043a000: D:\DebugEasy\lesson_2\Debug\lesson_2.exe
- 0000000074ae0000 - 0000000074b45000: C:\windows\system32\USP10.dll
- 0000000076180000 - 000000007619d000: C:\windows\system32\IMM32.DLL
- 0000000076eb0000 - 0000000076ec3000: C:\windows\system32\Secur32.dll
- 0000000077bd0000 - 0000000077c18000: C:\windows\system32\GDI32.dll
- 0000000077c20000 - 0000000077cbf000: C:\windows\system32\RPCRT4.dll
- 0000000077e10000 - 0000000077ea0000: C:\windows\system32\USER32.dll
- 0000000077f30000 - 0000000077fdb000: C:\windows\system32\ADVAPI32.dll
- 000000007c800000 - 000000007c92b000: C:\windows\system32\kernel32.dll
- 000000007c930000 - 000000007ca00000: C:\windows\system32\ntdll.dll
- 000000007f000000 - 000000007f009000: C:\windows\system32\LPK.DLL
這里清楚的指出0000000000400000 -
0000000000439000這個(gè)范圍的地址是系統(tǒng)分配給lesson_2.exe的。我們得到的錯(cuò)誤地址是0x401087,所以可以確定錯(cuò)誤就是
發(fā)生的lesson_2.exe模塊。實(shí)際上很多應(yīng)用都還有些自己的動(dòng)態(tài)庫之類的,定位到具體的模塊后才能更具體的結(jié)合代碼分析。有時(shí)候出錯(cuò)的地址是屬于
kernel32,ntdll.dll之類的系統(tǒng)函數(shù),這種情況下需要分析堆棧信息來確定錯(cuò)誤模塊。
2.分析當(dāng)前發(fā)生錯(cuò)誤的直接原因
我們看到錯(cuò)誤的指令是:idiv
eax,ecx。基本上訪問eax和ecx都不會(huì)有問題,不然就是cpu有問題了。所以應(yīng)該是除法指令出錯(cuò)??纯辞懊娴募拇嫫鞯男?
息,eax=00007fff,也就是RAND_MAX,ecx=00000000,除0錯(cuò)誤。對(duì)于我們這個(gè)例子,有了這個(gè)信息就足夠定位到問題了。不過
還是看看我們還能收集到些什么信息,畢竟實(shí)際環(huán)境可不會(huì)這么簡單。
3.分析當(dāng)前線程的調(diào)用堆棧。
ChildEBP RetAddr Args to Child
0063ff80 004015e2 00000001 00000000 00000000 lesson_2!Worker+0x67
0063ffb8 7c824829 003724b8 00000000 00000000 lesson_2!_threadstartex+0xb2
在堆棧反向跟蹤部分,這個(gè)信息很清晰。
_threadstartex系統(tǒng)函數(shù)調(diào)用了我們的線程函數(shù)Worker。當(dāng)前函數(shù)的返回地址是0x4015e2,接著是3個(gè)參數(shù)(注意:不一定都有效,
顯然不是所有的函數(shù)都有3個(gè)參數(shù)。實(shí)際上通過分析其dump文件能看到我們的例子中只有第一個(gè)參數(shù)有效,即1,是作為線程參數(shù)傳入的lpParam)。這
里我們結(jié)合map文件看看是不是跟這里記錄的一樣。在工程的debug目錄里,有個(gè)擴(kuò)展名為map的文件,用一般的文本編輯器就能打開。這文件太長了,節(jié)
選一部分看看:
- lesson_2
- Timestamp is 48e0197a (Mon Sep 29 06:50:11 2008)
- Preferred load address is 00400000
- Start Length Name Class
- 0001:00000000 0001bae7H .text CODE
- 0001:0001bae7 00010009H .textbss CODE
- 0002:00000000 00002207H .rdata DATA
- 0002:00002207 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 0000407cH .data DATA
- 0003:00004ab0 00001b94H .bss DATA
- 0004:00000000 00000014H .idata$2 DATA
- 0004:00000014 00000014H .idata$3 DATA
- 0004:00000028 000001b0H .idata$4 DATA
- 0004:000001d8 000001b0H .idata$5 DATA
- 0004:00000388 0000076bH .idata$6 DATA
- Address Publics by Value Rva+Base Lib:Object
- 0001:00000020 ?Worker@@YGIPAX@Z 00401020 f lesson_2.obj
- 0001:000000f0 _main 004010f0 f lesson_2.obj
- 0001:00000212 _Sleep@4 00401212 f kernel32:KERNEL32.dll
- 0001:00000218 _GetCurrentThreadId@0 00401218 f kernel32:KERNEL32.dll
- 0001:0000021e _CloseHandle@4 0040121e f kernel32:KERNEL32.dll
- 0001:00000224 _WaitForMultipleObjects@16 00401224 f kernel32:KERNEL32.dll
- 0001:00000230 _printf 00401230 f LIBCMTD:printf.obj
- 0001:000002d0 _srand 004012d0 f LIBCMTD:rand.obj
- 0001:000002e0 _rand 004012e0 f LIBCMTD:rand.obj
- 0001:00000320 __chkesp 00401320 f LIBCMTD:chkesp.obj
- 0001:00000360 _system 00401360 f LIBCMTD:system.obj
- 0001:00000470 __beginthreadex 00401470 f LIBCMTD:threadex.obj
- 0001:00000630 __endthreadex 00401630 f LIBCMTD:threadex.obj
- 0001:00000680 _mainCRTStartup 00401680 f LIBCMTD:crt0.obj
- 0001:000007d0 __amsg_exit 004017d0 f LIBCMTD:crt0.obj
- 。。。。。。省略若干。。。。。
- Line numbers for .\Debug\lesson_2.obj(d:\debugeasy\lesson_2\lesson_2.cpp) segment .text
- 18 0001:00000020 19 0001:00000038 21 0001:00000044 22 0001:00000056
- 23 0001:0000005d 25 0001:0000006d 26 0001:0000009b 27 0001:000000ac
- 28 0001:000000ae 29 0001:000000b0 32 0001:000000f0 33 0001:00000108
- 34 0001:0000011a 36 0001:00000132 37 0001:00000152 39 0001:00000154
- 40 0001:00000161 42 0001:0000017a 44 0001:00000192 45 0001:000001a9
- 47 0001:000001ab 48 0001:000001b8 49 0001:000001c5 50 0001:000001c7
其中顯示了程序名,程序的時(shí)間戳,程序中的代碼分布,預(yù)定的加載地址,各個(gè)函數(shù)的起始地址,包括相對(duì)虛擬地址
(RVA)以及文件行與RVA的對(duì)應(yīng)關(guān)系等信息。我們看到預(yù)定的加載地址和我們?cè)谀K清單中看到的加載地址相同。很好,省得計(jì)算相對(duì)位置。一般來說Exe
總是加載到0x400000,并且總是能得到這個(gè)位置的。而動(dòng)態(tài)庫的話,墨認(rèn)是0x10000000,顯然如果有2個(gè)動(dòng)態(tài)庫就沒法都加載到同一個(gè)地址,需
要計(jì)算RVA。
剛才從堆棧反向跟蹤信息中我們看到當(dāng)前Worker函數(shù)的返回地址為0x401e52,在map文件中找找。很快
就能看到__beginthreadex函數(shù)是從0x401470開始,到0x401630結(jié)束。那就說明地址0x401e52就在
__beginthreadex函數(shù)中。這就對(duì)了,因?yàn)槲覀兙褪怯胈_beginthreadex函數(shù)啟動(dòng)的線程函數(shù)Worker,是吧~。順手看一下剛
才出錯(cuò)的地址0x401087,看看是在哪。再看看map文件,第一個(gè)函數(shù)就是?Worker@@YGIPAX@Z,
地址是0x00401020至0x4010f0(main函數(shù)),所以0x401087就是在執(zhí)行Worker函數(shù)中的指令。那么具體在哪一行呢?因?yàn)槌?
錯(cuò)的函數(shù)Worker在文件lesson_2.cpp中,所以我們查找lesson_2.cpp,就找到了下面的信息:
- Line numbers for .\Debug\lesson_2.obj(d:\debugeasy\lesson_2\lesson_2.cpp) segment .text
- 15 0001:00000020 16 0001:00000038 18 0001:00000044 19 0001:00000056
- 20 0001:0000005d 22 0001:0000006d 23 0001:0000009b 24 0001:000000ac
- 25 0001:000000ae 26 0001:000000b0 29 0001:000000f0 30 0001:00000108
- 31 0001:0000011a 33 0001:00000132 34 0001:00000152 46 0001:00000154
- 37 0001:00000161 39 0001:0000017a 41 0001:00000192 42 0001:000001a9
- 44 0001:000001ab 45 0001:000001b8 46 0001:000001c5 47 0001:000001c7
我們看到前面是個(gè)數(shù)字,18、19、21啥的,后面是0001:00000020之類。前面的就是cpp文件中的行號(hào),后面是相對(duì)虛擬地址,我們的
0x401087在哪呢?又回到剛才說的默認(rèn)加載地址了。map中看到默認(rèn)加載地址是0x400000,而我們的exe在內(nèi)存中也是加載到
0x400000,所以相對(duì)位置就是RVA = 0x401087(錯(cuò)誤地址) - 0x400000(模塊在內(nèi)存中實(shí)際加載的地址) =
0x1067。不對(duì)啊,這里最大才到0x1c7,這是怎~么個(gè)情況?因?yàn)槊總€(gè)PE模塊之前都有0x1000字節(jié)的dos
stub和PE格式信息。所以0x1087還要減掉0x1000,就剩下0x87了。再看看,大于0x5d,小于0x6d,所以就是第22行??纯创a中
的22行,printf("tid[%u] tmp = %d\n", dwTid, RAND_MAX/(rand()%cnThreadnum));沒錯(cuò),就是在這里RAND_MAX/0了~。說明前面根據(jù)Dr.Watson的日志分析的結(jié)果都是正確的,沒騙你噢~
有時(shí)候堆棧反向跟蹤的信息并不準(zhǔn)確,多半是因?yàn)楹瘮?shù)中的局部變量在memcpy,strcpy之類的函數(shù)復(fù)制的數(shù)據(jù)過長,導(dǎo)致堆棧訪問越界。這種情況下,要仔細(xì)分析原始堆棧轉(zhuǎn)儲(chǔ),以后再說。
在實(shí)際工作中,利用Dr.Watson輔助生成錯(cuò)誤日志,對(duì)于開發(fā)人員確實(shí)是很有幫助的。有需要的朋友,可以考慮這么試試。
那今天就寫到這吧~ 天亮啦,飯去~