軟件調試(7) - 調試體系之內核調試原理

內核調試器通過內核調試引擎 控制或調試內核

內核其他部份->內核調試引擎->內核調試器

調試器通過內核調試協議 訪問和控制目標系統


內核調試API:
KdAPI - 類似遠程調用方式訪問內核

與系統內核的接口函數
與調試器通信函數
斷點管理 - 使用KdpBreakpointTable記錄斷點
內核調試api - 提供調試信息給內核調試引擎
系統內核控制函數 - KdEnterDebugger 與 KdExitDebugger
管理函數 - KdEnableDebugger 與KdDisableDebugger
ETW支持函數 - event log...
驅動程序更新服務 -
本地內核調試支持(XP後)

kdcom.dll 負責通信部份
收/發數據包
- KdSendPacket
- KdReceivePacket
接收電源狀態變化
- KdD0Transition ,喚醒系統用
- KdD3Transition ,休眠時用

內核調試引擎初始化:

BIOS->Harddisk引導程式->執行NTLDR.exe->CPU初始化->切換保護模式->啟用分頁機制->通過配置boot.ini得到windows系統的系統目錄並加載系統內核文(ntoskrnl.exe)先檢查他的導入表並加載依賴文(包括內核調試通信模塊KDCOM.dll)->讀取注册表的hive->加載其中定義的啟動類型的驅動程序->完成所有工作->找到內核文件的PE文件頭找到入口函數->kISystemStartup->進入初始化三部曲->

1. 調用HalInitializeProcessor() 初始化CPU
2. 調用KdInitSystem 初始化內核調試引擎
3. 調用KiInitializeKenrel開始內核初始化
KiInitializeKenrel->KeInitializeThread -> ExpInitializeExecutive()

ExpInitializeExecutive進行大量工作包括進程管理器初始化:
進程管理器初始化:
- 定義進程/線程內核對象類型
- 建立記錄系統中所有進程的鏈表->並使用PsActiveProcessHead全局變量指向鏈表
- 為初始的進程創建一個進程對象(PsIdleProcess),命為Idle
- 創建系統進程和線程 並將Phase1Initialization函數作為線程起始地址
由於當前線程IRQL很高 因此這線程不會即時被調用

Phase1Initialization:

- 調用KeStartAllProcess()初始化所有CPU, 這個函數會構結好cpu狀態然後調用HAL的HalStartNextProcessor

- 再次調用KdInitSystem() 並調用 KdDebuggerInitialize1來初始化內核調試通信擴展DLL

- 最後創建第一個映像文件創建的過程(smss.exe), 會話管理器

- 會話管理器會初始化Windows子系統, 創建子系統進程和登錄進程, WinLogon.exe,後者會創建LSASS和Service.EXE 最後顯示登錄界面


第一次調用KdInitSystem
--------------------------------------------------------
KdDebuggerInitialize0:

內核入口函數 KiSystemStartup->KdInitSystem
進行以下動作:
1. 初始化調試數據鏈表(KdpDebuggerDataListHead)
2. 初始化KdDebuggerDataBlock數據結構(含內核基,模塊鏈表指針,調試數據鏈表指針)
3. 根據參數指針指向的LOADER_PARAMETER_BLOCK結構尋找調試有關的選項,然後保存到變量中

4. XP之後的系統: KdDebuggerInitialize() 來對通信擴展模塊進行階段0初始化,以下初始化以下的全局變量:

- KdPitchDebugger : BOOLEAN 用來標誌是否顯式抑制內核調試,當啟動選項中包含/NODEBUG選項,這個變量會被設為TRUE

- KdDebuggerEnable: BOOLEAN, 用來標誌內核調試是否被啟用了,當選項中包含/DEBUG或/DEBUGPORT,而且不包含/NODEBUG, 他為TRUE

- KiDebugRoutine: 函數指針類型, 用來標誌內核調試引擎的異常處理回調函數,當引擎活動時,它指向KdpTrap 否則KdpStub

- KdpBreakpointTable: 結構數組,用來記錄代碼斷點,每個元素為breakpoint_entry結構
第二次調用KdInitSystem
---------------------------------------------------
KdDebuggerInitialize1:
Phase1Initialization->KdInitSystem -> KeQueryPerformanceCounter-> KdPerformanceCounterRate(性能計數器頻率)

數據包
------------------------------------------------------
中斷包, 供調試器通知內核調試引擎中斷到調試器
信息包, 傳輸信息/調試命令
控制包, 建立通信連接或控制通信流程,如確認收到數據

