程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> Windows用戶態調試器原理

Windows用戶態調試器原理

編輯:C++入門知識

           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。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved