Windows用戶態調試器原理 Windows操作系統提供了一組API來支持調試器。 這些API可以分為三類: 創建調試目標的API; 在調試循環中處理調試事件的API。 查看和修改調試目標的API。 接下來將會分別對這三種API進行介紹。 創建調試目標 在調試器工作之前,需要創建調試目標。用戶態調試器有兩種創建調試目標的方法:一是創建新進程,二是附加到一個運行的進程。采用這兩種方法中的任一種後,該進程就成為了調試目標。操作系統將調試器與調試目標關聯起來。 調試器創建調試目標是通過調用CreateProcess並傳入DEBUG_PROCESS標志。 如: [cpp] STARTUPINFO si={0}; si.cb=sizeof(si); PROCESS_INFORMATION pi={0}; bool ret=CreateProcesss(NULL,argv[1],NULL,NULL,false, DEBUG_PROCESS,NULL,NULL,&si,&pi); 調試器附加到一個運行的進程是通過調用DebugActiveProcess來實現的。 DebugActiveProcess 此函數允許將調試器捆綁到一個正在運行的進程上。 [cpp] BOOL DebugActiveProcess(DWORD dwProcessId ) dwProcessId:欲捆綁進程的進程標識符 如果函數成功,則返回非零值;如果失敗,則返回零 無論采用哪一種方法,調試器與操作系統的交互都是相同的。這種調試器被稱為活動調試器(living debuger)。每個調試器只能有一個調試目標。 調試循環 在初學Windows時我們一定接觸過消息循環。調試循環與此類似。 while(當調試不結束時) { //等待操作系統發送調試事件。 //處理調試事件。 //通知調試目標執行相應操作。 } 在調試目標被調試時,進程執行的一些操作會以事件的方式通知調試器。例如動態庫的加載與卸載、新線程的創建和銷毀以及代碼或處理器拋出的異常都會通知調試器。 當有事件需要通知調試器時,操作系統會首先掛起調試目標的所有線程,然後把事件通知調試器。並且等待調試器通知其繼續執行。 調試器會調用WaitForDebugEvent來等待事件通知的到來 。當有事件通知到來時此函數返回,返回的事件信息被封裝在DEBUG_EVENT結構中。這個結構包含事件的類型等其他信息。 事件類型有以下幾種: WaitForDebugEvent 此函數用來等待被調試進程發生調試事件。 [cpp] BOOL WaitForDebugEvent(LPDEBUG_ENENT lpDebugEvent, DWORD dwMilliseconds) lpDebugEvent :指向接收調試事件信息的DEBUG_ ENENT結構的指針 dwMilliseconds:指定用來等待調試事件發生的毫秒數,如果 這段時間內沒有調試事件發生,函數將返回調用者;如果將該參數指定為INFINITE,函數將一直等待直到調試事件發生 如果函數成功,則返回非零值;如果失敗,則返回零 在調試器調用WaitForDebugEvent返回後,得到事件通知,然後解析DEBUG_EVENT結構,並對事件進行響應,處理完成後調試器將會調用ContinueDebugEvent,並根據參數來通知調試目標執行相應操作。 ContinueDebugEvent函數 此函數允許調試器恢復先前由於調試事件而掛起的線程。 [cpp] BOOL ContinueDebugEvent(DWORD dwProcessId,DWORD dwThreadId, DWORD dwContinueStatus ) dwProcessId 為被調試進程的進程標識符 dwThreadId 為欲恢復線程的線程標識符 dwContinueStatus指定了該線程將以何種方式繼續,包含兩個定義值DBG_CONTINUE和DBG_EXCEPTION_NOT_HANDLED 如果函數成功,則返回非零值;如果失敗,則返回零。 具體實現為: [cpp] DWORD Condition=DBG_CONTINUE; while(Condition) { DEBUG_EVENT DebugEvent={0}; WaitForDebugEvent(&DebugEvent,INFINITE);//等待調試事件 ProcessEvenet(DebugEvent)//處理調試事件。 ContinueDebugEvent(DebugEvent.dwProcessId,DebugEvent.dwThreadId,Condition);//通知調試目標繼續執行。 } ProcessEvent用於對調試事件進行處理。它是用戶自定義函數。 在該函數內會對DEBUG_EVENT結構進行解析。 DEBUG_EVENT結構為: [cpp] typedef struct _DEBUG_EVENT { DWORD dwDebugEventCode; DWORD dwProcessId; DWORD dwThreadId; union { EXCEPTION_DEBUG_INFO Exception; CREATE_THREAD_DEBUG_INFO CreateThread; CREATE_PROCESS_DEBUG_INFO CreateProcessInfo; EXIT_THREAD_DEBUG_INFO ExitThread; EXIT_PROCESS_DEBUG_INFO ExitProcess; LOAD_DLL_DEBUG_INFO LoadDll; UNLOAD_DLL_DEBUG_INFO UnloadDll; OUTPUT_DEBUG_STRING_INFO DebugString; RIP_INFO RipInfo; } u; } DEBUG_EVENT, *LPDEBUG_EVENT; 處理通知代碼如下: [cpp] DWORD ProcessEvent(DEBUG_EVENT de) { switch(de.dwDebugEvent.Code) { case EXCEPTION_DEBUG_EVENT: { } break; case CREATE_THREAD_DEBUG_EVENT: { } break; case CREATE_PROCESS_DEBUG_EVENT: { } break; case EXIT_THREAD_DEBUG_EVENT: { } break; case EXIT_PROCESS_DEBUG_EVENT: { } break; case LOAD_DLL_DEBUG_EVENT: { } break; case OUTPUT_DEBUG_STRING_EVENT: { } break; ...... } return DBG_CONTINUE; } 調試事件介紹 OUTPUT_DEBUG_STRING_EVENT事件 很多程序員在調試程序時喜歡將執行的結果或中間步驟輸出,用以檢查程序執行的正確與否。在很多系統中這是很不方便的。但我們可以使用調試輸出命令,將某些需要顯示的結果輸出到輸出窗口中。如vc的TRACE宏。其實在TRACE宏內部是調用OutputDebugString來實現的 。調試器會把調試目標輸出的字符串通過事件處理代碼顯示出來。在DEBUG_EVENT 結構中有一個DebugString成員。 該結構定義為: [cpp] typedef struct _OUTPUT_DEBUG_STRING_INFO { LPSTR lpDebugStringData; WORD fUnicode; WORD nDebugStringLength; } OUTPUT_DEBUG_STRING_INFO, *LPOUTPUT_DEBUG_STRING_INFO; 在此結構中有一個lpDebugStringData成員,它保存被輸出字符串的地址。nDebugStringLength為字符串長度。fUnicode表示是ANSI還是UNICODE字符。 下面為處理OUTPUT_DEBUG_STRING_EVENT事件的代碼: [cpp] case OUTPUT_DEBUG_STRING_EVENT: { OUTPUT_DEBUG_STRING_INFO oi=de.u.DebugString; WCHAR *msg=ReadRemoteString(調試目標句柄, oi.lpDebugStringData,oi.nDebugStringLength,oi.fUnicode); std::wcout<<msg; break; } ReadRemoteString是用戶自定義函數。在此函數內部是調用ReadProcessMemory從調試目標進程內讀取字符串。具體不再介紹。 ReadProcessMemory 讀取指定進程的某區域內的數據。 [cpp] BOOL ReadProcessMemory(HANDLE hProcess, LPCVOID lpBassAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T * lpNumberOfBytesRead) hProcess:進程的句柄 lpBassAddress:欲讀取區域的基地址 lpBuffer:保存讀取數據的緩沖的指針 nSize:欲讀取的字節數 lpNumberOfBytesRead:存儲已讀取字節數的地址指針 如果函數成功,則返回非零值;如果失敗,則返回零 處理EXCEPTION_DEBUG_EVENT事件 當調試目標在調試時發生異常時,操作系統將會向調試器發送EXCEPTION_DEBUG_EVENT事件通知 當發生此事件時,DEBUG_EVENT結構包含的是一個EXCEPTION_DEBUG_INFO結構。 [cpp] typedef struct _EXCEPTION_DEBUG_INFO { EXCEPTION_RECORD ExceptionRecord; DWORD dwFirstChance; } EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO; ExceptionRecord成員包含了異常信息的一個副本。如異常碼,異常引發地址以及異常參數等。定義如下: [cpp] typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD; dwFirstChance告訴調試器是否是第一輪通知這個異常。 從操作系統的角度來看,調試器必須對異常進行解析,並且將DBG_CONTINUE或者是DBG_EXECPTION_NOT_HANDLED作為參數傳遞給ContinueDebugEvent。如果執行DBG_CONTINUE,則操作系統認為該異常已經被妥善處理了。因此從產生異常的地址開始回復程序的執行。如果傳入DBG_EXCEPTION_NOT_HANDLED,則告訴操作系統該異常並未被處理,操作系統將繼續分發異常。 [cpp] case EXCEPTION_DBUG_EVENT: { std::cout<<”異常碼為”<<std::hex<<debugEvent.u.Exception.ExceptionRecord.ExceptionCode<<std::endl; //在switch判斷異常類型,並執行相應操作。 switch(debugEvent.u.Exception.ExceptionRecord.ExceptionCode) { case EXCEPTION_BREAKPOINT: break; case EXCEPTION_SINGLE_STEP: beak; return DBG_CONTINUE; } break; } 在調試循環中,從WaitForDebugEvent中返回以及調用ContinueDebugEvent之間的這段時間內,調試目標不會執行,因此它的狀態也將保持不變。當調試目標被掛起時,調試器就進入了交互模式,接收用戶的各種指令,並按照不同指令執行不同操作。 調試事件到來的順序 當我們啟動調試目標時,調試器接收到的第一個事件是CREATE_THREAD_DEBUG_EVENT。接下來是加載dll的事件。每加載一個,都會產生一個這樣的事件。 當所有模塊都被加載到進程地址空間後,調試目標就准備好運行了,調試器此時也做好了接收通知的准備。此時是設置斷點的最佳時機。 在調試目標退出之前調試器會收到 EXIT_DEBUG_PROCESS_EVENT通知。此後調試器不能收到加載到進程地址空間的dll從進程卸載的UNLOAD_DLL_DEBUG_EVENT通知。 前面介紹的調試事件都是由Windows操作系統發出的,來通知調試器。但是調試目標也會發出自己的異常。調試器在處理這些異常時可以選擇與其他調試事件一樣的處理方式。 Windows操作系統使用結構化異常處理(SEH)機制將處理器引發的異常傳遞給內核及用戶態程序。每個SEH異常都有一個無符號整形的異常碼來唯一標識。這個異常碼是由系統在異常發生時指定的。這些異常碼使用了操作系統開發人員定義的公開異常碼。例如訪問違規異常異常碼為0xC0000005,斷點異常為0xC80000003。為了方便記憶,這些異常碼被定義為常量。其名字形如STATUS_XXX。如 #define STATUS_BREAKPOINT ((NTSTATUS)0x80000003L) 由於異常碼很難記憶,因此Windows調試器中包含了一些更容易記住的別名來控制調試器的行為。例如斷點異常0x80000003 的別名是bpe。C++異常碼0xE06D7363別名為eh。