中斷包內容固定為1到4個字節的0x62 即字符b
信息包用來在內核調試引擎和調試器之間傳輸狀態信息或命令,其長度是不固定的,但都是以一個KD_PACKET結構開始,然後跟隨不定長的數據,最後以0xAA結束

信息包結構如下:
typedef strcut _KD_PACKET{
 ULONG PacketLeader;
 USHORT PacketType;
 USHORT ByteCount;
 ULONG PacketID;
 ULONG Checksum;
}KD_PACKET, *PKD_PACKET;

PacketLeader: 永遠是4字節0x30303030
PacketType如下:

PACKET_TYPE_KD_STATE_CHANGE(1) : 供內核調試引擎向調試器報告狀態變化,包頭後是一個DBGKD_WAIT_STATE_CHANGE結構, 調試器收到後應該發送確認控制包

PACKET_TYPE_KD_STATE_MANIPULATE(2) : 供調試器調用內核調試引擎的各種調試服務(稱為KdAPI),在內核調試引擎收到後,先發送確認控制包, 然後再通過這個類型的信息包回復給調試器,調試器收到後再發送確認信息

PACKET_TYPE_DEBUG_IO(3) : 供內核調試引擎向調試器輸出字符串或通過調試器征詢用戶輸入

PACKET_TYPE_KD_STATE_CHANGE64(7) : PACKET_TYPE_KD_STATE_CHANGE類似, 但是頭結構後跟隨的是一個DBGKD_WAIT_STATE_CHANGE64結構, 這個子類型是為了支持64位而增加的

PACKET_TYPE_KD_POLL_BREAKIN(8) : 在WINXP將通信函數獨立存放於KDCOM.DLL 改為調用通信函數的KdReceivePacket來輪詢是否有中斷包,為了與接收正常包相區別,引入包類型(8)作為參數 其他4個參數為0

PACKET_TYPE_KD_TRACE_IO(9) : 用於將ETW信息輸出到調試器

PACKET_TYPE_KD_CONTROL_REQUEST(10) :用法不詳

PACKET_TYPE_KD_FILE_IO(11): 用來從調試器讀取/更新內核文件(驅動程序), windbg的.kdfiles命令就是依靠這種機制實現


ByteCount: KD_PACKET結構後所跟隨的數據長度
PacketId: 用來標誌數據包,對於內核調試引擎所發送的信息包,其編排方式與通信連接方式有關
Checksum: 數據包中所有字節的代數和,供接收方來驗證數據

控制包結構如下:
-------------------------------------------------------------
PACKET_TYPE_KD_ACKNOWLEDGE(4):確認收到對方信息包,PACKETID為收到包的包ID
PACKET_TYPE_KD_RESENT(5): 要求對方重新發送正在發送的信息包,或尚未得到確認上一個信息包,PACKETID總為0x12062F
PACKET_TYPE_KD_RESET(6): 重新建立連接

報告狀態變化:
--------------------------------------------------------------
PACKET_TYPE_KD_STATE_CHANGE(1) 或 PACKET_TYPE_KD_STATE_CHANGE64(7)
typedef struct _DBGKD_WAIT_STATE_CHANGE32
{
ULONG NewState;    //狀態號碼  
USHORT ProcessorLevel;    //CPU級別
USHORT Processor;    //CPU號
ULONG NumberProcessors;    //活動中的處理器個數
ULONG Thread;     //ETHREAD地址
ULONG ProgramCounter;    //程序指針
union      
{
DBGKM_EXCEPTION32 Exception;   //異常信息
DBGKD_LOAD_SYMBOLS32 LoadSymbols;  //映像文件信息
} u;
DBGKD_CONTROL_REPORT ControlReport;  //附屬報告
CONTEXT Context;    //上下文狀態
} DBGKD_WAIT_STATE_CHANGE32, *PDBGKD_WAIT_STATE_CHANGE32;
其中NewState等於DbgKdExceptionStateChange(0x00003030L) 或 DbgKdLoadSymbolsStateChange(0x00003031L), 分別代表異常類狀態變化加載符號類狀態變化,

ProcessorLevel為與CPU架構相關的處理器級別(不是IRQL)通常為0, ProcessorLevel為與CPU架構相關的CPU號.

NumberProcessors為當時系統中的cpu個數,多cpu系統,此數字也可能為1 比如在啟動過程中,其他CPU是在執行體階段1初始化時才被喚醒的

Thread為發生狀態變化線程ETHREAD結構地

ProgramCounter為觸發狀態變化的EIP

聯合結構u的內容根據NewState決定,如果Newstate為DbgKdExceptionStateChange, 那一個DBGKM_EXCEPTION32,否則便是一個DBGKD_LOAD_SYMBOL32結構

