[Win32]一個偵錯程式的實現(六)顯示原始碼

Yuri800發表於2016-09-29

上一篇文章介紹了除錯符號以及DbgHelp的載入和清理,這回我們使用它來實現一個顯示原始碼的功能。該功能的實際使用效果如下圖所示:

該功能不僅僅是顯示原始碼,還要顯示每一行程式碼對應的地址。實現該功能大概需要進行以下的步驟:

①獲取下一條要執行的指令的地址。

②通過除錯符號獲取該地址對應哪個原始檔的哪一行。

③對於其它的行,通過除錯符號獲取它對應的地址。

 

第一步可以通過獲取EIP暫存器的值來完成,相關的內容已經在第四篇文章中進行了講解,這裡不再重複。下面講一下如何實現第二個和第三個步驟。

 

獲取原始檔以及行號

在除錯符號中,記錄了每一行原始碼對應的地址。通過DbgHelpSymGetLineFromAddr64函式可以由地址獲取原始檔路徑以及行號。該函式的宣告如下:

 BOOL WINAPI SymGetLineFromAddr64(
     HANDLE hProcess,
     DWORD64 dwAddr,
     PDWORD pdwDisplacement,
     PIMAGEHLP_LINE64 Line
 );

hProcess引數是符號處理器的識別符號,dwAddr是指令的地址。pdwDisplacement是一個輸出引數,用於獲取dwAddr相對於它所在行的起始地址的偏移量,以位元組為單位。之所以需要這麼一個引數,是因為一行程式碼可能對應多條彙編指令,有了它就可以知道下一條要執行的指令位於這一行程式碼的哪個位置。例如,int b = 3 * a + a;這行程式碼對應以下的彙編指令:

 8B 45 F8    mov    eax,dword ptr [a] 
 6B C0 03    imul   eax,eax,3 
 03 45 F8    add    eax,dword ptr [a] 
 89 45 EC    mov    dword ptr [b],eax 

如果分別以這四條指令的地址呼叫SymGetLineFromAddr64函式,那麼通過pdwDisplacement返回的值分別是0369

 

第四個引數是指向IMAGEHLP_LINE64結構體的指標,該結構體用來儲存有關於行的資訊,其宣告如下:

typedef struct _IMAGEHLP_LINE64 {
    DWORD SizeOfStruct;
    PVOID Key;
    DWORD LineNumber;
    PTSTR FileName;
    DWORD64 Address;
} IMAGEHLP_LINE64, *PIMAGEHLP_LINE64;

SizeOfStruct欄位儲存結構體的大小,在呼叫SymGetLineFromAddr64之前需要初始化這個欄位,否則函式呼叫會失敗。Key欄位是由作業系統保留的,我們不需要使用它。FileNameLineNumber欄位分別是原始檔的絕對路徑以及行號。Address是該行的起始地址。

要注意,FileName欄位是一個指向字串的指標,而這個字串的儲存空間並不需要我們自己分配,我們也不需要釋放這個指標指向的記憶體。實際上這個指標指向了除錯符號內的某個地方,我們可以讀取這些資料,但是不能修改其中的資料,一旦這些資料被修改,其它的DbgHelp函式可能會出現奇怪的問題。如果一定要修改這個字串,要先將它複製到另一個地方再進行操作。我很奇怪為什麼這個欄位不是PCTSTR型別的,這樣的話就不必擔心這個字串被修改了。

呼叫SymGetLineFromAddr64成功的條件有兩個:一是dwAddr的值所在的模組已經通過SymLoadModule64函式載入到符號處理器中;二是該模組含有SymGetLineFromAddr64所需的除錯符號資訊。如果第一個條件沒有滿足,GetLastError返回126;如果第二個條件沒有滿足,GetLastError返回487

下面是呼叫SymGetLineFromAddr64的一個例子:


//獲取EIP
CONTEXT context;
GetDebuggeeContext(&context);

//獲取原始檔以及行資訊
IMAGEHLP_LINE64 lineInfo = { 0 };
lineInfo.SizeOfStruct = sizeof(lineInfo);
DWORD displacement = 0;

if (SymGetLineFromAddr64(
    GetDebuggeeHandle(),
    context.Eip,
    &displacement,
    &lineInfo) == FALSE) {

    DWORD errorCode = GetLastError();
        
    switch (errorCode) {

        // 126 表示還沒有通過SymLoadModule64載入模組資訊
        case 126:
            std::wcout << TEXT("Debug info in current module has not loaded.") << std::endl;
            return;

        // 487 表示模組沒有除錯符號
        case 487:
            std::wcout << TEXT("No debug info in current module.") << std::endl;
            return;

        default:
            std::wcout << TEXT("SymGetLineFromAddr64 failed: ") << errorCode << std::endl;
            return;
    }
}

