天天看點

[Win32]一個調試器的實作(二)調試事件的處理

上一篇文章說到了調試循環的寫法,這回講一下調試器應該如何處理各種調試事件。

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結構體描述了它的詳細資訊。對這種調試事件的處理是最麻煩的,因為異常的種類非常多,對每種異常的處理也不相同。另外,此類調試事件也是實作斷點和單步執行的關鍵。

由于關于這類調試事件的篇幅太多,是以将其放到下一篇文章中講解。