Win32 調試接口設計與實現淺析 [1] 用戶態調試器結構初探

http://flier_lu.blogone.net/?id=1307208

Win32 調試接口設計與實現淺析

    所謂調試器實際上是一個很寬泛的概念,凡是能夠以某種形式監控其他程序執行過程的程序,都可以泛稱爲調試器。在Windows平臺上,根據調試器的實現原理大概可以將之分爲三類:內核態調試器、用戶態調試器和僞代碼調試器。
    內核態調試器直接工作在操作系統內核一級,在硬件與操作系統之間針對系統核心或驅動進行調試,常見的有SoftICE、WinDbg、WDEB386和i386KD等等;用戶態調試器則通過操作系統提供的調試接口,在操作系統和用戶態程序之間針對用戶態程序進行調試,常見的有各種開發環境如VC/Delphi自帶的調試器,OllyDbg等等;僞代碼調試器則使用目標系統自定義的調試接口,調試由用戶態程序支持的腳本語言或虛擬機代碼,常見的如JVM/CLR的調試工具、VB的pcode調試器、Active Script調試器等等。
    因爲僞代碼調試器跟具體系統實現相關性太強,不具備原理層面的通用性,本系列文章儘量不涉及其內容,以後如果有機會可以再討論一下JVM/CLR/Active Script提供的調試接口;用戶態調試器應用最廣泛,參考資料也較爲完整,我會花較大精力和大家探討;核心態調試器則跟操作系統結合較爲緊密,加上我也不是太熟悉,只能盡力而爲了,呵呵。歡迎大家提出批評指正意見和建議 :)
    此外強烈推薦John Robbins在MSDN的Bugslayer專欄,以及其所著的<Debugging Applications>一書(中文版《應用程序調試技術》),此書中對調試器從原理到應用都有很全面的講解。

[1] 用戶態調試器結構初探

    用戶態調試器直接使用Win32 API提供的調試接口,遵循Win32的事件驅動的設計思想,其實現思路非常簡單,基本框架僞代碼如下:

以下爲引用?lt;/B>

//啓動要調試的進程或掛接調試器到已運行的進程上
CreateProcess(..., DEBUG_PROCESS, ...) or DebugActiveProcess(dwProcessId)

DEBUG_EVENT de;
BOOL bContinue = TRUE;
DWORD dwContinueStatus;

while(bContinue)
{
  bContinue = WaitForDebugEvent(&de, INFINITE);

  switch(de.dwDebugEventCode)
  {
  ...
  default:
    {
      dwContinueStatus = DBG_CONTINUE;
      break;
    }
  }

  ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
}



    在調試器開始調試的時候,會啓動被調試程序的新進程或者掛接(attach)到一個已運行進程上,此時Win32系統會啓動調試接口的服務器端;然後調試器調用WaitForDebugEvent函數等待調試服務器端的調試事件被引發;調試器根據調試事件進行相應的處理;最後調用ContinueDebugEvent函數請求調試服務器繼續執行被調試進程,以等待並處理下一個調試事件。

    首先我們大致看看調試接口的服務器端的實現思路:調試服務的服務器端接口實際上是存在於被調試進程的調試端口(Debug Port),此核心對象實現上跟Win32的完成端口類似,都是通過一個核心隊列實現的LPC端口。啓動調試服務器實際上就是掛接Win32的調試子系統到被調試進程,並在被調試進程內構造調試端口。調試器通過調試端口與Win32的調試子系統通訊;調試子系統響應系統操作所引發的調試事件,並通過調試端口將調試事件分發給核心態/用戶態調試器。

    建立被調試程序的新進程時,需要在CreateProcess函數的dwCreationFlags參數設置DEBUG_ONLY_THIS_PROCESS或DEBUG_PROCESS標誌位,以表示新建的進程需要被調試。CreateProcess函數的調用路徑如下

以下爲引用:

CreateProcessA/CreateProcessW (kernel32.dll)
CreateProcessInternalW (kernel32.dll)
NtCreateProcessEx (ntoskrnl.dll)
PspCreateProcess (ntos/ps/create.c:969)



    CreateProcessInternalW函數根據傳入的dwCreationFlags參數,決定是否要構造端口核心對象用於調試端口,並設置PEB的相應調試標誌;PspCreateProcess會根據傳入參數的調試選項和端口對象句柄,選擇是否創建目標進程的調試端口;如果要創建則將傳入的端口句柄轉換成內核對象引用,保存在被調試程序進程的EPROCESS->DebugPort字段裏。
    Win32 API提供的IsDebuggerPresent函數就是通過判斷CreateProcessInternalW函數在PEB中設置的標誌位來判斷當前進程是否被調試的。IsDebuggerPresent函數僞代碼如下:

以下爲引用:

BOOL IsDebuggerPresent(void)
{
  return NtCurrentTeb()->ProcessEnvironmentBlock->BeingDebugged;
}



    TEBPEB的結構可在::URL::http://www.ntinternals.net 上找到。

    不過此種方法很容易被調試器直接修改PEB內存結構所欺騙,故而有另外一種直接通過檢查EPROCESS->DebugPort字段是否被使用,來判斷此進程是否正在被調試的方法。以前水木上也有過幾次討論,如blowfish的《檢測debugger的方法補遺》一文給出的代碼。Windows XP/2003開始由Win32 API提供的 CheckRemoteDebuggerPresent 函數也是使用相同的思路,通過調用 NtQueryInformationProcess 函數查詢調試端口實現的,僞代碼如下:

