1. 程式人生 > >[Win32]一個調試器的實現(五)調試符號

[Win32]一個調試器的實現(五)調試符號

獨立 普通 mic proc 相關信息 預處理 arc const 負責

一個調試器應該可以跟蹤被調試程序執行到了什麽地方,顯示下一條將要執行的語句,顯示各個變量的值,設置斷點,進行單步執行等等,這些功能都需要一個基礎設施的支持,那就是調試符號。

什麽是調試符號

我們知道,在exedll等可執行文件中保存的數據大部分都是二進制指令,CPU直接讀取這些指令並執行。那麽調試器是如何知道每條指令對應哪個源文件的哪一行代碼呢?它又是如何知道每個變量和函數的名稱,並顯示變量的值呢?很顯然,可執行文件的二進制數據中不可能包含這麽多信息,這一切都是由調試符號來支持的。

所謂符號,簡單來說就是源代碼中每個對象的名稱。例如變量、函數、類型等,它們都有一個名稱,以及其它的相關信息:變量有類型、地址等信息;函數有返回值類型、參數類型、地址等信息;類型有長度等信息。編譯器在編譯每個源文件的時候都會收集該源文件中的符號的信息,在生成目標文件的時候將這些信息保存到符號表中。鏈接器使用符號表中的信息將各個目標文件鏈接成可執行文件,同時將多個符號表整合成一個文件,這個文件就是用於調試的符號文件,它既可以嵌入可執行文件中,也可以獨立存在。

符號文件中包含的信息可多可少,這樣可以避免泄露程序的信息。調試版程序的符號文件包含了所有的調試信息,而發行版程序的符號文件只包含非常少的調試信息,甚至沒有符號文件。

符號文件有多種不同的格式,不同的編譯器可能使用不同的格式。目前Visual Studio默認使用的是PDB格式,生成項目之後,在Debug或者Release文件夾下都可以找到與生成的文件同名的PDB文件。本文以及接下來的文章中,均使用PDB格式的符號文件來進行調試。

使用調試符號

Windows提供了兩種方法讓我們可以訪問調試符號,分別是DbgHelpDebug Help Library)和DIADebug Interface Access

)。DIA是基於COM的,對於不熟悉COM的人使用起來會比較麻煩;而使用DbgHelp就像使用普通的Windows API那樣,比較容易。本文以及接下來的文章中,使用的都是DbgHelp

使用DbgHelp的程序需要加載DbgHelp.dll這個動態鏈接庫,Windows自帶這個文件,位於C:\Windows\System32。但是Windows自帶的通常是較低版本的文件,所以最好是獲取一個最新版本的,將其與程序的可執行文件放在同一個目錄中,這樣既可以使用最新的DbgHelp,又不需要改動系統文件。

獲取最新DbgHelp.dll的一個方法是下載Windows Debugging Tools

,地址為http://msdn.microsoft.com/en-us/windows/hardware/gg463009.aspx。不過這個工具包很大,為了這一個小小的文件可能要下載很長時間。其實在Visual Studio 2010中已包含了最新版本的DbgHelp(至少在寫作本文的時候是如此),路徑是C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\dbghelp.dll。(假設Visual Studio 2010安裝在C:\Program Files

為了在程序中使用DbgHelp,你需要先完成以下的事情:

打開項目屬性對話框,定位到“配置屬性”-“鏈接器”-“輸入”,在右邊的“附加依賴項”中添加dbghelp.lib

有一點需要註意,DbgHelp使用DBGHELP_TRANSLATE_TCHAR這個預定義標記來決定是否使用Unicode字符串,而不是UNICODE標記。所以,如果你的程序使用Unicode字符串,那就定位到“配置屬性”-C/C++-“預處理器”,在右邊的“預處理器定義”中添加DBGHELP_TRANSLATE_TCHAR

最後,在需要使用DbgHelp的源文件中,包含Windows.hDbgHelp.h頭文件即可。(Windows.h需要包含在DbgHelp.h的前面)