接下來的ControlReport和Context都是與CPU寄存器或指令有關

訪問目標系統:
----------------------------------------------------------------
調試器通過及送PacketType等於PACKET_TYPE_KD_STATE_MANIPULATE(2)的信息包,請求內核調試引擎的服務以訪問目標系統,在KD_PACKET結構後跟隨的結構是DBGKD_MANIPULATE_STATE32或64
typedef struct _DBGKD_MANIPULATE_STATE32                               // 5 elements, 0x34 bytes (sizeof)
{
/*0x000*/     ULONG32      ApiNumber;      //API代號
/*0x004*/     UINT16       ProcessorLevel;     //處理器級別
/*0x006*/     UINT16       Processor;      //處理器編號
/*0x008*/     LONG32       ReturnStatus;     //返回狀態
union                                                      // 20 elements, 0x28 bytes (sizeof) 
{
/*0x00C*/        struct _DBGKD_READ_MEMORY32 ReadMemory;                 // 3 elements, 0xC bytes (sizeof) 讀內存32位 
/*0x00C*/         struct _DBGKD_WRITE_MEMORY32 WriteMemory;              // 3 elements, 0xC bytes (sizeof) 寫內存32位
/*0x00C*/         struct _DBGKD_READ_MEMORY64 ReadMemory64;              // 3 elements, 0x10 bytes (sizeof)讀內存64位
/*0x00C*/         struct _DBGKD_WRITE_MEMORY64 WriteMemory64;            // 3 elements, 0x10 bytes (sizeof)寫內存64位
/*0x00C*/         struct _DBGKD_GET_CONTEXT GetContext;                  // 1 elements, 0x4 bytes (sizeof) 讀取上下文
/*0x00C*/         struct _DBGKD_SET_CONTEXT SetContext;                  // 1 elements, 0x4 bytes (sizeof) 返置上下文
/*0x00C*/         struct _DBGKD_WRITE_BREAKPOINT32 WriteBreakPoint;      // 2 elements, 0x8 bytes (sizeof) 設置斷點
/*0x00C*/         struct _DBGKD_RESTORE_BREAKPOINT RestoreBreakPoint;    // 1 elements, 0x4 bytes (sizeof) 恢復斷點
/*0x00C*/         struct _DBGKD_CONTINUE Continue;                       // 1 elements, 0x4 bytes (sizeof) 恢復執行
/*0x00C*/         struct _DBGKD_CONTINUE2 Continue2;                     // 3 elements, 0x20 bytes (sizeof) 恢復執行
/*0x00C*/         struct _DBGKD_READ_WRITE_IO32 ReadWriteIo;             // 3 elements, 0xC bytes (sizeof)  IO讀寫,32位
/*0x00C*/         struct _DBGKD_READ_WRITE_IO_EXTENDED32 ReadWriteIoExtended;  // 6 elements, 0x18 bytes (sizeof) IO讀寫
/*0x00C*/         struct _DBGKD_QUERY_SPECIAL_CALLS QuerySpecialCalls;         // 1 elements, 0x4 bytes (sizeof)  查詢特殊調用
/*0x00C*/         struct _DBGKD_SET_SPECIAL_CALL32 SetSpecialCall;             // 1 elements, 0x4 bytes (sizeof)  設置特殊調用
/*0x00C*/         struct _DBGKD_SET_INTERNAL_BREAKPOINT32 SetInternalBreakpoint; // 2 elements, 0x8 bytes (sizeof) 設置內部斷點
/*0x00C*/         struct _DBGKD_GET_INTERNAL_BREAKPOINT32 GetInternalBreakpoint; // 7 elements, 0x1C bytes (sizeof) 獲取內部斷點
/*0x00C*/         struct _DBGKD_GET_VERSION32 GetVersion32;                      // 14 elements, 0x28 bytes (sizeof) 獲取版本結構
/*0x00C*/         struct _DBGKD_BREAKPOINTEX BreakPointEx;                       // 2 elements, 0x8 bytes (sizeof)  增強的斷點操作
/*0x00C*/         struct _DBGKD_READ_WRITE_MSR ReadWriteMsr;                     // 3 elements, 0xC bytes (sizeof) 讀寫msr
/*0x00C*/         struct _DBGKD_SEARCH_MEMORY SearchMemory;                      // 4 elements, 0x18 bytes (sizeof) 搜索內存
}u;
}DBGKD_MANIPULATE_STATE32, *PDBGKD_MANIPULATE_STATE32;

