軟件調試(5) - 深入探討Windows調試體系(調試信息傳遞)

參與者:
被調試進程
調試進程
調試子系統(內核)


調試子系統主要分為3部份:
- 位於NTDLL中的支持函數 DbgUi
- 位於內核文件中的支持函數 DbgSs
- 調試子系統服務器 Dbg
while(WaitForDebugEvent(&DbgEvt,INFINITE))
{

  //處理等待得到的事件
  //處理後恢復調試目標繼續執行
  ContinueDebugEvent(DbgEvt.dwProcessID, DbgEvt.dwThreadId,dwContinueStatus); 
}

WaitForDebugEvent - 用於等待和接收調試事件,收到事件後,調試器會根據事件的類型(事件ID)來分發和處理, 在處理調試事件的過程中,被調試進程是處於掛起狀態,處理調試事件後,調試器用ContinueDebugEvent將處理結果回復給調試子系統,讓被調試程序繼續執行,調試器則再次調用WaitForDebugEvent 等下一個調試事件


調試子系統的內核部份
------------------------------------
收集調試事件後=>以一個消息結構發送給調試子系統 => 使其保存在調試子系統的調試消息隊列中 => 子系統與調試器靠一個內核對象來同步=>當有調試消息需要讀取=>調試子系統服務器會設置這個待器線程被喚起


內核中: DBGKM_APIMSG
調試API: DEBUG_EVENT

由於兩者結構不一, 需要轉化過程

子系統服務器會將自己使用的結構轉化為NTDLL使用的DEBUG_WAIT_STATE_CHANGE,
NTDLL再將這個結構轉化為調試器使用的DEBUG_EVENT結構

內核中Dbgk收集例程將所有調試事件分為8類
--------------------------------------------------
typedef enum _DBGKM_APINUMBER
{
DbgkmExceptionApi = 0, // 异常
DbgkmCreateThreadApi = 1, // 创建线程
DbgkmCreateProcessApi = 2, // 创建进程
DbgkmExitThreadApi = 3, // 退出线程
DbgkmExitProcessApi = 4, // 进程退出
DbgkmLoadDllApi = 5, // 映射DLL
DbgkmUnloadDllApi = 6, // 反映射DLL
DbgkmErrorReportApi = 7, // 内部错误
DbgkmMaxApiNumber = 8, // 这组常量的最大值
} DBGKM_APINUMBER;

大題: Dbgk 採集調試信息

進程和線程創建消息
--------------------------------------------------
內核中進程或線程管理相關函數: Ps/Psp開頭 ,如PspCreateSystemThread,PspShutdownThread, 統稱為進程管理器

當進程建立用戶態線程時 要做的事:

1. 建立必要的內核對象與數據結構(如ETHREAD)
2. 分配線程棧空間
3. 掛起線程(創建線程時CREATE_SUSPEND)
4. 建立線程後,進程管理器(建立線程內核函數)會調用PspUserThreadStartup例程, 準備啟動該線程
5. 為了支持調試PspUserThreadStartup總是會調用調試子系統的內核函數DbgkCreateThread, 以便讓調試子系統得到處理機會
6. DbgkCreateThread會檢查新創建線程所在的進程是否正在被調試(如EPROCESS中的Debugport是否為空),

如果為空=>立即返回,如果不為空=>則檢查該進程的用戶態運行時間是否為0=> 用於判斷該線程是否為進程中第一個線程

如果是則通過DbgkpSendApiMessage()函數向DebugPort發送DbgkmCreateProcessApi消息,如果不是則發送DbgKmCreateThreadApi消息.

調試器最終收到的進程創建(CREATE_PROCESS_DEBUG_EVENT,值為3)和線程創建(CREATE_THREAD_DEBUG_EVENT,值為2)事件就是源於這兩個消息


進程和線程退出消息
---------------------------------------------------
1. 進程管理器的PspExitThread函數負責線程的退出和清除,為了支持調試

