上一篇文章說到了調試循環的寫法,這回講一下調試器應該如何處理各種調試事件。
RIP_EVENT
關于這種調試事件的文檔資料非常少,即使提到也隻是用“系統錯誤”或者“内部錯誤”一筆帶過。既然如此,我們也不需要對其進行什麼處理,隻要輸出一條資訊或者幹脆忽略它即可。
OUTPUT_DEBUG_STRING_EVENT
當被調試程序調用OutputDebugString時就會引發該類調試事件,OUTPUT_DEBUG_STRING_INFO結構體描述了關于該事件的詳細資訊。在MSDN中,對該結構體各字段的解釋是:lpDebugStringData字段是字元串在被調試程序的程序空間内的位址;nDebugStringLength字段是以字元為機關的字元串的長度;fUnicode訓示字元串是否Unicode編碼的。根據我個人的實驗觀察,發現隻有第一個字段的解釋是對的。實際上,無論調用OutputDebugStringA還是OutputDebugStringW,字元串都會以ANSI編碼來表示。如果是調用OutputDebugStringW,那麼會先将字元串轉換成ANSI編碼之後再調用OutputDebugStringA(這個過程在MSDN内有描述)。是以fUnicode的值永遠都是0,而nDebugStringLength是以位元組為機關的字元串長度,而不是以字元為機關。
既然字元串是在被調試程序的位址空間内,我們就要使用ReadProcessMemory函數讀取這個字元串。下面的代碼展示了這個過程:
void OnOutputDebugString(const OUTPUT_DEBUG_STRING_INFO* pInfo) {
BYTE* pBuffer = (BYTE*)malloc(pInfo->nDebugStringLength);
SIZE_T bytesRead;
ReadProcessMemory(
g_hProcess,
pInfo->lpDebugStringData,
pBuffer,
pInfo->nDebugStringLength,
&bytesRead);
int requireLen = MultiByteToWideChar(
CP_ACP,
MB_PRECOMPOSED,
(LPCSTR)pBuffer,
pInfo->nDebugStringLength,
NULL,
0);
TCHAR* pWideStr = (TCHAR*)malloc(requireLen * sizeof(TCHAR));
MultiByteToWideChar(
CP_ACP,
MB_PRECOMPOSED,
(LPCSTR)pBuffer,
pInfo->nDebugStringLength,
pWideStr,
requireLen);
std::wcout << TEXT("Debuggee debug string: ") << pWideStr << std::endl;
free(pWideStr);
free(pBuffer);
}
g_hProcess是類型為HANDLE的全局變量。在啟動被調試程序之後就将它的句柄賦給了g_hProcess。
LOAD_DLL_DEBUG_EVENT
加載一個DLL子產品之後引發該類調試事件,LOAD_DLL_DEBUG_INFO結構體描述了它的詳細資訊。lpImageName這個字段可能會使你想在調試器中輸出DLL的檔案名,然而這行不通。MSDN上的解釋是,lpImageName的值是檔案名字元串在被調試程序的程序空間内的位址,但是這個值可能為NULL,即使不為NULL,通過ReadProcessMemory讀取到的内容也可能是NULL。是以,想通過這個字段擷取DLL的檔案名并不可靠。
那麼,通過hFile字段來擷取檔案名如何?沒有Windows API可以直接通過檔案句柄擷取檔案名,想要這麼做的話必須繞一個大圈子,詳細的方法請參考
實際上hFile是與dwDebugInfoFileOffset和nDebugInfoSize一起使用的,用于擷取DLL檔案的調試資訊。一般情況下我們不需要這麼做,是以隻要調用CloseHandle關閉這個句柄即可。記住!關閉這個句柄非常重要,如果不這麼做的話會引起資源洩漏。
我的想法是,先通過EnumProcessModules枚舉被調試程序的子產品,然後通過GetModuleInformation擷取子產品的基位址,将這個基位址與LOAD_DLL_DEBUG_INFO結構體的lpBaseOfDll字段進行比較,如果相等的話就通過GetModuleFileNameEx擷取DLL的檔案名。可是我在實驗這個方法的時候EnumProcessModules總是傳回FALSE,GetLastError傳回299,這是什麼原因呢?
這可能是因為當調試器在處理這類調試事件時,被調試程序還沒有啟動完畢,所需要的子產品還未全部加載完成,是以無法擷取它的子產品資訊。
UNLOAD_DLL_DEBUG_EVENT
解除安裝一個DLL子產品的時候引發該類調試事件。一般情況下隻要輸出一條資訊或者忽略它即可。
CREATE_PROCESS_DEBUG_EVENT
建立程序之後的第一個調試事件,CREATE_PROCESS_DEBUG_INFO結構體描述了該類調試事件的詳細資訊。該結構體有三個字段是句柄,分别是hFile,hProcess和hThread,同樣要記得使用CloseHandle關閉它們!
EXIT_PROCESS_DEBUG_EVENT
被調試程序結束時引發此類調試事件,EXIT_PROCESS_DEBUG_INFO結構體描述了它的詳細資訊。或許你能做的隻有輸出dwExitCode這個字段的值。
CREATE_THREAD_DEBUG_EVENT
建立一個線程之後引發此類調試事件,CREATE_THREAD_DEBUG_INFO結構體描述了它的詳細資訊。同樣要記住用CloseHandle關閉hThread字段!
EXIT_THREAD_DEBUG_EVENT
一個線程結束之後引發此類調試事件,EXIT_THREAD_DEBUG_INFO結構體描述了它的詳細資訊。對此同樣也隻能輸出dwExitCode的值。
EXCEPTION_DEBUG_EVENT
發生異常時引發此類調試事件,EXCEPTION_DEBUG_INFO結構體描述了它的詳細資訊。對這種調試事件的處理是最麻煩的,因為異常的種類非常多,對每種異常的處理也不相同。另外,此類調試事件也是實作斷點和單步執行的關鍵。
由于關于這類調試事件的篇幅太多,是以将其放到下一篇文章中講解。