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中調試子系統的實現思路。