加載調試符號

一個進程會有多個模塊,每個模塊都有它自己的符號文件,有關符號文件的信息保存在模塊的可執行文件中。DbgHelp通過符號處理器(Symbol Handler)來處理模塊的符號文件。符號處理器位於調試器進程中,每個被調試的進程對應一個符號處理器。通常,調試器在被調試進程啟動的時候創建符號處理器,在被調試進程結束的時候清理相應符號處理器占用的資源。

創建一個符號處理器使用SymInitialize函數,該函數聲明如下:

1 BOOL WINAPI SymInitialize(
2 HANDLE hProcess,
3 PCTSTR UserSearchPath,
4 fInvadeProcess
5 );

第一個參數是被調試進程的句柄,它是符號管理器的標識符,其它的DbgHelp函數都需要這樣一個參數值指明使用哪個符號管理器。實際上這個參數不一定是句柄:當fInvadeProcess參數為TRUE時,它必須是一個有效的進程句柄;當fInvadeProcessFALSE時,它可以是任意一個唯一的數值。

fInvadeProcess的作用是指示是否加載進程所有模塊的調試符號,如果該參數為FALSE,那麽SymInitialize只是創建一個符號處理器,不加載任何模塊的調試符號,此時需要我們自己調用SymLoadModule64函數來加載模塊;如果為TRUESymInitialize會遍歷進程的所有模塊,並加載其調試符號,所以在這種情況下hProcess必須是一個有效的進程句柄。

fInvadeProcessTRUE時,第二個參數UserSearchPath指示SymInitialize函數去哪裏尋找符號文件。使用PDB符號文件的可執行文件中已包含有符號文件的絕對路徑,如果符號文件不存在,SymInitialize就會使用UserSearchPath指定的路徑去尋找符號文件。該參數可指定多個路徑,以分號(;)分割。如果該參數為NULL,那麽SymInitialize會按照以下的順序尋找符號文件:

調試器進程的工作目錄;

_NT_SYMBOL_PATH環境變量指定的路徑;

_NT_ALTERNATE_SYMBOL_PATH環境變量指定的路徑。

如果在以上路徑中仍然找不到符號文件,SymInitialize並不會返回FALSE,而是返回TRUE。也就是說,它成功創建了符號處理器,並且加載了模塊的信息,但是沒有加載調試符號(關於如何判斷某個模塊是否加載了調試符號,下文會有講解)。實際上,SymInitialize幾乎不會返回FALSE,然而在某種情況下它會這麽做,下面會有關於這方面的說明。

根據對SymInitialize的描述,有兩種方法可以加載調試符號。第一種方法是在調用SymInitialize的時候第三個參數傳入TRUE,由它負責加載每個模塊的調試符號。這種方法的好處是方便,但是有一個前提:被調試進程必須初始化完畢。我曾經嘗試在處理CREATE_PROCESS_DEBUG_EVENT事件的時候使用這種方法加載調試符號,但SymInitialize總是返回FALSEGetLastError返回-1。這是因為在處理CREATE_PROCESS_DEBUG_EVENT事件時,被調試進程需要的模塊還未加載完成,處於一個不完整的狀態。所以,應該等到被調試進程初始化之後才使用這種方法。由於每個進程在初始化完畢之後都會引發一個斷點異常,所以加載調試符號的最好的時機就是在處理這個初始斷點的時候。關於初始斷點的內容在講解斷點的時候會提及。

第二種方法是在調用SymInitialize的時候第三個參數傳入FALSE,然後對每個模塊調用SymLoadModule64函數加載調試符號。我們可以在處理CREATE_PROCESS_DEBUG_EVENTLOAD_DLL_DEBUG_EVENT事件時分別加載exe文件和dll文件的調試符號。SymLoadModule64函數的聲明如下:

