延續前兩篇 Win32 除錯 API 介紹,本文將深入探討如何實作「單步追蹤」功能,並撰寫一個能計算被除錯程式執行指令總數的除錯器範例。
單步追蹤的核心在於 x86 CPU 的 旗標暫存器(EFLAGS)第 8 位——陷阱旗標(Trap Flag, TF)。當 TF=1 時,CPU 進入單步模式:每執行完一條指令,即產生一個除錯例外(Debug Exception,向量 1),並自動清除 TF。除錯器透過捕捉此例外,即可在每條指令間暫停、檢查暫存器/記憶體狀態。
GetThreadContext,指定 ContextFlags = CONTEXT_CONTROL,取得被除錯執行緒的旗標暫存器值。CONTEXT 結構中的 regFlag 成員的 TF 位(bit 8,值 0x100)設為 1。SetThreadContext 寫回修改後的上下文。ContinueDebugEvent 讓被除錯程式繼續執行。EXCEPTION_DEBUG_EVENT,且 ExceptionCode == EXCEPTION_SINGLE_STEP。本程式先透過開啟檔案對話框選擇目標可執行檔,以 DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS 建立程序,進入除錯迴路,於 EXCEPTION_BREAKPOINT(程式剛載入、尚未執行第一條指令)啟用單步模式,其後每收到 EXCEPTION_SINGLE_STEP 即遞增計數器,並重新設定 TF,直到 EXIT_PROCESS_DEBUG_EVENT 結束。
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
.data
AppName db "Win32 Debug Example no.4",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0Dh,0Ah
db "Total Instructions executed : %lu",0
TotalInstruction dd 0
.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
context CONTEXT <>
.code
start:
; 開啟檔案對話框
mov ofn.lStructSize, SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile, 512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke GetStartupInfo, addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, \
DEBUG_PROCESS + DEBUG_ONLY_THIS_PROCESS, NULL, NULL, \
addr startinfo, addr pi
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.if DBEvent.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ExitProc, TotalInstruction
invoke MessageBox, 0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
.break
.elseif DBEvent.dwDebugEventCode == EXCEPTION_DEBUG_EVENT
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT
; 程式剛載入,啟用單步模式
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext, pi.hThread, addr context
or context.regFlag, 100h ; 設定 TF (bit 8)
invoke SetThreadContext, pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode == EXCEPTION_SINGLE_STEP
inc TotalInstruction ; 計數器 +1
invoke GetThreadContext, pi.hThread, addr context
or context.regFlag, 100h ; 重新設定 TF
invoke SetThreadContext, pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.endif
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
invoke CloseHandle, pi.hProcess
invoke CloseHandle, pi.hThread
.endif
invoke ExitProcess, 0
end start
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext, pi.hThread, addr context
or context.regFlag, 100h
invoke SetThreadContext, pi.hThread, addr context
invoke ContinueDebugEvent, ..., DBG_CONTINUE
Windows 在被除錯程式即將執行第一條指令前,會送出 EXCEPTION_BREAKPOINT(實為 INT 3)。此時取得執行緒上下文,設定 regFlag 的 bit 8(0x100),寫回後讓程式進入單步模式。
.elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode == EXCEPTION_SINGLE_STEP
inc TotalInstruction
invoke GetThreadContext, pi.hThread, addr context
or context.regFlag, 100h
invoke SetThreadContext, pi.hThread, addr context
invoke ContinueDebugEvent, ..., DBG_CONTINUE
每當單步例外觸發,TotalInstruction 遞增。由於 CPU 在產生除錯例外後會自動清除 TF,必須再次讀取上下文、設定 TF、寫回,才能繼續追蹤下一條指令。
單步追蹤極慢。 現代 CPU 每秒可執行數十億指令,但每條指令都需經過除錯器處理、上下文切換、使用者模式/核心模式轉換,實際速度可能只有數千指令/秒。對大型程式(如啟動瀏覽器)可能需等待 10 分鐘以上才能結束。實務上單步追蹤僅用於極小範圍的程式碼分析,或搭配條件斷點、硬體斷點使用。
本文示範如何利用 GetThreadContext/SetThreadContext 操作旗標暫存器的 TF 位,配合 Win32 除錯事件迴路,實作指令級單步追蹤與計數。此技術是除錯器、動態分析工具、反混淆研究的基礎建構塊。