操作系統或程序在運行,難免會遇到各種各樣的錯誤,如除零,非法內存訪問,文件打開錯誤,內存不足,磁盤讀寫錯誤,外設操作失敗。爲了保證系統在遇到錯誤時不至於崩潰,仍能夠健壯穩定地繼續運行下去,windows會對運行在其中的程序提供一次補救的機會來處理錯誤這種機制就是異常處理機制。 S.E.H即異常處理結構體(Structure Exception Handler),它是windows異常處理機制所採用的重要數據結構,每個S.E.H包含兩個DWORD指針:S.E.H鏈表指針和異常處理函數句柄,共8個字節,如下圖
下面分別對用戶模式下的原始型SEH和封裝型SEH 進行討論。
原始型SEH
SEH 的進程相關類型是整個進程作用範圍的異常處理函數,通過WIN32 API 函數SetUnhandledExceptionFilter 進行註冊,而操作系統內部使用一個全局變量來記錄這個頂層的處理函數,因此只能有一個全局性的異常處理函數。而線程相關類型的作用範圍是本線程內,並且可註冊多個,甚至可以嵌套註冊。兩者相比線程相關類型在實際應用中使用較爲廣泛,因此此處重點對此類型進行研究。
當線程初始化時,會自動向棧中安裝一個異常處理結構,作爲線程默認的異常處理。SEH 最基本的數據結構是保存在堆棧中的稱爲EXCEPTION_REGISTRATION 的結構體,結構體包括2個元素:第1個元素是指向下一個EXCEPTION_REGISTRATION 結構的指針(prev),第2個元素是指向異常處理程序的指針(handler)。這樣一來,基於堆棧的異常處理程序就相互連接成一個鏈表。異常處理結構在堆棧中的典型分佈如圖1 所示。最頂端的異常處理結構通過線程控制塊(TEB)0 Byte
偏移處指針標識,即FS:[0]處地址。
用於進行實際異常處理的函數原型可表示如下:
EXCEPTION_DISPOSITION __cdecl _except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext
)
該函數的最重要的2個參數是指向_EXCEPTION_RECORD 結構的ExceptionRecord 參數和指向_CONTEXT 結構的ContextRecord 參數,前者主要包括異常類別編碼、異常發生地址等重要信息;後者主要包括異常發生時的通用寄存器、調試寄存器和指令寄存器的值等重要的線程執行環境。而用於註冊異常處理函數的典型彙編代碼可表示如下:
PUSH handler ; handler 是新的異常處理函s數地址
PUSH FS:[0] ;指向原來的處理函數的地址壓入棧內
MOV FS:[0],ESP ;註冊新的異常處理結構
當異常發生時,操作系統的異常分發函數在進行初始處理後,如果異常沒有被處理就會開始在上圖所示的線程堆棧上遍歷異常處理鏈,直到異常被處理,如果仍沒有註冊函數處理異常,則將異常交給缺省處理函數或直接結束產生異常的進程。
封裝型SEH
通過使用_try{}/_except(){}/_finally{}等關鍵字,使開發人員更方便地在軟件中使用SEH 是封裝型SEH 的主要特點。該機制的異常處理數據結構定義如下:
struct VC_EXCEPTION_REGISTRATION
{
VC_EXCEPTION_REGISTRATION* prev;
FARPROC handler;
scopetable_entry* scopetable; //指向scopetable 數組指針
int _index; //在scopetable_entry 中索引
DWORD _ebp; //當前EBP 值
}
顯而易見,結構體中後3 個成員是新增的。而scopetable_entry 的結構如下所示:
struct scopetable_entry
{
DWORD prev_entryindex; //前一scopetable_entry 的索引
FARPROC lpfnFilter; //過濾函數地址
FARPROC lpfnHandler; //處理異常代碼地址
}
封裝型SEH 的基本思想是爲每個函數內的_try{}塊建立一scopetable表,每個_try{}塊對應於scopetable中的一項,該項指向_try{}塊對應的scopetable_entry 結構,該結構含有與_except(){}/_finally{}對應的過濾函數和處理函數。若有_try{}塊嵌套,則在scopetable_entry 結構的prev_entryindex成員中指明,多層嵌套形成單向鏈表。而每個函數只註冊一個VC_EXCEPTION_REGISTRATION
結構, 該結構中的handler 成員是一個重要的運行時庫函數_except_handler3。該異常處理回調函數負責對結構中的成員進行設置,查找處理函數並根據處理結果決定是繼續執行還是讓系統繼續遍歷外層SEH 鏈。爲了弄清看似複雜的封裝型SEH 原理,此處通過分析一個簡單的使用封裝型SEH 的函數的反彙編實現,從而深入地瞭解封裝型SEH 的實現過程。該函數的C 語言實現如下:
void A()
{
__try // 0 號try 塊
{
__try // 1 號try 塊
{
*(PDWORD)0 = 0;
}
__except(EXCEPTION_CONTINUE_SEARCH)
{
printf("Exception Handler!");
}
}
__finally
{
puts("in finally");
}
}
對應該函數的序言部分反彙編代碼如下:
push ebp
mov ebp, esp
push -1
push offset _A_scopetable
push offset _except_handler3
mov eax, large fs:0
push eax
mov large fs:0, esp
顯而易見, 壓入堆棧的結構與VC_EXCEPTION_REGISTRATION 結構是一致的。查找scopetable 的地址爲0x00422048,在調試器中查找該地址起始的內容如下:
FFFFFFFF ;scopetable_entry0 的prev_entryindex 值
00000000 ;lpfnFilter 地址值,爲0,對應_finally{}
004010EE ;lpfnHandler 地址值
00000000 ;scopetable_entry1 的prev_entryindex 值
004010C6 ;lpfnFilter 地址值
004010C9 ;lpfnHandler 地址值
…
第1 組值對應0 號try 塊,而該塊對應_finally{}塊,所以過濾函數地址爲0;第2 組值對應1 號try 塊,而該塊對應_except(){}塊,所以有過濾函數和處理函數的地址。進一步查看0x004010EE 地址處的反彙編代碼如下:
PUSH OFFSET ??_C@0L@PEFD@in?5finally?$AA@; ”in finally”
CALL puts
ADD ESP,4
RETN
顯然上述語句與_finally{}塊中的C 語言語句是對應的,而其他的地址經過查找也是分別對應的。在進入0 號try 塊時的反彙編語句如下:
MOV DWORD PTR SS:[EBP-4],0 ;對應第1 個_try 語句
MOV DWORD PTR SS:[EBP-4],1 ;對應第2 個_try 語句
而在退出_try 塊時對應的反彙編語句如下:…
MOV DWORD PTR SS:[EBP-4],0 ;退出第2 個_try 塊
MOV DWORD PTR SS:[EBP-4],-1 ;退出第1 個_try 塊
…
根據異常處理的堆棧結構可知, [EBP-4] 處值就是VC_EXCEPTION_REGISTRATION 結構中_index 的值。異常處理機制在進入和退出每個_try 塊前設置相應的_index 值,這樣就可正確處理封裝型SEH 內發生的各種異常。以上通過實例進一步驗證和明確了封裝型SHE 的內部機理,它只是擴展了原始型SEH 的功能,簡化了軟件開發人員的工作。
利用
未完。。。