技術分享圖片 1 DWORD64 WINAPI SymLoadModule64(
2 HANDLE hProcess,
3 HANDLE hFile,
4 PCSTR ImageName,
5 PCSTR ModuleName,
6 DWORD64 BaseOfDll,
7 DWORD SizeOfDll
8 ); 技術分享圖片

第一個參數是符號處理器的標識符,也就是在調用SymInitialize時第一個參數的值。第二個參數是模塊文件的句柄,該函數通過這個文件句柄來獲取有關符號文件的信息。你可能記得在CREATE_PROCESS_DEBUG_INFOLOAD_DLL_DEBUG_INFO結構體中都有一個hFile的字段,這個字段剛好可以用在SymLoadModule64函數上。

第三個參數ImageName用於指定模塊文件的路徑和名稱,當第二個參數為NULL時,SymLoadModule64會通過這裏指定的路徑和名稱去尋找模塊文件。一般情況下都不會使用這個參數,因為我們可以使用更可靠的hFile參數。

第四個參數ModuleName為該模塊賦予一個名稱,在使用其它DbgHelp函數的時候可以通過這個名稱來引用模塊。如果該參數為NULLSymLoadModule64會使用符號文件的文件名作為模塊名稱。

第五個參數BaseOfDll是模塊加載到進程地址空間之後的基地址。這個參數很重要,因為符號文件中每個符號的地址都是相對於模塊基地址的偏移地址,而不是絕對地址,這樣的話,不論模塊被加載到哪個地址,它的符號文件都是可用的。當然,這一切的前提是你將正確的模塊基地址傳給了SymLoadModule64函數。幸運的是,CREATE_PROCESS_DEBUG_INFOLOAD_DLL_DEBUG_INFO結構體中已包含了一個lpBaseOfImage字段,我們直接使用即可,不必為了獲取模塊基地址而大動幹戈。

至於最後一個參數SizeOfDll,表示模塊文件的大小。我還不知道這個參數的作用,也不知道應該傳一個什麽樣的值給它。我一直都給它傳一個0,即使如此SymLoadModule64也能正常工作。所以我們還是暫且將它放在一旁,將註意力轉移到別的地方吧。

添加了加載調試符號的代碼之後,處理CREATE_PROCESS_DEBUG_EVENT事件的代碼大概像下面這樣子:

技術分享圖片 1 BOOL OnProcessCreated(const CREATE_PROCESS_DEBUG_INFO* pInfo) {
2
3 //初始化符號處理器
4 //註意,這裏不能使用pInfo->hProcess,因為g_hProcess和pInfo->hProcess
5 //的值並不相同,而其它DbgHelp函數使用的是g_hProcess。
6 if (SymInitialize(g_hProcess, NULL, FALSE) == TRUE) {
7
8 //加載模塊的調試信息
9 DWORD64 moduleAddress = SymLoadModule64(
10 g_hProcess,
11 pInfo->hFile,
12 NULL,
13 NULL,
14 (DWORD64)pInfo->lpBaseOfImage,
15 0);
16
17 if (moduleAddress == 0) {
18
19 std::wcout << TEXT("SymLoadModule64 failed: ") << GetLastError() << std::endl;
20 }
21 }
22 else {
23
24 std::wcout << TEXT("SymInitialize failed: ") << GetLastError() << std::endl;
25 }
26
27 CloseHandle(pInfo->hFile);
28 CloseHandle(pInfo->hThread);
29 CloseHandle(pInfo->hProcess);
30
31 return TRUE;
32 } 技術分享圖片

處理LOAD_DLL_DEBUG_EVENT事件的代碼:

技術分享圖片 1 BOOL OnDllLoaded(const LOAD_DLL_DEBUG_INFO* pInfo) {
2
3 //加載模塊的調試信息
4 DWORD64 moduleAddress = SymLoadModule64(
5 g_hProcess,
6 pInfo->hFile,
7 NULL,
8 NULL,
9 (DWORD64)pInfo->lpBaseOfDll,
10 0);
11
12 if (moduleAddress == 0) {
13
14 std::wcout << TEXT("SymLoadModule64 failed: ") << GetLastError() << std::endl;
15 }
16
17 CloseHandle(pInfo->hFile);
18
19 return TRUE;
20 } 技術分享圖片

