Windows用戶態程序高效排錯 -- 排錯的工具:調試器Windbg

這一部分主要介紹用戶態調試相關的知識和工具。包括:彙編、異常(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

接下來讓我們在getcharBufferchangeto4p函數上分別設定斷點。要通過函數名設定斷點,首先要讓Windbg加載對應的PDB文件。通過Windbg的File→Symbol File Path菜單可以設定PDB文件的搜索路徑。通過這個菜單我們把路徑設定到C:/PDB。設定好了後,接下來可以用x命令找到程序中getcharBufferchangeto4p函數的二進制入口地址。找到地址後,就可以用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命令在getcharBufferchangeto4p函數上設定了斷點,所以右邊窗口對應函數前有紅色小方塊表示斷點。

除了使用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

http://www.codeproject.com/debug/cdbntsd4.asp

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章