軟件調試(8) - 探討從應用層到內核層的中斷與異常管理

硬件異常
IDT
在idtr讀取idt表基址
-------------------------
門描述符:
任務門 - 用於任務切換 里面包含用於選擇任務狀態段(tss)的段選擇子
中斷門 - 用於描述中斷處理例程入口
陷阱門 - 用於描述異常處理例程入口


任務門: 描述的是一個TSS段->CPU要做的是切換到這個TSS段所代表的線程,然後開始執行這個線程,TSS段是用來保存任務信息的一段內存區,其格式由CPU定義

Windows會為每個CPU創建3-4個tss
1: 處理NMI
2: 處理#DF(double fault) 異常中再異常
3: 處理機器檢查異常

軟件異常
--------------------------
EXCEPTION_RECORD 結構
typdef struct _EXCEPTION_RECORD{
 DWORD ExceptionCode    //異常代碼
 DWORD ExceptionFlags    //異常標志
 struct _EXCEPTION_RECORD* ExceptionRecord; //相關的另一個異常
 PVOID ExceptionAddress;    //異常發生地址
 DWORD NumberParameters;    //參數數組元素
 ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //參數數組

}EXCEPTION_RECORD, *PEXCEPTION_RECORD;

#define EXCEPTION_BREAKPOINT STATUS_BREAKPOINT
#define EXCEPTION_SINGLE_STEP STATUS_SINGLE_STEP


登記CPU異常
---------------------------
KiTrapXX例程在完成針對本異常的動作後,會調用CommonDispatchException->KiDispatchException
並通過寄存器將如下信息傳送到函數
1. 異常代碼->EAX
2. 異常指令地址->EBX
3. 其他信息作為附帶參數(最多3個),EDX(參數1),ESI(參數2),EDI(參數3),ECX(參數個數)
CommonDispatchException被調用->棧中分配EXCEPTION_RECORD結構, 把以上信息存到結構
再調用KiDispatchException分發異常函數(如正在調試則發送調試消息)

登記軟件異常
----------------------------
軟件異常是通過直/間接調用內核服務NtRaiseException產生
用戶模式=>kernel32!RaiseException() API來調用這個內核服務 供應用程序產生自定義的異常

函數原形:
void RaiseException(DWORD dwExceptionCode, //異常代碼,可以是  
                    DWORD dwExceptionFlags,
                    DWORD nNumberOfArguments,
                    const DWORD* lpArguments
                    ); 
應用層:
kernel32!RaiseException -> 參數放到EXCEPTION_RECORD後 -> ntdll!RtlRaiseException->線程上下文環境放入CONTEXT結構->ntdll!NtRaiseException->nt!NtRaiseException->nt!KiRaiseException->KeContextToKframes->KiDispatchException

內核層:
nt!RtlRaiseException->nt!NtRaiseException->nt!KiRaiseException->KeContextToKframes->KiDispatchException


函數原型:
NTSTATUS KiRaiseException(IN PEXCEPTION_RECORD ExceptionRecord, //指向異常記錄指針
  IN PCONTEXT ContextRecord,                                    //線程上下文結構指針
  IN PKEXCEPTION_FRAME ExceptionFrame,                          //x86永遠是空
  IN PKTRAP_FRAME TrapFrame,                                    //棧幀基址址(進入函數時的EBP,相對於上層函數的ESP)
  IN BOOLEAN FirstChane)                                        //第一輪或第二輪處理
只要是異常不論是硬件或軟件=>最終會由KiDispatchException分發

KiDispatchException
-----------------------------------------------------------------------------------------
void KiDispatchException(IN PEXCEPTION_RECORD ExceptionRecord, //同上
IN PKEXCEPTION_FRAME ExceptionFrame, //x86永遠是空
IN PKTRAP_FRAME TrapFrame, //KTRAP_FRAME結構 用於形容異常時處理器狀態包括寄存器
IN KPROCESSOR_MODE PreviousMode, //0:內核/1:用戶態
IN BOOLEAN FirstChance)

1. KiDispatchException ->KeContextFromKframes先建立CONTEXT結構

2.用戶態
第一次處理:
KiDispatchException->DbgkpForwardException->傳送消息失敗->打包好數據,然後把EIP指向KeUserExceptionDispatcer返回用戶態↓

KeUserExceptionDispatcer ->ntdll!RtlDispatchException返回TRUE->ZwContinue->成功運行
或KeUserExceptionDispatcer ->ntdll!RtlDispatchException返回FALSE->Ntdll!ZwRaiseException->nt!NtRaiseException->nt!kiDispatchException->進行第二次處理↓

第二次處理-> DbgkForwardException->獲取DebugPort成功 ->傳送消息成功
否則-> DbgkForwardException->ExceptionPort ->傳送消息成功
-> 傳送再次-> ZwTerminateThread -> KeBugCheckEx

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



3.以上成功->KeContextToKframes填充CONTEXT結構

KiDebugRoutine作用: 標識內核調試是否啟動,啟用時指向KdpTrap 否則指向KdpStub

RtlDispatchException 作用: 嘗試找出SEH -> RtlGetRegistrationHead(透過FS:0寄存器,x86下存放TIB)-> SEH鏈表 (鏈表尾部永遠保存著UnhandledExceptionFilter函數),如果RtlDispatchException返回FALSE表示前面所有節點沒有處理便會找到他 -> 結束進程(沒有調試器時) / 導致RtlDispatchException返回FALSE

KeContextToKframes作用:把Context結構中的信息復制到當前線程的內核棧,然後把ExcpetionRecord中的異常代碼最高位清0
以便軟件產生的異常與CPU異常區分開

*DbgkpForwardException第二個參數影響調用DbgkpSendApiMessage/DbgkpSendApiMessageLpc哪一個 , 後者不會掛起進程,但仍會堵塞線程(詳見WRK)

ZwTerminateProcess(NtCurrentProcess(), ExceptionRecord->ExceptionCode);
KeBugCheckEx(KERNEL_MODE_EXCEPTION_NOT_HANDLED, ExceptionRecord->ExceptionCode, (ULONG)ExceptionRecord->ExceptionAddress, (ULONG)TrapFrame, 0);

簡單總結:
KiDispatchException步驟:
1. 如第一次處理失敗後,打包好數據跳到R3
2. 如跳到R3後在R3找不到SEH,準備下一次處理
3. 進行第二次處理->為了到達KiDispatchException再一次觸發ZwRaiseException,這次firstChance為false
4. 第二次嘗試發送消息給調試端口,異常端口,再失敗則中斷線程






參考: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