[Win32]一個調試器的實現(五)調試符號
一個調試器應該可以跟蹤被調試程序執行到了什麽地方,顯示下一條將要執行的語句,顯示各個變量的值,設置斷點,進行單步執行等等,這些功能都需要一個基礎設施的支持,那就是調試符號。
什麽是調試符號
我們知道,在exe、dll等可執行文件中保存的數據大部分都是二進制指令,CPU直接讀取這些指令並執行。那麽調試器是如何知道每條指令對應哪個源文件的哪一行代碼呢?它又是如何知道每個變量和函數的名稱,並顯示變量的值呢?很顯然,可執行文件的二進制數據中不可能包含這麽多信息,這一切都是由調試符號來支持的。
所謂符號,簡單來說就是源代碼中每個對象的名稱。例如變量、函數、類型等,它們都有一個名稱,以及其它的相關信息:變量有類型、地址等信息;函數有返回值類型、參數類型、地址等信息;類型有長度等信息。編譯器在編譯每個源文件的時候都會收集該源文件中的符號的信息,在生成目標文件的時候將這些信息保存到符號表中。鏈接器使用符號表中的信息將各個目標文件鏈接成可執行文件,同時將多個符號表整合成一個文件,這個文件就是用於調試的符號文件,它既可以嵌入可執行文件中,也可以獨立存在。
符號文件中包含的信息可多可少,這樣可以避免泄露程序的信息。調試版程序的符號文件包含了所有的調試信息,而發行版程序的符號文件只包含非常少的調試信息,甚至沒有符號文件。
符號文件有多種不同的格式,不同的編譯器可能使用不同的格式。目前Visual Studio默認使用的是PDB格式,生成項目之後,在Debug或者Release文件夾下都可以找到與生成的文件同名的PDB文件。本文以及接下來的文章中,均使用PDB格式的符號文件來進行調試。
使用調試符號
Windows提供了兩種方法讓我們可以訪問調試符號,分別是DbgHelp(Debug Help Library)和DIA(Debug Interface Access
使用DbgHelp的程序需要加載DbgHelp.dll這個動態鏈接庫,Windows自帶這個文件,位於C:\Windows\System32。但是Windows自帶的通常是較低版本的文件,所以最好是獲取一個最新版本的,將其與程序的可執行文件放在同一個目錄中,這樣既可以使用最新的DbgHelp,又不需要改動系統文件。
獲取最新DbgHelp.dll的一個方法是下載Windows Debugging Tools
為了在程序中使用DbgHelp,你需要先完成以下的事情:
打開項目屬性對話框,定位到“配置屬性”-“鏈接器”-“輸入”,在右邊的“附加依賴項”中添加dbghelp.lib。
有一點需要註意,DbgHelp使用DBGHELP_TRANSLATE_TCHAR這個預定義標記來決定是否使用Unicode字符串,而不是UNICODE標記。所以,如果你的程序使用Unicode字符串,那就定位到“配置屬性”-“C/C++”-“預處理器”,在右邊的“預處理器定義”中添加DBGHELP_TRANSLATE_TCHAR。
最後,在需要使用DbgHelp的源文件中,包含Windows.h和DbgHelp.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時,它必須是一個有效的進程句柄;當fInvadeProcess為FALSE時,它可以是任意一個唯一的數值。
fInvadeProcess的作用是指示是否加載進程所有模塊的調試符號,如果該參數為FALSE,那麽SymInitialize只是創建一個符號處理器,不加載任何模塊的調試符號,此時需要我們自己調用SymLoadModule64函數來加載模塊;如果為TRUE,SymInitialize會遍歷進程的所有模塊,並加載其調試符號,所以在這種情況下hProcess必須是一個有效的進程句柄。
當fInvadeProcess為TRUE時,第二個參數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總是返回FALSE,GetLastError返回-1。這是因為在處理CREATE_PROCESS_DEBUG_EVENT事件時,被調試進程需要的模塊還未加載完成,處於一個不完整的狀態。所以,應該等到被調試進程初始化之後才使用這種方法。由於每個進程在初始化完畢之後都會引發一個斷點異常,所以加載調試符號的最好的時機就是在處理這個初始斷點的時候。關於初始斷點的內容在講解斷點的時候會提及。
第二種方法是在調用SymInitialize的時候第三個參數傳入FALSE,然後對每個模塊調用SymLoadModule64函數加載調試符號。我們可以在處理CREATE_PROCESS_DEBUG_EVENT和LOAD_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_INFO和LOAD_DLL_DEBUG_INFO結構體中都有一個hFile的字段,這個字段剛好可以用在SymLoadModule64函數上。
第三個參數ImageName用於指定模塊文件的路徑和名稱,當第二個參數為NULL時,SymLoadModule64會通過這裏指定的路徑和名稱去尋找模塊文件。一般情況下都不會使用這個參數,因為我們可以使用更可靠的hFile參數。
第四個參數ModuleName為該模塊賦予一個名稱,在使用其它DbgHelp函數的時候可以通過這個名稱來引用模塊。如果該參數為NULL,SymLoadModule64會使用符號文件的文件名作為模塊名稱。
第五個參數BaseOfDll是模塊加載到進程地址空間之後的基地址。這個參數很重要,因為符號文件中每個符號的地址都是相對於模塊基地址的偏移地址,而不是絕對地址,這樣的話,不論模塊被加載到哪個地址,它的符號文件都是可用的。當然,這一切的前提是你將正確的模塊基地址傳給了SymLoadModule64函數。幸運的是,CREATE_PROCESS_DEBUG_INFO和LOAD_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]一個調試器的實現(五)調試符號