判斷符號文件的格式

前面說過,SymInitialize在找不到符號文件的情況下仍然會返回TRUE,此時它只加載了模塊的信息,而沒有加載調試符號。SymLoadModule64函數同樣如此。那麽,如何知道某個模塊是否含有調試信息呢?或者,如何知道某個模塊的符號文件使用哪種格式呢?可以通過調用SymGetModuleInfo64函數來獲取這些信息。該函數的聲明如下:

1 BOOL WINAPI SymGetModuleInfo64(
2 HANDLE hProcess,
3 DWORD64 dwAddr,
4 PIMAGEHLP_MODULE64 ModuleInfo
5 );

第一個參數是符號處理器的標識符,現在你應該對它很熟悉了。第二個參數是模塊的基地址,也就是在調用SymLoadModule64時傳給BaseOfDll參數的值。第三個參數是指向IMAGEHLP_MODULE64結構體的指針,調用函數完成之後模塊的信息將會保存到這個結構體中。

IMAGEHLP_MODULE64結構體含有非常多的字段,不過我們一般只關心其中的一個:SymType。這個字段指示模塊使用的是哪種格式的符號文件,其可能的取值如下:

SymCoff

COFF格式。

SymCv

CodeView 格式。

SymDeferred

調試符號是延遲加載的。下文會提及。

SymDia

DIA 格式。

SymExport

符號是從DLL文件的導出表中生成的。

SymNone

沒有調試符號。

SymPdb

PDB格式。

SymSym

使用.sym類型的符號文件。

SymVirtual

SymLoadModuleEx函數的最後一個參數有關,還未知道什麽意思。

在調用SymGetModuleInfo64之前需要將IMAGEHLP_MODULE64結構體的SizeOfStruct字段設置為sizeof(IMAGEHLP_MODULE64)

延遲加載調試符號

在上面SymType的取值列表中有一個SymDeferred的值,它表示什麽意思呢?DbgHelp支持延遲加載調試符號,意思是說在調用SymLoadModule64時,只加載模塊信息,不加載調試符號,等到真正使用的時候才加載。這樣做的好處是可以節省內存,避免加載了符號而不使用的情況。

如果要開啟這個特性,可以使用SymSetOptions函數:

1 SymSetOptions(SYMOPT_DEFERRED_LOADS);

該函數需要在調用SymInitialize之前調用。

所謂“真正使用的時候”究竟是什麽時候,我也搞不清楚。我在開啟了延遲加載調試符號的情況下調用SymGetLineFromAddr64獲取源文件路徑和行號信息時總是失敗,而關閉了這個特性之後卻成功了,這說明並不是所有需要訪問調試符號的DbgHelp函數都會使調試符號加載進來。所以,為了確保DbgHelp函數可以正確執行,我建議不要開啟這項特性。

清理調試符號

在被調試進程結束的時候必須刪除與之對應的符號處理器,以及清理它占用的資源。只要在處理EXIT_PROCESS_DEBUG_EVENT事件的時候調用SymCleanup函數就可以完成這個操作,該函數接受一個符號處理器的標識符。

另外,在dll文件卸載的時候也應該清理與之相關的調試符號,避免占用內存。這要在處理UNLOAD_DLL_DEBUG_EVENT事件時調用SymUnloadModule64函數。該函數接受一個符號處理器的標識符,以及模塊的基地址,我們可以直接使用UNLOAD_DLL_DEBUG_INFO結構體中唯一的字段lpBaseOfDll

示例代碼

示例代碼按照本文的描述添加了對調試符號的加載和清理代碼,改動不是很大。

http://files.cnblogs.com/zplutor/MiniDebugger5.rar

jpg改rar 技術分享圖片

[Win32]一個調試器的實現(五)調試符號