以下爲引用:

BOOL CheckRemoteDebuggerPresent(HANDLE hProcess, PBOOL pbDebuggerPresent)
{
  enum PROCESS_INFO_CLASS { ProcessDebugPort = 7 };

  if(hProcess && pbDebuggerPresent)
  {
    HANDLE hPort;

    *pbDebuggerPresent = NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessDebugPort, &hPort, sizeof(hPort), NULL)) ? TRUE : FALSE;

    return *pbDebuggerPresent;
  }
  return FALSE;
}



    與直接創建被調試程序的新進程不同,調試已啓動進程的 DebugActiveProcess 函數首先連接到Win32 系統調試服務器的端口上,然後激活當前正在運行的被調試進程的調試端口。DebugActiveProcess的僞代碼如下:

以下爲引用:

BOOL DebugActiveProcess(DWORD dwProcessId)
{
  if(DbgUiConnectToDbg())
  {
    HANDLE hProcess = ProcessIdToHandle(dwProcessId);

    if(hProcess)
    {
      DbgUiDebugActiveProcess(hProcess);
      NtClose(hProcess);
    }
  }
  return FALSE;
}



    DbgUiConnectToDbg函數(ntos/dll/dlluistb.c:27)嘗試連接核心提供的調試子系統端口(名爲"/DbgUiApiPort"),如果成功連接會獲得一個端口對象(保存在DbgUiApiPort NtCurrentTeb()->DbgSsReserved[1]),和一個調試狀態轉換的信號燈句柄(保存在DbgStateChangeSemaphore NtCurrentTeb()->DbgSsReserved[0])用於等待調試事件。僞代碼如下:

以下爲引用:

#define DbgStateChangeSemaphore (NtCurrentTeb()->DbgSsReserved[0])
#define DbgUiApiPort (NtCurrentTeb()->DbgSsReserved[1])

NTSTATUS DbgUiConnectToDbg( VOID )
{
  NTSTATUS st = NtConnectPort(&DbgUiApiPort, L"//DbgUiApiPort", ..., &DbgStateChangeSemaphore);

  if(NT_SUCCESS(st))
  {
    NtRegisterThreadTerminatePort(DbgUiApiPort);
  }
  else
  {
    DbgUiApiPort = NULL;
  }
  return st;
}



    如果連接調試子系統成功,則調用NtRegisterThreadTerminatePort函數(ntos/ps/psdelete.c:1202)將調試端口加入到當前線程控制塊的終止端口列表(ETHREAD->TerminationPortList)中。在線程結束的之前,會激活此列表中的端口,給調試器一個清理的機會。

    DbgUiDebugActiveProcess函數完成具體的激活被調試進程的調試服務器的功能。僞代碼如下:

以下爲引用:

#define DbgUiApiPort (NtCurrentTeb()->DbgSsReserved[1])

void DbgUiDebugActiveProcess(HANDLE hProcess)
{
  return NtDebugActiveProcess(DbgUiApiPort) &&
         DbgUiIssueRemoteBreakin(hProcess) &&
         DbgUiStopDebugging(hProcess);
}



    至於這幾個函數的具體實現,等後面章節詳細分析Win32調試子系統時再詳細講解,呵呵

    在被調試進程啓動了調試支持後,調試器調用WaitForDebugEvent函數等待調試事件的發生。此函數實際上是對DbgUiWaitStateChange函數(ntos/dll/dlluistb.c:93)的一個簡單包裝,通過等待DbgUiConnectToDbg函數獲得的調試事件信號燈來完成實際功能。如果成功獲得調試事件,還會通過NtRequestWaitReplyPort函數(ntos/lpc/lpcsend.c:717)向調試服務器通報DbgUiWaitStateChangeApi消息。

    在處理完調試事件後,調試器調用的ContinueDebugEvent函數是DbgUiContinue函數的一個簡單包裝,也是使用NtRequestWaitReplyPort函數向調試服務器通報DbgUiContinueApi消息。

    在完成調試功能後,WinXP/2003還提供了DebugActiveProcessStop函數停止調試。僞代碼如下:

以下爲引用:

BOOL DebugActiveProcessStop(DWORD dwProcessId)
{
  HANDLE hProcess = ProcessIdToHandle(dwProcessId);

  if(hProcess)
  {
    CloseAllProcessHandles(dwProcessId);
    DbgUiStopDebugging(hProcess);
    if(NtClose(hProcess))
      return TRUE;
  }
  return FALSE;
}



    DbgUiStopDebugging函數(ntdll.dll)調用ZwRemoveProcessDebug函數(ntoskrnl.exe)關閉指定進程的調試端口,實現上是傳入端口句柄和進程句柄,調用0xC7號系統服務完成最終功能。這個暫時就不深入討論了,就此打住 :P

    在瞭解這些後,對用戶態調試器的實現應該就有了一個框架性的瞭解:其結構就是一個基於事件的模型,然後通過向調試子系統請求調試事件並完成具體操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章