2. 如果是最後一個線程-> PspExitThread會調用DbgkExitProcess(如debugport不為0, 也不掛起任何進程)
2. 否則->PspExitThread會調用DbgExitThread (如debugport不為0 , 掛起當前進程)

3. 最後他們會通過DbgkpSendApiMessage 向DebugPort發送DbgKmExitThread或DbgKmExitProcessApi消息

4. 收到消息後 調試器收到線程退出或進程退出的事件(EXIT_THREAD_DEBUG_EVENT和EXIT_PROCESS_DEBUG_EVENT,分別值為4和5), 來自以上兩個消息

判斷最後一個線程?

模塊映射/卸載
---------------------------------------------------
1. 調用LoadLibrary/UnloadLibrary 直到系統調用 => NtMapViewOfSection/NtUnMapViewOfSection

2. NtMapViewOfSection/NtUnMapViewOfSection 調用一下 => MmMapViewOfSection/MmUnMapViewOfSection 最後調用一下 => DbgkpMapViewOfSection/DbgkpUnMapViewOfSection

3. 它們判斷debugport是否為0 , 不為0 則透過DbgkpSendApiMessage發送DbgKmLoadDllApi或DbgKmUnLoadDllApi消息

4. 調試器最終收到Load_DLL_DEBUG_EVENT,值為6), 反映射(UNLOAD_DLL_DEBUG_EVENT,值為7) 來自以上兩個消息


異常分發
----------------------------------------------------
1: 內核會使用KiDispatchException函數分發異常(最多兩次處理機會)
2: 每一次處理機會也會調用調試子系統的DbgkForwardException函數通知調試子系統, KiDispatchException會通過一個boolean決定向debugport或是exceptionport發送消息
3: 如debugport的話=>DbgkForwardException判斷debugport 不為空,便透過DbgkpSendApiMessage發送DbgKmExceptionApi消息
4: 調試器收到異常時(值為1)/輸出調試字符串(值為8)事件, 都是源於DbgKmExceptionApi消息

總結:
內核函數調用:(啟動/退出進程或線程,模塊映射/卸載 , 異常分發/輸出調試字符串) => Dbgk採集例程 => 由採集例程判斷debugport => DbgKpSendApiMessage/發送調試事件/信息 或者 返回


調試消息結構
---------------------------------------------------
調試子系統的內核函數使用以下結構:
typedef struct _DBGKM_APIMSG { 
  PORT_MESSAGE h;                                        //XP之前使用
  DBGKM_APINUMBER ApiNumber;                             //調試消息類型
  NTSTATUS ReturnedStatus;                               //+0x1C 調試器回復狀態
  union { 
   DBGKM_EXCEPTION Exception; 
   DBGKM_CREATE_THREAD CreateThread; 
   DBGKM_CREATE_PROCESS CreateProcessInfo; 
   DBGKM_EXIT_THREAD ExitThread; 
   DBGKM_EXIT_PROCESS ExitProcess; 
   DBGKM_LOAD_DLL LoadDll; 
   DBGKM_UNLOAD_DLL UnloadDll;    
  } u;                                                //0x20
} DBGKM_APIMSG, *PDBGKM_APIMSG;
union內容會因ApiNumber更改而更改!

Dbgk採集例程會在確定,它會填充這個結構!

發送消息Api: DbgkpSendApiMessage(xp之前) / DbpKpQueueMessage(xp或之後)
---------------------------------------------------
它會根據參數決定是否等待=>一般情況需要等待


控制被調試進程API
---------------------------------------------------
調試子系統向調試器發送過程: 發送前會調用DbgkpSuspendProcess => 發送後 => 調試子系統服務器提醒調試器來讀取消息後並作處理後 => 回復給調試子系統 => 喚醒 調用DbgkpResumeProcess
內核函數:
DbgkpSuspendProcess => KeFreezeAllThreads()  => 除了調用KeFreezeAllThreads的線程外,其他線程會被凍結
- 他會調用 
DbgkpResumeProcess  => KeThawAllThreads() => 唳醒被調試進程中的全部線程
*它們都是堵塞的,調用KeWaitForSingleObject等使線程等待




