Win32 調試接口設計與實現淺析 [2] 調試事件

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


[2] 調試事件

    前面說到 Win32 下的用戶態調試器實際上就是一個while循環,循環體內先等待一個調試事件,然後處理之,最後將控制權交還給調試服務器,就好像一個窗口消息循環一樣。調試事件的核心實際上就是一個DEBUG_EVENT結構,在WinBase.h文件中定義如下:

以下爲引用:

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;



    dwDebugEventCode字段給出此調試事件的類型,dwProcessId和dwThreadId字段分別給出調試事件發生的進程和線程ID號。

    調試事件一般有以下幾類:

以下爲引用:

#define EXCEPTION_DEBUG_EVENT       1
#define CREATE_THREAD_DEBUG_EVENT   2
#define CREATE_PROCESS_DEBUG_EVENT  3
#define EXIT_THREAD_DEBUG_EVENT     4
#define EXIT_PROCESS_DEBUG_EVENT    5
#define LOAD_DLL_DEBUG_EVENT        6
#define UNLOAD_DLL_DEBUG_EVENT      7
#define OUTPUT_DEBUG_STRING_EVENT   8
#define RIP_EVENT                   9



    CREATE_PROCESS_DEBUG_EVENT事件在創建一個新的進程的第一個線程時被引發;相應的EXIT_PROCESS_DEBUG_EVENT事件在被調試的進程結束最後一個線程運行時被引發;每次新建/退出一個線程時會有CREATE_THREAD_DEBUG_EVENT/EXIT_THREAD_DEBUG_EVENT事件被引發;每次載入/卸載一個DLL時會有LOAD_DLL_DEBUG_EVENT/UNLOAD_DLL_DEBUG_EVENT事件被引發;被調試程序使用OutputDebugString函數輸出一個調試字符串時調試器會接受到一個OUTPUT_DEBUG_STRING_EVENT事件;異常被引發時調試器會接受到一個第一時間的EXCEPTION_DEBUG_EVENT事件,如果調試器不處理此異常,則進入被調試程序的正常SEH調用鏈,如果被調試進程也不處理,則會再次引發此事件;RIP_EVENT則一般用於報告錯誤事件。
    一般來說程序的調試事件按照如下順序被引發:

以下爲引用:

CREATE_PROCESS_DEBUG_EVENT

LOAD_DLL_DEBUG_EVENT x n                            // 靜態載入的DLL

CREATE_THREAD_DEBUG_EVENT & EXIT_THREAD_DEBUG_EVENT // 多線程程序中成對出現

LOAD_DLL_DEBUG_EVENT & UNLOAD_DLL_DEBUG_EVENT       // 動態載入 DLL 時成對出現

EXCEPTION_DEBUG_EVENT x n                           // 隨機出現

OUTPUT_DEBUG_STRING_EVENT x n                       // 程序寫調試信息時出現

EXIT_PROCESS_DEBUG_EVENT



    接下來我們詳細分析每種調試事件被引發的原因和時機。具體的調試事件內容這裏就不羅嗦了,有興趣寫調試器的朋友可以參考MSDN和<Debugging Applications>中相關內容。

    首先是建立進程的CREATE_PROCESS_DEBUG_EVENT事件和建立線程的CREATE_THREAD_DEBUG_EVENT事件。這兩個事件都是由DbgkCreateThread函數(ntos/dbgk/dbgkproc.h:211)引發的。此函數首先檢查當前線程是否是具有調試端口的活動線程;然後檢查當前線程是否是進程的創建的第一個線程;如果不是第一個線程,或者調試器是掛接(attach)到一個活動進程上(判斷依據是此進程是否佔用過用戶態的CPU時間),則向調試子系統的調試服務器引發CREATE_THREAD_DEBUG_EVENT事件;否則轉而報告CREATE_PROCESS_DEBUG_EVENT事件。

    DbgkCreateThread函數僞代碼如下:

以下爲引用:

