介紹
在軟件開發和維護的過程中,調試是最有具有價值的技能之一,它幾乎被用在產品生命週期的每個階段。首先,開發人員很明顯會遇到很多錯誤。這些錯誤多種多樣,有邏輯錯誤、語法錯誤、編譯器方面的錯誤等等。其次,當測試更多的高級的情況或當軟件在和其他環境交互的時候,質量保證部門人員可能會遇到困難。最後,產品發佈之後,公司必須對它提供一些支持。顧客拿到軟件產品之後,調試並沒有結束,錯誤通常都會升級,並被返回公司等待調試。
這篇教程的目的
這篇教程僅僅是關於調試的一個介紹。這篇是”教程 #1”,如果反饋好的話,我會繼續寫。有許多複雜的調試技術以及問題,以至於我們不知道從哪裏開始着手。這篇教程從最初開始幫助你瞭解調試是怎麼一回事。我希望把高級調試的世界展現給初級和中級程序員,而不是簡單的重新編譯,或者簡單的MessageBox和Printf調試。
調試器和操作系統
你可以到下面這個網站下載最新的微軟提供的調試器。
http://www.microsoft.com/whdc/ddk/debugging
CDB,NTSD和Windbg
這篇文章討論的情況一般都是Windows2000以及更高版本。我們將要談論的這三個調試器是CDB,NTSD和WinDbg。Windows2000以及更高版本一般系統內都內置NTSD!所以,如果你想快速調試,就不需要另外再安裝軟件了。
那麼這又有什麼不一樣呢?文檔上說”NTSD不需要控制檯支持,而CDB需要”。這是真的,NTSD確實不需要,而CDB需要。然而,我發現還有更多的不同之處。第一,老版本的NTSD不支持PDB符號文件,它們只支持DBG符號文件!我還發現NTSD不支持符號服務器,而CDB支持。老版本的NTSD不能創建內存dump,還有其他一些問題,比如NTSD只支持2種斷點命令。NTSD相對於CDB的一個優勢就是它不需要控制檯。
當你正在調試用戶態服務或者登陸到系統之前的進程時,不需要控制檯窗口這個特點是非常重要的。如果沒有用戶登陸到系統,你就不能創建控制檯窗口。你可以設置一個命令選項-d,讓NTSD和已經連接上的內核調試器通信(CDB也有相同的選項)。這樣就能通過內核調試器來調試系統啓動期間的進程。當你已經可以用內核調試器調試進程,用戶態調試器能給你更多的靈活性。這已經超出了介紹章節的範圍,只要消化這個概念就行了。
除了很少的一些差異,WinDbg和CDB幾乎一樣。WinDbg是GUI程序,CDB是控制檯程序,這是第一個不同。WinDbg還支持內核調試和源碼級調試。
Visual C++調試器
我不使用這個調試器,並且我不推薦使用它。第一個原因就是它非常消耗資源。它加載的非常慢,並且包含了很多沒用的東西,以至於成爲累贅。第二個原因就是安裝完這個調試器後,你需要重啓,我一般都是在沒有安裝調試器的機器上工作。並且VC++非常大,安裝費時。
Windows 9x/ME
在Windows 9x/ME的機器上,我們該怎麼辦呢?你可以使用WinDbg。對所有系統,和調試有關的API都是一樣的,因此Windbg能運行在Windows 9x/ME上。我唯一的擔心就是WinDbg會檢測當前系統是否Windows 9x並且會不允許調試。我最近發現事實上不會這樣。剩下的問題就是,最新的WinDbg是MSI安裝包,並且不允許安裝在Windows 9x系統上。我們可以在NT系統上安裝,然後共享這個目錄或者拷貝到CD上共享。這會有一些影響,比如若NT和9x系統放置的數據在內存的不同地方,你就不能使用所有的!xxx命令。那麼符號能使用嗎?是的,PDB可以使用。但是當設置了ba r1 xxxxx之後,單步走非常慢。這篇文章的內容不包括Windows 9x/ME。
設置環境
開始調試前,這是非常重要的一步。把系統配置成你喜歡的狀態,並且包含所有你需要的工具。
符號和符號服務器
符號是調試操作中很重要的一步。你可以從微軟的一個地址下載相應操作系統的所有符號。問題是,你需要很大的硬盤空間,並且不如你在一臺機器上調試許多操作系統,這可能會非常麻煩。
爲了適應這個需要,微軟提供了”符號服務器”這個功能。這會幫助你得到正確的符號。符號服務器的地址http://msdl.microsoft.com/download/symbols。如果你把符號路徑設置爲這個地址,調試器將會自動你需要的系統符號。你自己的程序的符號需要你自己設置。
映像文件執行選項
這是註冊表中的一個位置,當一個程序開始運行,這個註冊表位置會自動將調試器附加到程序。這個註冊表位置如下:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
在這個鍵下,你只要創建一個子鍵,取名爲你想要調試的程序,比如”myapplication.exe”。如果你以前沒有使用過這個功能,可能會有一個默認鍵值”Your Application Here”或類似的值,重命名它即可。
這個鍵下的一個值是”Debugger”,你可以在這裏設置需要開啓的調試器,”Your Application Here”下面默認的這個值爲”ntsd -d”。你不能使用這個,除非已經有內核調試器附加在系統上,所以要去掉”-d”選項。
注意:使用”-d”選項,並且當前沒有內核調試器附加在系統上,每次啓動程序都有可能導致系統鎖死。必須非常小心。如果內核調試器已經設置好了,你可以使用”g”命令解鎖系統。
另外一個值爲”GlobalFlags”。這是另外一個可以用於調試的工具,然而它超出了本篇的範圍。如果你想知道更多,可以看”gflags.exe”。
內核調試設置
如果需要內核調試,首先你需要以調試模式啓動系統。雖然在系統屬性裏面可以用GUI的方式設置,我還是建議直接編輯boot.ini文件。在C:\盤找到boot.ini文件,它是一個隱藏的系統文件。
小心:不正確的編輯這個文件將會讓你無法啓動。
這個啓動文件內容類似下面:
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS.0=
"Microsoft Windows XP Professional" /fastdetect
我將複製Operating Systems下面的第一行:
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS.0=
"Microsoft Windows XP Professional" /fastdetect
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS.0=
"Microsoft Windows XP Professional"
/fastdetect /debug /debugport=COM1 /baudrate=115200
複製的這行包含了你的設置。/debug,然後是/debugport=port,最後是/baudrate=baudrate。調試端口是你需要使用的串行端口,這是你需要設置的硬件,你還需要另外一臺機器來完成設置。除了使用COM端口,你還可以使用IEEE 1394,這個速度會更快一些。
當你再次啓動後,選擇”Debugger Enabled”選項來啓動調試模式。
(其實我們一般都用虛擬機來完成這部分的設置,具體可以在論壇上搜,有很多教程。)
環境變量
我一般會把_NT_SYMBOL_PATH環境變量設置爲微軟的符號服務器和我自己的符號目錄。你可以在 系統屬性->高級->環境變量 裏面設置這個值。
默認調試器
當有任何崩潰出現在系統上,將會調用缺省調試器。默認情況下,它被設置爲”Doctor Watson”。這個註冊表鍵在如下位置:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug
我會把”Auto”設爲1,”Debugger”設爲需要的調試器。
彙編
我十分推薦你學習彙編語言。這篇教程不會給你展示源碼級調試,因爲我從來不這樣做並且我也不會。源碼級調試帶來的問題是,你不會總是有源代碼,並且有時候從源代碼看不出問題在哪。如果你瞭解系統的組成,你可以很容易的逆向系統來找到你需要的信息,這個源碼級調試不能帶給你的。
我討厭源碼級調試的另外一個原因是,如果源碼和符號不符合,調試器將會給你錯誤的信息。這意味着如果你爲你的項目創建了多個版本,你不得不找到和你正在調試的程序符合的版本。
讓我們開始吧
這篇教程是第一部分,如果你們喜歡,我們寫更多,並且會更深入。這篇將會解決兩個簡單的用戶態編程問題。
發佈版本的符號
首先,怎樣爲發佈版程序創建符號呢?很簡單,創建一個make文件。
我一般使用的編譯選項如下:
/nologo /MD /W3 /Oxs /Zi /I "..\..\inc" /D "WIN32" /D "_WINDOWS"
/Fr$(OBJDIR)\\ /Fo$(OBJDIR)\\ /Fd$(OBJDIR)\\ /c
我一般使用的鏈接選項如下:
/nologo /subsystem:console
/out:$(TARGETDIR)\$(TARGET)/pdb:<YourProjectName>.pdb
/debug /debugtype:both
/LIBPATH:"..\..\..\bin\lib"
這將會爲你的工程創建.pdb文件。當然,根據VC++7的介紹,他們已經放棄使用.DBG(因此/debugtype:both在這個編譯器上可能會出現錯誤)。.DBG是.PDB的簡單版本並且它不包含源碼信息,精確的符號察看。它甚至不包含參數或其他一些東西。如果你還在使用這樣的編譯器,你需要做下面的工作:
rebase -b 0x00100000 -x $(TARGETDIR) -a $(TARGETDIR)\$(TARGET)
-b選項後面是重定位的新的可執行文件的內存地址。如果你使用默認的Visual Studio方式創建文件,那可能會比這個更小一點,不過,你不會獲得符號。產生的代碼是一樣的,並且會有相應的優化。不同的是,這些文件會更有用,不論你在哪裏使用,你都能得到符號信息。
記住,最棒的調試時機總是在你還沒有重新生成可執行文件之前。一旦你不得不重新生成可執行文件,你必須知道,你已經改變了這個文件在內存中的位置。你也可能改變了文件執行的速度。如果你需要重現這個錯誤,這將是非常重要的!如果需要4天才能引起這個錯誤,那該怎麼辦呢?如果能的話,最好在發生的時候就去處理它。
簡單的Access Violation錯誤
我們來看一個簡單的”Access Violation”錯誤,這很常見。解決這個問題可以分爲三步。
1. 誰觸發了這個訪問?哪個模塊?
2. 它要訪問哪裏?這塊內存在哪?
3. 爲什麼它要訪問這塊內存?它要幹什麼?
這些是一般情況下解決這個問題的方法,其中第2條又是最重要的。然而,解決1和3的問題可以幫助解決2的問題。
我創建了一個簡單的崩潰的程序。我已經把我的默認調試器設置爲CDB,並且我運行了這個程序。我也爲這個可執行文件創建了符號並把_NT_SYMBOL_PATH設置爲微軟的符號服務器。
當我們運行這個程序,我們將會看到下面的情況:
C:\programs\DirectX\Games\src\Games\temp\bin>temp
Microsoft (R) Windows Debugger Version 6.3.0005.1
Copyright (c) Microsoft Corporation. All rights reserved.
*** wait with pending attach
Symbol search path is:
SRV*c:\symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00400000 00404000 C:\programs\DirectX\Games\src\Games\temp\bin\temp.e
xe
ModLoad: 77f50000 77ff7000 C:\WINDOWS.0\System32\ntdll.dll
ModLoad: 77e60000 77f46000 C:\WINDOWS.0\system32\kernel32.dll
ModLoad: 77c10000 77c63000 C:\WINDOWS.0\system32\MSVCRT.dll
ModLoad: 77dd0000 77e5d000 C:\WINDOWS.0\system32\ADVAPI32.DLL
ModLoad: 78000000 78086000 C:\WINDOWS.0\system32\RPCRT4.dll
(ee8.c38): Access violation - code c0000005 (!!! second chance !!!)
eax=00000000 ebx=7ffdf000 ecx=00001000 edx=00320608 esi=77c5aca0 edi=77f944a8
eip=77c3f10b esp=0012fb0c ebp=0012fd60 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
MSVCRT!_output+0x18:
77c3f10b 8a18 mov bl,[eax] ds:0023:00000000=??
0:000>
第一個要注意的就是,這個錯誤發生在MSVCRT.DLL當中。這很明顯,因爲調試器給我們顯示了類似的信息,格式爲<module>!<nearest symbol>+offset。這意味着最近的符號是_output,並且我們運行到了裏面的+18h處。因此我們假設自己正處於_output函數當中。
(ee8.c38): Access violation - code c0000005 (!!! second chance !!!)
eax=00000000 ebx=7ffdf000 ecx=00001000 edx=00320608 esi=77c5aca0 edi=77f944a8
eip=77c3f10b esp=0012fb0c ebp=0012fd60 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
MSVCRT!_output+0x18:
77c3f10b 8a18 mov bl,[eax] ds:0023:00000000=??
0:000>
如果我們想驗證這個信息,我們應該怎麼做?
<0:000> x *!
start end module name
00400000 00404000 temp (deferred)
77c10000 77c63000 MSVCRT (pdb symbols)
c:\symbols\msvcrt.pdb\3D6DD5921\msvcrt.pdb
77dd0000 77e5d000 ADVAPI32 (deferred)
77e60000 77f46000 kernel32 (deferred)
77f50000 77ff7000 ntdll (deferred)
78000000 78086000 RPCRT4 (deferred)
這個命令展示所有模塊的列表以及它們的開始和結束位置。我們的錯誤地址是77c3f10b,77c10000<=77c3f10b<=77c63000,因此可以確認錯誤發生在MSVCRT。下面我們來確定這個地址在哪。
有幾種方法可以完成,我們可以反彙編代碼,找出這個地址,還可以看棧回溯。首先我們來看看_output函數的反彙編代碼。
0:000> u MSVCRT!_output
MSVCRT!_output:
77c3f0f3 55 push ebp
77c3f0f4 8bec mov ebp,esp
77c3f0f6 81ec50020000 sub esp,0x250
77c3f0fc 33c0 xor eax,eax
77c3f0fe 8945d8 mov [ebp-0x28],eax
77c3f101 8945f0 mov [ebp-0x10],eax
77c3f104 8945ec mov [ebp-0x14],eax
77c3f107 8b450c mov eax,[ebp+0xc]
0:000> u
MSVCRT!_output+0x17:
77c3f10a 53 push ebx
77c3f10b 8a18 mov bl,[eax]
即使你不懂彙編,你也會發現一些東西。首先,我們可以發現這個內存地址在EAX當中。它是CPU的一個寄存器,但是我們可以把它看作一個變量。環繞EAX的[]符號相當於C語言中的*MyPointer。這意味着我們正在引用EAX指向的地址。那麼EAX又是怎麼來的?EAX來自[EBP+0Ch],你可以把它看作DWORD *EBP,EAX=EBP[3].這是因爲在彙編語言中,沒有類型。EAX是一個32位寄存器,EBP+12相當於一個DWORD指針加3。
下面我們可以看到MOV EBP,ESP。ESP是棧指針。參數都是壓入棧中,返回地址和局部變量都是在棧中。ESP指向棧的位置。在內存中,一個C函數調用約定的存放如下:
[Parameter n]
...
[Parameter 2]
[Parameter 1]
[Return Address]
並且,我們看到了PUSH EBP。PUSH就是把某個東西壓入棧,因此我們在棧中保存了EBP以前的值。此時,棧的狀態如下:
[Parameter n]
...
[Parameter 2]
[Parameter 1]
[Return Address]
[Previous EBP]
然後我們又把EBP設爲ESP,我們可以把它看作是一個指針,棧就相當於一個DWORD類型的數組。因此,棧中各個變量與EBP的對應如下:
[Parameter n] == [EBP + n*4 + 4] (The formula)
...
[Parameter 2] == [EBP + 12]
[Parameter 1] == [EBP + 8]
[Return Address] == [EBP + 4]
[Previous EBP] == [EBP + 0]
對於我們這個例子來說,我們的變量是_output的第二個參數。那麼,下面該怎麼辦呢?我們來反彙編調用函數!我們知道EBP+4指向返回地址,或者我們也可以得到棧回溯。
0:000> kb
ChildEBP RetAddr Args to Child
0012fd60 77c3e68d 77c5aca0 00000000 0012fdb0 MSVCRT!_output+0x18
0012fda4 0040102f 00000000 00000000 00403010 MSVCRT!printf+0x35
0012ff4c 00401125 00000001 00323d70 00322ca8 temp!main+0x2f
0012ffc0 77e814c7 77f944a8 00000007 7ffdf000 temp!mainCRTStartup+0xe3
0012fff0 00000000 00401042 00000000 78746341 kernel32!BaseProcessStart+0x23
0:000>
“KB”命令能得到棧回溯。我們不會總是得到完整的棧回溯,我們會在更加深入的教程中講解這點。在這篇簡單的教程裏,我們假定我們得到了完整的棧回溯。我們注意到,這個函數是printf,並且printf調用_output。我們來反彙編printf,注意我們不用每次都反彙編整個函數,將其分割成幾段即可。有時,我們能從棧回溯中很簡單就找到錯誤發生點,而這些函數也都非常簡單,我們可以非常輕鬆的跟蹤他們。
0:000> u MSVCRT!_output
MSVCRT!_output:
77c3f0f3 55 push ebp
77c3f0f4 8bec mov ebp,esp
77c3f0f6 81ec50020000 sub esp,0x250
77c3f0fc 33c0 xor eax,eax
77c3f0fe 8945d8 mov [ebp-0x28],eax
77c3f101 8945f0 mov [ebp-0x10],eax
77c3f104 8945ec mov [ebp-0x14],eax
77c3f107 8b450c mov eax,[ebp+0xc]
0:000> u
MSVCRT!_output+0x17:
77c3f10a 53 push ebx
77c3f10b 8a18 mov bl,[eax]
77c3f10d 33c9 xor ecx,ecx
77c3f10f 84db test bl,bl
77c3f111 0f8445070000 je MSVCRT!_output+0x769 (77c3
77c3f117 56 push esi
77c3f118 57 push edi
77c3f119 8bf8 mov edi,eax
0:000> u MSVCRT!printf
MSVCRT!printf:
77c3e658 6a10 push 0x10
77c3e65a 68e046c177 push 0x77c146e0
77c3e65f e8606effff call MSVCRT!_SEH_prolog (77c354
77c3e664 bea0acc577 mov esi,0x77c5aca0
77c3e669 56 push esi
77c3e66a 6a01 push 0x1
77c3e66c e8bdadffff call MSVCRT!_lock_file2 (77c394
77c3e671 59 pop ecx
0:000> u
MSVCRT!printf+0x1a:
77c3e672 59 pop ecx
77c3e673 8365fc00 and dword ptr [ebp-0x4],0x0
77c3e677 56 push esi
77c3e678 e8c7140000 call MSVCRT!_stbuf (77c3fb44)
77c3e67d 8945e4 mov [ebp-0x1c],eax
77c3e680 8d450c lea eax,[ebp+0xc]
77c3e683 50 push eax
77c3e684 ff7508 push dword ptr [ebp+0x8]
0:000> u
MSVCRT!printf+0x2f:
77c3e687 56 push esi
77c3e688 e8660a0000 call MSVCRT!_output (77c3f0f3)
_output的第二個參數是[EBP+8],並且有PUSH EBP和MOV EBP,ESP,因此這和我之前說的情況一樣。但是也不總是這樣的,要視具體情況而定,我們慢慢再深入講解。
因此,我們可以確定printf的第一個參數在內存中的哪個地方,並且printf是我們的程序發出的調用。從錯誤信息,我們知道EAX是0,因此我們在對一個空指針解引用。
77c3f10b 8a18 mov bl,[eax] ds:0023:00000000=??
下面是我們寫的代碼:
int main(int argc, char *argv[])
{
char *TheLastParameter[100];
sprintf(*TheLastParameter, "The last parameter is %s", argv[argc]);
printf(*TheLastParameter);
return 0;
}
這段代碼有很多問題,然後因爲空指針,錯誤發生在printf。奇怪的是,並沒有在sprintf()處出現錯誤。那麼,我們該怎樣只用KB命令解決這個問題呢?
0:000> kb
ChildEBP RetAddr Args to Child
0012fd60 77c3e68d 77c5aca0 00000000 0012fdb0 MSVCRT!_output+0x18
0012fda4 0040102f 00000000 00000000 00403010 MSVCRT!printf+0x35
0012ff4c 00401125 00000001 00323d70 00322ca8 temp!main+0x2f
0012ffc0 77e814c7 77f944a8 00000007 7ffdf000 temp!mainCRTStartup+0xe3
0012fff0 00000000 00401042 00000000 78746341 kernel32!BaseProcessStart+0x23
0:000>
我們有符號和棧回溯,第一個參數是0,並且我們調用了這個函數。這是很簡單的情況,我會試着講述一些技巧,讓你發現問題的所在之處。知道棧是如何組成的,在內存中的狀態,這對你找出問題所在之處非常有幫助,因爲僅僅一個”kb”命令不會讓你一眼就能看出問題。
不按預期運行的程序
這也是一個很常見的錯誤。你運行了程序,但是你並沒有看到正確的輸出,或者總是有一些錯誤信息。這個常見的問題有可能非常容易解決,也可能很複雜。那麼解決這種問題的步驟如何呢?
1. 什麼東西沒有工作?
2. 哪些API或模塊可能會和這個問題有關?
3. 什麼原因導致這些API不正常的工作?
這些步驟不是必須的。下面讓我們來看一個例子,關於打開文件的。
HANDLE hFile;
DWORD dwWritten;
hFile = CreateFile("c:\MyFile.txt", GENERIC_READ,
0, NULL, OPEN_EXISTING, 0, NULL);
if(hFile != INVALID_HANDLE_VALUE)
{
WriteFile(hFile, "Test", strlen("Test"), &dwWritten, NULL);
CloseHandle(hFile);
}
這是我們的代碼。你可能會想使用GetLastError(),然後重新編譯,並將錯誤打印出來。但是,你並不需要這樣做,在這個例子中非常簡單。下面我們來看看,打開調試器,並且會停在這個函數。有符號的幫助,一切都很簡單,CreateFile是一個導出的符號,我們總是能斷在這個函數上。
C:\programs\DirectX\Games\src\Games\temp\bin>cdb temp
Microsoft (R) Windows Debugger Version 6.3.0005.1
Copyright (c) Microsoft Corporation. All rights reserved.
CommandLine: temp
Symbol search path is:
SRV*c:\symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00400000 00404000 temp.exe
ModLoad: 77f50000 77ff7000 ntdll.dll
ModLoad: 77e60000 77f46000 C:\WINDOWS.0\system32\kernel32.dll
ModLoad: 77c10000 77c63000 C:\WINDOWS.0\system32\MSVCRT.dll
(2a0.94): Break instruction exception - code 80000003 (first chance)
eax=00241eb4 ebx=7ffdf000 ecx=00000004 edx=77f51310 esi=00241eb4 edi=00241f48
eip=77f75a58 esp=0012fb38 ebp=0012fc2c 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!DbgBreakPoint:
77f75a58 cc int 3
0:000> bp temp!main
0:000> g
我們在main函數上下一個斷點,然後使用”g”命令讓其運行。當我們到達斷點,用”p”命令單步走,直到CreateFile函數。
Breakpoint 0 hit
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401000 esp=0012ff50 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main:
00401000 51 push ecx
0:000> p
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401001 esp=0012ff4c ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x1:
00401001 56 push esi
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401002 esp=0012ff48 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x2:
00401002 57 push edi
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401003 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x3:
00401003 33ff xor edi,edi
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401005 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x5:
00401005 57 push edi
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401006 esp=0012ff40 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x6:
00401006 57 push edi
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401007 esp=0012ff3c ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x7:
00401007 6a03 push 0x3
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401009 esp=0012ff38 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x9:
00401009 57 push edi
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=0040100a esp=0012ff34 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0xa:
0040100a 57 push edi
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=0040100b esp=0012ff30 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0xb:
0040100b 6800000080 push 0x80000000
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401010 esp=0012ff2c ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x10:
00401010 6810304000 push 0x403010
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401015 esp=0012ff28 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x15:
00401015 ff1504204000 call dword ptr [temp!_imp__CreateFileA (00402004)]{kernel3
2!CreateFileA (77e7b476)} ds:0023:00402004=77e7b476
0:000> p
eax=ffffffff ebx=7ffdf000 ecx=77f939e3 edx=00000002 esi=00000000 edi=00000000
eip=0040101b esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei ng nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000286
temp!main+0x1b:
0040101b 8bf0 mov esi,eax
在調用CreateFile函數之後,EAX中將存儲返回值。我們注意到其值爲ffffffff,也就是”Invalid Handle Value”,我們還想知道GetLastError的值,它存儲在fs:34這個位置。FS是TEB選擇子,我們可以把它dump出來。
0:000> dd fs:34
0038:00000034 00000002 00000000 00000000 00000000
0038:00000044 00000000 00000000 00000000 00000000
0038:00000054 00000000 00000000 00000000 00000000
0038:00000064 00000000 00000000 00000000 00000000
0038:00000074 00000000 00000000 00000000 00000000
0038:00000084 00000000 00000000 00000000 00000000
0038:00000094 00000000 00000000 00000000 00000000
0038:000000a4 00000000 00000000 00000000 00000000
CDB還有一種更快速的方式能做到這點,!gle:
0:000> !gle
LastErrorValue: (Win32) 0x2 (2) - The system cannot find the file specified.
LastStatusValue: (NTSTATUS) 0xc0000034 - Object Name not found.
0:000>
找不到對應的文件。那麼問題出在哪呢?我們需要進一步調試,我們來看看傳遞給CreateFile的參數。
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401010 esp=0012ff2c ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x10:
00401010 6810304000 push 0x403010
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401015 esp=0012ff28 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x15:
00401015 ff1504204000 call dword ptr [temp!_imp__CreateFileA
(00402004)]{kernel32!CreateFileA (77e7b476)} ds:0023:00402004=77e7b476
幸運的是,這是內存中的一個常量,它不太可能會改變,因爲我們並沒有運行到離CreateFile太遠。
然後,我們可以使用”da”,”dc”,“du”命令。”da”命令打印出ANSI字符串,”du”打印出Unicode字符串,”dc”和”dd”類似,不過它是打印出所有的字符,包括不可顯示的。我們知道這是一個ANSI字符串,”da”命令:
0:000> da 403010
00403010 "c:MyFile.txt"
0:000>
我們看到這是錯誤的字符串,我們應該用c:\\MyFile.txt來代替它。
因此,我們找到並更正這個錯誤,但是,我們還是不能寫入。我們再進一步調試。
重新載入一遍。
C:\programs\DirectX\Games\src\Games\temp\bin>cdb temp
Microsoft (R) Windows Debugger Version 6.3.0005.1
Copyright (c) Microsoft Corporation. All rights reserved.
CommandLine: temp
Symbol search path is:
SRV*c:\symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00400000 00404000 temp.exe
ModLoad: 77f50000 77ff7000 ntdll.dll
ModLoad: 77e60000 77f46000 C:\WINDOWS.0\system32\kernel32.dll
ModLoad: 77c10000 77c63000 C:\WINDOWS.0\system32\MSVCRT.dll
(80c.c94): Break instruction exception - code 80000003 (first chance)
eax=00241eb4 ebx=7ffdf000 ecx=00000004 edx=77f51310 esi=00241eb4 edi=00241f48
eip=77f75a58 esp=0012fb38 ebp=0012fc2c 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!DbgBreakPoint:
77f75a58 cc int 3
0:000> bp temp!main
0:000> g
Breakpoint 0 hit
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401000 esp=0012ff50 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main:
00401000 51 push ecx
0:000> p
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401001 esp=0012ff4c ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x1:
00401001 56 push esi
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401002 esp=0012ff48 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x2:
00401002 57 push edi
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401003 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x3:
00401003 33ff xor edi,edi
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401005 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x5:
00401005 57 push edi
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401006 esp=0012ff40 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x6:
00401006 57 push edi
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401007 esp=0012ff3c ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x7:
00401007 6a03 push 0x3
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401009 esp=0012ff38 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x9:
00401009 57 push edi
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=0040100a esp=0012ff34 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0xa:
0040100a 57 push edi
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=0040100b esp=0012ff30 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0xb:
0040100b 6800000080 push 0x80000000
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401010 esp=0012ff2c ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x10:
00401010 6810304000 push 0x403010
0:000>
eax=77c5c9e4 ebx=7ffdf000 ecx=00322cf8 edx=00322cf8 esi=00000000 edi=00000000
eip=00401015 esp=0012ff28 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x15:
00401015 ff1504204000 call dword ptr [temp!_imp__CreateFileA (00402004)]{kernel3
2!CreateFileA (77e7b476)} ds:0023:00402004=77e7b476
0:000>
eax=000007e8 ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=00000000 edi=00000000
eip=0040101b esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei ng nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000293
temp!main+0x1b:
0040101b 8bf0 mov esi,eax
0:000> p
走到這裏,我們看到EAX是一個有效的handle,繼續。
eax=000007e8 ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=0040101d esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei ng nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000293
temp!main+0x1d:
0040101d 83feff cmp esi,0xffffffff
0:000>
eax=000007e8 ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=00401020 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x20:
00401020 741b jz temp!main+0x3d (0040103d)
0:000>
eax=000007e8 ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=00401022 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x22:
00401022 8d442408 lea eax,[esp+0x8] ss:0023:0012ff4c=00322cf8
0:000>
eax=0012ff4c ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=00401026 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x26:
00401026 57 push edi
0:000>
eax=0012ff4c ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=00401027 esp=0012ff40 ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x27:
00401027 50 push eax
0:000>
eax=0012ff4c ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=00401028 esp=0012ff3c ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x28:
00401028 6a04 push 0x4
0:000>
eax=0012ff4c ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=0040102a esp=0012ff38 ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x2a:
0040102a 6820304000 push 0x403020
0:000>
eax=0012ff4c ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=0040102f esp=0012ff34 ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x2f:
0040102f 56 push esi
0:000>
eax=0012ff4c ebx=7ffdf000 ecx=77f59037 edx=00140608 esi=000007e8 edi=00000000
eip=00401030 esp=0012ff30 ebp=0012ffc0 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000213
temp!main+0x30:
00401030 ff1500204000 call dword ptr [temp!_imp__WriteFile (00402000)]{kernel32!
WriteFile (77e7f13a)} ds:0023:00402000=77e7f13a
0:000> p
eax=00000000 ebx=7ffdf000 ecx=77e7f1c9 edx=00000015 esi=000007e8 edi=00000000
eip=00401036 esp=0012ff44 ebp=0012ffc0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
temp!main+0x36:
00401036 56 push esi
我們調用了WriteFile並且EAX==0,這意味着調用返回錯誤,我們來看看其他變量。
第二個參數是對的,長度爲4:
0:000> da 403020
00403020 "Test"
第4個參數代表寫入的地字節數,爲0。
0:000> dd 012ff4c
0012ff4c 00000000 00401139 00000001 00322470
0012ff5c 00322cf8 00403000 00403004 0012ffa4
0012ff6c 0012ff94 0012ffa0 00000000 0012ff98
0012ff7c 00403008 0040300c 00000000 00000000
0012ff8c 7ffdf000 00000001 00322470 00000000
0012ff9c 8053476f 00322cf8 00000001 0012ff84
0012ffac e1176590 0012ffe0 00401200 004020c0
0012ffbc 00000000 0012fff0 77e814c7 00000000
下面我們檢查一下GetLastError的值。
0:000> !gle
LastErrorValue: (Win32) 0x5 (5) - Access is denied.
LastStatusValue: (NTSTATUS) 0xc0000022 - {Access Denied}
A process has requested access to an object,
but has not been granted those access rights.
0:000>
拒絕訪問?怎麼會這樣!檢查一下,我們只以READ權限打開這個文件,沒有WRITE權限,這就是問題所在。然後我們在代碼中修改這個錯誤。
hFile = CreateFile("c:\\MyFile.txt", GENERIC_READ,
0, NULL, OPEN_EXISTING, 0, NULL);
總結
這僅僅是對基本調試技術的一個介紹。例子都很簡單,但是其中展示的這些技術都是很有價值的。這是調試教程的第一篇,如果有人感興趣,我們繼續補上一些更加高級的內容。
對有些人來說,這篇教程可能會很簡單,對其他人來說又可能太難了。你不可能一天就變成一個熟練的調試專家,這是需要聯繫的。我建議大家,即使是很簡單的問題,也要試着去用調試器解決。聯繫得越多,工具使用的越熟練,你將會學習到更多。