當有異常發生時,CPU會通過IDT表找到異常處理函數,即內核中的KiTrapXX系列函數,然后轉去執行。但是,KiTrapXX函數通常只是對異常做簡單的表征和描述,為了支持調試和軟件自己定義的異常處理函數,系統需要將異常分發給調試器或應用程序的處理函數。
為了更好的管理異常,Windows系統定義了專門的數據結構EXCEPTION_RECORD來描述異常。
typedef 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;
ExceptionCode:異常代碼,32位整數。
ExceptionFlags:用來記錄異常標志,它的每一位代表一種標志。
ExceptionRecord:用來指向與該異常有關的另一個異常記錄。
ExceptionAddress;用來記錄異常地址,錯誤類異常與陷阱類異常會有區別。
NumberParameters:附加參數個數,即ExceptionInformation數組的有效個數。
登記CPU異常
對於CPU異常,KiTrapXX例程在完成針對本異常的特別動作后,通常會調用CommonDispatchException函數,它會在棧中分配一個EXCEPTION_RECORD結構,並把異常信息存儲到該結構中。在准備好這個結構后,它會調用內核中的KiDispatchExcption函數來分發異常。
登錄軟件異常
簡單來捉,軟件異常是通過直接或間接調用內核服務KiRaiseException而產生的。函數內部會把Context上下背景文復制到當前線程的內核棧,接下來調用KiDispatchExcption函數來進行分發。
綜上所述,不管什么異常最后都會調用內核中的KiDispatchExcption函數進行分發,也就是說Windows用統一的方式來管理異常。
VOID
KiDispatchException (
IN PEXCEPTION_RECORD ExceptionRecord,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame,
IN KPROCESSOR_MODE PreviousMode,
IN BOOLEAN FirstChance
)
ExceptionRecord:用來描述要分發的異常。
ExceptionFrame:指向的KTRAP_FRAME結構,用來描述異常發生時的處理器狀態,包括各種通用寄存器、調試寄存器、段寄存器等。
PreviousMode:枚舉,用來表示前一種狀態是內核模式還是用戶模式。
FirstChance:表示第幾輪分發。
下面先來看看KiDispatchException 分發示意圖。
從圖中我們可以看到,KiDispatchException會先調用KeContextFromKframes函數,目的是根據TrapFrame參數指向的KTRAP_FRAME結構產生一個CONTEXT結構,以供向調試器和異常處理器函數報告異常時使用。
接下來會根據模式是內核模式還是用戶模式進行分發。下面具體說明。
內核態異常的分發過程
對於第一輪異常KiDispatchException會試圖先通知內核調試器來處理異常,如果沒有處理異常,那么會調用RtlDispatchExcption,試圖尋找已經注冊的結構化異常處理器(SEH)。
如果也沒有找到,那么就會給內核調試器第二次處理的機會。仍然返回FLASE的話,就會調用KeBugCheckEx觸發藍屏。
用戶態異常的分發過程
首先,KiDispatchException會判斷是否發送給內核調試器,但內核調試器通常不處理用戶態異常,所以KiDispatchException會試圖發送給用戶態調試器,方法是調用DbgkForwardException。如果不成功,KiDispatchException下一步動作是試圖尋找異常處理塊來處理該異常,因為用戶異常發生在用戶態代碼中,異常處理塊也是在用戶態代碼中。所以需要轉到用戶態去執行。(這也就是相對於內核態異常的分發過程,用戶態異常的分發過程會麻煩一點的原因,具體方式不再累贅,參考《軟件調試》)如果最終也返回FALSE,那么就會分發第二輪。
結構化異常處理SEH
為了讓系統和應用程序代碼都可以簡單方便地支持異常處理,Windows定義了一套標准的機制來處理代碼的設計和編譯,這套機制被稱為結構化異常處理(Structured Exception Handling),簡稱SEH。
異常處理結構如下:
__try { //被保護塊 } __except(過濾表達式) { //異常處理塊 }
通過TEB結構的NtTib成員可以很容易的訪問進程的SEH鏈,方法很簡單。
TEB.NtTib.ExceptionList成員是TEB結構體的第一個成員。FS段寄存器指向段內存的起始地址,TEB結構體即位於此,所以通過下列公式可以輕松獲取TEB.NtTib.ExceptionList的地址。
TEB.NtTib.ExceptionList = FS:[0]
那么那匯編語言實現的話:
PUSH @Handler
PUSH DWORD PTR FS:[0]
MOV DWORD PTR FS:[0], ESP
向量化異常處理VEH
從WindowsXP開始,Windows還支持一種名為向量化異常處理的異常處理機制,簡稱VEH。
與SEH既可以在用戶態又可以在內核態不同,VEH只能在用戶態程序中。
VEH的基本思想是通過注冊一下的原型的回調函數來接收和處理異常。
LONG CALLBACK VectoredHandle(PEXCEPTION_POINTERS ExceptionInfo);
相應的,Windows公布了兩個API,AddVectoredExceptionHandle和RemoveVectoredExceptionHandle來分別注冊和注銷回調函數VectoredHandle。
例如:
PVOID AddVectoredExceptionHandle(ULONG FirstHandle, PVECTORED_EXCEPTION_HANDLE VectoredHandle)。
參數FirstHandle代表該函數被調用的順序,0表示希望最后調用, 1表示希望最先調用。如果注冊了多個回調函數,而且FirstHandle都是非零,那么最后注冊的最先被調用。
SEH與VEH區別和聯系:
從應用范圍:SEH可以在用戶態代碼中,也可以用在內核態代碼中,但是VEH只能用在用戶態代碼中。
從優先角度:對於同時注冊了SEH與VEH的代碼所觸發的異常,VEH比SEH先得到處理權。
從登記方式:SEH注冊信息是固定結構存儲在線程棧中,VEH的注冊信息是存儲在進程的內存堆中。
從作用域: VEH對整個進程都有效,具有全局性。SEH是動態建立在所在函數函數棧上的,會隨函數返回而銷毀。
從編譯角度:SEH的登記和注銷是依賴編譯器編譯時所產生的數據結構和代碼的,VEH的注冊和注銷都是通過系統調用的API顯示染成的,不需要經過編譯器的特殊處理。