調試引擎收到調試器發送的服務請求時,它會先發送確認包,然後再以同樣子類型(packetType等於PACKET_TYPE_KD_STATE_MANIPULATE)的數據包回復給調試器, 頭結構後依然是一個DBGKD_MANIPULATE_STATE結構,並且ApiNumber等於調試器請求的號碼,調試器收到後,也是先發送確認包,再發送下一個請求或繼續運行, 恢復目標系統執行

恢復目標系統執行
---------------------------------------------------------------
typedef struct _DBGKD_CONTINUE{
NTSTATUS ContinueStatus
}DBGKD_CONTINUE, *PDBGDK_CONTINUE;

ContinueStatus:
DBG_CONTINUE 恢復執行
DBG_EXCEPTION_HANDLED 恢復執行,並告知內核調試引擎已經處理異常狀態
DBG_EXCEPTION_NOT_HANDLED 告知內核調試引擎沒有處理異常
typedef struct _DBGKD_CONTINUE2{
NTSTATUS ContinueStatus:
DBGKD_CONTROL_SET ControlSet;
}DBGKD_CONTINUE2, *PDBGDK_CONTINUE2;
typedef struct _DBGKD_CONTROL_SET {
DWORD   TraceFlag;                 // 追蹤標誌,用於單步跟蹤
DWORD   Dr7;         // 調試控制寄存器,用於硬斷
DWORD   CurrentSymbolStart;        // 希望調試引擎進行本地跟蹤的起始地址
DWORD   CurrentSymbolEnd;          // 希望調試引擎進行本地跟蹤的結束地址
} DBGKD_CONTROL_SET, *PDBGKD_CONTROL_SET;
*因為DBGKD_CONTROL_SET結構中的內容是調試器每次恢復執行前都要設置..如果要使用DbgKdContinueApi就需要發送數據包,做這些設置,然後再次發送; 但DbgKdContinueApi2,就可以直接做兩件事

版本
---------------------------------------------------------------
不管目標是32/64, 所有信息包中的數據結構都使用64位, 調試器可以通過DbgKdGetVersionApi來讀取版本信息!

中斷到調試器
---------------------------------------------------------------
在調試器引擎向調試器報告狀態變化前, 它會調用KdEnterDebugger函數來凍結內核,直至收到調試器的恢復繼續執行命令(如DbgKdContinueApi和DbgKdContinueApi2)後,再調用KdExitDebugger恢復內核執行


KdEnterDebugger的主要操作包括:
1. 調用內核的調試支持函數KeFreezeExecutiorerrupts禁止中斷,對於多處理器的系統,它會將當前CPU的IRQL升高到HIGH_LEVEL,並凍結其他所有CPU

2. 鎖定調試通信端口, 即獲取全局變量KdpDebuggerLock所代表的鎖對象

3. 修改KdEnteredDebugger為TRUE

在執行後,整個系統送入一種簡單的單任務狀態,當前的cpu只執行當前的線程,其他CPU全被凍結

當前CPU接下來要做的是執行狀態變化報告函數,報告異常類狀態變化函數為KdpReportExceptionStateChange函數,如要報告的是符號加載類狀態變化則是KdpReportLoadSymbolStateChange


這兩個狀態變化報告函數在準備好狀態信息包的內容(即DBGKD_WAIT_STATE_CHANGE64/32數據結)後, 調用KdpSendWaitContinue函數來發送信息包,並與調試器進行對話,直到收到恢復執行命令


KdpSendWaitContinue的主要操作包括:

1. 它會先將狀態變化信息包發給調試器
2. 開始不停等待及處理來自調試器的操作狀態信息包


退出調試器
-----------------------------------------------------------
調試引擎收到調試器的恢復繼續執行命令(如DbgKdContinueApi和2)後會調用KdExitDebugger恢復內核執行

KdExitDebugger的主要操作作包括:
1. 調用KdRestore讓KDCOM.dll,恢復通信狀態
2. 對鎖定的調試通信端口解鎖
3. 調用KeThawExecution讓系統進入正常運行狀態, 包括恢復中斷,降低當前CPU的IRQL, 對於多CPU系統,會恢復其他CPU,直到下一次中斷包來臨

輪詢中斷包
-----------------------------------------------------------
為支持內核調試,系統的KeUpdateSystemTime函數,在每次更新系統時間會會檢查全局變量KdDebuggerEnabled,來判斷內核調試引擎是否被啟用,如他被啟用KdPollBreakIn函數會來查看調試器是否發送了中斷命令,如果是便調用DbgBreakPointWithStatus觸發斷點異常