r3程序發起系統調用=> Debugport不為空(正被調試/已附加) => 準備發送調試信息 => DbgkpSuspendProcess(停止調用線程外的所有線程) => 正式發送調試信息DbgkpQueueMessage等待(沒有線程執行)
=> 調試子系統服務器提醒調試器來讀取消息後並作處理後(調試器會苦苦等待EventsPresent對象)=> 回復給調試子系統=> 子系統首先喚醒等待線程,後者再調用DbgkpResumeProcess =>如常運行


*直到第5步 程序沒有線程運行,由於調用線程正在等待,而其他線程則被凍結


調試子系統服務器(xp之後)
-----------------------------------------------------
- 調試子系統服務器xp或之後移到內核態
- 新的子系統服務器以DebugObject為核心
- DebugObject:
typedef struct _DEBUG_OBJECT
{
KEVENT EventsPresent;   //+0x00 用於指示有調試事件發生的事件對象
FAST_MUTEX mutex;   //+0x10 用於同步的互斥對象
LIST_ENTRY StateEventListEntry //+0x30 保存調試事件的鏈表
ULONG Flags;    //+0x38 標志
}DEBUG_OBJECT, *PDEBUG_OBJECT
- StateEventListEntry(調試消息隊列)
- EventsPresent(用來同步調試器進程和被調試器進程), 子系統服務器通過設置此事件通知調試器讀取消息隊列中的調試消息
- 調試器進程通過WaitForDebugEvent API來等待=> NtWaitForDebugEvent
- 實際上他就等待EventsPresent
- 互斥對象(mutex)用來鎖定對這個結構的訪問, 防止多線程同時讀寫造成錯誤
- Flags 為1 代表結束調試會話時是否終止被調試進程


創建調試對象
--------------------------------------------------------
- NtCreateDebugObject => 調試子系統為當前線程創建一個調試對象(teb[DbgSsReserved[1]]字段)保存調試對象
- 與其他線程有分別


設置調試對象
--------------------------------------------------------
1: 建立新進程調試 => 在系統在創建進程時=> 把調試器線程的DbgSsReserved[1]字段保存的調試對象句柄傳給創建進程的內核服務,然後內核中的進程創建函數就會將這句柄對應的對象指針寫入eprocess
2: 附加調試 => 內核調用DbgkpSetProcessDebugObject函數來將一個創建好的調試對象附加到其參數所指定的進程對象的eprocess中
DbgkpSetProcessDebugObject還會調用=>DbgkpMarkProcessPeb函數設置Peb的beingDebugged字段



傳送調試事件
--------------------------------------------------------
kernel level debug_event 結構是StateEventListEntry的節點

DbgkpQueueMessage會根據參數中是否指定了不需等待的標志,決定是否要立刻通知調試器

如指定了便返回, 如果沒有指定=>便設置EventPresent對象, 通知調試器讀取消息, 然後調用KeWaitForSignleObject等待DEBUG_EVENT結構中的ContinueEvent =>等待調試器完成處理

*調試器處理後通過ContinueDebugEvent間接或直接調用NtDebugContinue內核服務,NtDebugContinue按照CLIENT_ID來找到要恢復的調試事件結構,然後設置他的ContinueEvent => 唳醒等待繼續運行


調試器的基本概念
--------------------------------------------------------
在調試會話建立後=>調試器工作線程便進入調試事件循環=>以等待調試事件(實際上是調用NtWaitForDebugEvent內核服務等待調試對象的EventsPresent對象)=>
=>調試器工作線程喚醒後=>讀取調試事件隊列=>讀到後會把kernel level debug_event結構轉化為r3的DEGUI_WAIT_STATE_CHANGE結構=>用戶處理後=>continue


轉換API:DbgkpConvertKernelToUserStateChange


參考:Windows軟件調試

Comments

Popular posts from this blog

Android Kernel Development - Kernel compilation and Hello World

How does Nested-Virtualization works?

Understanding ACPI and Device Tree