[轉(zhuǎn)載]通往WinDbg的捷徑(一)
原文:http://www.debuginfo.com/articles/easywindbg.html
譯者:arhat
時(shí)間:2006年4月13日
關(guān)鍵詞:CDB WinDbg
導(dǎo)言
你鐘情什么樣的調(diào)試器?如果你問(wèn)我這個(gè)問(wèn)題,我會(huì)回答是“Visual Studio + WinDbg”。我比較喜歡Visual Studio那樸實(shí)無(wú)華且易操作的接口,更喜歡它能迅速把我需要的信息以可視的形式展示出來(lái)。但遺憾的是,Visual Studio調(diào)試器無(wú)法獲取某些信息。例如,假設(shè)我想知道哪個(gè)線程正在占用特殊的臨界區(qū)?或者是哪個(gè)函數(shù)占用了大部分的棧空間?不用擔(dān)心,有WinDbg呢。它的命令能回答這些問(wèn)題,以及調(diào)試過(guò)程中出現(xiàn)的其它有趣的問(wèn)題。甚至不退出Visual Studio,WinDbg就可以附上目標(biāo)應(yīng)用程序??謝謝WinDbg支持入侵模式的調(diào)試(本文后面會(huì)詳細(xì)討論),我們可以把Visual Studio GUI和WinDbg的命令行結(jié)合起來(lái)使用。
唯一的問(wèn)題是WinDbg不太好用。需要花些時(shí)間適應(yīng)它的用戶(hù)界面,而掌握它的命令則要花更多的時(shí)間。但是假設(shè)你現(xiàn)在就需要它,馬上用它調(diào)試緊急的問(wèn)題?有什么快速簡(jiǎn)便的方法嗎?當(dāng)然。WinDbg的小弟CDB,功能和WinDbg差不多;因?yàn)樗腔诿钚械模杂闷饋?lái)更簡(jiǎn)單一些。在這篇文章里,我將把CDB作為Visual Studio調(diào)試器的補(bǔ)充,介紹怎樣使用CDB。在這篇文章里,你將會(huì)看到怎樣配置CDB,怎樣用它解決實(shí)際的問(wèn)題。另外,我還會(huì)提供一些批處理文件,它們可以隱藏CDB命令行接口的大部分復(fù)雜性,也讓你少打幾個(gè)字。
安裝與配置
安裝
當(dāng)然,在使用CDB前,必須先安裝并配置它。WinDbg和CDB是Debugging Tools for Windows 的一部分,可以從這里下載。安裝很簡(jiǎn)單,你可以用默認(rèn)設(shè)置安裝,除非你準(zhǔn)備用WinDbg SDK開(kāi)發(fā)應(yīng)用程序。(如果你準(zhǔn)備用SDK,需要選擇定制安裝,并啟用SDK安裝;推薦你把它安裝在不包含空格的目錄名的目錄中)。安裝完成后,安裝目錄里將包含所有必需的文件,包括WinDbg(windbg.exe)和CDB(cdb.exe)。
調(diào)試工具也支持“xcopy”類(lèi)型的安裝。也就是說(shuō),在一臺(tái)機(jī)器上安裝后,如果你想在其它的機(jī)器上使用,不用再安裝,直接把已經(jīng)安裝的目錄直接拷過(guò)去就行了。
符號(hào)文件服務(wù)器路徑
如果不能訪問(wèn)操作系統(tǒng)DLL的最新的符號(hào)文件,有些重要的WinDbg命令將不能正常工作。在以往,我們可以從微軟的FTP服務(wù)器上下載巨大的符號(hào)文件包,然后從中找出需要的符號(hào)文件。這非常浪費(fèi)時(shí)間,而且在操作系統(tǒng)更新或升級(jí)后,符號(hào)文件就過(guò)時(shí)了(因此也就變得毫無(wú)用處)。幸運(yùn)的是,現(xiàn)在有更簡(jiǎn)便的方法來(lái)獲得符號(hào)文件??符號(hào)文件服務(wù)器。WinDbg和Visual Studio都支持這個(gè)方法,在需要時(shí)直接從微軟維護(hù)的服務(wù)上下載最新的符號(hào)文件。有了符號(hào)文件服務(wù)器,我們?cè)僖膊挥孟螺d整個(gè)符號(hào)文件包了(那實(shí)在是太大了),因?yàn)檎{(diào)試器知道需要用到哪個(gè)DLLs,所以直接下載單個(gè)符號(hào)文件就行了。如果符號(hào)文件在操作系統(tǒng)更新或升級(jí)以后過(guò)時(shí)了,調(diào)試器會(huì)注意到這種情形,并再次下載必需的符號(hào)文件。
為了使符號(hào)文件服務(wù)器起作用,我們應(yīng)該讓調(diào)試器知道符號(hào)文件服務(wù)器的路徑。最簡(jiǎn)單的方法是在_NT_SYMBOL_PATH環(huán)境變量里指定符號(hào)文件服務(wù)器的路徑。可以用如下的路徑:
"srv*c:\symbolcache*http://msdl.microsoft.com/download/symbols"
(c:\symbolcache目錄將被用來(lái)保存從符號(hào)文件服務(wù)器下載下來(lái)的符號(hào)文件;當(dāng)然,你可以用任何有效的本地或網(wǎng)絡(luò)路徑)。例如:
set _NT_SYMBOL_PATH=srv*c:\symbols*http://msdl.microsoft.com/download/symbols
在你設(shè)置_NT_SYMBOL_PATH環(huán)境變量之后,就可以使用符號(hào)文件服務(wù)器了。關(guān)于符號(hào)文件服務(wù)器的更多信息,相關(guān)設(shè)置,以及可能會(huì)用到的排除故障的小技巧,可以從WinDbg的文檔中找到(Debuggers | Symbols section)。
如果你需要從一臺(tái)需登錄的代理服務(wù)器后訪問(wèn)符號(hào)文件服務(wù)器。參見(jiàn)本篇文章中CDB and proxy servers部分,以了解更多信息。
CDB 命令行基礎(chǔ)介紹
啟動(dòng)調(diào)試會(huì)話
當(dāng)我們使用新的調(diào)試器時(shí),第一個(gè)問(wèn)題通常是:怎樣開(kāi)始調(diào)試會(huì)話呢?像大多數(shù)調(diào)試器一樣,CDB允許我們調(diào)試應(yīng)用程序的新實(shí)例,或者附上一個(gè)已經(jīng)運(yùn)行的過(guò)程。啟動(dòng)新實(shí)例就象下面一樣簡(jiǎn)單:
cdb c:\myapp.exe
如果我們想附上已經(jīng)運(yùn)行的過(guò)程,可能會(huì)用上下列某個(gè)選項(xiàng):
----------------------------------------------------------------------------------------------------------------------
選項(xiàng) 描述 例子
----------------------------------------------------------------------------------------------------------------------
-p Pid 這個(gè)選項(xiàng)允許CDB附上指定進(jìn)程ID的進(jìn)程。可以用任務(wù)管理器或類(lèi)似的工具得到進(jìn)程ID。 cdb -p 1034
----------------------------------------------------------------------------------------------------------------------
-pn ExeName 這個(gè)選項(xiàng)允許CDB用指定的可執(zhí)行文件名(.exe)附上進(jìn)程。這個(gè)選項(xiàng)比“-p Pid”更
方便,因?yàn)槲覀兺ǔV缊?zhí)行的程序名,不必在任務(wù)管理器中尋找進(jìn)程的ID。但是如果
多個(gè)進(jìn)程使用同一個(gè)名字(CDB將報(bào)錯(cuò)),就不能用這個(gè)選項(xiàng)了。 cdb -pn myapp.exe
----------------------------------------------------------------------------------------------------------------------
-psn ServiceName 這個(gè)選項(xiàng)允許CDB附上指定服務(wù)的進(jìn)程。例如,假如你想附上Windows Management
Instrumentation服務(wù),應(yīng)該用WinMgmt作為服務(wù)名。 cdb -psn MyService
----------------------------------------------------------------------------------------------------------------------
CDB也可以分析故障轉(zhuǎn)儲(chǔ)。用-z選項(xiàng)打開(kāi)故障轉(zhuǎn)儲(chǔ):
cdb -z DumpFile
例如:
cdb -z c:\myapp.dmp
結(jié)束調(diào)試會(huì)話
啟動(dòng)新的調(diào)試會(huì)話后,CDB會(huì)顯示它自己的命令行提示符。你可以在這個(gè)提示符下執(zhí)行CDB支持的任何命令。
'q'命令結(jié)束調(diào)試會(huì)話并退出CDB:
0:000> q
quit:
>
警告:當(dāng)你結(jié)束調(diào)試會(huì)話,退出CDB時(shí),操作系統(tǒng)也將終止被調(diào)試的程序。如果你想退出CDB并保持被調(diào)試程序,可以用.detach命令(Windows XP或更新的操作系統(tǒng)才支持),或者用非入侵的模式(下面討論)。
運(yùn)行命令
雖然可以在CDB命令行提示符下執(zhí)行調(diào)試器命令,但在命令行里指定需要的命令通常更快一些,用-c選項(xiàng)。
cdb -pn myapp.exe -c "command1;command2"
(用分號(hào)分隔多個(gè)命令)
例如,下列命令行將把CDB附上我們的應(yīng)用程序,顯示已加載的模塊,然后退出:
cdb -pn myapp.exe -c "lm;q"
注意,在命令列表的結(jié)尾加上'q'命令??將在所有的調(diào)試器命令執(zhí)行后關(guān)閉CDB。
入侵模式調(diào)試
在默認(rèn)情況下,當(dāng)我們用CDB調(diào)試一個(gè)已經(jīng)運(yùn)行的進(jìn)程時(shí),它通常作為全功能的調(diào)試器附上進(jìn)程(使用Win32 Debugging API)。在這種模式下,可以設(shè)置斷點(diǎn),單步調(diào)試代碼,得到各種調(diào)試事件的通知(例如,異常,加載/卸載模塊,啟動(dòng)/退出線程,等等)。Visual Studio也可以做到這些,并提供更友好的用戶(hù)界面。另外,每個(gè)進(jìn)程每次只能被一個(gè)調(diào)試器附上。這是否意味著如果我們用Visual Studio調(diào)試器調(diào)試應(yīng)用程序,就不能再用CDB得到它的附加信息了?不,不完全是這樣,因?yàn)槌巳δ苷{(diào)試模式外,CDB還支持入侵調(diào)試模式。
CDB以入侵模式附上目標(biāo)進(jìn)程時(shí),并沒(méi)有使用Win32 Debugging API,而是先暫停目標(biāo)進(jìn)程的所有線程,執(zhí)行用戶(hù)指定的命令。在所有的命令執(zhí)行之后,CDB退出之前,恢復(fù)暫停的線程。因此,目標(biāo)進(jìn)程可以繼續(xù)運(yùn)行,好像什么事也沒(méi)發(fā)生一樣。即使像Visual Studio之類(lèi)的全功能調(diào)試器正在調(diào)試目標(biāo)進(jìn)程,CDB仍可以用入侵模式附上它,并獲得所需要的信息。在CDB完成任務(wù)并分離附上的進(jìn)程后,我們可以繼續(xù)用Visual Studio調(diào)試器調(diào)試這個(gè)應(yīng)用程序。
怎么啟用CDB的入侵模式?用-pv命令行選項(xiàng)。例如,下列命令行將以入侵模式附上應(yīng)用程序,顯示已加載模塊的列表,然后退出。在CDB退出之后,應(yīng)用程序?qū)⒗^續(xù)運(yùn)行。
cdb -pv -pn myapp.exe -c "lm;q"
把輸出內(nèi)容保存到日志文件
有些CDB命令的輸出內(nèi)容可能會(huì)很長(zhǎng),從控制臺(tái)窗口閱讀十分不便。因此,把輸出內(nèi)容保存到日志文件,再用其它的編輯器查看會(huì)更好一些,CDB允許我們用-loga和-logo選項(xiàng)來(lái)實(shí)現(xiàn)('-loga <filename>'把輸出內(nèi)容追加到指定文件的結(jié)尾;而'-logo <filename>'將覆蓋原有的文件,如果文件已經(jīng)存在的話)。
在我們的例子命令(列出目標(biāo)進(jìn)程里的模塊)里增加記錄功能,把輸出內(nèi)容保存到當(dāng)前目錄的out.txt文件里:
cdb -pv -pn myapp.exe -logo out.txt -c "lm;q"
源行號(hào)信息
CDB支持的另外一個(gè)重要選項(xiàng)是-lines。這個(gè)選項(xiàng)打開(kāi)源行號(hào)信息支持,例如,當(dāng)報(bào)告調(diào)用棧時(shí),允許CDB顯示源文件及源行號(hào)。(在默認(rèn)情況下,源行號(hào)支持是關(guān)閉的,CDB不顯示源文件/行號(hào)信息)。
CDB 和代理服務(wù)器
如果你在需要登錄的代理服務(wù)器后用CDB,在默認(rèn)情況下,將不能訪問(wèn)符號(hào)文件服務(wù)器。原因是在默認(rèn)配置下,當(dāng)CDB嘗試連接符號(hào)文件服務(wù)器時(shí),不顯示代理服務(wù)器的登錄提示。為了更改這個(gè)行為,使我們可以訪問(wèn)符號(hào)文件服務(wù)器,需要在命令行之前加上兩條命令:
!sym prompts;.reload
例如:
cdb -pv -pn myapp.exe -logo out.txt -c "!sym prompts;.reload;lm;q"
啟動(dòng)消息
當(dāng)CDB調(diào)試新應(yīng)用程序,附上已經(jīng)存在的進(jìn)程,或打開(kāi)故障轉(zhuǎn)儲(chǔ)時(shí),將顯示一系列的啟動(dòng)消息。CBD命令(可以用-c選項(xiàng)指定,或手動(dòng)輸入)的輸出內(nèi)容跟在這些消息之后。通常情況下,啟動(dòng)消息只顯示一些無(wú)關(guān)緊要信息;但是如果在執(zhí)行時(shí)出錯(cuò)了,它將包含這個(gè)問(wèn)題的描述,有時(shí)候也會(huì)提供解決方法。
例如,下列輸出內(nèi)容通知我們沒(méi)有設(shè)置符號(hào)路徑,因此,有些調(diào)試器命令不能工作:
D:\Progs\DbgTools>cdb myapp.exe
Microsoft (R) Windows Debugger Version 6.5.0003.7
Copyright (c) Microsoft Corporation. All rights reserved.
CommandLine: myapp.exe
Symbol search path is: *** Invalid ***
****************************************************************************
* Symbol loading may be unreliable without a symbol search path. *
* Use .symfix to have the debugger choose a symbol path. *
* After setting your symbol path, use .reload to refresh symbol locations. *
****************************************************************************
總結(jié)
這里是一些常見(jiàn)的CDB命令行模板,本篇文章的剩下部分將會(huì)用到它們(我們總是用同樣的模板,然后根據(jù)我們要解決的問(wèn)題,改變-c選項(xiàng)內(nèi)部的命令行列表)。
用入侵模式附上運(yùn)行的進(jìn)程(通常是進(jìn)程ID),執(zhí)行一組命令,并把輸出內(nèi)容保存在out.txt文件里:
cdb -pv -p <processid> -logo out.txt -lines -c "command1;command2;...;commandN;q"
用入侵模式附上運(yùn)行的進(jìn)程(用可執(zhí)行文件名),執(zhí)行一組命令,并把輸出內(nèi)容保存在out.txt文件里:
cdb -pv -pn <exename> -logo out.txt -lines -c "command1;command2;...;commandN;q"
用入侵模式附上運(yùn)行的進(jìn)程(通常是服務(wù)名),執(zhí)行一組命令,并把輸出內(nèi)容保存在out.txt文件里:
cdb -pv -psn <servicename> -logo out.txt -lines -c "command1;command2;...;commandN;q"
打開(kāi)故障轉(zhuǎn)儲(chǔ)文件,執(zhí)行一組命令,并把輸出內(nèi)容保存在out.txt文件里:
cdb -z <dumpfile> -logo out.txt -lines -c "command1;command2;...;commandN;q"
如果我們?cè)谛枰卿浀拇矸?wù)器后使用CDB,要訪問(wèn)符號(hào)文件服務(wù)器,需要增加兩條命令。例如:
cdb -pv -pn <exename> -logo out.txt -lines -c "!sym prompts;.reload;command1;command2;...;commandN;q"
好像要打好多字?其實(shí)不是這樣,稍后,我將提供一些批處理文件,它們將為我們隱藏重復(fù)的命令行選項(xiàng),把要我們輸入的內(nèi)容減至最小。
解決實(shí)際的問(wèn)題
調(diào)試死鎖問(wèn)題
當(dāng)我們的應(yīng)用程序掛起或停止響應(yīng)時(shí),最自然的問(wèn)題是:它現(xiàn)在正在做什么?它在哪里被困住了?當(dāng)然,我們可以用Visual Studio調(diào)試器附上應(yīng)用程序,檢查所有線程的調(diào)用棧。但我們同樣可以用CDB,而且會(huì)更快一些。下列命令將使CDB以入侵模式附上應(yīng)用程序,打印所有的調(diào)用棧,把結(jié)果保存在日志文件里,然后退出:
cdb -pv -pn myapp.exe -logo out.txt -lines -c "~*kb;q"
('kb'命令要求CDB打印當(dāng)前線程的調(diào)用棧;'~*'前綴要求CDB在進(jìn)程所有已存在的線程里重復(fù)執(zhí)行'kb'命令)。
[/URL] DeadLockDemo.cpp是一個(gè)演示典型的死鎖問(wèn)題的例子。如果你編譯并運(yùn)行,它的工作線程馬上會(huì)被困住,如果我們運(yùn)行上述的命令來(lái)查看應(yīng)用程序的線程正在做什么,將看到下列類(lèi)似的內(nèi)容(在這,以及后面,我們將省略啟動(dòng)消息):
. 0 Id: 6fc.4fc Suspend: 1 Teb: 7ffdf000 Unfrozen
ChildEBP RetAddr Args to Child
0012fdf8 7c90d85c 7c8023ed 00000000 0012fe2c ntdll!KiFastSystemCallRet
0012fdfc 7c8023ed 00000000 0012fe2c 0012ff54 ntdll!NtDelayExecution+0xc
0012fe54 7c802451 0036ee80 00000000 0012ff54 kernel32!SleepEx+0x61
0012fe64 004308a9 0036ee80 a0f63080 01c63442 kernel32!Sleep+0xf
0012ff54 00432342 00000001 003336e8 003337c8 DeadLockDemo!wmain+0xd9
[c:\tests\deadlockdemo\deadlockdemo.cpp @ 154]
0012ffb8 004320fd 0012fff0 7c816d4f a0f63080 DeadLockDemo!__tmainCRTStartup+0x232
[f:\rtm\vctools\crt_bld\self_x86\crt\src\crt0.c @ 318]
0012ffc0 7c816d4f a0f63080 01c63442 7ffdd000 DeadLockDemo!wmainCRTStartup+0xd
[f:\rtm\vctools\crt_bld\self_x86\crt\src\crt0.c @ 187]
0012fff0 00000000 0042e5aa 00000000 78746341 kernel32!BaseProcessStart+0x23
1 Id: 6fc.3d8 Suspend: 1 Teb: 7ffde000 Unfrozen
ChildEBP RetAddr Args to Child
005afc14 7c90e9c0 7c91901b 000007d4 00000000 ntdll!KiFastSystemCallRet
005afc18 7c91901b 000007d4 00000000 00000000 ntdll!ZwWaitForSingleObject+0xc
005afca0 7c90104b 004a0638 00430b7f 004a0638 ntdll!RtlpWaitForCriticalSection+0x132
005afca8 00430b7f 004a0638 005afe6c 005afe78 ntdll!RtlEnterCriticalSection+0x46
005afd8c 00430b15 005aff60 005afe78 003330a0 DeadLockDemo!CCriticalSection::Lock+0x2f
[c:\tests\deadlockdemo\deadlockdemo.cpp @ 62]
005afe6c 004309f1 004a0638 f3d065d5 00334fc8 DeadLockDemo!CCritSecLock::CCritSecLock+0x35
[c:\tests\deadlockdemo\deadlockdemo.cpp @ 90]
005aff6c 004311b1 00000000 f3d06511 00334fc8 DeadLockDemo!ThreadOne+0xa1
[c:\tests\deadlockdemo\deadlockdemo.cpp @ 182]
005affa8 00431122 00000000 005affec 7c80b50b DeadLockDemo!_callthreadstartex+0x51
[f:\rtm\vctools\crt_bld\self_x86\crt\src\threadex.c @ 348]
005affb4 7c80b50b 003330a0 00334fc8 00330001 DeadLockDemo!_threadstartex+0xa2
[f:\rtm\vctools\crt_bld\self_x86\crt\src\threadex.c @ 331]
005affec 00000000 00431080 003330a0 00000000 kernel32!BaseThreadStart+0x37
2 Id: 6fc.284 Suspend: 1 Teb: 7ffdc000 Unfrozen
ChildEBP RetAddr Args to Child
006afc14 7c90e9c0 7c91901b 000007d8 00000000 ntdll!KiFastSystemCallRet
006afc18 7c91901b 000007d8 00000000 00000000 ntdll!ZwWaitForSingleObject+0xc
006afca0 7c90104b 004a0620 00430b7f 004a0620 ntdll!RtlpWaitForCriticalSection+0x132
006afca8 00430b7f 004a0620 006afe6c 006afe78 ntdll!RtlEnterCriticalSection+0x46
006afd8c 00430b15 006aff60 006afe78 003332e0 DeadLockDemo!CCriticalSection::Lock+0x2f
[c:\tests\deadlockdemo\deadlockdemo.cpp @ 62]
006afe6c 00430d11 004a0620 f3e065d5 00334fc8 DeadLockDemo!CCritSecLock::CCritSecLock+0x35
[c:\tests\deadlockdemo\deadlockdemo.cpp @ 90]
006aff6c 004311b1 00000000 f3e06511 00334fc8 DeadLockDemo!ThreadTwo+0xa1
[c:\tests\deadlockdemo\deadlockdemo.cpp @ 202]
006affa8 00431122 00000000 006affec 7c80b50b DeadLockDemo!_callthreadstartex+0x51
[f:\rtm\vctools\crt_bld\self_x86\crt\src\threadex.c @ 348]
006affb4 7c80b50b 003332e0 00334fc8 00330001 DeadLockDemo!_threadstartex+0xa2
[f:\rtm\vctools\crt_bld\self_x86\crt\src\threadex.c @ 331]
006affec 00000000 00431080 003332e0 00000000 kernel32!BaseThreadStart+0x37
調(diào)用棧(和源行號(hào))暗示ThreadOne正在占用臨界區(qū)CritSecOne并等待臨界區(qū)CritSecTwo,然而ThreadTwo正占用臨界區(qū)CritSecTwo并等待臨界區(qū)CritSecOne。這是典型的“lock acquisition order”死鎖例子,在那里,兩個(gè)線程需要得到同一組同步的對(duì)象,以不同的順序使用。如果你想避免這種類(lèi)型的死鎖,必須保證所有的線程以相同的順序得到所需的同步對(duì)象(在這個(gè)例子里,ThreadOne和ThreadTwo能同意首先得到CritSecOne,然后得到CritSecTwo來(lái)避免死鎖)。
在默認(rèn)情況下,'kb'命令只顯示調(diào)用棧的前20幀。如果你想查看更多的棧幀,你可以顯式指明顯示的棧幀數(shù)量(例如,'kb100'命令要求調(diào)試器顯示100幀)。在WinDbg會(huì)話里,可以用.kframes命令改變隨后命令的默認(rèn)限制。
我們的例子只包含了三個(gè)簡(jiǎn)單的線程,很容易看出哪個(gè)線程應(yīng)該為死鎖負(fù)責(zé)。在大應(yīng)用程序里,很難找出可疑的線程并進(jìn)行驗(yàn)證。那我們應(yīng)該怎么做呢?在大部分情況下,我們應(yīng)該知道那個(gè)沒(méi)有正常運(yùn)轉(zhuǎn)的線程(否則,我們?cè)趺磿?huì)注意到應(yīng)用程序出現(xiàn)異常了呢?)。通常,這個(gè)線程是在等待同步對(duì)象,這個(gè)對(duì)象因?yàn)槟承┰驎簳r(shí)不可用。這個(gè)對(duì)象為什么不可用呢?如果我們知道哪個(gè)線程正在占用這個(gè)對(duì)象(擁有它,換句話說(shuō)),應(yīng)該能答出這個(gè)問(wèn)題。如果這個(gè)對(duì)象碰巧在臨界區(qū),!locks命令應(yīng)該能幫助我們識(shí)別出它的當(dāng)前所有者。當(dāng)不帶參數(shù)使用時(shí),這條命令顯示應(yīng)用程序線程正在占用的臨界區(qū)的列表。輸出的內(nèi)容不包括已釋放的臨界區(qū)。
讓我看看實(shí)際使用中的!locks命令:
cdb -pv -pn myapp.exe -logo out.txt -lines -c "!locks;q"
下面是這條命令的輸出內(nèi)容(同樣以DeadLockDemo.cpp為例):
CritSec DeadLockDemo!CritSecOne+0 at 004A0620
LockCount 1
RecursionCount 1
OwningThread 3d8
EntryCount 1
ContentionCount 1
*** Locked
CritSec DeadLockDemo!CritSecTwo+0 at 004A0638
LockCount 1
RecursionCount 1
OwningThread 284
EntryCount 1
ContentionCount 1
*** Locked
仔細(xì)查看了40個(gè)臨界區(qū)
查看!locks命令的輸出(尤其是OwningThread字段),我們可以推斷出臨界區(qū)CritSecOne被ID為0x3d8的線程占用,臨界區(qū)CritSecTwo被ID為0x284的線程占用。我們可以在'kb'命令的輸出內(nèi)容(在前面的輸出里)里找出這些IDs對(duì)應(yīng)的線程。
如果應(yīng)用程序使用其它種類(lèi)的同步對(duì)象(例如,互斥),識(shí)別它們的所有者將更難一些(需要內(nèi)核調(diào)試器),我準(zhǔn)備在以后的文章中再介紹這部分內(nèi)容。
調(diào)試CPU高消耗的問(wèn)題
對(duì)大多數(shù)軟件來(lái)說(shuō),太高的CPU消耗率(根據(jù)任務(wù)管理器的顯示,在單CPU上接近100%)明顯指出軟件中有bug。通常意味著應(yīng)用程序的某個(gè)線程陷入了死循環(huán)。當(dāng)然,調(diào)試這個(gè)問(wèn)題的、最普通的方法是用Visual Studio調(diào)試器附上這個(gè)進(jìn)程,查找哪個(gè)線程在搗亂。但是我們應(yīng)該檢查哪個(gè)線程呢?CDB為我們提供了簡(jiǎn)便的方法??!runaway命令。當(dāng)不帶參數(shù)使用時(shí),這條命令顯示應(yīng)用程序每個(gè)線程執(zhí)行用戶(hù)模式代碼時(shí)所花的時(shí)間(使用另外的參數(shù),可以顯示在內(nèi)核模式下所花的時(shí)間,自線程啟動(dòng)后占用的時(shí)間等)。
如下是在CDB下使用這條命令的示例:
cdb -pv -pn myapp.exe -logo out.txt -c "!runaway;q"
下面是!runaway命令的輸出示例:
0:000> !runaway
User Mode Time
Thread Time
1:358 0 days 0:00:47.408
2:150 0 days 0:00:03.495
0:d8 0 days 0:00:00.000
看起來(lái)好像是ID為0x358的線程占用了大部分的CPU時(shí)間。但這個(gè)消息還不足以證明線程0x358就是罪魁禍?zhǔn)祝驗(yàn)檫@條命令顯示的CPU時(shí)間是線程在它整個(gè)生命期中所花的。我們還需要進(jìn)一步查看線程所用CPU時(shí)間的變化情況。讓我們?cè)俅芜\(yùn)行這條命令。這次,我們可以看到類(lèi)似于下列的內(nèi)容:
0:000> !runaway
User Mode Time
Thread Time
1:358 0 days 0:00:47.408
2:150 0 days 0:00:06.859
0:d8 0 days 0:00:00.000
現(xiàn)在,我們可以把這個(gè)輸出內(nèi)容與上次的輸出內(nèi)容做個(gè)比較,找出CPU時(shí)間增長(zhǎng)最快的線程。在這個(gè)例子里,很明顯就是線程0x150。現(xiàn)在,我們可以用Visual Studio調(diào)試器附上這個(gè)應(yīng)用程序,切換到這個(gè)線程下,檢查它為什么轉(zhuǎn)個(gè)不停。
調(diào)試棧溢出
當(dāng)我們想找出棧溢出異常的原因時(shí),CDB也非常有幫助。當(dāng)然,無(wú)控制的遞歸調(diào)用是棧溢出最典型的原因,通常來(lái)說(shuō),查看損壞了的線程的調(diào)用棧,找出它從哪里脫離控制就可以了。Visual Studio在這方面可以做的很好,那為什么還要用CDB呢?讓我們?cè)O(shè)想一個(gè)更復(fù)雜的例子。例如,假設(shè)我們的應(yīng)用程序中包含一個(gè)依賴(lài)遞歸的算法?我們?cè)谠O(shè)計(jì)算法時(shí)使用有符號(hào)數(shù),在所有可能的情形下控制遞歸的運(yùn)行,但某個(gè)時(shí)候棧仍溢出了。為什么?或許是因?yàn)樵谀撤N情況下,算法使用的某些函數(shù)占用了太多的棧空間。我們?cè)趺创_定函數(shù)占用的總的棧空間呢?不幸地是,Visual Studio調(diào)試器沒(méi)有簡(jiǎn)便的方法可以做到。
即使調(diào)用棧沒(méi)有顯示任何遞歸的跡象時(shí),應(yīng)用程序也可能會(huì)出現(xiàn)棧溢出異常。例如,查看StackOvfDemo.cpp例子。如果你編譯,并在調(diào)試器下運(yùn)行它,將立刻出現(xiàn)棧溢出。但此刻的調(diào)用棧看起來(lái)一切正常:
StackOvfDemo.exe!_woutput
StackOvfDemo.exe!wprintf
StackOvfDemo.exe!ProcessStringW
StackOvfDemo.exe!ProcessStrings
StackOvfDemo.exe!main
StackOvfDemo.exe!mainCRTStartup
KERNEL32.DLL!_BaseProcessStart@4
顯然,調(diào)用棧上的某個(gè)函數(shù)使用了太多的棧空間。但是我們?cè)趺凑页鲞@個(gè)函數(shù)呢?不用擔(dān)心,有了CDB的'kf'命令的幫助,可以顯示每個(gè)函數(shù)在調(diào)用棧上占用的字節(jié)數(shù)。在應(yīng)用程序還停在Visual Studio調(diào)試器里的時(shí)候,我們可以運(yùn)行下列命令:
cdb -pv -pn stackovfdemo.exe -logo out.txt -c "~*kf;q"
('kf'默認(rèn)顯示調(diào)用棧上最后的20幀,像我們?cè)?#8220;調(diào)試死鎖問(wèn)題”部分討論的那樣。如果你想多顯示一些,可以增加前綴,例如,~*kf1000。另外要注意的是,~*kf將報(bào)告所有線程的調(diào)用棧。如果應(yīng)用包含大量的線程,它就不太適合了,這時(shí),可以把它改成'~~[tid]kf', 'tid'是目標(biāo)線程的線程ID(例如,'~~[0x3a8]kf'))
這條命令顯示的內(nèi)容如下:
. 0 Id: 210.3a8 Suspend: 1 Teb: 7ffde000 Unfrozen
Memory ChildEBP RetAddr
00033440 0041aca5 StackOvfDemo!_woutput+0x22
44 00033484 00415eed StackOvfDemo!wprintf+0x85
d8 0003355c 00415cc5 StackOvfDemo!ProcessStringW+0x2d
fc878 0012fdd4 00415a44 StackOvfDemo!ProcessStrings+0xe5
108 0012fedc 0041c043 StackOvfDemo!main+0x64
e4 0012ffc0 7c4e87f5 StackOvfDemo!mainCRTStartup+0x183
30 0012fff0 00000000 KERNEL32!BaseProcessStart+0x3d
注意第一列的內(nèi)容??它報(bào)告棧上函數(shù)所占用的字節(jié)數(shù)。很顯然,ProcessStrings函數(shù)用了可用棧空間的最大份額,因此,它可能要為棧溢出負(fù)責(zé)。
如果你想知道ProcessStrings函數(shù)為什么需要如此多的棧空間,這里有一些解釋。這個(gè)函數(shù)使用ATL的A2W宏把字符串從ANSI格式轉(zhuǎn)換成Unicode格式,這個(gè)宏在內(nèi)部用_alloca函數(shù)在棧上分配內(nèi)存。用_alloca分配的內(nèi)存只有當(dāng)它的調(diào)用者(在這個(gè)例子里是ProcessStrings)返回后才被釋放。直到ProcessStrings返回控制之前,A2W(因此,也就是_alloca)在棧上為每個(gè)后續(xù)的調(diào)用分配另外的空間,這將迅速耗盡棧空間。
底線:不要在循環(huán)里使用_alloca。