使用 WinDbg 偵錯 x86 Assembly 編寫的可執行檔案



Programming 最重要的環節就是偵錯。就像高階語言需要逐行偵錯一樣,Assembly 也需要一個 Source-Level 的偵錯工具進行逐行偵錯。本章介紹使用微軟自家的 WinDbg (Windows Debugger 的縮寫)來進行逐行偵錯。


WinDbg 包含在微軟的開發套件 Windows SDK 裡。什麼?!你已有隨 Visual Studio 安裝的 Windows SDK?很抱歉,WinDbg 並不包含在內。你需要獨立安裝 Windows SDK。


請移玉步到此下載 Windows 10 SDK:


https://go.microsoft.com/fwlink/p/?linkid=2120843


其實只需要安裝 Debugging 的組件就好了(笑



為了讓使用 Assembly 編寫的可執行檔可以被 WinDbg 偵錯,組譯和鏈接時就要把 WinDbg 使用的相關偵錯情報匯入可執行檔裡。


使用 NASM 組譯時加上參數 -g 代表要求加入偵錯訊息,例如:


nasm -f Win32 -gCV8 test.asm


CV8 是 -g 的其中一個 format,必需緊接 -g 不可有空格,表示偵錯訊息要使用微軟 WinDbg 能理解的 CodeView 8 格式,而非 Linux gcc 使用的格式。


使用link.exe鏈接時也要加上特別參數,例如:


link test.obj /DEBUG /PDB:test.pdb /DYNAMICBASE "kernel32.lib" /LIBPATH:你的LIB文件路徑 /ENTRY:main


參數 /DEUBG 指示鏈接時生成 PDB(Program DataBase)檔案,儲存了所有模組中符號(例如你定義的變數和函式的名稱)跟位址的清單、程式碼檔案名稱、程式碼行號資訊、程式碼索引等訊息,並將 PDB 路徑存入可執行檔中。 PDB 需與執行檔在同一個資料夾中。


參數 /PDB 指示由 /DEBUG 參數生成的 PDB 檔案的名稱。沒有 /DEBUG 則不會生成 PDB 檔。


然後執行 WinDbg:


按下 Windows 鍵 + S-> 鍵入 "WinDbg" -> 選擇 WinDbg (X86) 或者 WinDbg (X64)


進入 WinDbg 的介面,在功能表選擇:


File -> Open Executable...(或按下 Ctrl + E)


選擇要偵錯的可執行檔案開啟。WinDbg 會在此時開始執行你的可執行檔案,彈出它的視窗,我們先不用理這個視窗,鼠標點擊 WinDbg 返回即可。


然後在功能表選擇:


File -> Open Source File...(或按下 Ctrl + O)


在開啟檔案的對話方塊視窗中,瀏覽剛開啟的可執行檔案的原始檔案的目錄,在「檔案類型」下拉選單選擇 Assembly 原始檔案格式,然後開啟原始檔案。


這時你的 WinDbg 會有兩個「浮動」的視窗在 WinDbg 的主視窗上(或只有 Command 視窗浮動),雙擊或拖曳它們的標題列可在「泊定」和「浮動」之間切換,筆者習慣先把 Command 視窗拖到 WinDbg 的主視窗「泊定」,再把原始檔案的視窗拖到 WinDbg 的主視窗泊定。原始檔案視窗在左側,Command 視窗在右側:



好,現在我們可以正式開始偵錯了。


鼠標單擊原始代碼視窗中的 main 函數內的第一行,使鼠標停在此行。然後在功能表選擇:


Debug -> Run to Cursor(或按下 F7)


WindDbg 會執行你的可執行檔案並停在 main 函數的第一行。此時,我們可以在功能表選擇:


Debug -> Step Over (或按下 F10)


WinDbg 會執行並停在下一行。如此類推。


我們還可以在 call 函數呼叫語句按下 F11 或 F8 (Debug -> Step Into)進入函式,但不要在呼叫 API 函式用 F11 或 F8,否則會當機。


另外我們可以打開寄存器視窗來監察寄存器的變動:


在功能表選擇 View -> Registers(或按下 Alt + 4)


筆者習慣將它設為永遠浮動:


右鍵點擊它的標題列 -> 勾選 always float


另外,為更方便監察寄存器的變動,可以讓剛變動的寄存器自動排在最前:


點擊它功能表上的 Customize... -> 勾選 Display modified register values first


如果你不需要看到子寄存器,你可以隱藏它們,這樣(在 32 位)就只會出現 eax,不會出現 ax,ah 和 al 了:


點擊它功能表上的 customize... -> 勾選 Do not display subregisters


還有打開記憶體視窗,監察記憶體每個位元組的變動(筆者也是習慣讓它永遠浮動):


在功能表選擇 View -> Memory(或按下 Alt + 5)


在 Memory 視窗上的 Virtual 欄位輸入 &變數名(例如 &num1)便可以查看該變數所在的記憶體位址。你亦可輸入寄存器名(例如 ebp 或者 ebp + 4),查看寄存器內數值或其偏移量所指向的記憶體位址。


謹記在 Windows 作業系統下所有數值是以 Little Endian 方式儲存在記憶體,即「低有效 Byte(類似於最低有效位)在高有效 Byte 的前面」。例如有一變數 num1,數值為 01234567h,在你查看記憶體視窗時,會發現它以這樣的形成被存儲在記憶體:


67 45 23 01


你還可以在必要時查看可執行文件的「反組譯」:


在功能表選擇 View -> Disassembly(或按下 Alt + 7)


反組譯類似於把你的可執行檔還原為 Assembly Language 原始碼,但比你的 Assembly Language 原始碼更貼近真實。例如所有符號已替換成位址或值,可以用來檢查你的每一行代碼是否會形成符合你預期的機器語言(opcode)。