獲取行的地址

通過SymGetLineFromAddr64可以獲取指令對應的原始檔以及行號,那麼能不能根據原始檔路徑以及行號獲取行的地址呢?當然可以,SymGetLineFromName64函式就是用作此目的的。該函式的宣告如下:

BOOL WINAPI SymGetLineFromName64(
    HANDLE hProcess,
    PCTSTR ModuleName,
    PCTSTR FileName,
    DWORD dwLineNumber,
    PLONG lpDisplacement,
    PIMAGEHLP_LINE64 Line
);

該函式與SymGetLineFromAddr64很相似,都是通過IMAGEHLP_LINE64結構體來返回行的資訊,並且都有一個displacement輸出引數,不過這個引數在兩個函式中的意義大不相同,下面將會詳述。首先來看一下其它引數的含義。

ModuleName用於指定模組的名稱,上一篇文章講解SymLoadModule64函式時提到的ModuleName引數就可以用在這個地方(奇怪的是SymLoadModule64ModuleName引數是PCSTR型別,而SymGetLineFromName64ModuleName引數卻是PCTSTR型別)。當FileName引數只指定了檔名,而多個模組中含有同名的原始檔時,SymGetLineFromName64就使用這個引數確定使用哪個模組的原始檔。如果各個模組都沒有同名的原始檔,或者FileName指定的是絕對路徑時,這個引數就沒有必要了,指定為NULL即可。

FileNamedwLineNumber 引數分別指定原始檔和行號。FileName可以是檔名,也可以是絕對路徑,正如上面的描述那樣。dwLineNumber是任意非零值,即使行號在原始檔中不存在,甚至是負數,SymGetLineFromName64也會返回TRUE!那麼我們如何知道指定的行號是否有效呢?只要檢查displacement的值即可。大多數情況下,displacement表示指定行與最接近該行的有效行的行號之差,而且有效行的行號要小於等於指定行的行號。可以用下面的式子表示(式中的變數均使用函式引數的名字):

*lpDisplacement = dwLine - Line->LineNumber  (dwLine >= Line->LineNumber)
所謂有效行即能夠產生彙編指令的行(能產生彙編指令才會有對應的地址),例如int a = 1 + 1;是有效行,而int a;和空白行則不屬於有效行。用以下的程式碼為例進行說明:

int wmain(int argc, wchar_t** argv) {

    int a = 1 + 1;



    int b = 2 + 2;

    return 0;
}

dwLine = 2時,Line->LineNumber = 1*lpDisplacement = 1

dwLine = 4, 5, 6時,Line->LineNumber = 3*lpDisplacement = 1, 2 , 3

dwLine = 7時,Line->LineNumber = 7*lpDisplacement = 0

dwLine = 12時,Line->LineNumber = 10*lpDisplacement = 2

 

由第四個例子可以看出,如果指定的行號大於原始檔的行數,則函式返回最後一行有效行的資訊,displacement為指定行號與該有效行行號的差,同樣符合上面的式子。

如果dwLine0,那麼SymGetLineFromName64返回FALSEGetLastError返回1168。奇怪的是,dwLine為負數竟然也可以呼叫成功,此時函式返回最後一行有效行的資訊,displacementINT_MAX + dwLine

綜上所述,要判斷指定的行是否為有效行,只要檢查displacement是否為0即可。

示例程式碼

好了,知道了如何獲取行號以及行的地址之後就可以實現顯示原始碼的功能了,詳細的方法請參考示例程式碼。使用這個功能時要注意原始檔必須與被除錯程式和除錯符號同步,如果修改了原始碼而沒有重新編譯連結的話,顯示的程式碼肯定是錯誤的。

現在MiniDebugger中增加了一個命令:

l [after] [before]

顯示當前正在執行的那一行以及附近的程式碼。after指定顯示當前那一行程式碼的後面多少行,before指定顯示當前那一行程式碼的前面多少行。如果省略的話,預設取值為10

如果在執行s命令啟動了被除錯程式之後立即執行l命令,會得到“SymGetLineFromAddr64 failed: 6”的錯誤資訊,這是因為此時還沒有建立符號處理器。要至少執行一次g命令之後才可以使用l命令。


作者:Zplutor
出處:http://www.cnblogs.com/zplutor/
本文版權歸作者和部落格園共有,歡迎轉載。但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。

相關文章