首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 組合語言

Win32 除錯 API 實戰(三):單步追蹤與指令計數器實作

     編輯:組合語言

前言

延續前兩篇 Win32 除錯 API 介紹,本文將深入探討如何實作「單步追蹤」功能,並撰寫一個能計算被除錯程式執行指令總數的除錯器範例。

理論基礎:CPU 陷阱旗標與單步模式

單步追蹤的核心在於 x86 CPU 的 旗標暫存器(EFLAGS)第 8 位——陷阱旗標(Trap Flag, TF)。當 TF=1 時,CPU 進入單步模式:每執行完一條指令,即產生一個除錯例外(Debug Exception,向量 1),並自動清除 TF。除錯器透過捕捉此例外,即可在每條指令間暫停、檢查暫存器/記憶體狀態。

Win32 API 實作單步追蹤的流程

  1. 呼叫 GetThreadContext,指定 ContextFlags = CONTEXT_CONTROL,取得被除錯執行緒的旗標暫存器值。
  2. 將 CONTEXT 結構中的 regFlag 成員的 TF 位(bit 8,值 0x100)設為 1。
  3. 呼叫 SetThreadContext 寫回修改後的上下文。
  4. 呼叫 ContinueDebugEvent 讓被除錯程式繼續執行。
  5. 等待除錯事件:被除錯程式執行一條指令後,觸發 EXCEPTION_DEBUG_EVENT,且 ExceptionCode == EXCEPTION_SINGLE_STEP。
  6. 若需繼續追蹤下一條指令,重複步驟 1-4(因 TF 已被 CPU 自動清除)。

完整範例程式碼

本程式先透過開啟檔案對話框選擇目標可執行檔,以 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

關鍵程式碼解析

1. 攔截初始中斷點啟用單步

.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),寫回後讓程式進入單步模式。

2. 處理單步例外並持續追蹤

.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 除錯事件迴路,實作指令級單步追蹤與計數。此技術是除錯器、動態分析工具、反混淆研究的基礎建構塊。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved