第二章排錯的工具:調試器Windbg(上)

感謝博主 http://book.51cto.com/art/200711/59731.htmweb

《Windows用戶態程序高效排錯》第二章主要介紹用戶態調試相關的知識和工具。本文主要講了排錯的工具:調試器Windbg。編程

 

 

第二章 彙編、異常、內存、同步和調試器
——重要的知識點和神兵利器
數組

這一部分主要介紹用戶態調試相關的知識和工具。包括:彙編、異常(exception)、內存佈局、堆(heap)、棧(stack)、CRT(C Runtime)、handle/Criticalsection/thread context/windbg/ dump/live debug和Dr Watson等。
書中不會對知識點做全面的介紹,而是針對知識點在調試中過程當中應該如何使用進行說明。知識點自己在下面兩本書中有很是詳細的介紹:sass

Programming Applications for Microsoft Windows 
Debugging Applications for Windows     

2.1  排錯的工具:調試器Windbgsession

本節介紹調試器Windbg的相關知識。Windbg的使用貫穿本書不少章節,它是分析問題的高效工具。
Windbg的下載地址是:數據結構

Install Debugging Tools for Windows 32-bit Version
http://www.microsoft.com/whdc/devtools/debugging/installx86.mspxapp

建議安裝到C:\Debuggers目錄,後面的例子默認用這個目錄。dom

開發人員寫完代碼,一般會在Visual Studio中按F5直接進行調試。使用Visual Studio自帶調試器可以很是方便地在源代碼上設定斷點,檢查程序的中間變量,單步驟執行。在完成代碼階段,Visual Studio自帶的調試器可以很是方便地作源代碼級別的排錯。ide

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的最好文檔。每一個命令的詳細說明,均可以在裏面找到。

調試器能夠直觀地看到下面一些信息:

進程運行的狀態和系統狀態,好比進程運行了多少時間,環境變量是什麼。
當前進程加載的全部EXE/DLL的詳細信息。
某一個地址上的彙編指令。
查看內存地址的內容和屬性,好比是否可寫。
每一個的call stack(須要symbol)。
Call stack上每一個函數的局部變量。
格式化地顯示程序中的數據結構(須要symbol)。
查看和修改內存地址上的資料或者寄存器上的資料。
部分操做系統管理的數據結構,好比Heap、Handle、CriticalSection等。

在Visual Studio調試器中,要查看上面的信息,須要在不少調試窗口中切換。而在Windbg中,只須要簡單的命令就能夠完成。

調試器的另一個做用是設定條件斷點。能夠設定在某一個指令地址上停下來,也能夠設定當某一個內存地址等於多少的時候停下來,或者當某一個exception/notification發生的時候停下來。還能夠進入一個函數調用的時候停下來,或跳出當前函數調用的時候停下來。停下來後可讓調試器自動運行某些命令,記錄某些信息,而後讓調試器自動判斷某些條件來決定是否要繼續運行。經過簡單的條件斷點功能,能夠很方便地實現下面一些任務:

當某一個函數被調用的時候,在調試器輸出窗口中打印出函數參數。
計算某一個變量被修改了多少次。
監視一個函數調用了哪些子函數,分別被調用了多少次。
每次拋C++異常的時候自動產生dump文件。

在Visual Studio調試器中也可以設定條件斷點,但靈活性和功能遠不能跟Windbg相比。

2.1.2  符號文件(Symbol file),把二進制和源代碼對應起來

當用VC/VB編譯生成EXE/DLL後,每每會同時生成PDB文件。PDB裏面包含的是EXE/DLL的符號信息。
符號是指代碼中使用到的類型和名字。好比下面這些都是符號包含的內容:

代碼所定義的Class的名字,Class的全部成員的名字和全部成員的類型。
變量的名字和變量的類型。
函數的名字,函數全部參數的名字和類型,以及函數的返回值。

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;
}   

 

 


這段牖岬賈滷覽!1覽:罌吹降慕涌諶繽?.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

 

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命令能夠搜索內存。好比用在下面的地方:

1. 尋找內存泄漏的線索。好比知道當前內存泄漏的內容是一些固定的字符串,就能夠在 DLL區域搜索這些字符串出現的地址,而後再搜索這些地址用到什麼代碼中,找出這些內存是從什麼地方開始分配的。
2. 尋找錯誤代碼的根源。好比知道當前程序返回了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 /1 /c @$csp @$ra;g

 

 

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
http://www.codeproject.com/debug/cdbntsd4.asp
相關文章
相關標籤/搜索