由於系統時鐘中斷(INT 0) 從內核啟動後就會經常發生,所以可靠性高


內核態的KiDispatchException和KdpTrap
-----------------------------------------------------------

內核態調試整體流程:
KiDispatchException->第一次處理-> 直接調用KiDebugRoutine(KdpTrap)-> nt!RtlDispatchException(嘗試尋找SEH) -> KeContextToKframes -> Context轉回到TrapFrame
->第二次處理-> 直接調用KiDebugRoutine(KdpTrap)-> KeBugCheckEx -> 再失敗觸發藍屏(BSOD)

- 內核有異常發生時也是靠KiDispatchException分發異常
- 他會調用全局變量KiDebugRoutine所指向的函數,當調試引擎被啟用時他指向KdpTrap,意味著內核異常發生時,他會調用KdpTrap

KdpTrap函數根據ExceptionRecord判斷所發生的異常,特別是標誌異常類型的ExceptionRecord->ExceptionCode字段, 對於斷點指令異常,調試異常,和所有第二輪異常,KdpTrap伯調用KdpReport向調試器報告異常:
1. 調用KdEnterDebugger凍結內核其他部份
2. 調用KiSaveProcessorControlState保存CPU控制狀態
3. 調用KdpReportExceptionStateChange向調試器報告異常狀態變化,他發送信息包後,會接收和處理調試器的各種調試命令 直到恢復執行命令
4. 調用KiRestoreProcessorControlState恢復CPU狀態
5. 調用KdExitDebugger恢復內核執行

KdpTrap返回值是真 -> 代表已處理, 否則還未處理


調試服務
------------------------------------------------------------
1. 打印調試信息 如DbgPrint函數
2. 用戶輸入
3. 報告加載事件
4. 報告卸載時間

當需要執行以上任務時,系統觸發一個軟件異常,編號0x2D,通常把這個異常稱為調試服務(DebugService)異常->KiDebugService->KiTrap03->KiDispatchException->KdpTrap->最後傳給調試器

即使全部是int3服中斷,但KiTrap03可以按照ExceptionInformation數組區分真正調試服務

打印調試信息
------------------------------------------------------------
與用戶態OutputDebugString API類似,
DbgPrint
DbgPrintEx
vDbgPrintEx

簡單處理後調用vDbgPrintExWithPrefix函數 -> 調用DebugPrint請求調試服務->DebugService-> 執行INT 0x2D


加載調試符號
------------------------------------------------------------

如啟用了調試引擎->KdpTrap->按照附加信息檢測加載符號請求後調用KdpSymbol函數,內部再調用KdEnterDebugger凍結內核->KdpReportLoadSymbolsStateChange->KdExitDebugger

最早的調試機會
------------------------------------------------------------
與ITP這樣的硬件調試工具相比,使用內核調試引擎進行調試的一個不足是無法在調試引擎初始化之前出現的問題,比如無法調試操作系統加載程序

KiSystemStartup -> HalInitializeProcessor初始化啟動CPU-> 調用KiInitializeKernel之前 第一次調用KdInitSystem時
當KdInitSystem第一次被調用並完成基本初始化工作後,會特意調用DbgLoadImageSymbols函數向調試器報告內核模塊和HAL模塊加載事件

開機後第一次(同時為整個調試服務流程):

*系統的模塊加載函數成功後會調用MiDriverLoadSucceeded函數->DbgLoadImageSymbols函數->DebugService2->((int 0x2D->KiDebugService)->(int3->kiTrap03->省略->kiTrap按照KiDebugService接收的附加信息向內核調試器發送信息如:DbgKdLoadSymbolStateChange)->返回

*當調試器收到這個信息包時->調試器收到這個信息包後會先發送確認包->然後通過類型2(操縱狀態)信息包請求GetKdGetVersionApi, 以讀取調試引擎和目標系統的版本,他會使用一個_DBGKD_GET_VE
的結構

*WINDBG調試器會執行初始化方法:InitFromKdVersion會執行初始化方法 並在窗口顯示

*接下來調試器調用DbgKdReadVirtualMemoryApi獲取OS其他信息

*彈出框架->初始斷點

開機調試

初始斷點
------------------------------------------------------------
啟動過程:
KiSystemStartup-> KdInitSystem-> [KiInitializeKernel->KiInitSystem->ExpInitializeExecutive(階段0)] -> (階段1)

省略部份
* CommonDispatchException->KiDispatchException->向調試器發送消息

Comments

Popular posts from this blog

Android Kernel Development - Kernel compilation and Hello World

How does Nested-Virtualization works?

Understanding ACPI and Device Tree