VOID DbgkCreateThread(PVOID StartAddress)
{
  if(!PsGetCurrentProcess()->DebugPort || PsGetCurrentThread()->DeadThread)
  {
    return;
  }

  PsLockProcess(Process,KernelMode,PsLockWaitForever); // 鎖定進程中所有線程

  if(PsGetCurrentProcess()->Pcb.UserTime &&
     PsGetCurrentProcess()->CreateProcessReported == FALSE)
  {
    PsGetCurrentProcess()->CreateProcessReported = TRUE;

    // 引發 CREATE_PROCESS_DEBUG_EVENT 事件
  }
  else
  {
    // 引發 CREATE_THREAD_DEBUG_EVENT 事件
  }

  PsUnlockProcess(PsGetCurrentProcess());
}



    Win32在創建用戶態線程的時候,大致流程如下:

以下爲引用:

CreateThread (kernel32.dll)
CreateRemoteThread (kernel32.dll)
NtCreateThread (ntoskrnl.exe)
PspCreateThread (ntos/ps/create.c:237)



    PspCreateThread函數在創建用戶態線程時,使用PspUserThreadStartup函數(ntos/ps/create.c:1639)作爲線程入口函數,因此線程被創建後直接進入此函數。PspUserThreadStartup函數對非僵死線程和沒有結束的線程初始化其APC;然後調用DbgkCreateThread函數通知調試器採取相應動作;最後將進程的用戶態CPU時間設置爲1,以標示此進程已啓動。對一種特殊線程,非僵死線程但線程啓動時已經停止,則直接調用DbgkCreateThread然後立刻調用PspExitThread,以通知調試器採取相應動作。PspUserThreadStartup函數僞代碼如下:

以下爲引用:

VOID PspUserThreadStartup(IN PKSTART_ROUTINE StartRoutine, IN PVOID StartContext)
{
  if(!PsGetCurrentThread()->DeadThread && !PsGetCurrentThread()->HasTerminated)
  {
    // 初始化線程 APC
  }
  else
  {
    if(!PsGetCurrentThread()->DeadThread)
    {
      DbgkCreateThread(StartContext);
    }
    PspExitThread(STATUS_THREAD_IS_TERMINATING);
  }

  DbgkCreateThread(StartContext);

  if(PsGetCurrentProcess()->Pcb.UserTime == 0)
  {
    PsGetCurrentProcess()->Pcb.UserTime = 1;
  }
}



    與DbgkCreateThread函數對應的是DbgkExitThread函數(ntos/dbgk/dbgkproc.c:384)和DbgkExitProcess函數(ntos/dbgk/dbgkproc.c:439),分別向調試服務器引發EXIT_THREAD_DEBUG_EVENT和EXIT_PROCESS_DEBUG_EVENT事件。
    這兩個函數由系統內核退出線程的PspExitThread函數(ntos/ps/psdelete.c:622)在合適的時候調用。PspExitThread函數檢測當前進程PCB的線程列表是否只有當前線程一個線程,如果沒有其他線程則調用DbgkExitProcess函數,否則調用DbgkExitThread函數。

    Win32 系統中載入和卸載DLL,實際的函數調用流程如下:

以下爲引用:

LoadLibrary (kernel32.dll)
LoadLibraryEx (kernel32.dll)
BasepLoadLibraryAsDataFile (kernel32.dll)
NtMapViewOfSection (ntos/mm/mapview.c:204)
MmMapViewOfSection (ntos/mm/mapview.c:699)



    NtMapViewOfSection函數在調用MmMapViewOfSection函數(ntos/mm/mapview.c:699)完成實際的內存文件映射之後,會根據映射節的標記位以及目標進程是否是當前進程,判斷是否要調用DbgkMapViewOfSection函數(ntos/dbgk/dbgkproc.c:495),通知調試服務器有新的映象文件被加載。與之對應MmUnmapViewOfSection函數(ntos/mm/umapview.c:88)也在判斷標誌位和目標進程是否是當前進程後,在函數末尾調用DbgkUnMapViewOfSection函數(ntos/dbgk/dbgkproc.c:567)通知調試服務器有映象文件被卸載。

    與前面的幾種事件不同,OutputDebugString函數(kernel32.dll)實際上是通過異常實現的。而且有趣的是,這個函數是爲數不多的W後綴Unicode版本實現上轉而調用A後綴Ansi版本,完成實際功能的例子。OutputDebugStringA函數(kernel32.dll)實際上使用RaiseException函數引發了一個異常號爲0x40010006的軟件異常,並將字符串的指針和長度作爲異常參數傳遞。

    DbgkForwardException函數(ntos/dbgk/dbgkport.c:96)作爲實際引發EXCEPTION_DEBUG_EVENT調試事件的函數,在系統的異常分發KiDispatchException函數(ntos/ke/i386/exceptn.c:797)中被調用。KiDispatchException函數根據異常被引發時的狀態,分別完成核心和用戶態的異常處理工作。

    對核心態異常,首先給核心調試程序一個處理機會,然後試圖分發到基於幀的SEH異常鏈去,沒有被處理的話則再給核心調試程序一個機會,如果還是沒被處理,就只能調用KeBugCheckEx函數(ntos/ke/bugcheck.c:157)藍屏了,呵呵。
    對用戶態異常,還是首先試圖讓核心調試器處理,如果不行才調用DbgkForwardException函數分發,沒有被處理的話則多次嘗試,如果還是沒被處理,就停止線程並報告異常給用戶。KiDispatchException函數僞代碼如下:

以下爲引用:

VOID KiDispatchException (IN PEXCEPTION_RECORD ExceptionRecord, IN PKEXCEPTION_FRAME ExceptionFrame,
                          IN PKTRAP_FRAME TrapFrame, IN KPROCESSOR_MODE PreviousMode, IN BOOLEAN FirstChance)
{
  CONTEXT ContextFrame;

  KeContextFromKframes(TrapFrame, ExceptionFrame, &ContextFrame); // 從核心異常幀(Frame)構造異常上下文(Context)

  if (ExceptionRecord->ExceptionCode == STATUS_BREAKPOINT) // 處理調試斷點 int 3
  {
    ContextFrame.Eip--;
  }

  if (PreviousMode == KernelMode)
  {
    if (FirstChance == TRUE)
    {
      if (KiDebugRoutine && KiDebugRoutine(..., FALSE) != FALSE) goto Handle1

      if(RtlDispatchException(ExceptionRecord, &ContextFrame) == TRUE) goto Handled1;
    }

    if (KiDebugRoutine && KiDebugRoutine(..., TRUE) != FALSE) goto Handle1

    KeBugCheckEx(...); // 核心錯誤,以可控方式崩潰 -_-b 說白了就是Deadth Blue Screen,呵呵
  }
  else // PreviousMode = UserMode
  {
    if (FirstChance == TRUE)
    {
      if (KiDebugRoutine && KiDebugRoutine(..., FALSE) != FALSE) goto Handle1

      if (DbgkForwardException(ExceptionRecord, TRUE, FALSE)) goto Handled2;

      // 將異常信息轉換到用戶模式,並嘗試分發
    }

    if (DbgkForwardException(ExceptionRecord, TRUE, TRUE))
    {
        goto Handled2;
    }
    else if (DbgkForwardException(ExceptionRecord, FALSE, TRUE))
    {
        goto Handled2;
    }
    else
    {
        ZwTerminateThread(NtCurrentThread(), ExceptionRecord->ExceptionCode);
       KeBugCheckEx(...);
    }
  }

Handled1:
  KeContextToKframes(TrapFrame, ExceptionFrame, &ContextFrame,
                     ContextFrame.ContextFlags, PreviousMode);

Handled2:
}



    DbgkForwardException函數分別針對DebugException和SecondChance參數的三種組合被調用。DebugException爲True時向調試端口發送信息,否則向異常端口發送。

    至此,我們對幾種常見的調試事件的引發機制就大概有了一個瞭解,下一節將介紹將這些調試事件和最終用戶態調試器關聯起來的Win32中調試子系統的實現思路。

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