WinDBG 的命令簡(難)潔(懂),多(難)樣(記),功(反)能(人)強(類)。許多同學(xué)都感覺它功能強大但是不容易上手。今天我介紹另一種方式希望對大家有所幫助。
WinDBG 是一個調(diào)試器,那么什么是調(diào)試呢?我的理解,調(diào)試可以認(rèn)為是確認(rèn)應(yīng)用程序在此刻的狀態(tài)正確與否。如果狀態(tài)正確,那么一切正常,如果狀態(tài)錯誤,那么說明應(yīng)用程序的執(zhí)行出現(xiàn)了問題。而應(yīng)用程序的狀態(tài)是什么呢?其實就是應(yīng)用程序在內(nèi)存中的表示。那么,如果我們能夠理解應(yīng)用程序在內(nèi)存中的構(gòu)成,那么我們就可以找到各種各樣的狀態(tài),從而判斷程序運行的正確性了。
因此,WinDBG 的命令是什么并不重要,重要的是你知道如何用這些命令探索應(yīng)用程序各種狀態(tài)之間的聯(lián)系。下面我們就先簡要的看一看應(yīng)用程序的狀態(tài)是什么樣子的(我以 CLR 的應(yīng)用程序為例)。
應(yīng)用程序在內(nèi)存中是這樣的
這篇文章的關(guān)鍵在于思路而非包羅萬象的介紹應(yīng)用程序在內(nèi)存中的表示。如果要深入探索,還需要多下下功夫。
應(yīng)用程序的最上層結(jié)構(gòu)是進(jìn)程,進(jìn)程中包含了正在執(zhí)行的若干線程。
- 一個線程實際上包含線程的上下文(例如一些安全上下文,當(dāng)前線程的線程內(nèi)存儲結(jié)構(gòu)等等),還有當(dāng)前線程的函數(shù)棧。
- 函數(shù)棧包含若干的調(diào)用棧幀。每一個調(diào)用棧幀實際上代表了一個函數(shù)調(diào)用。包含調(diào)用的地址,本地棧變量,參數(shù),以及引用的堆上的變量。
- 變量所引用的對象在內(nèi)存中也是有一定的結(jié)構(gòu)的。變量實際上就是一個托管的指針。
- 而對象包含對象頭部信息(同步信息塊、運行時類型信息),和對象本身的數(shù)據(jù)(成員變量)
- 運行時類型信息中又包含 MethodTable 信息。
- 而對象所在的堆可能是第 0 代托管堆、第 1 代托管堆和第 2 代托管堆。還有可能在 LOH 上(大對象堆)。
如果我們畫一張圖,那么這張圖就類似這樣:

調(diào)試就是一次旅游
那么調(diào)試就是從這張圖的任意一個點進(jìn)入,并且通過應(yīng)用程序運行時狀態(tài)的各個部分的關(guān)系,通過圖中的線進(jìn)行任意的游走,確定每一個部分是否正確的過程。
通過上述分析,那么我們只要解決兩個事情就可以了:
- 找一個入口點
- 在狀態(tài)的各個部分之間游走
我們不妨來嘗試一下。
找到一個入口點
現(xiàn)在請查一下 winDBG 的命令手冊,你可以從網(wǎng)上搜到各種各樣的資料。例如如下幾個:
- 用如下的命令可以找到 Thread(線程)相關(guān)的信息:
~、!runaway、!threads、~$thread id$s、!threadpool - 用如下的命令可以找到托管堆上存儲的變量的信息:
!dumpheap、!pe - 用如下的命令可以找到特定類型的 MethodTable 信息:
!name2ee - 在使用 sosex 擴展的情況下,可以用如下的命令獲得托管堆存儲的對象信息:
!gcgen、!dumpgen - 使用如下的命令可以直接獲得對象的同步對象的信息:
!synblk
在狀態(tài)的各個部分之間游走
現(xiàn)在我們有一個切入點了,那么我們就可以通過各個部分之間的關(guān)系將應(yīng)用程序執(zhí)行涉及到的信息一個不剩的找出來。例如:
- 如果想從線程的信息查看線程的調(diào)用棧的信息,可以先使用
~$thread id$s切換到線程的上下文,然后使用!clrstack命令查看調(diào)用棧信息。 - 如果想從調(diào)用棧查找某一個棧幀的局部變量和參數(shù)的話可以分別使用:
!clrstack -l和!clrstack -p - 如果想從變量的地址查看變量的存儲結(jié)構(gòu)那么可以使用
!dumpobj或者!dumparray命令。 - 如果有一個變量的地址但是想知道變量被那一個調(diào)用棧引用可以使用
!gcroot命令,再通過!clrstack確認(rèn)特定的棧幀。
旅游總結(jié)
綜合前兩個小標(biāo)題的內(nèi)容我們可以制定如下的旅游攻略:

此圖不全,僅僅是為了拋磚,希望各位能夠自己總結(jié),形成自己的知識體系。
旅游也有套路
應(yīng)用程序的內(nèi)部結(jié)構(gòu)還是有些復(fù)雜的。為了節(jié)省時間,大家都會總結(jié)各種套路。這就需要大家整理思路了。
例如,對于某次高的 CPU 占用。我選擇先從線程入手。首先使用 !runaway 命令查看線程的 CPU 時間。發(fā)現(xiàn) 20 多個線程沒完沒了的執(zhí)行了若干小時。而后通過 !clrstack 查看線程的調(diào)用棧發(fā)現(xiàn)這些線程的調(diào)用棧都在調(diào)用一個叫做 RenameXxx 的函數(shù)。
其實應(yīng)用程序很少會出現(xiàn)巧合,因此懷疑這個函數(shù)有問題。使用 !clrstack -p 查看了一下函數(shù)的參數(shù)和局部變量,發(fā)現(xiàn)一個參數(shù)含有一個字符串變量,其長度已達(dá) 1MB。使用 !dumpobj 發(fā)現(xiàn)其內(nèi)容為:fileName (1) (1) (1) (1) ... 至此基本斷定這個函數(shù)里有死循環(huán)的邏輯了。
網(wǎng)上還有很多既有的套路。例如 Tess 的博客。強烈建議各位讀者也用自己的博客、小本子、Evernote、OneNote 等各種媒介記錄自己的旅游套路。不知不覺你就會成為 winDBG 調(diào)試的老司機。
其實,這篇博客不光是為 winDBG 寫的。