Win32 乙個偵錯程式的實現(八)單步執行

2021-07-08 13:43:36 字數 3259 閱讀 5886

****:

上回講解了如何實現斷點功能,這回講解如何實現與斷點緊密相關的單步執行功能。單步執行有三種型別:stepin,stepover和stepout,它們的實現方式比較多樣化,單獨實現它們的話並不困難,但是將它們整合到一起就比較困難了,特別是加上斷點功能之後,程式的邏輯更加難以理解。本文首先單獨講解每種單步執行的原理,最後講解如何將它們整合到一起。這都是我個人的實現方法,大家可以用來參考。(注意:本文所講的單步執行是源**級別的,而不是指令級別的。)

stepin原理

stepin即逐條語句執行,遇到函式呼叫時進入函式內部。當使用者對偵錯程式下達stepin命令時,偵錯程式的實現方式如下:

①通過除錯符號獲取當前指令對應的行資訊,並儲存該行的資訊。

②設定tf位,開始cpu的單步執行。

③在處理單步執行異常時,獲取當前指令對應的行資訊,與①中儲存的行資訊進行比較。如果相同,表示仍然在同一行上,轉到②;如果不相同,表示已到了不同的行,結束stepin。

stepover原理

stepover即逐條語句執行,遇到函式呼叫時不進入函式內部。當使用者對偵錯程式下達stepover命令時,偵錯程式的實現方式如下:

①通過除錯符號獲取當前指令對應的行資訊,並儲存該行的資訊。

②檢查當前指令是否call指令。如果是,則在下一條指令設定乙個斷點,然後讓被除錯程序繼續執行;如果不是,則設定tf位,開始cpu的單步執行,跳到④。

③處理斷點異常時,恢復斷點所在指令第乙個位元組的內容。然後獲取當前指令對應的行資訊,與①中儲存的行資訊進行比較,如果相同,跳到②;否則停止stepover。

④處理單步執行異常時,獲取當前指令對應的行資訊,與①中儲存的行資訊進行比較。如果相同,跳到②;否則停止stepover。

stepover的原理與stepin基本相同,唯一的不同就是遇到call指令時跳過,跳過的方法是在下一條指令設定斷點,然後令被除錯程序全速執行,觸發斷點之後再設定tf位,進行cpu的單步執行。在全速執行時,使用者設定的斷點和被除錯程序中的斷點不應該忽略,而應該像正常的執行那樣中斷並通知使用者,此時應該停止stepover。在進行cpu的單步執行時,則應該像stepin那樣忽略斷點。stepover需要斷點來實現,所以應該將stepover使用的斷點和其它型別的斷點區分開來。

stepover的乙個難點就是如何知道當前指令是否call指令,以及獲取它的長度。這是比較困難的問題,需要一些指令格式方面的知識,可以參考《call指令有多少種寫法》(一文。在這篇文章的最後提供了一張call指令的格式表,我們只需要按照這張表來寫**進行判斷即可。

stepout原理

stepout即跳出當前正在執行的函式,立即回到上一層函式。當使用者對偵錯程式下達stepout命令時,偵錯程式的實現方式如下:

①獲取當前函式的ret指令位址,並在該指令設定斷點,讓被除錯程序繼續執行。

②處理斷點異常時,恢復斷點所在指令的第乙個位元組。從執行緒棧的頂部中獲取返回位址,在該位址設定斷點,然後讓被除錯程序繼續執行。

③再次處理斷點異常,恢復斷點所在指令的第乙個位元組,結束stepout。

stepout有兩個難點:一是如何獲取ret指令的位址,二是如何獲取函式的返回位址。對於第乙個問題,可以使用symfromaddr函式來獲取函式的相關資訊。symfromaddr函式的作用是由位址獲取相應符號的資訊,符號可以是變數或者函式。其宣告如下:

bool winapi symfromaddr(

handle hprocess,

dword64 address,

pdword64 displacement,

psymbol_info symbol

);

引數address可以是任意位址,如果該位址屬於乙個變數,那麼函式返回該變數的符號資訊;如果該位址屬於乙個函式,那麼返回該函式的符號資訊;如果該位址不對應任何符號,函式返回false。

我們可以將當前被除錯程序的eip作為位址傳給symfromaddr,以得到當前函式的資訊。symbol_info的address欄位儲存了函式第一條指令的位址,size欄位儲存了函式所有指令的位元組長度,由於ret指令是函式的最後一條指令,所以address加上size再減去ret指令的長度就是ret指令的位址。那麼接下來的問題就是要獲得ret指令的長度。ret指令比call指令簡單得多,只有兩種形式,一種只有乙個位元組長,十六進製制值為0xc3或0xcb;另一種有三個位元組長,第乙個位元組的十六進製制值為0xc2或0xca,後面兩個位元組是esp將要加上的值。

第二個難點是獲取函式的返回位址。如果你熟悉函式的呼叫過程,肯定知道在即將執行ret指令時,執行緒棧的頂部,也就是esp所指的記憶體位置,必定是函式的返回位址。這就是在ret指令設定斷點的目的。

在進行stepout時,由於被除錯程序是全速執行的,所以不能忽略其它斷點,而且在觸發斷點時要取消stepout使用的斷點。另外stepout使用的斷點也要與其它型別的斷點區分開來。

整合

上文介紹的三種單步執行都需要斷點以及cpu單步執行的支援,這意味著它們的處理邏輯都集中在斷點異常和單步執行異常的處理函式中,這會使**的邏輯複雜,難以理解。在這裡我嘗試理清這個思路,讓大家對示例**有更好的理解。

初始斷點

被除錯程序中的斷點

偵錯程式設定的斷點

除了初始斷點之外,另外兩種斷點在下文中統稱為普通斷點。新增了單步執行的功能之後,新增了兩種型別的斷點:

stepover斷點

stepout斷點

斷點型別的增加使得斷點異常的處理函式變得更加複雜,因此有必要將所有斷點的處理都放在第一次接收斷點異常時,以降低複雜度,所以此時就需要手動將eip減1了。

上文說過,在被除錯程序逐條指令執行時,應該忽略所有普通斷點,忽略的意思是不通知使用者。為了達到此目的,需要儲存被除錯程序的執行狀態(全速執行還是逐條指令執行),然後對這些狀態進行檢查。

下圖是斷點異常的處理過程:

下圖是單步執行異常的處理過程:

上面兩幅流程圖中,stepover斷點的處理和單步執行異常的處理有相同的邏輯,在實現時應該將這些共同邏輯抽取到乙個函式裡。

示例**

這次minidebugger中新增了三種單步執行的命令,分別如下:

in    stepin

over  stepover

out   stepout

如何建立乙個win32程式

首先要寫類似於main函式的winmain,int winapi winmain hinstance hinstance,hinstance hprevinstance,lpstr lpcmdline,int ncmdshow winapi是幹什麼的呢?它是呼叫宣告,相當於 stdcal。c 預設的...

建立乙個win32視窗程式

include stdafx.h 2hinstance g hinst null 3 視窗處理函式 lresult callback wndproc hwnd hwnd,視窗控制代碼 uint nmsg,視窗訊息id wparam wparam,訊息傳來的兩個引數 lparam lparam 返回預...

第乙個Win32程式

第乙個win32視窗.cpp 定義應用程式的入口點。include stdafx.h include 第乙個win32視窗.h include include stdio.h hwnd hwnd pchar szoutbuff lresult callback wndproc hwnd hwnd,u...