[Win32]一个调试器的实现(六)显示源代码
上一篇文章介紹了調試符號以及DbgHelp的加載和清理,這回我們使用它來實現一個顯示源代碼的功能。該功能的實際使用效果如下圖所示:
該功能不僅僅是顯示源代碼,還要顯示每一行代碼對應的地址。實現該功能大概需要進行以下的步驟:
①獲取下一條要執行的指令的地址。
②通過調試符號獲取該地址對應哪個源文件的哪一行。
③對于其它的行,通過調試符號獲取它對應的地址。
?
第一步可以通過獲取EIP寄存器的值來完成,相關的內容已經在第四篇文章中進行了講解,這里不再重復。下面講一下如何實現第二個和第三個步驟。
?
獲取源文件以及行號
在調試符號中,記錄了每一行源代碼對應的地址。通過DbgHelp的SymGetLineFromAddr64函數可以由地址獲取源文件路徑以及行號。該函數的聲明如下:
1?BOOL?WINAPI?SymGetLineFromAddr64(2?????HANDLE?hProcess,
3?????DWORD64?dwAddr,
4?????PDWORD?pdwDisplacement,
5?????PIMAGEHLP_LINE64?Line
6?);
hProcess參數是符號處理器的標識符,dwAddr是指令的地址。pdwDisplacement是一個輸出參數,用于獲取dwAddr相對于它所在行的起始地址的偏移量,以字節為單位。之所以需要這么一個參數,是因為一行代碼可能對應多條匯編指令,有了它就可以知道下一條要執行的指令位于這一行代碼的哪個位置。例如,int b = 3 * a + a;這行代碼對應以下的匯編指令:
1?8B?45?F8????mov????eax,dword?ptr?[a]?2?6B?C0?03????imul???eax,eax,3?
3?03?45?F8????add????eax,dword?ptr?[a]?
4?89?45?EC????mov????dword?ptr?[b],eax?
如果分別以這四條指令的地址調用SymGetLineFromAddr64函數,那么通過pdwDisplacement返回的值分別是0,3,6和9。
?
第四個參數是指向IMAGEHLP_LINE64結構體的指針,該結構體用來保存有關于行的信息,其聲明如下:
1?typedef?struct?_IMAGEHLP_LINE64?{2?????DWORD?SizeOfStruct;
3?????PVOID?Key;
4?????DWORD?LineNumber;
5?????PTSTR?FileName;
6?????DWORD64?Address;
7?}?IMAGEHLP_LINE64,?*PIMAGEHLP_LINE64;
SizeOfStruct字段保存結構體的大小,在調用SymGetLineFromAddr64之前需要初始化這個字段,否則函數調用會失敗。Key字段是由操作系統保留的,我們不需要使用它。FileName和LineNumber字段分別是源文件的絕對路徑以及行號。Address是該行的起始地址。
?
要注意,FileName字段是一個指向字符串的指針,而這個字符串的存儲空間并不需要我們自己分配,我們也不需要釋放這個指針指向的內存。實際上這個指針指向了調試符號內的某個地方,我們可以讀取這些數據,但是不能修改其中的數據,一旦這些數據被修改,其它的DbgHelp函數可能會出現奇怪的問題。如果一定要修改這個字符串,要先將它復制到另一個地方再進行操作。我很奇怪為什么這個字段不是PCTSTR類型的,這樣的話就不必擔心這個字符串被修改了。
?
調用SymGetLineFromAddr64成功的條件有兩個:一是dwAddr的值所在的模塊已經通過SymLoadModule64函數加載到符號處理器中;二是該模塊含有SymGetLineFromAddr64所需的調試符號信息。如果第一個條件沒有滿足,GetLastError返回126;如果第二個條件沒有滿足,GetLastError返回487。
?
下面是調用SymGetLineFromAddr64的一個例子:
?1?//獲取EIP?2?CONTEXT?context;
?3?GetDebuggeeContext(&context);
?4?
?5?//獲取源文件以及行信息
?6?IMAGEHLP_LINE64?lineInfo?=?{?0?};
?7?lineInfo.SizeOfStruct?=?sizeof(lineInfo);
?8?DWORD?displacement?=?0;
?9?
10?if?(SymGetLineFromAddr64(
11?????GetDebuggeeHandle(),
12?????context.Eip,
13?????&displacement,
14?????&lineInfo)?==?FALSE)?{
15?
16?????DWORD?errorCode?=?GetLastError();
17?????????
18?????switch?(errorCode)?{
19?
20?????????//?126?表示還沒有通過SymLoadModule64加載模塊信息
21?????????case?126:
22?????????????std::wcout?<<?TEXT("Debug?info?in?current?module?has?not?loaded.")?<<?std::endl;
23?????????????return;
24?
25?????????//?487?表示模塊沒有調試符號
26?????????case?487:
27?????????????std::wcout?<<?TEXT("No?debug?info?in?current?module.")?<<?std::endl;
28?????????????return;
29?
30?????????default:
31?????????????std::wcout?<<?TEXT("SymGetLineFromAddr64?failed:?")?<<?errorCode?<<?std::endl;
32?????????????return;
33?????}
34?}
?
獲取行的地址
通過SymGetLineFromAddr64可以獲取指令對應的源文件以及行號,那么能不能根據源文件路徑以及行號獲取行的地址呢?當然可以,SymGetLineFromName64函數就是用作此目的的。該函數的聲明如下:
1?BOOL?WINAPI?SymGetLineFromName64(2?????HANDLE?hProcess,
3?????PCTSTR?ModuleName,
4?????PCTSTR?FileName,
5?????DWORD?dwLineNumber,
6?????PLONG?lpDisplacement,
7?????PIMAGEHLP_LINE64?Line
8?);
該函數與SymGetLineFromAddr64很相似,都是通過IMAGEHLP_LINE64結構體來返回行的信息,并且都有一個displacement輸出參數,不過這個參數在兩個函數中的意義大不相同,下面將會詳述。首先來看一下其它參數的含義。
?
ModuleName用于指定模塊的名稱,上一篇文章講解SymLoadModule64函數時提到的ModuleName參數就可以用在這個地方(奇怪的是SymLoadModule64的ModuleName參數是PCSTR類型,而SymGetLineFromName64的ModuleName參數卻是PCTSTR類型)。當FileName參數只指定了文件名,而多個模塊中含有同名的源文件時,SymGetLineFromName64就使用這個參數確定使用哪個模塊的源文件。如果各個模塊都沒有同名的源文件,或者FileName指定的是絕對路徑時,這個參數就沒有必要了,指定為NULL即可。
?
FileName和dwLineNumber 參數分別指定源文件和行號。FileName可以是文件名,也可以是絕對路徑,正如上面的描述那樣。dwLineNumber是任意非零值,即使行號在源文件中不存在,甚至是負數,SymGetLineFromName64也會返回TRUE!那么我們如何知道指定的行號是否有效呢?只要檢查displacement的值即可。大多數情況下,displacement表示指定行與最接近該行的有效行的行號之差,而且有效行的行號要小于等于指定行的行號。可以用下面的式子表示(式中的變量均使用函數參數的名字):
*lpDisplacement?=?dwLine?-?Line->LineNumber??(dwLine?>=?Line->LineNumber)所謂有效行即能夠產生匯編指令的行(能產生匯編指令才會有對應的地址),例如int a = 1 + 1;是有效行,而int a;和空白行則不屬于有效行。用以下的代碼為例進行說明:
?1?int?wmain(int?argc,?wchar_t**?argv)?{?2?
?3?????int?a?=?1?+?1;
?4?
?5?
?6?
?7?????int?b?=?2?+?2;
?8?
?9?????return?0;
10?}
①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為指定行號與該有效行行號的差,同樣符合上面的式子。
?
如果dwLine為0,那么SymGetLineFromName64返回FALSE,GetLastError返回1168。奇怪的是,dwLine為負數竟然也可以調用成功,此時函數返回最后一行有效行的信息,displacement為INT_MAX + dwLine。
?
綜上所述,要判斷指定的行是否為有效行,只要檢查displacement是否為0即可。
?
示例代碼
好了,知道了如何獲取行號以及行的地址之后就可以實現顯示源代碼的功能了,詳細的方法請參考示例代碼。使用這個功能時要注意源文件必須與被調試程序和調試符號同步,如果修改了源代碼而沒有重新編譯鏈接的話,顯示的代碼肯定是錯誤的。
?
現在MiniDebugger中增加了一個命令:
?
l [after] [before]
顯示當前正在執行的那一行以及附近的代碼。after指定顯示當前那一行代碼的后面多少行,before指定顯示當前那一行代碼的前面多少行。如果省略的話,默認取值為10。
?
如果在執行s命令啟動了被調試進程之后立即執行l命令,會得到“SymGetLineFromAddr64 failed: 6”的錯誤信息,這是因為此時還沒有創建符號處理器。要至少執行一次g命令之后才可以使用l命令。
?
http://files.cnblogs.com/zplutor/MiniDebugger6.rar
轉載于:https://www.cnblogs.com/zplutor/archive/2011/03/27/1997198.html
總結
以上是生活随笔為你收集整理的[Win32]一个调试器的实现(六)显示源代码的全部內容,希望文章能夠幫你解決所遇到的問題。