這一部分主要介紹用戶態調試相關的知識和工具。包括:彙編、異常(exception)、內存佈局、堆(heap)、棧(stack)、CRT(C Runtime)、handle/Criticalsection/thread context/windbg/ dump/live debug和Dr Watson等。
書中不會對知識點作全面的介紹,而是針對知識點在調試中過程中應該如何使用進行說明。知識點本身在下面兩本書中有非常詳細的介紹:
Programming Applications for Microsoft Windows
Debugging Applications for Windows
2.1 排錯的工具:調試器Windbg
本節介紹調試器Windbg的相關知識。Windbg的使用貫穿本書很多章節,它是分析問題的高效工具。
Windbg的下載地址是:
Install Debugging Tools for Windows 32-bit Version
http://www.microsoft.com/whdc/devtools/debugging/installx86.mspx
建議安裝到C:/Debuggers目錄,後面的例子默認用這個目錄。
開發人員寫完代碼,通常會在Visual Studio中按F5直接進行調試。使用Visual Studio自帶調試器能夠非常方便地在源代碼上設定斷點,檢查程序的中間變量,單步驟執行。在完成代碼階段,Visual Studio自帶的調試器能夠非常方便地做源代碼級別的排錯。
Visual Studio調試器的典型用例是源代碼級別的排錯。與其相比,Windbg並不是一款針對特殊用例的調試器。Windbg提供了一個GUI界面,也可以在源代碼上直接用F5設定斷點,但更多的情況下,調試人員會直接用文本的方式輸入調試命令,Windbg執行對應的操作,用文本的方式返回對應的結果。Windbg的調試命令覆蓋了Windows平臺提供的所有調試功能。
本節首先對調試器和符號文件作大致的介紹,然後針對常用的Windbg調試命令作演示。接下來介紹Windbg中強大而靈活的條件斷點,最後介紹調試器目錄下的相關工具。
對調試器深入的瞭解後,相信讀者就能體會到Windbg和Visual Studio調試器設計上的區別,選用最合適的調試器來解決問題。
書中不會從Windbg的基本使用方法說起,而是着重介紹調試器原理,常用的命令,Windbg的高級用法和相關的工具。如果讀者從來沒有使用過Windbg,下面的文章可以提供幫助:
DebugInfo:
http://www.debuginfo.com/
Windows Debuggers: Part 1: A WinDbg Tutorial
http://www.codeproject.com/debug/windbg_part1.asp
2.1.1 調試器的功能:檢查代碼和資料,保存dump文件,控制程序的執行
調試器,無論是Visual Studio調試器還是Windbg,都是用來觀察和控制目標進程的工具。對於用戶態的進程,調試器可以查看用戶態內存空間和寄存器上的資料。對於不同類型的數據和代碼,調試器能方便地把這些信息用特定的格式區分和顯示出來。調試器還可以把一個目標進程某一時刻的所有信息寫入一個文件(dump),直接打開這個文件分析。調試器還可以通過設置斷點的機制來控制目標程序什麼時候停下來接受檢查,什麼時候繼續運行。
關於調試器的工作原理,請參考Debugging Applications for Windows這本書。
Windbg及其相關工具的下載地址:
http://www.microsoft.com/whdc/devtools/debugging/installx86.mspx
在安裝好Windbg後,可以在windbg.exe的主窗口按F1彈出幫助。這是瞭解和使用Windbg的最好文檔。每個命令的詳細說明,都可以在裏面找到。
調試器可以直觀地看到下面一些信息:
l 進程運行的狀態和系統狀態,比如進程運行了多少時間,環境變量是什麼。
l 當前進程加載的所有EXE/DLL的詳細信息。
l 某一個地址上的彙編指令。
l 查看內存地址的內容和屬性,比如是否可寫。
l 每個的call stack(需要symbol)。
l Call stack上每個函數的局部變量。
l 格式化地顯示程序中的數據結構(需要symbol)。
l 查看和修改內存地址上的資料或者寄存器上的資料。
l 部分操作系統管理的數據結構,比如Heap、Handle、CriticalSection等。
在Visual Studio調試器中,要查看上面的信息,需要在很多調試窗口中切換。而在Windbg中,只需要簡單的命令就可以完成。
調試器的另外一個作用是設定條件斷點。可以設定在某一個指令地址上停下來,也可以設定當某一個內存地址等於多少的時候停下來,或者當某一個exception/notification發生的時候停下來。還可以進入一個函數調用的時候停下來,或跳出當前函數調用的時候停下來。停下來後可以讓調試器自動運行某些命令,記錄某些信息,然後讓調試器自動判斷某些條件來決定是否要繼續運行。通過簡單的條件斷點功能,可以很方便地實現下面一些任務:
l 當某一個函數被調用的時候,在調試器輸出窗口中打印出函數參數。
l 計算某一個變量被修改了多少次。
l 監視一個函數調用了哪些子函數,分別被調用了多少次。
l 每次拋C++異常的時候自動產生dump文件。
在Visual Studio調試器中也能夠設定條件斷點,但靈活性和功能遠不能跟Windbg相比。
2.1.2 符號文件(Symbol file),把二進制和源代碼對應起來
當用VC/VB編譯生成EXE/DLL後,往往會同時生成PDB文件。PDB裏面包含的是EXE/DLL的符號信息。
符號是指代碼中使用到的類型和名字。比如下面這些都是符號包含的內容:
l 代碼所定義的Class的名字,Class的所有成員的名字和所有成員的類型。
l 變量的名字和變量的類型。
l 函數的名字,函數所有參數的名字和類型,以及函數的返回值。
PDB文件除了包含符號外,還負責把符號和該符號所處的二進制地址聯繫起來。比如有一個全局變量叫做gBuffer,PDB文件不僅僅記錄了gBuffer的類型,還能讓調試器找到保存gBuffer的內存地址。
有了符號文件,當在調試器中試圖讀取某一個內存地址的時候,調試器會嘗試在對應的PDB文件中配對,看這個內存地址是否有符號對應。如果能夠找到,調試器就可以把對應的符號顯示出來。這樣,極大程度上方便了開發人員的觀察。對於操作系統EXE/DLL,微軟也提供了對應的符號文件下載地址。
默認情況下,符號文件中包含了所有的結構、函數,以及對應的源代碼信息。微軟提供的Windows符號文件去掉了源代碼信息、函數參數定義和一些內部數據結構的定義。
2.1.3 一個簡單的上手程序
接下來用一個簡單的例子演示一下Windbg的基本使用。下面這段代碼的目的是把字符串"6969,3p3p"中的所有3都修改爲4。
#include "stdafx.h"
#include "stdlib.h"
char* getcharBuffer()
{
return "6969,3p3p";
}
void changeto4p(char * buffer)
{
while(*buffer)
{
if(*buffer == '3')
*buffer='4';
buffer++;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
printf("%s/n","Any key continue...");
getchar();
char *str=getcharBuffer();
changeto4p(str);
printf("%s",str);
return 0;
}
這段代碼會導致崩潰。崩潰後看到的接口如圖2.1所示。
圖2.1
接下來,一起用Windbg來看看上述對話框的具體含義是什麼。
在啓動Windbg調試以前,首先把程序對應的PDB文件放到一個指定的文件夾。上面程序的EXE叫做crashscreen-shot.exe,把編譯時候生成的crashscreen-shot.pdb文件拷貝到C:/PDB文件夾。同時把程序的主CPP文件拷貝到C:/SRC文件夾。
接下來啓動Windbg。像用Visual Studio調試程序一樣,我們需要在調試器中運行對應的EXE。所以在Windbg的主窗口中,使用File→Open Executable菜單找到crashscreen-shot.exe,然後打開。
Windbg不會讓目標進程立刻開始運行。相反,Windbg這時會停下來,讓用戶有機會對進程啓動過程進行排錯,或者進行一些準備工作,比如設定斷點,如圖2.2所示。
圖2.2
上面的主窗口就是Windbg輸出結果的地方。下面的0:000>提示符後面是用戶輸入命令的地方。輸入命令g, 讓程序繼續運行,如圖2.3所示。
圖2.3
從程序的輸出可以看到,程序已經開始運行了,在等待用戶的輸入。如果要用調試器讓程序暫停接受檢查,可以在Windbg中用Ctrl + Break快捷鍵,或者用Debug→Break命令完成,如圖2.4所示。
圖2.4
接下來讓我們在getcharBuffer和changeto4p函數上分別設定斷點。要通過函數名設定斷點,首先要讓Windbg加載對應的PDB文件。通過Windbg的File→Symbol File Path菜單可以設定PDB文件的搜索路徑。通過這個菜單我們把路徑設定到C:/PDB。設定好了後,接下來可以用x命令找到程序中getcharBuffer和changeto4p函數的二進制入口地址。找到地址後,就可以用bp命令在這兩個地址上設定斷點了,如圖2.5所示。
圖2.5
設定好斷點後,繼續用g命令恢復程序的執行,輸入任一鍵後,會看到程序在getcharBuffe斷點上停下,如圖2.6所示。
圖2.6
輸入k命令,可以檢查當前的callstack,如圖2.7所示。
圖2.7
從上面的輸出可以看到,__tmainCRTStartup函數調用了wmain函數,然後wmain函數調用了getcharBuf函數。同時還可以看到每個函數的源代碼路徑,以及函數對應的行數。
Windbg也同時支持源代碼級別的調試。通過File→Open菜單,打開放到C:/SRC中的源代碼文件,看到Windbg UI變爲如圖2.8所示。
圖2.8
左邊窗口是對應的源代碼,右邊窗口是Windbg的輸入和輸出。由於已經通過了bp命令在getcharBuffer和changeto4p函數上設定了斷點,所以右邊窗口對應函數前有紅色小方塊表示斷點。
除了使用bp命令,還可以直接在右邊窗口中用鼠標定位光標,然後摁F5設定條件斷點。如果要單步執行,可以像在Visual Studio裏一樣用F10或者F11。
接下來繼續調試。首先輸入bc 1命令清除第二個斷點,然後輸入命令g讓程序繼續運行,會發現調試器會停在如圖2.9中所示的位置。
左邊窗口中changeto4p前的紅色方塊消失,原因是我們取消了第二個斷點。左邊*buffer=’4’反藍顯示,表示這一行是當前進程正在執行指令對應的源代碼。
右邊窗口中下面這3行表明了調試器暫停進程執行的原因是發生了Access Violation,就是訪問違例錯誤:
(df8.9a0): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
圖2.9
下面的兩行輸出說明當前導致問題的指令位於004117f6地址上,是一個mov指令。該指令正把十六進制值36(就是4的ASCII)寫入EAX寄存器指向的內存。該指令位於函數crashscreen_shot!changeto4p入口偏移0x36的地方。
crashscreen_shot!changeto4p+0x36:
004117f6 c60034 mov byte ptr [eax],34h ds:0023:004157a5=33
該條指令其實就是導致崩潰的指令。指令位於地址0x004117f6上,跟崩潰時看到的錯誤截圖中所描述的指令地址一樣。
該指令試圖寫內存的時候發生了問題。用DC命令可以看目標內存(EAX)上的數據是什麼(圖2.10)。
dc命令的輸出結果是:
0:000> dc eax
004157a5 70337033 25000000 00000a73 00000000 3p3p...%s.......
004157b5 5f000000 6e005f00 74006100 76006900 ..._._.n.a.t.i.v
004157c5 5f006500 74007300 72006100 75007400 .e._.s.t.a.r.t.u
004157d5 5f007000 74007300 74006100 20006500 .p._.s.t.a.t.e.
004157e5 3d003d00 5f002000 69005f00 69006e00 .=.=. ._._.i.n.i
004157f5 69007400 6c006100 7a006900 64006500 .t.i.a.l.i.z.e.d
00415805 00000000 00000000 00000000 00000000 ................
00415815 53000000 6b636174 6f726120 20646e75 ...Stack around
圖2.10
該輸出的左邊一列是內存地址,第一個地址是需要顯示的起始地址004157a5,接下來每一行地址的間距是4個DWORD。右邊中間內存地址上的資料,用DWORD方式顯示。最右邊是這些數據對應的ASCII字符。這裏看到,eax指向的地址是004157a5,值是0x70337033,對應的ASCII資料是3p3p,跟程序設計相吻合。同時該內存地址也跟崩潰時看到的錯誤界面中所描述的指令地址一樣。
接下來!address命令的輸出就有點讓人沮喪了。Windbg提示對應的Symbol沒有設定正確。!address命令可以檢查對應內存頁的屬性,而內存頁的屬性是操作系統維護的。所以這裏需要加載操作系統模塊的PDB文件。在我們的C:/PDB目錄中,並沒有操作系統的PDB文件,所以命令無法正常執行。微軟提供了操作系統對應的PDB文件的下載地址,下面一篇文章介紹了設定的方法:
http://www.microsoft.com/whdc/devtools/debugging/debugstart.mspx
設定好了後,重新運行!address命令,得到正確的輸出(圖2.11)。
注意看,輸出的結果是:
0:000> !address eax
00400000 : 00415000 - 00002000
Type 01000000 MEM_IMAGE
Protect 00000002 PAGE_READONLY
State 00001000 MEM_COMMIT
Usage RegionUsageImage
FullPath crashscreen-shot.exe
由於這塊內存是隻讀的,所以mov指令會導致崩潰。
圖2.11
Visual Studio調試器中能方便地檢查局部變量的值。在Windbg中,可以通過x命令完成,或者通過View→Locals菜單打開局部變量窗口查看(圖2.12)。
圖2.12
從這個例子中可以看到,Windbg除了能用多種方式完成基本的斷點設定,單步執行,變量檢查,檢查內存資料,顯示callstack外,還可以看到系統相關的更多信息,比如內存頁的屬性,這是Visual Studio做不到的。後面的例子會演示更多的Windbg命令,包括線程切換,顯示DLL信息,反彙編,搜索內存,異常上下文恢復和複雜的條件斷點。
至於程序的“69694p4p”爲何在只讀的存儲器上,請參考:
What's wrong with you ? char *p
http://www.vchelp.net/itbookreview/view_paper.asp?paper_id=534
2.1.4 用Internet Explorer來操練調試器的基本命令
下面用Internet Explorer作爲目標進程演示Windbg中更多的調試命令。
用Windbg來調試目標進程,有兩種方法,分別是通過調試器啓動,和用調試器直接監視(attach)正在運行的進程。
通過File→Open Executable菜單,可以選擇對應的EXE在調試器中啓動。通過File→Attach to a process可以選擇一個正在運行的進程進行調試。
打開IE,訪問www.msdn.com, 然後啓動Windbg,按F6,選擇剛剛啓動的(最下面)iexplorer.exe進程。
IE的PDB文件也需要從微軟的網站上下載。具體做法請參考上一節的鏈接。在我本地,我的symbol路徑設定如下:
SRV*D:/websymbols*http://msdl.microsoft.com/download/symbols;D:/MyAppSymbol
這裏的D:/websymbols目錄是用來保存從msdl.microsoft.com上自動下載的操作系統符號文件。而我自己編譯生成的符號文件,我都手動拷貝到D:/MyAppSymbol路徑下。
接下來,在Windbg的命令窗口中(如果看不到可以用Alt+1打開),運行下面命令。
vertarget檢查進程概況
vertarget命令顯示當前進程的大致信息:
0:026> vertarget
Windows Server 2003 Version 3790 (Service Pack 1) MP (2 procs) Free x86 compatible
Product: Server, suite: Enterprise TerminalServer SingleUserTS
kernel32.dll version: 5.2.3790.1830 (srv03_sp1_rtm.050324-1447)
Debug session time: Thu Apr 27 13:53:50.414 2006 (GMT+8)
System Uptime: 15 days 1:59:13.255
Process Uptime: 0 days 0:07:34.508
Kernel time: 0 days 0:00:01.109
User time: 0 days 0:00:00.609
上面的0:026>是命令提示符,026表示當前的線程ID。後面會介紹切換線程的命令,到時候就可以看到提示符的變化。
跟大多數的命令輸出一樣,vertarget的輸出非常明白直觀,顯示當前系統的版本和運行時間。
!peb 顯示Process Environment Block
接着可以用!peb命令來顯示Process Environment Block。由於輸出太長,這裏就省略了。
lmvm 檢查模塊的加載信息
用lmvm命令可以看任意一個DLL/EXE的詳細信息,以及symbol的情況:
0:026> lmvm msvcrt
start end module name
77ba0000 77bfa000 msvcrt (deferred)
Image path: C:/WINDOWS/system32/msvcrt.dll
Image name: msvcrt.dll
Timestamp: Fri Mar 25 10:33:02 2005 (4243785E)
CheckSum: 0006288A
ImageSize: 0005A000
File version: 7.0.3790.1830
Product version: 6.1.8638.1830
File flags: 0 (Mask 3F)
File OS: 40004 NT Win32
File type: 1.0 App
File date: 00000000.00000000
Translations: 0409.04b0
CompanyName: Microsoft Corporation
ProductName: Microsoft® Windows® Operating System
InternalName: msvcrt.dll
OriginalFilename: msvcrt.dll
ProductVersion: 7.0.3790.1830
FileVersion: 7.0.3790.1830 (srv03_sp1_rtm.050324-1447)
FileDescription: Windows NT CRT DLL
LegalCopyright: © Microsoft Corporation. All rights reserved.
命令的第二行顯示deferred,表示目前並沒有加載msvcrt的symbol,可以用.reload命令來加載。在加載前,可以用!sym命令來打開symbol加載過程的詳細輸出:
.reload / !sym 加載符號文件
默認情況下,調試器不會加載所有的symbol文件。只有某個調試器命令需要使用symbol的時候,調試器纔在設定的符號文件路徑中檢查和加載。!sym命令可以讓調試器在自動尋找symbol的時候給出詳細的信息,比如搜索和下載的路徑。.reload命令可以讓調試器加載指定模塊的symbol。
0:026> !sym noisy
noisy mode - symbol prompts on
0:026> .reload /f msvcrt.dll
SYMSRV: msvcrt.pd_ from http://msdl.microsoft.com/download/symbols: 80847 bytes copied
DBGHELP: msvcrt - public symbols
c:/websymbols/msvcrt.pdb/62B8BDC3CC194D2992DCFAED78B621FC1/msvcrt.pdb
0:026> lmvm msvcrt
start end module name
77ba0000 77bfa000 msvcrt (pdb symbols) c:/websymbols/msvcrt.pdb/62B8BDC3CC194D2992DCFAED78B621FC1/msvcrt.pdb
Loaded symbol image file: C:/WINDOWS/system32/msvcrt.dll
Image path: C:/WINDOWS/system32/msvcrt.dll
Image name: msvcrt.dll
Timestamp: Fri Mar 25 10:33:02 2005 (4243785E)
CheckSum: 0006288A
ImageSize: 0005A000
File version: 7.0.3790.1830
Product version: 6.1.8638.1830
File flags: 0 (Mask 3F)
File OS: 40004 NT Win32
File type: 1.0 App
File date: 00000000.00000000
Translations: 0409.04b0
CompanyName: Microsoft Corporation
ProductName: Microsoft® Windows® Operating System
InternalName: msvcrt.dll
OriginalFilename: msvcrt.dll
ProductVersion: 7.0.3790.1830
FileVersion: 7.0.3790.1830 (srv03_sp1_rtm.050324-1447)
FileDescription: Windows NT CRT DLL
LegalCopyright: © Microsoft Corporation. All rights reserved.
可以看到,symbol從msdl.microsoft.com自動下載後加載。
lmf 列出當前進程中加載的所有模塊
lmf命令可以列出當前進程中加載的所有DLL文件和對應的路徑:
0:018> lmf
start end module name
00d40000 00dda000 iexplore C:/Program Files/Internet Explorer/iexplore.exe
04320000 043c9000 atiumdva C:/Windows/system32/atiumdva.dll
10000000 1033d000 googletoolbar2 c:/program files/google/googletoolbar2.dll
37f00000 37f0f000 Cjktl32 E:/Program Files/Powerword 2003/Cjktl32.dll
r,d,e 寄存器,內存的檢查和修改
r命令顯示和修改寄存器上的值。
d命令顯示內存地址上的值。
e命令修改內存地址上的值。
顯示寄存器:
0:018> r
eax=7ffdc000 ebx=00000000 ecx=00000000 edx=7707f06d esi=00000000 edi=00000000
eip=77032ea8 esp=054efc14 ebp=054efc40 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!DbgBreakPoint:
77032ea8 cc int 3
如果需要修改寄存器,比如把eax的值修改爲0x0,可以用 r eax=0。
用d命令顯示esp 寄存器指向的內存,默認爲byte格式。
0:018> d esp
054efc14 a9 f0 07 77 e9 ef 4e 05-00 00 00 00 00 00 00 00 ...w..N.........
054efc24 00 00 00 00 18 fc 4e 05-00 00 00 00 7c fc 4e 05 ......N.....|.N.
054efc34 f2 8b ff 76 a1 f5 03 77-00 00 00 00 4c fc 4e 05 ...v...w....L.N.
054efc44 33 38 b4 75 00 00 00 00-8c fc 4e 05 bd a9 02 77 38.u......N....w
054efc54 00 00 00 00 25 ef 4e 05-00 00 00 00 00 00 00 00 ....%.N.........
054efc64 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
054efc74 58 fc 4e 05 00 00 00 00-ff ff ff ff f2 8b ff 76 X.N............v
054efc84 a1 e2 03 77 00 00 00 00-00 00 00 00 00 00 00 00 ...w............
用dd命令直接指定054efc14 地址,第二個d表示用DWORD格式。除了DWORD外,還有db(byte),du(Unicode),dc(char)等等。詳細信息請參考幫助文檔中d命令的說明。
0:018> dd 054efc14
054efc14 7707f0a9 054eefe9 00000000 00000000
054efc24 00000000 054efc18 00000000 054efc7c
054efc34 76ff8bf2 7703f5a1 00000000 054efc4c
054efc44 75b43833 00000000 054efc8c 7702a9bd
054efc54 00000000 054eef25 00000000 00000000
054efc64 00000000 00000000 00000000 00000000
054efc74 054efc58 00000000 ffffffff 76ff8bf2
054efc84 7703e2a1 00000000 00000000 00000000
e命令可以用來修改內存地址。跟d命令一樣,e命令後面也可以跟類型後綴。比如ed命令表示用DWORD的方式修改。下面的命令把054efc14 地址上的值修改爲11112222。
0:018> ed 054efc14 11112222
修改完成後,用dd命令檢查054efc14 地址上的值。後面的 L4參數指定內存區間的長度長度爲4個DWORD。這樣輸出就只有1行,而不是默認的8行。
0:018> dd 054efc14 L4
054efc14 11112222 40a15c00 00000000 40a15c00
有了上面幾個命令,就可以訪問和修改當前進程中的所有內存。這些命令的參數和格式非常靈活,詳細內容參考幫助文檔。
!address顯示內存頁信息
該命令在前面的例子中就用到過,可以顯示某一個地址上的頁信息:
0:001> !address 7ffde000
7ffde000 : 7ffde000 - 00001000
Type 00020000 MEM_PRIVATE
Protect 00000004 PAGE_READWRITE
State 00001000 MEM_COMMIT
Usage RegionUsagePeb
如果不帶參數,可以顯示更詳細的統計信息。
S 搜索內存
S命令可以搜索內存。比如用在下面的地方:
4. 尋找內存泄漏的線索。比如知道當前內存泄漏的內容是一些固定的字符串,就可以在 DLL區域搜索這些字符串出現的地址,然後再搜索這些地址用到什麼代碼中,找出這些內存是從什麼地方開始分配的。
5. 尋找錯誤代碼的根源。比如知道當前程序返回了0x80074015這樣的一個代碼,但是不知道這個代碼是由哪一個內層函數返回的,就可以在代碼區搜索0x80074015,找到可能返回這個代碼的函數。
下面就是訪問sina.com的時候,用Windbg搜索ie裏面www.sina.com.cn的結果:
0:022> s -u 0012ff40 L?80000000 "www.sina.com.cn"
001342a0 0077 0077 0077 002e 0073 0069 006e 0061 w.w.w...s.i.n.a.
00134b82 0077 0077 0077 002e 0073 0069 006e 0061 w.w.w...s.i.n.a.
00134f2e 0077 0077 0077 002e 0073 0069 006e 0061 w.w.w...s.i.n.a.
0013570c 0077 0077 0077 002e 0073 0069 006e 0061 w.w.w...s.i.n.a.
結合S命令和前面介紹的修改內存命令,根本不需要用什麼金山遊俠就可以查找/修改遊戲中主角的生命了 :-)
接下來,看看跟線程相關的命令。
!runaway 檢查線程的CPU消耗
!runaway可以顯示每一個線程所耗費usermode CPU時間的統計信息:
0:001> !runaway
User Mode Time
Thread Time
0:83c 0 days 0:00:00.406
13:bd4 0 days 0:00:00.046
10:ac8 0 days 0:00:00.046
24:4f4 0 days 0:00:00.031
11:d8c 0 days 0:00:00.015
26:109c 0 days 0:00:00.000
25:1284 0 days 0:00:00.000
23:12cc 0 days 0:00:00.000
22:16c0 0 days 0:00:00.000
21:57c 0 days 0:00:00.000
20:c00 0 days 0:00:00.000
19:14e8 0 days 0:00:00.000
18:1520 0 days 0:00:00.000
16:9dc 0 days 0:00:00.000
15:1654 0 days 0:00:00.000
14:13f4 0 days 0:00:00.000
9:104c 0 days 0:00:00.000
8:1760 0 days 0:00:00.000
7:cc8 0 days 0:00:00.000
6:530 0 days 0:00:00.000
5:324 0 days 0:00:00.000
4:178c 0 days 0:00:00.000
3:1428 0 days 0:00:00.000
2:1530 0 days 0:00:00.000
1:448 0 days 0:00:00.000
上面輸出的第一列是線程編號和線程id。後一列對應的是該線程在用戶態模式中的總繁忙時間。在該命令加上f參數,還可以看到內核態的繁忙時間。當進程內存佔用率高的時候,通過該命令可以方便地找到對應的繁忙線程。
~ 切換目標線程
用~命令,可以顯示線程信息和在不同線程之間切換:
0:001> ~
0 Id: c0.83c Suspend: 1 Teb: 7ffdd000 Unfrozen
. 1 Id: c0.448 Suspend: 1 Teb: 7ffdb000 Unfrozen
2 Id: c0.1530 Suspend: 1 Teb: 7ffda000 Unfrozen
3 Id: c0.1428 Suspend: 1 Teb: 7ffd9000 Unfrozen
4 Id: c0.178c Suspend: 1 Teb: 7ffd8000 Unfrozen
5 Id: c0.324 Suspend: 1 Teb: 7ffdc000 Unfrozen
6 Id: c0.530 Suspend: 1 Teb: 7ffd7000 Unfrozen
7 Id: c0.cc8 Suspend: 1 Teb: 7ffd6000 Unfrozen
8 Id: c0.1760 Suspend: 1 Teb: 7ffd5000 Unfrozen
9 Id: c0.104c Suspend: 1 Teb: 7ffd4000 Unfrozen
10 Id: c0.ac8 Suspend: 1 Teb: 7ffd3000 Unfrozen
11 Id: c0.d8c Suspend: 1 Teb: 7ff9f000 Unfrozen
13 Id: c0.bd4 Suspend: 1 Teb: 7ff9d000 Unfrozen
14 Id: c0.13f4 Suspend: 1 Teb: 7ff9c000 Unfrozen
15 Id: c0.1654 Suspend: 1 Teb: 7ff9b000 Unfrozen
16 Id: c0.9dc Suspend: 1 Teb: 7ff9a000 Unfrozen
18 Id: c0.1520 Suspend: 1 Teb: 7ff96000 Unfrozen
19 Id: c0.14e8 Suspend: 1 Teb: 7ff99000 Unfrozen
20 Id: c0.c00 Suspend: 1 Teb: 7ff97000 Unfrozen
21 Id: c0.57c Suspend: 1 Teb: 7ff95000 Unfrozen
22 Id: c0.16c0 Suspend: 1 Teb: 7ff94000 Unfrozen
23 Id: c0.12cc Suspend: 1 Teb: 7ff93000 Unfrozen
24 Id: c0.4f4 Suspend: 1 Teb: 7ff92000 Unfrozen
25 Id: c0.1284 Suspend: 1 Teb: 7ff91000 Unfrozen
26 Id: c0.109c Suspend: 1 Teb: 7ff90000 Unfrozen
0:001> ~0s
eax=0013e7c4 ebx=00000000 ecx=0013e7c4 edx=0000000b esi=001642e8 edi=00000000
eip=7c82ed54 esp=0013eb3c ebp=0013ed98 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
ntdll!KiFastSystemCallRet:
7c82ed54 c3 ret
0:000>
上面的~0s命令,把當前線程切換到0號線程,也就是主線程。切換後提示符會變爲0:000。
k,kb,kp,kv,kn 檢查call stack
k命令顯示當前線程的call stack。跟d命令一樣,k後面可以跟很多後綴,比如kb、kp、kn、kv、kL等。這些後綴控制了顯示的格式和信息量。具體信息請參考幫助文檔和動手實踐。
0:000> k
ChildEBP RetAddr
0013eb38 7739d02f ntdll!KiFastSystemCallRet
0013ed98 75ecb30f USER32!NtUserWaitMessage+0xc
0013ee24 75ed7ce5 BROWSEUI!BrowserProtectedThreadProc+0x44
0013fea8 779ac61e BROWSEUI!SHOpenFolderWindow+0x22c
0013fec8 0040243d SHDOCVW!IEWinMain+0x129
0013ff1c 00402748 iexplore!WinMain+0x316
0013ffc0 77e523cd iexplore!WinMainCRTStartup+0x186
0013fff0 00000000 kernel32!BaseProcessStart+0x23
可以結合~和k命令,來顯示所有線程的call stack. 輸入~*k試一下。
u 反彙編
u命令把指定地址上的代碼翻譯成彙編輸出。
0:000> u 7739d023
USER32!NtUserWaitMessage:
7739d023 b84a120000 mov eax,0x124a
7739d028 ba0003fe7f mov edx,0x7ffe0300
7739d02d ff12 call dword ptr [edx]
7739d02f c3 ret
如果符號文件加載正確,可以用uf命令直接反彙編整個函數,比如uf USER32! NtUserWaitMessage。
x 查找符號的二進制地址
有了符號文件,調試器就能查找源代碼符號和該符號所處的二進制地址之間的映射。如果要找一個符號保存在什麼二進制地址上,可以用x命令:
0:000> x msvcrt!printf
77bd27c2 msvcrt!printf = <no type information>
上面的命令找到了printf函數的入口地址在77bd27c2。
0:001> x ntdll!GlobalCounter
7c99f72c ntdll!GlobalCounter = <no type information>
上面的命令表示ntdll!GlobalCounter這個變量保存的地址是7c99f72c。
(注意: 符號對應的是變量和變量所在的地址,不是變量的值。上面的命令不是說ntdll!GlobalCounter這個變量的值是7c99f72c。要找到變量的值,需要用d命令讀取內存地址來獲取。)
x命令還支持通配符,比如 x ntdll!*命令列出ntdll模塊中所有的符號,以及對應的二進制地址。由於輸出太長,這裏就省略了。
dds 對應二進制地址的符號
dds打印內存地址上的二進制值,同時自動搜索二進制值對應的符號。比如要看看當前stack 中保存了哪些函數地址,就可以檢查ebp指向的內存:
0:000> dds ebp
0013ed98 0013ee24
0013ed9c 75ecb30f BROWSEUI!BrowserProtectedThreadProc+0x44
0013eda0 00163820
0013eda4 0013ee50
0013eda8 00163820
0013edac 00000000
0013edb0 0013ee10
0013edb4 75ece83a BROWSEUI!__delayLoadHelper2+0x23a
0013edb8 00000005
0013edbc 0013edcc
0013edc0 0013ee50
0013edc4 00163820
0013edc8 00000000
0013edcc 00000024
0013edd0 75f36d2c BROWSEUI!_DELAY_IMPORT_DESCRIPTOR_SHELL32
0013edd4 75f3a184 BROWSEUI!_imp__SHGetInstanceExplorer
0013edd8 75f36e80 BROWSEUI!_sz_SHELL32
0013eddc 00000001
0013ede0 75f3726a BROWSEUI!urlmon_NULL_THUNK_DATA_DLN+0x116
0013ede4 7c8d0000 SHELL32!_imp__RegCloseKey <PERF> (SHELL32+0x0)
0013ede8 7c925b34 SHELL32!SHGetInstanceExplorer
這裏dds命令從ebp指向的內存地址0013ed98 開始打印。第1列是內存地址的值,第2列是地址上對應的二進制數據,第3列是二進制數據對應的符號。上面的命令自動找到了75ecb30f對應的符號是BROWSEUI!BrowserProtectedThreadProc+0x44。
COM Interface和C++ Vtable裏面的成員函數都是順序排列的,所以dds命令可以方便地找到虛函數表中具體的函數地址。比如用下面的命令可以找到OpaqueDataInfo類型中虛函數對應的實際函數地址。
首先用x命令找到OpaqueDataInfo虛函數表地址:
0:000> x ole32!OpaqueDataInfo::`vftable'
7768265c ole32!OpaqueDataInfo::`vftable' = <no type information>
77682680 ole32!OpaqueDataInfo::`vftable' = <no type information>
接下來dds命令可以打印出虛函數表中的函數名字:
0:000> dds 7768265c
7768265c 77778245 ole32!ServerLocationInfo::QueryInterface
77682660 77778254 ole32!ScmRequestInfo::AddRef
77682664 77778263 ole32!ScmRequestInfo::Release
77682668 77779d26 ole32!OpaqueDataInfo::Serialize
7768266c 77779d3d ole32!OpaqueDataInfo::UnSerialize
77682670 77779d7a ole32!OpaqueDataInfo::GetSize
77682674 77779dcb ole32!OpaqueDataInfo::GetCLSID
77682678 77779deb ole32!OpaqueDataInfo::SetParent
7768267c 77779e18 ole32!OpaqueDataInfo::SerializableQueryInterface
77682680 777799b5 ole32!InstantiationInfo::QueryInterface
77682684 77689529 ole32!ServerLocationInfo::AddRef
77682688 776899cc ole32!ScmReplyInfo::Release
7768268c 77779bcd ole32!OpaqueDataInfo::AddOpaqueData
77682690 77779c43 ole32!OpaqueDataInfo::GetOpaqueData
77682694 77779c99 ole32!OpaqueDataInfo::DeleteOpaqueData
77682698 776a8cf6 ole32!ServerLocationInfo::GetRemoteServerName
7768269c 776aad96 ole32!OpaqueDataInfo::GetAllOpaqueData
776826a0 77777a3b ole32!CDdeObject::COleObjectImpl::GetClipboardData
776826a4 00000021
776826a8 77703159 ole32!CClassMoniker::QueryInterface
776826ac 77709b01 ole32!CErrorObject::AddRef
776826b0 776edaff ole32!CClassMoniker::Release
776826b4 776ec529 ole32!CClassMoniker::GetUnmarshalClass
776826b8 776ec546 ole32!CClassMoniker::GetMarshalSizeMax
776826bc 776ec589 ole32!CClassMoniker::MarshalInterface
776826c0 77702ca9 ole32!CClassMoniker::UnmarshalInterface
776826c4 776edbe1 ole32!CClassMoniker::ReleaseMarshalData
776826c8 776e5690 ole32!CDdeObject::COleItemContainerImpl::LockContainer
776826cc 7770313b ole32!CClassMoniker::QueryInterface
776826d0 7770314a ole32!CClassMoniker::AddRef
776826d4 776ec5a8 ole32!CClassMoniker::Release
776826d8 776ec4c6 ole32!CClassMoniker::GetComparisonData
2.1.5 檢查程序資料的小例子
下面用一個小例子來演示如何具體地觀察程序中的數據結構。
首先在debug模式下編譯並且按Ctrl+F5運行下面的代碼:
struct innner
{
char arr[10];
};
class MyCls
{
private:
char* str;
innner inobj;
public:
void set(char* input)
{
str=input;
strcpy(inobj.arr,str);
}
int output()
{
printf(str);
return 1;
}
void hold()
{
getchar();
}
};
void foo1()
{
MyCls *pcls=new MyCls();
void *rawptr=pcls;
pcls->set("abcd");
pcls->output();
pcls->hold();
};
void foo2()
{
printf("in foo2/n");
foo1();
};
void foo3()
{
printf("in foo3/n");
foo2();
};
int _tmain(int argc, _TCHAR* argv[])
{
foo3();
return 0;
}
當console等待輸入的時候,啓動Windbg,然後用F6加載目標進程。
用~0s命令切換到主線程,查看callstack:
0:000> knL
# ChildEBP RetAddr
00 0012f7a0 7c821c94 ntdll!KiFastSystemCallRet
01 0012f7a4 7c836066 ntdll!NtRequestWaitReplyPort+0xc
02 0012f7c4 77eaaba3 ntdll!CsrClientCallServer+0x8c
03 0012f8bc 77eaacb8 kernel32!ReadConsoleInternal+0x1b8
04 0012f944 77e41990 kernel32!ReadConsoleA+0x3b
05 0012f99c 10271754 kernel32!ReadFile+0x64
06 0012fa28 10271158 MSVCR80D!_read_nolock+0x584
07 0012fa74 10297791 MSVCR80D!_read+0x1a8
08 0012fa9c 102a029b MSVCR80D!_filbuf+0x111
09 0012faf0 102971ce MSVCR80D!getc+0x24b
0a 0012fafc 102971e8 MSVCR80D!_fgetchar+0xe
0b 0012fb04 0041163b MSVCR80D!getchar+0x8
0c 0012fbe4 00413f82 exceptioninject!MyCls::hold+0x2b
0d 0012fcec 0041169a exceptioninject!foo1+0xa2
0e 0012fdc0 004114fa exceptioninject!foo2+0x3a
0f 0012fe94 004116d3 exceptioninject!foo3+0x3a
10 0012ff68 00412016 exceptioninject!wmain+0x23
11 0012ffb8 00411e5d exceptioninject!__tmainCRTStartup+0x1a6
12 0012ffc0 77e523cd exceptioninject!wmainCRTStartup+0xd
13 0012fff0 00000000 kernel32!BaseProcessStart+0x23
.frame 在棧中切換以便檢查局部變量
上面callstack中每一行前面的序號叫做frame number。通過.frame命令,可以切換到對應的函數中檢查局部變量。比如exceptioninject!foo1 這個函數前面的 frame number是d,於是執行.frame d命令:
0:000> .frame d
0d 0012fcec 0041169a exceptioninject!foo1+0xa2 [d:/xiongli/today/exceptioninject/exceptioninject/exceptioninject.cpp @ 72]
x命令顯示當前frame的局部變量。在foo1函數中,兩個局部變量分別是pcls和rawptr:
0:000> x
0012fce4 pcls = 0x0039ba80
0012fcd8 rawptr = 0x0039ba80
dt 格式化顯示資料
在符號文件加載的情況下,dt命令格式化顯示變量的資料和結構:
0:000> dt pcls
Local var @ 0x12fce4 Type MyCls*
0x0039ba80
+0x000 str : 0x00416648 "abcd"
+0x004 inobj : inner
上面的命令打印出pcls的類型是MyCls指針,指向的地址是0x0039ba80,其中的兩個class成員的偏移分別在+0和+4,對應的值在第2列顯示。加上-b -r參數可以顯示inner class和數組的信息:
0:000> dt pcls -b -r
Local var @ 0x12fce4 Type MyCls*
0x0039ba80
+0x000 str : 0x00416648 "abcd"
+0x004 inobj : innner
+0x000 arr : "abcd"
[00] 97 'a'
[01] 98 'b'
[02] 99 'c'
[03] 100 'd'
[04] 0 ''
[05] 0 ''
[06] 0 ''
[07] 0 ''
[08] 0 ''
[09] 0 ''
對於任意的地址,也可以手動指定符號類型來格式化顯示。比如把0x0039ba80地址上的數據用MyCls類型來顯示:
0:000> dt 0x0039ba80 MyCls
+0x000 str : 0x00416648 "abcd"
+0x004 inobj : innner
2.1.6 用Windbg控制程序進行實時調試(Live Debug)
除了檢查靜態資料外,調試器還能夠控制和觀察代碼的執行。
1. wt命令
wt命令的作用是watch and trace data。 它可以跟蹤一個函數的所有執行過程,並且給出統計信息。
2. 設定斷點
Windbg裏面可以設定靈活而強大的條件斷點。比如可以通過條件斷點實現這樣的功能:當某個全局變量被修改100次以後,同時stack上的第2個參數是100,那麼就停下來進入調試模式;如果第2個參數是200,那麼就生成1個dump文件,否則就只打印出當前的callstack,然後繼續運行。
Wt Watch and Trace, 跟蹤執行的強大命令
還是對於上面那個程序。
首先用bp (break point) 命令在foo3上面設斷點:
0:001> bp exceptioninject!foo3
breakpoint 0 redefined
然後用g命令讓程序執行:
0:001> g
執行到foo3上的時候,調試器停下來了:
Breakpoint 0 hit
eax=0000000a ebx=7ffd7000 ecx=0043780e edx=10310bd0 esi=0012fe9c edi=0012ff68
eip=004114c0 esp=0012fe98 ebp=0012ff68 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
exceptioninject!foo3:
004114c0 55 push ebp
用bd(breakpoint disable)命令取消設定好的斷點,以免打擾wt的執行:
0:000> bd 0
用wt命令監視foo3的執行,深度設定成2(-l2參數):
0:000> wt -l2
Tracing exceptioninject!foo3 to return address 0041186a
60 0 [ 0] exceptioninject!foo3
28 0 [ 1] MSVCR80D!printf
5 0 [ 2] MSVCR80D!__iob_func
32 5 [ 1] MSVCR80D!printf
12 0 [ 2] MSVCR80D!_lock_file2
35 17 [ 1] MSVCR80D!printf
5 0 [ 2] MSVCR80D!__iob_func
38 22 [ 1] MSVCR80D!printf
50 0 [ 2] MSVCR80D!_stbuf
46 72 [ 1] MSVCR80D!printf
5 0 [ 2] MSVCR80D!__iob_func
49 77 [ 1] MSVCR80D!printf
575 0 [ 2] MSVCR80D!_output_l
52 652 [ 1] MSVCR80D!printf
5 0 [ 2] MSVCR80D!__iob_func
57 657 [ 1] MSVCR80D!printf
33 0 [ 2] MSVCR80D!_ftbuf
60 690 [ 1] MSVCR80D!printf
7 0 [ 2] MSVCR80D!printf
71 697 [ 1] MSVCR80D!printf
63 768 [ 0] exceptioninject!foo3
1 0 [ 1] exceptioninject!ILT+380(__RTC_CheckEsp)
2 0 [ 1] exceptioninject!_RTC_CheckEsp
64 771 [ 0] exceptioninject!foo3
1 0 [ 1] exceptioninject!ILT+340(?foo2YAXXZ)
60 0 [ 1] exceptioninject!foo2
71 0 [ 2] MSVCR80D!printf
63 71 [ 1] exceptioninject!foo2
1 0 [ 2] exceptioninject!ILT+380(__RTC_CheckEsp)
2 0 [ 2] exceptioninject!_RTC_CheckEsp
64 74 [ 1] exceptioninject!foo2
1 0 [ 2] exceptioninject!ILT+215(?foo1YAXXZ)
108 0 [ 2] exceptioninject!foo1
70 183 [ 1] exceptioninject!foo2
1 0 [ 2] exceptioninject!ILT+380(__RTC_CheckEsp)
2 0 [ 2] exceptioninject!_RTC_CheckEsp
73 186 [ 1] exceptioninject!foo2
70 1031 [ 0] exceptioninject!foo3
1 0 [ 1] exceptioninject!ILT+380(__RTC_CheckEsp)
2 0 [ 1] exceptioninject!_RTC_CheckEsp
73 1034 [ 0] exceptioninject!foo3
1107 instructions were executed in 1106 events (0 from other threads)
Function Name Invocations MinInst MaxInst AvgInst
MSVCR80D!__iob_func 4 5 5 5
MSVCR80D!_ftbuf 1 33 33 33
MSVCR80D!_lock_file2 1 12 12 12
MSVCR80D!_output_l 1 575 575 575
MSVCR80D!_stbuf 1 50 50 50
MSVCR80D!printf 3 7 71 49
exceptioninject!ILT+215(?foo1YAXXZ) 1 1 1 1
exceptioninject!ILT+340(?foo2YAXXZ) 1 1 1 1
exceptioninject!ILT+380(__RTC_CheckEsp) 4 1 1 1
exceptioninject!_RTC_CheckEsp 4 2 2 2
exceptioninject!foo1 1 108 108 108
exceptioninject!foo2 1 73 73 73
exceptioninject!foo3 1 73 73 73
0 system calls were executed
eax=00000073 ebx=7ffd7000 ecx=00437c7e edx=10310bd0 esi=0012fe9c edi=0012ff68
eip=0041186a esp=0012fe9c ebp=0012ff68 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
exceptioninject!wmain+0x4a:
0041186a ebe1 jmp exceptioninject!wmain+0x2d (0041184d)
上面wt命令一直監視到foo3函數執行完爲止。隨着函數的執行,Windbg打印出foo3調用過的子函數。如果需要更詳細的信息,可以調整深度參數的值。
wt命令最後給出統計信息。無論是觀察函數執行過程和分支,或者是評估性能,wt命令都是很有幫助的。
斷點和條件斷點 (condition breakpoint),高效地控制觀測目標
Windbg中的斷點分爲3種,命令格式和功能如下:
1. bp+地址/函數名字可以在某個地址上設定斷點。當程序運行到這個地址的時候斷點觸發。
2. ba (break on access)用來設定訪問斷點,在某個地址被讀/寫的時候斷點觸發。
3. Exception斷點。當發生某個Exception/Notification的時候斷點觸發。詳情請參考Windbg幫助中的sx(Set Exception)小結。
第1種格式前面已經實踐過。第2種格式的斷點也很簡單。比如程序有一個全局變量,符號是testapp!g_Buffer。要想在程序修改這個變量的時候停下來,可以使用下面的命令設定斷點:
ba w4 testapp!g_Buffer
上面的w4表示需要檢查的類型和長度。W4中的W表示類型爲寫(Write),4表示長度爲4字節。testapp!g_Buffer是符號的名字,調試器會自動轉換成該符號所在的內存地址。所以該斷點的作用是:監視一塊內存地址區域,起點是testapp!g_Buffer所在的地址,長度爲4字節。當有代碼對該塊地址任意位置有寫操作發生的時候,調試器就把程序斷下來。
其實設置ba斷點的原理很簡單。在設置斷點後,調試器通過API把所監視地址的頁面屬性改爲不可訪問。這樣當有代碼訪問這塊地址的時候,就會引起訪問異常。這樣調試器就可以監視內存的讀寫操作,作出相應判斷。
第3種命令用來監視異常。調試器能捕獲程序中所有的異常,但是並不是說任何異常發生的時候調試器就一定要把程序斷下來。調試人員可以通過sx命令來指定異常發生時候的對應操作。下面3條命令達到的效果是,當Access Violation異常發生的時候,調試器就停下來。當DLL Load事件發生的時候,調試器就只是在屏幕上輸出。當C++ exception發生的時候,調試器什麼都不做。
Sxe av
Sxn ld
Sxd eh
關於異常的詳細說明,在後面的小結有詳細介紹。
條件斷點(condition breakpoint)的是指在上面3種基本斷點停下來後,執行一些自定義的判斷。詳細說明參考Windbg幫助中的Setting a Conditional Breakpoint小結。
在基本斷點命令後加上自定義調試命令,可以讓調試器在斷點觸發停下來後,執行調試器命令。每個命令之間用分號分割。
下面這個命令,在exceptioninject!foo3上設斷點,每次斷下來後,先用k顯示callstack,然後用.echo命令輸出簡單的字符串‘breaks’,最後g命令繼續執行:
0:001> bp exceptioninject!foo3 "k;.echo 'breaks';g"
breakpoint 0 redefined
0:001> g
ChildEBP RetAddr
0012fe94 0041186a exceptioninject!foo3
0012ff68 00412016 exceptioninject!wmain+0x4a
0012ffb8 00411e5d exceptioninject!__tmainCRTStartup+0x1a6
0012ffc0 77e523cd exceptioninject!wmainCRTStartup+0xd
0012fff0 00000000 kernel32!BaseProcessStart+0x23
'breaks'
ChildEBP RetAddr
0012fe94 0041186a exceptioninject!foo3
0012ff68 00412016 exceptioninject!wmain+0x4a
0012ffb8 00411e5d exceptioninject!__tmainCRTStartup+0x1a6
0012ffc0 77e523cd exceptioninject!wmainCRTStartup+0xd
0012fff0 00000000 kernel32!BaseProcessStart+0x23
'breaks'
更復雜一點的例子是:
int i=0;
int _tmain(int argc, _TCHAR* argv[])
{
while(1)
{
getchar();
i++;
foo3();
}
return 0;
}
條件斷點的命令是:
ba w4 exceptioninject!i "j (poi(exceptioninject!i)<0n40) '.printf /"exceptioninject!i value is:%d/",poi(exceptioninject!i);.echo;g';'.echo stop!'"
首先ba w4 exceptioninject!i表示在修改exceptioninject!i這個全局變量的時候停下來。j(judge)命令的作用是對後面的表達式作條件判斷,如果爲true,執行第1個單引號裏面的命令,否則執行第2個單引號裏面的命令。
條件表達式是(poi“exceptioninject!i”<0n40)。在Windbg中,exceptioninject!i符號表示符號所在的內存地址,而不是符號的數值,相當於C語言中的 &操作符的作用。Windbg命令poi的作用是取這個地址上的值,相當於C語言中的*操作符。所以這個條件的意思就是判斷exceptioninject!i的值,是否小於十進制(Windbg中十進制用0n當前綴)的40。
如果爲真,那麼就執行第1個單引號:
printf /"exceptioninject!i value is:%d/",poi(exceptioninject!i);.echo;g
這一個單引號裏面有3個命令:.printf、.echo 和g,這裏的printf語法跟C中printf函數語法一樣。不過由於這個printf命令本身是在ba命令的雙引號裏面,所以需要用/來轉義printf中的引號。轉義的結果是:
printf “exceptioninject!i valus is %d”, poi(exceptioninject!i)
所以第1個單引號命令的作用是:
1)打印出當前exceptioninject!i的值;2).echo命令換行;3)g命令繼續執行。
如果爲假,那麼就執行第2個單引號:.echo stop! 這個命令就是顯示stop,由於後面沒有g命令,所以windbg會停下。運行輸出如下:
0:001> ba w4 exceptioninject!i "j (poi(exceptioninject!i)<0n40) '.printf /"exceptioninject!i value is:%d/",poi(exceptioninject!i);.echo;g';'.echo stop!'"
breakpoint 0 redefined
0:001> g
exceptioninject!i value is:35
exceptioninject!i value is:36
exceptioninject!i value is:37
exceptioninject!i value is:38
exceptioninject!i value is:39
stop!
eax=00000028 ebx=7ffd5000 ecx=5e186b9c edx=10310bd0 esi=0012fe9c edi=0012ff68
eip=00411872 esp=0012fe9c ebp=0012ff68 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
exceptioninject!wmain+0x52:
00411872 e856f8ffff call exceptioninject!ILT+200(?foo3YAXXZ) (004110cd)
僞寄存器,幫助保存調試的中間信息
考慮這樣的情況,如果要記錄某一個函數被執行了多少次,應該怎麼做?簡單的做法就是修改代碼,在對應的函數入口做記錄。可是,如果要記錄的函數是系統API呢?
下面的命令可以統計VirtualAllocEx被執行了多少次:
bp kernel32!VirtualAllocEx "r $t0=@$t0+1;.printf /"function executes: %d times /",@$t0;.echo;g"
這裏用到的$t0就是Windbg提供的僞寄存器。可以用來存儲中間信息。這裏用它來存儲函數執行的次數。r命令可以用來查看,修改寄存器(CPU寄存器和Windbg的僞寄存器都有效)的值。隨便挑一個繁忙的進程,用這個命令設定斷點後觀察:
0:009> bp kernel32!VirtualAllocEx "r $t0=@$t0+1;.printf /"function executes: %d times /",@$t0;.echo;g"
0:009> g
function executes: 1 times
function executes: 2 times
function executes: 3 times
function executes: 4 times
…
關於僞寄存器信息,可以參考幫助文檔中的Pseudo-Register Syntax小結。
Step Out的實現
Step Out的定義是“Target executes until the current function is complete.”。Windbg中是如何實現這個功能的呢?根據這個定義,可以簡單地在當前函數的返回地址上設定bp 斷點就可以了。當前函數的返回地址保存在函數入口時候的EBP+4上。但如果簡單地在EBP+4上面設定斷點有兩個問題:
1. 無法區分遞歸調用和函數返回,甚至其他線程對該地址的調用。
2. 第一次觸發後不會自動清除端點,可能會多次觸發。
如果觀察windbg中step out的實現,可以看到:
bp /1 /c @$csp @$ra;g
這裏的/1參數使得斷點在觸發後自動清除,避免了第2個問題,/c @$csp參數通過指定callstack 的最小深度避免了第1個問題。而$ra僞寄存器直接表示當前函數的返回地址。多方便 :-)
2.1.7 遠程調試(Remote debug)
遠程調試是讓調試人員遠程地操作調試器的一種手段。
在調試WinForm程序的時候,如果要保持目標程序一直全屏運行,就沒辦法在同一臺機器上切換到調試器輸入命令。使用remote debug可以解決這樣的情況,避免調試器對目標程序的干擾。
如果開發團隊中的開發人員在兩個城市,通過遠程調試可以節省創建多個調試環境的時間。如果某些問題只能在固定的機器上重現,遠程調試讓排錯過程簡單便利。
在Windbg中,一種方法使用.server命令在本地創建一個TCP端口或者通過named pipe,使得遠程的Windbg可以連接到本地調試。雙方都可以輸入命令,執行結果在雙方的Windbg上都顯示出來。具體介紹參考Windbg中關於.server命令的幫助。
另外一種更爲強大的方法是使用DbgSrv。DbgSrv是一個調試服務。跟.server命令不同的地方在於,.server只是簡單地通過重定向方便遠程調試人員檢查,而實際的調試工作都發生在目標機器上。DbgSrv則是讓非常必要的調試動作發生在目標機器上,而次要的調試功能,比如加載PDB顯示符號等,發生在調試人員的機器上。在DbgSrv出現以前,調試系統的核心服務,比如lsass.exe進程,需要同時結合用戶態調試器和內核調試器,而且符號文件必須位於目標機器上。DbgSrv的出現讓這個過程大爲簡化,請參考:
Debugging LSASS ... oh what fun, it is to ride..
http://blogs.msdn.com/spatdsg/archive/2005/12/27/507265.aspx
2.1.8 如何通過Windbg命令行讓中文魔獸爭霸運行在英文系統上
買了中文版的魔獸爭霸,但家裏的Windows卻是英文版。中文的魔獸爭霸必須要運行到中文的操作系統上,否則就報告操作系統語言不匹配。
爲了解決這個問題,首先能想到的就是到Windows的地區設置裏面去把國家改爲中國。嘗試後發現問題依舊。看來魔獸爭霸判斷的並非本地Local設置,而是操作系統的語言版本。怎麼辦呢?重裝系統?去網上找破解?其實Windbg就可以解決問題。
獲取系統語言版本的API是GetSystemDefaultUILanguage。所以可以在這個 API上設定條件斷點,然後觀察魔獸爭霸判斷語言版本的邏輯是怎樣的。
用Windbg啓動war3.exe,然後在GetSystemDefaultUILanguage上設定斷點。API觸發後,發現調用完這個API後,war3.exe的下一條語句是一個cmp eax,ChineselanID,判斷當前是否是中文系統。如果不是中文,就退出程序。
那好,在cmp這條語句上設定斷點,然後用下面的命令把eax修改成中文語言符的 ID,就可以欺騙程序,讓程序認爲當前系統是中文:
r eax = 0n2052
eax被修改成了中文的語言符後,接下來的cmp執行結果就跟中文系統上的一樣了,war3就可以正確運行了。每次都要做這樣的修改麻煩得很,爲了簡化這個過程,創建內容爲如下的script文件,在GetSystemDefaultUILanguage API返回前把ax設定爲0n2052:
bp kernel32!GetSystemDefaultUILanguage+0x2c "r ax=0n2052;g"
每次啓動魔獸爭霸都要手動設定斷點是很麻煩的事情。如果要簡化整個過程,可以採用下面文章中介紹的方法,讓war3.exe啓動的時候自動啓動Windbg,通過-cf參數自動執行我們的條件斷點來達到欺騙war3的目的。
How to debug Windows services
http://support.microsoft.com/?kbid=824344
2.1.9 Dump文件
前面提到過,dump文件是進程的內存鏡像。可以把程序的執行狀態通過調試器保存到dump文件中。當在調試器中打開dump文件時,使用前面介紹的命令檢查,看到的結果跟用調試器檢查進程是一樣的。
在Windbg中可以通過.dump命令保存進程的dump文件。比如下面的命令把當前進程的鏡像保存爲c:/testdump.dmp文件:
.dump /ma C:/testdump.dmp
其中的/ma參數表示dump文件應該包含進程的完整信息,包括整個用戶態的內存,這樣dump文件尺寸會比較大,信息非常全面。如果不使用/ma參數,保存下來的dump文件只包含了部分重要資料,比如寄存器和線程棧空間,文件尺寸會比較小,無法分析所有的數據。
在Windbg中,通過File→Open Crash Dump菜單可以打開dump文件進行分析。打開dump文件後,運行調試命令看到的信息和狀態,就是dump文件保存時進程的狀態。通過dump文件能夠方便地保存發生問題時進程的狀態,方便事後分析。
2.1.10 CDB、NTSD和重定向到Kernel Debugging
除了Windbg,另外有兩個調試器分別叫做CDB和NTSD。Windbg、CDB和NTSD三者使用的命令都完全一樣。只是Windbg提供了窗口接口,剩下兩個是基於命令行的工具。NTSD位於system32目錄下,不需要特別安裝。
這3個工具其實都使用了同樣的調試引擎dbgeng.dll。關於調試引擎的詳細信息,請參考:
Symbols and Crash Dumps
http://msdn.microsoft.com/msdnmag/issues/02/06/Bugslayer/
由於CDB和NTSD採用命令行標準輸入輸出,所以可以很方便地通過重定向來控制這兩個工具。一個典型的用例就是可以把用戶態的調試重定向到Kernel Debugger。這樣只需要一個Debugging Session就可以同時控制核心態和用戶態的調試例程。詳細信息請參考Windbg 幫助中的CDB and NTSD小結。
2.1.11 Debugger Extension,擴展Windbg的功能
Debugger Extension相當於是用戶自定義,可編程的Windbg插件。一個最有用的extension就是.NET Framework 提供的sos.dll。它可以用來檢查.NET程序中的內存、線程、callstack、appdomain、assembly等等信息。關於sos.dll後面會作詳細講解。關於如何開發自己的Debugger Extension,可以參考:
Debug Tutorial Part 4: Writing WINDBG Extensions