原著:Matt Pietrek
翻譯:
VCKBASE
譯注:本文都翻譯了一大半了,才發現網上已經有一篇董巖的譯本(
http://www.diybl.com/course/3_program/c++/cppsl/200866/122881.html
),
看完他的譯文,感覺所有涉及關鍵技術的地方董巖翻譯得非常到位,所以自己就沒有再花時間往下譯,本文后面的部分基本上都是采用了董巖的譯文。此外,董巖在譯文后面附帶有一篇附錄,專門解釋“prolog 和 epilog”,后來我發現 MSDN 庫中對此有專門的解釋,內容也更豐富,所以就用微軟文檔中的內容取而代之了。特此說明。
原文出處: A Crash Course on the Depths of Win32? Structured Exception Handling
本文假設你熟悉 C++,Win32
摘要
Win32 結構化異常處理其核心是操作系統提供的服務,你能找到的關于 SEH 的所有文檔都是描述一個特定的編譯器運行時庫,這個運行庫包裝著操作系統實現。在本文中,我將一層一層對 SEH 進行剝離,以便展現其最基本的概念。
在 Win32 操作系統提供的所有功能中,使用最廣泛但最缺乏文檔描述的也許就是結構化異常處理了(SEH),當你考慮 Win32 結構化異常處理時,你也許會想到諸如 _try,_finally 以及 _except 這些術語。你能在任何有關 Win32 的書中發現對 SEH 很好的描述(即使是 remedial)。即便是 Win32 SDK 也具備有相當完整的使用 _try,_finally 和 _except 進行結構化異常處理的概述。
有了這些文檔,那為何還說 SEH 缺乏文檔呢?其實,Win32 結構化異常處理是操作系統提供的一個服務。你能找到的關于 SEH 的所有文檔都是描述特定編譯器的運行時庫,這個運行庫對操作系統實現進行包裝。_try,_finally 和 _except 這些關鍵字沒有任何神奇的地方。微軟的操作系統及其編譯器系列定義這些關鍵字和用法。其他的編譯器提供商則只是沿用這些語義。雖然借助編譯器層的 SEH 可以挽回一些原始操作系統級 SEH 處理不良口碑,但在大眾眼里對原始操作系統 SEH 細節的處理感覺依舊。
我收到人們大量的e-mail,都是想要實現編譯器級的 SEH 處理,又無法找到操作系統功能提供的相關文檔。通常我都是建議參考 Visual C++ 或者 Borland C++ 運行庫源代碼。唉,出于一些未知的原因,編譯器級的 SEH 似乎是一個大的秘密,微軟和 Borland 都不提供其對 SEH 支持的核心層源代碼。
在本文中,我將一層一層對 SEH 進行解剖,以便展現其最基本的概念。我打算通過代碼產生和運行時庫支持將操作系統提供的功能和編譯器提供的功能分開。當我深入代碼考察關鍵的操作系統例程時,我將使用 Intel 平臺上的 Windows NT4.0 作為基礎。但我將要描述的大多數內容同樣適用于其它處理器上運行的應用。
我打算避免涉及到真正的 C++ 異常處理,它們使用 catch(),而不是 _except。其實,真正的 C++ 異常處理實現非常類似于本文中描述的內容。但是 C++ 異常處理有一些額外的復雜性會影響我想要涉及的概念。
通過深入研究晦澀的 .H 和 .INC 文件來歸納 Win32 SEH 構成,我發現有一個信息源之一就是 IBM OS/2 頭文件(尤其是 BSEXCPT.H)。為此你不要覺得大驚小怪。。此處描述的 SEH 機制在其源頭被定義時,微軟仍然開發 OS/2 平臺(譯注: OS/2 平臺起初是IBM 和 微軟共同研發的,后來由于種種原因兩個公司沒有再繼續下去)。所以你會發現Win32 下的 SEH 和 OS/2 下的 SEH 極其相似。
SEH 淺析
從整體來看,SEH 的可謂不可一世,絕對壓倒一切,我將從細微之處開始,用我自己的方式一層一層研究。如果你是一張白紙,以前從沒接觸過結構化異常處理,那就最好不過了。如果你以前使用過 SEH。那就嘗試清理你頭腦中的 _try,GetExceptionCode 和 EXCEPTION_EXECUTE_HANDLER 等諸如此類的詞,權當自己是個新手。做一個深呼吸,準備好了嗎?好,我們開始。
想象一下,我告訴你某個線程出錯了,操作系統給你一個機會通知了這個線程錯誤,或者再具體一點,當線程出錯后,操作系統調用某個用戶定義的回調函數。這個回調函數可以所任何它想做的事情。例如,它可以修復任何原因導致的錯誤,或者播放一個 .wav 文件。不管回調函數做什么,其最后總是返回一個值,這個值告訴系統下一步做什么。(這里描述的情況不一定完全一樣,但足夠接近。)
假定當你的代碼出現了混亂,你不得不回來,想看看回調函數是什么樣子的?換句話說,你想知道什么樣的異常信息呢?其實這無關緊要,因為 Win32 已經幫你決定了。一個異常回調函數就象下面這樣:
EXCEPTION_DISPOSITION __cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext );
該原型出自標準的 Win32 頭文件 EXCPT.H,初看就有那么一點不同凡響。如果你慢慢研究,其實并沒有那么糟。例如,忽略返回類型(EXCEPTION_DISPOSITION)。基本上你看到的就是一個叫做 _except_handler 的函數,這個函數帶有四個參數。
第一個參數是指向 EXCEPTION_RECORD 結構指針,該結構在 WINNT.H 中定義如下:
typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD;
ExceptionCode 參數是由操作系統賦值給異常的一個數。你可以在 WINNT.H 文件中搜一下“STATUS_”開始的 #defines 內容便可以得到一系列不同的異常編碼。例如 STATUS_ACCESS_VIOLATION 是大家再熟悉不過的異常編碼了,其值是 0xC0000005。更復雜的異常編碼可以從 Windows NT DDK 的 NTSTATUS.H 文件中找到。EXCEPTION_RECORD 結構中的第四個元素是異常發生的地址。剩下的 EXCEPTION_RECORD 域現在可以忽略,不用管它。
_except_handler 回調函數的第二個參數是指向建立者框架(establisher frame)結構的指針,在 SEH 中它是一個至關重要的參數,但現在可以不用關心它。
_except_handler 回調函數的第三個參數是 CONTEXT 結構的指針。CONTEXT 結構在 WINNT.H 中定義,它表示特定線程異常發生時寄存器的值:
typedef struct _CONTEXT { DWORD ContextFlags; DWORD Dr0; DWORD Dr1; DWORD Dr2; DWORD Dr3; DWORD Dr6; DWORD Dr7; FLOATING_SAVE_AREA FloatSave; DWORD SegGs; DWORD SegFs; DWORD SegEs; DWORD SegDs; DWORD Edi; DWORD Esi; DWORD Ebx; DWORD Edx; DWORD Ecx; DWORD Eax; DWORD Ebp; DWORD Eip; DWORD SegCs; DWORD EFlags; DWORD Esp; DWORD SegSs; } CONTEXT;
此外,這個 CONTEXT 結構與 GetThreadContext 和 SetThreadContext API 函數使用的結構是相同的。
_except_handler 回調函數的第四個參數是 DispatcherContext。現在也可以忽略它。
為了簡化起見,當異常發生時,你有一個回調函數被調用。此回調函數帶四個參數,其中三個是結構指針。在這些結構中,某些域是很重要的,其余的不是那么重要。關鍵是 _except_handler 回調函數接收
很多信息,比如發生了什么類型的異常,在哪里發生的。利用這些信息,異常回調機制需要確定要做什么。
雖然我迫不急但地想拋出例子程序示范 _except_handler 回調的運行,但還有一些事情不能漏掉,需要說明。特別是當錯誤發生時,操作系統如何知道到哪里調用?答案仍然涉及另外一個結構 EXCEPTION_REGISTRATION。你將自始自終在本文中看到這個結構,所以不要掠過這部分內容。我能找到正式定義 EXCEPTION_REGISTRATION 結構的唯一地方是 EXSUP.INC 文件,該文件來自 Visual C++ 運行庫的源:
_EXCEPTION_REGISTRATION struc prev dd ? handler dd ? _EXCEPTION_REGISTRATION ends
你還將看到該結構在 WINNT.H 文件中定義的 NT_TIB 結構中被引用為 _EXCEPTION_REGISTRATION_RECORD。唉,除此之外,沒有什么地方能找到 _EXCEPTION_REGISTRATION_RECORD 的定義,所以我只能使用 EXSUP.INC 文件中定義的匯編語言結構。這也是我為什么在本文前述內容中說過的 SEH 缺乏文檔的一個例證。
不管怎樣,讓我們回到手頭的問題,當某個異常發生時,OS 如何知道到哪里調用回調函數?EXCEPTION_REGISTRATION 由兩個域構成,第一個你現在可以忽略。第二個域是句柄,它包含 _except_handler 回調函數的指針。這讓你更接近一點了,但目前問題來了,OS 在哪里查找并發現 EXCEPTION_REGISTRATION 結構?
為了回答這個問題,回想一下結構化異常處理是以線程為基礎,并作用在每個線程上,明白這一點是有助于理解的。也就是說,每個線程具備其自己的異常處理回調函數。在我
1996年5月的專欄文章
中,我描述了一個關鍵的 Win32 數據結構——線程信息塊(即 TEB 和 TIB)。該數據結構的某些域在 Windows NT、Windows 95、Win32s 和 OS/2 平臺上是一樣的。TIB 中的第一個 DWORD 是指向線程 EXCEPTION_REGISTRATION 結構的指針。在 Intel Win32 平臺上,FS 寄存器總是指向當前的 TIB。因此,在 FS:[0]位置,你能找到 EXCEPTION_REGISTRATION 結構的指針。
現在我們知道了,當異常發生時,系統檢查出錯線程的 TIB 并獲取 EXCEPTION_REGISTRATION 結構的指針。這個結構中就有一個 _except_handler 回調函數的指針。這些信息足以讓操作系統知道在哪里以及如何調用 _except_handler 函數,如圖二所示:
圖二 _except_handler 函數
通過前面的描述,我寫了一個小程序來對操作系統層的結構化異常進行示范。程序代碼如下:
//================================================== // MYSEH - Matt Pietrek 1997 // Microsoft Systems Journal, January 1997 // FILE: MYSEH.CPP // To compile: CL MYSEH.CPP //================================================== #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> DWORD scratch; EXCEPTION_DISPOSITION __cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext ) { unsigned i; // Indicate that we made it to our exception handler printf( "Hello from an exception handler/n" ); // Change EAX in the context record so that it points to someplace // where we can successfully write ContextRecord->Eax = (DWORD)&scratch; // Tell the OS to restart the faulting instruction return ExceptionContinueExecution; } int main() { DWORD handler = (DWORD)_except_handler; __asm { // 創建 EXCEPTION_REGISTRATION 結構: push handler // handler函數的地址 push FS:[0] // 前一個handler函數的地址 mov FS:[0],ESP // 裝入新的EXECEPTION_REGISTRATION結構 } __asm { mov eax,0 // EAX清零 mov [eax], 1 // 寫EAX指向的內存從而故意引發一個錯誤 } printf( "After writing!/n" ); __asm { // 移去我們的 EXECEPTION_REGISTRATION 結構記錄 mov eax,[ESP] // 獲取前一個結構 mov FS:[0], EAX // 裝入前一個結構 add esp, 8 // 將 EXECEPTION_REGISTRATION 彈出堆棧 } return 0; }
代碼中只有兩個函數,main 函數使用了三部分內聯匯編塊 ASM。第一個 ASM 塊通過兩個 PUSH 指令(即:“PUSH handler”和“PUSH FS:[0]”)在堆棧上建立一個 EXCEPTION_REGISTRATION 結構。PUSH FS:[0] 保存以前 FS:[0] 的值,它是結構的一部分,但目前這個值對我們不重要。重要的是在堆棧上有一個 8-byte 的 EXCEPTION_REGISTRATION 結構。緊接著的指令(MOV FS:[0],ESP)是讓線程信息塊中的第一個 DWORD 指到新的 EXCEPTION_REGISTRATION 指令。
如果你想知道為什么我要在堆棧上建立這個 EXCEPTION_REGISTRATION 結構,而不是使用全局變量,有一個很好的理由。當你使用編譯器的 _try/_except 時,編譯器也會在堆棧上建立 EXCEPTION_REGISTRATION 結構。我只是向你簡要地揭示你使用 _try/_except 時編譯器所做的事情。讓我們回到 main 函數,下一個 __asm 塊是通過把 EAX 寄存器清零(MOV EAX,0),然后把此寄存器的值作為內存地址讓下一條指令(MOV [EAX],1)向此地址寫入數據而故意引發一個錯誤。最后一個 __asm 塊是清除這個簡單的異常處理例程:首先它恢復以前的 FS:[0] 內容,然后它將 EXCEPTION_REGISTRATION 結構記錄從堆棧中彈出(ADD ESP,8)。
現在,假設你正在運行 MYSEH.EXE 并會看到所發生的事情。當 MOV [EAX],1 指令執行時,它導致一個數據訪問違例。系統察看 TIB 中的 FS:[0] 并找到 EXCEPTION_REGISTRATION 結構指針。此結構中則有一個指向 MYSEH.CPP 中 _except_handler 函數的指針。系統則將四個必須的參數(我在前面描述過這四個參數)壓入堆棧并調用 _except_handler 函數。
一旦進入 _except_handler,代碼首先通過 printf 指示“哈!這里是我干的!”。接著,_except_handler 修復導致出錯的問題。即 EAX 寄存器指向某個不能寫入的內存地址(地址 0)。修復方法是在改變 CONTEXT 結構中的 EAX 的值,以便它指向某個允許進行寫入操作的位置。在這個簡單的程序中,DWORD 變量(scratch)是故意為此而設計的。_except_handler 函數最后一個動作時返回 ExceptionContinueExecution 值,它在標準的 EXCPT.H 文件中定義。
當操作系統看到返回值為 ExceptionContinueExecution。它就認為你已經修復了問題,并且引起錯誤的指令應該被重新執行。因為我的 _except_handler 函數強制 EAX 寄存器指向合法內存,MOV EAX,1 指令再次執行,函數 main 一切正常。看,這并不復雜,不是嗎?
進一步深入
有了前面的最簡單的例子,讓我們再回過頭去填補一些空白。雖然這個異常回調機制很棒,但它并不是一個完美的解決方案。對于稍微復雜一些的應用程序來說,僅用一個函數就能處理程序中任何地方都可能發生的異常是相當困難的。一個更實用的方案應該是有多個異常處理例程,每個例程針對程序的特定部分。不知你是否知道,實際上,操作系統提供的正是這個功能。
還記得系統用來查找異常回調函數的 EXCEPTION_REGISTRATION 結構嗎?這個結構的第一個成員,稱為 prev,前面我們曾把它忽略掉了。它實際上是一個指向另外一個 EXCEPTION_REGISTRATION 結構的指針。這第二個 EXCEPTION_REGISTRATION 結構可以有一個完全不同的處理函數。然后呢,它的 prev 域可以指向第三個 EXCEPTION_REGISTRATION 結構,依次類推。簡單地說,就是有一個 EXCEPTION_REGISTRATION 結構鏈表。線程信息塊的第一個 DWORD(在基于 Intel CPU 的機器上是 FS:[0])總是指向這個鏈表的頭部。
操作系統要這個 EXCEPTION_REGISTRATION 結構鏈表做什么呢?原來,當異常發生時,系統遍歷這個鏈表以便查找其中的一個EXCEPTION_REGISTRATION 結構,其例程回調(異常處理程序)同意處理該異常。在 MYSEH.CPP 的例子中,異常處理程序通過返回ExceptionContinueExecution 表示它同意處理這個異常。異常回調函數也可以拒絕處理這個異常。在這種情況下,系統移向鏈表的下一個EXCEPTION_REGISTRATION 結構并詢問它的異常回調函數,看它是否愿意處理這個異常。圖四顯示了這個過程:
圖四 查找處理異常的 EXCEPTION_REGISTRATION 結構
一旦系統找到一個處理該異常的某個回調函數,它就停止遍歷結構鏈表。
下面的代碼 MYSEH2.CPP 就是一個異常處理函數不處理某個異常的例子。為了使代碼盡量簡單,我使用了編譯器層面的異常處理。main 函數只設置了一個 __try/__except塊。在__try 塊內部調用了 HomeGrownFrame 函數。這個函數與前面的 MYSEH 程序非常相似。它也是在堆棧上創建一個 EXCEPTION_REGISTRATION 結構,并且讓 FS:[0] 指向此結構。在建立了新的異常處理程序之后,這個函數通過向一個 NULL 指針所指向的內存處寫入數據而故意引發一個錯誤:
*(PDWORD)0 = 0;
這個異常處理回調函數,同樣被稱為_except_handler,卻與前面的那個截然不同。它首先打印出 ExceptionRecord 結構中的異常代碼和標志,這個結構的地址是作為一個指針參數被這個函數接收的。打印出異常標志的原因稍后就會明白。因為_except_handler 函數并沒有打算修復出錯的代碼,因此它返回 ExceptionContinueSearch。這導致操作系統繼續在 EXCEPTION_REGISTRATION 結構鏈表中搜索下一個 EXCEPTION_REGISTRATION結構。接下來安裝的異常回調函數是針對 main 函數中的__try/__except塊的。__except 塊簡單地打印出“Caught the exception in main()”。此時我們只是簡單地忽略這個異常來表明我們已經處理了它。 以下是 MYSEH2.CPP:
//================================================= // MYSEH2 - Matt Pietrek 1997 // Microsoft Systems Journal, January 1997 // FILE: MYSEH2.CPP // 使用命令行CL MYSEH2.CPP編譯 //================================================= #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> EXCEPTION_DISPOSITION __cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext ) { printf( "Home Grown handler: Exception Code: %08X Exception Flags %X", ExceptionRecord->ExceptionCode, ExceptionRecord->ExceptionFlags ); if ( ExceptionRecord->ExceptionFlags & 1 ) printf( " EH_NONCONTINUABLE" ); if ( ExceptionRecord->ExceptionFlags & 2 ) printf( " EH_UNWINDING" ); if ( ExceptionRecord->ExceptionFlags & 4 ) printf( " EH_EXIT_UNWIND" ); if ( ExceptionRecord->ExceptionFlags & 8 ) // 注意這個標志 printf( " EH_STACK_INVALID" ); if ( ExceptionRecord->ExceptionFlags & 0x10 ) // 注意這個標志 printf( " EH_NESTED_CALL" ); printf( "/n" ); // 我們不想處理這個異常,讓其它函數處理吧 return ExceptionContinueSearch; } void HomeGrownFrame( void ) { DWORD handler = (DWORD)_except_handler; __asm { // 創建EXCEPTION_REGISTRATION結構: push handler // handler函數的地址 push FS:[0] // 前一個handler函數的地址 mov FS:[0],ESP // 安裝新的EXECEPTION_REGISTRATION結構 } *(PDWORD)0 = 0; // 寫入地址0,從而引發一個錯誤 printf( "I should never get here!/n" ); __asm { // 移去我們的EXECEPTION_REGISTRATION結構 mov eax,[ESP] // 獲取前一個結構 mov FS:[0], EAX // 安裝前一個結構 add esp, 8 // 把我們EXECEPTION_REGISTRATION結構彈出堆棧 } } int main() { __try { HomeGrownFrame(); } __except( EXCEPTION_EXECUTE_HANDLER ) { printf( "Caught the exception in main()/n" ); } return 0; }
這里的關鍵是執行流程。當一個異常處理程序拒絕處理某個異常時,它實際上也就拒絕決定流程最終將從何處恢復。只有接受某個異常的異常處理程序才能決定待所有異常處理代碼執行完畢之后流程將從何處繼續執行。這個規則暗含的意義非常重大,雖然現在還不是顯而易見。
當使用結構化異常處理時,如果一個函數有一個異常處理程序但它卻不處理某個異常,這個函數就有可能非正常退出。例如在 MYSEH2中 HomeGrownFrame 函數就不處理異常。由于在鏈表中后面的某個異常處理程序(這里是 main 函數中的)處理了這個異常,因此出錯指令后面的 printf 就永遠不會執行。從某種程度上說,使用結構化異常處理與使用 setjmp 和 longjmp 運行時庫函數有些類似。
如果你運行 MYSEH2,會發現其輸出有些奇怪。看起來好像調用了兩次 _except_handler 函數。根據你現有的知識,第一次調用當然可以完全理解。但是為什么會有第二次呢?
Home Grown handler: Exception Code: C0000005 Exception Flags 0 Home Grown handler: Exception Code: C0000027 Exception Flags 2 EH_UNWINDING Caught the Exception in main()
比較一下以“Home Grown Handler”開頭的兩行,就會看出它們之間有明顯的區別。第一次異常標志是0,而第二次是2。這個問題說來話就長了。實際上,當一個異常處理回調函數拒絕處理某個異常時,它會被再一次調用。但是這次回調并不是立即發生的。這有點復雜。我需要把異常發生時的情形好好梳理一下。
當異常發生時,系統遍歷 EXCEPTION_REGISTRATION 結構鏈表,直到它找到一個處理這個異常的處理程序。一旦找到,系統就再次遍歷這個鏈表,直到處理這個異常的結點為止。在這第二次遍歷中,系統將再次調用每個異常處理函數。關鍵的區別是,在第二次調用中,異常標志被設置為2。這個值被定義為 EH_UNWINDING。(EH_UNWINDING 的定義在 Visual C++ 運行時庫源代碼文件 EXCEPT.INC 中,但 Win32 SDK 中并沒有與之等價的定義。)
EH_UNWINDING 表示什么意思呢?原來,當一個異常處理回調函數被第二次調用時(帶 EH_UNWINDING 標志),操作系統給這個函數一個最后清理的機會。什么樣的清理呢?一個絕好的例子是 C++ 類的析構函數。當一個函數的異常處理程序拒絕處理某個異常時,通常執行流程并不會正常地從那個函數退出。現在,想像一下定義了一個C++類的實例作為局部變量的函數。C++規范規定析構函數必須被調用。這帶 EH_UNWINDING 標志的第二次回調就給這個函數一個機會去做一些類似于調用析構函數和__finally 塊之類的清理工作。
在異常已經被處理完畢,并且所有前面的異常幀都已經被展開之后,流程從處理異常的那個回調函數決定的地方開始繼續執行。一定要記住,僅僅把指令指針設置到所需的代碼處就開始執行是不行的。流程恢復執行處的代碼的堆棧指針和棧幀指針(在Intel CPU上是 ESP 和EBP)也必須被恢復成它們在處理這個異常的函數的棧幀上的值。因此,這個處理異常的回調函數必須負責把堆棧指針和棧幀指針恢復成它們在包含處理這個異常的 SEH 代碼的函數的堆棧上的值。
通常,展開操作導致堆棧上處理異常的幀以下的堆棧區域上的所有內容都被移除了,就好像我們從來沒有調用過這些函數一樣。展開的另外一個效果就是 EXCEPTION_REGISTRATION 結構鏈表上處理異常的那個結構之前的所有 EXCEPTION_REGISTRATION 結構都被移除了。這很好理解,因為這些 EXCEPTION_REGISTRATION 結構通常都被創建在堆棧上。在異常被處理后,堆棧指針和棧幀指針在內存中比那些從 EXCEPTION_REGISTRATION 結構鏈表上移除的 EXCEPTION_REGISTRATION 結構高。圖六顯示了我說的情況。
幫幫我!沒有人處理它!
迄今為止,我實際上一直在假設操作系統總是能在 EXCEPTION_REGISTRATION 結構鏈表中 的某個地方找到一個異常處理程序。如果找不到怎么辦呢?實際上,這幾乎不可能發生。因為操作系統暗中已經為每個線程都提供了一個默認的異常處理程序。這個默認的異常處理程序總是鏈表的最后一個結點,并且它總是選擇處理異常。它進行的操作與其它正常的異常處理回調函數有些不同,下面我會說明。
讓我們來看一下系統是在什么時候插入了這個默認的、最后一個異常處理程序。很明顯它需要在線程執行的早期,在任何用戶代碼開始執行之前。
下面是我為 BaseProcessStart 函數寫的偽代碼。它是 Windows NT KERNEL32.DLL 的一個內部例程。這個函數帶一個參數——線程入口點函數的地址。BaseProcessStart 運行在新進程的上下文環境中,并且從該進程的第一個線程的入口點函數開始執行。
BaseProcessStart 偽碼 BaseProcessStart( PVOID lpfnEntryPoint ) { DWORD retValue DWORD currentESP; DWORD exceptionCode; currentESP = ESP; _try { NtSetInformationThread( GetCurrentThread(), ThreadQuerySetWin32StartAddress, &lpfnEntryPoint, sizeof(lpfnEntryPoint) ); retValue = lpfnEntryPoint(); ExitThread( retValue ); } _except(// 過濾器-表達式代碼 exceptionCode = GetExceptionInformation(), UnhandledExceptionFilter( GetExceptionInformation() ) ) { ESP = currentESP; if ( !_BaseRunningInServerProcess ) // 常規進程 ExitProcess( exceptionCode ); else // 服務 ExitThread( exceptionCode ); } }
在這段偽碼中,注意對 lpfnEntryPoint 的調用被封裝在一個__try 和 __except 塊中。正是此__try 塊安裝了默認的、異常處理程序鏈表上的最后一個異常處理程序。所有后來注冊的異常處理程序都被安裝在此鏈表中這個結點的前面。如果 lpfnEntryPoint 函數返回,那么表明線程一直運行到完成并且沒有引發異常。這時 BaseProcessStart 調用 ExitThread 使線程退出。
另一方面,如果線程引發了一個異常但是沒有異常處理程序來處理它時,該怎么辦呢?這時,執行流程轉到 __except 關鍵字后面的括號中。在 BaseProcessStart 中,這段代碼調用 UnhandledExceptionFilter 這個 API,稍后我會講到它。現在對于我們來說,重要的是 UnhandledExceptionFilter 這個API包含了默認的異常處理程序。
如果 UnhandledExceptionFilter 返回 EXCEPTION_EXECUTE_HANDLER,這時 BaseProcessStart 中的__except 塊開始執行。而__except塊所做的只是調用 ExitProcess 函數去終止當前進程。稍微想一下你就會理解了。常識告訴我們,如果一個進程引發了一個錯誤而沒有異常處理程序去處理它,這個進程就會被系統終止。你在偽代碼中看到的正是這些。
對于上述內容我還有一點要補充。如果引發錯誤的線程是作為服務來運行的,并且是基于線程的服務,那么__except 塊并不調用 ExitProcess,而是調用 ExitThread。不能僅僅因為一個服務出錯就終止整個服務進程。
UnhandledExceptionFilter 中的默認異常處理程序都做了什么呢?當我在一個技術講座上問起這個問題時,響應者寥寥無幾。幾乎沒有人知道當未處理異常發生時,到底操作系統的默認行為是什么。簡單地演示一下這個默認的行為也許會讓很多人豁然開朗。我運行一個故意引發錯誤的程序,其結果如下(如圖八)。
表面上看,UnhandledExceptionFilter 顯示了一個對話框告訴你發生了一個錯誤。這時,你被給予了一個機會要么終止出錯進程,要么調試它。但是幕后發生了許多事情,我會在文章最后詳細講述它。
正如我讓你看到的那樣,當異常發生時,用戶寫的代碼可以(并且通常是這樣)獲得機會執行。同樣,在操作過程中,用戶寫的代碼可以執行。此用戶編寫的代碼也可能有缺陷并可能引發另一個異常。由于這個原因,異常處理回調函數也可以返回另外兩個值: ExceptionNestedException 和 ExceptionCollidedUnwind。很明顯,它們很重要。但這是非常復雜的問題,我并不打算在這里詳細講述它們。要想理解其基本概念真的太困難了。
編譯器級的SEH
雖然我在前面偶爾也使用了__try 和__except,但迄今為止幾乎我寫的所有內容都是關于操作系統方面對 SEH 的實現。然而看一下我那兩個使用操作系統的原始 SEH 的小程序別扭的樣子,編譯器對這個功能進行封裝實在是非常有必要的。現在讓我們來看一下 Visual C++ 是如何在操作系統對 SEH 功能實現的基礎上來創建它自己的結構化異常處理支持的。
在繼續往下討論之前,記住其它編譯器可以使用原始的系統 SEH 來做一些完全不同的事情這一點是非常重要的。沒有誰規定編譯器必須實現 Win32 SDK 文檔中描述的__try/__except 模型。例如 Visual Basic 5.0 在它的運行時代碼中使用了結構化異常處理,但是那里的數據結構和算法與我這里要講的完全不同。
如果你把 Win32 SDK 文檔中關于結構化異常處理方面的內容從頭到尾讀一遍,一定會遇到下面所謂的“基于幀”的異常處理程序模型:
__try { // 這里是被保護的代碼 } __except (過濾器表達式) { // 這里是異常處理程序代碼 }
簡單地說,某個函數__try 塊中的所有代碼是由 EXCEPTION_REGISTRATION 結構來保護的,該結構建立在此函數的堆棧幀上。在函數的入口處,這個新的 EXCEPTION_REGISTRATION 結構被放在異常處理程序鏈表的頭部。在__try 塊結束后,相應的 EXCEPTION_REGISTRATION 結構從這個鏈表的頭部被移除。正如我前面所說,異常處理程序鏈表的頭部被保存在 FS:[0] 處。因此,如果你在調試器中單步跟蹤時能看到類似下面的指令
MOV DWORD PTR FS:[00000000],ESP 或者 MOV DWORD PTR FS:[00000000],ECX
就能非常確定這段代碼正在進入或退出一個__try/__except塊。
既然一個__try 塊對應著堆棧上的一個 EXCEPTION_REGISTRATION 結構,那么 EXCEPTION_REGISTRATION 結構中的回調函數又如何呢?使用 Win32 的術語來說,異常處理回調函數對應的是過濾器表達式(filter-expression)代碼。事實上,過濾器表達式就是__except 關鍵字后面的小括號中的代碼。就是這個過濾器表達式代碼決定了后面的大括號中的代碼是否執行。
由于過濾器表達式代碼是你自己寫的,你當然可以決定在你的代碼中的某個地方是否處理某個特定的異常。它可以簡單的只是一句 “EXCEPTION_EXECUTE_HANDLER”,也可以先調用一個把p計算到20,000,000位的函數,然后再返回一個值來告訴操作系統下一步做什么。隨你的便。關鍵是你的過濾器表達式代碼必須是我前面講的有效的異常處理回調函數。
我剛才講的雖然相當簡單,但那只不過是隔著有色玻璃看世界罷了。現實是非常復雜的。首先,你的過濾器表達式代碼并不是被操作系統直接調用的。事實上,各個 EXCEPTION_REGISTRATION 結構的 handler 域都指向了同一個函數。這個函數在 Visual C++ 的運行時庫中,它被稱為__except_handler3。正是這個__except_handler3 調用了你的過濾器表達式代碼,我一會兒再接著說它。
對我前面的簡單描述需要修正的另一個地方是,并不是每次進入或退出一個__try 塊時就創建或撤銷一個 EXCEPTION_REGISTRATION 結構。相反,在使用 SEH 的任何函數中只創建一個 EXCEPTION_REGISTRATION 結構。換句話說,你可以在一個函數中使用多個 __try/__except 塊,但是在堆棧上只創建一個 EXCEPTION_REGISTRATION 結構。同樣,你可以在一個函數中嵌套使用 __try 塊,但 Visual C++ 仍舊只是創建一個 EXCEPTION_REGISTRATION 結構。
如果整個 EXE 或 DLL 只需要單個的異常處理程序(__except_handler3),同時,如果單個的 EXCEPTION_REGISTRATION 結構就能處理多個__try 塊的話,很明顯,這里面還有很多東西我們不知道。這個技巧是通過一個通常情況下看不到的表中的數據來完成的。由于本文的目的就是要深入探索結構化異常處理,那就讓我們來看一看這些數據結構吧。
擴展的異常處理幀
Visual C++ 的 SEH 實現并沒有使用原始的 EXCEPTION_REGISTRATION 結構。它在這個結構的末尾添加了一些附加數據。這些附加數據正是允許單個函數(__except_handler3)處理所有異常并將執行流程傳遞到相應的過濾器表達式和__except 塊的關鍵。我在 Visual C++ 運行時庫源代碼中的 EXSUP.INC 文件中找到了有關 Visual C++ 擴展的 EXCEPTION_REGISTRATION 結構格式的線索。在這個文件中,你會看到以下定義(已經被注釋掉了):
;struct _EXCEPTION_REGISTRATION{ ; struct _EXCEPTION_REGISTRATION *prev; ; void (*handler)( PEXCEPTION_RECORD, ; PEXCEPTION_REGISTRATION, ; PCONTEXT, ; PEXCEPTION_RECORD); ; struct scopetable_entry *scopetable; ; int trylevel; ; int _ebp; ; PEXCEPTION_POINTERS xpointers; ;};
在前面你已經見過前兩個域:prev 和 handler。它們組成了基本的 EXCEPTION_REGISTRATION 結構。后面三個域:scopetable(作用域表)、trylevel 和_ebp 是新增加的。scopetable 域指向一個 scopetable_entry 結構數組,而 trylevel 域實際上是這個數組的索引。最后一個域_ebp,是 EXCEPTION_REGISTRATION 結構創建之前棧幀指針(EBP)的值。
_ebp 域成為擴展的 EXCEPTION_REGISTRATION 結構的一部分并非偶然。它是通過 PUSH EBP 這條指令被包含進這個結構中的,而大多數函數開頭都是這條指令(通常編譯器并不為使用FPO優化的函數生成標準的堆棧幀,這樣其第一條指令可能不是 PUSH EBP。但是如果使用了SEH的話,那么無論你是否使用了FPO優化,編譯器一定生成標準的堆棧幀)。這條指令可以使 EXCEPTION_REGISTRATION 結構中所有其它的域都可以用一個相對于棧幀指針(EBP)的負偏移來訪問。例如 trylevel 域在 [EBP-04]處,scopetable 指針在[EBP-08]處,等等。(也就是說,這個結構是從[EBP-10H]處開始的。)
緊跟著擴展的 EXCEPTION_REGISTRATION 結構下面,Visual C++ 壓入了另外兩個值。緊跟著(即[EBP-14H]處)的一個DWORD,是為一個指向 EXCEPTION_POINTERS 結構(一個標準的Win32 結構)的指針所保留的空間。這個指針就是你調用 GetExceptionInformation 這個API時返回的指針。盡管SDK文檔暗示 GetExceptionInformation 是一個標準的 Win32 API,但事實上它是一個編譯器內聯函數。當你調用這個函數時,Visual C++ 生成以下代碼:
MOV EAX,DWORD PTR [EBP-14]
GetExceptionInformation 是一個編譯器內聯函數,與它相關的 GetExceptionCode 函數也是如此。此函數實際上只是返回 GetExceptionInformation 返回的數據結構(EXCEPTION_POINTERS)中的一個結構(EXCEPTION_RECORD)中的一個域(ExceptionCode)的值。當 Visual C++ 為 GetExceptionCode 函數生成下面的指令時,它到底是想干什么?我把這個問題留給讀者。(現在就能理解為什么SDK文檔提醒我們要注意這兩個函數的使用范圍了。)
MOV EAX,DWORD PTR [EBP-14] ; 執行完畢,EAX指向EXCEPTION_POINTERS結構 MOV EAX,DWORD PTR [EAX] ; 執行完畢,EAX指向EXCEPTION_RECORD結構 MOV EAX,DWORD PTR [EAX] ; 執行完畢,EAX中是ExceptionCode的值
現在回到擴展的 EXCEPTION_REGISTRATION 結構上來。在這個結構開始前的8個字節處(即[EBP-18H]處),Visual C++ 保留了一個DWORD來保存所有prolog代碼執行完畢之后的堆棧指針(ESP)的值(實際生成的指令為MOV DWORD PTR [EBP-18H],ESP)。這個DWORD中保存的值是函數執行時ESP寄存器的正常值(除了在準備調用其它函數時把參數壓入堆棧這個過程會改變 ESP寄存器的值并在函數返回時恢復它的值外,函數在執行過程中一般不改變ESP寄存器的值)。
看起來好像我一下子給你灌輸了太多的信息,我承認。在繼續下去之前,讓我們先暫停,來回顧一下 Visual C++ 為使用結構化異常處理的函數生成的標準異常堆棧幀,它看起來像下面這個樣子:
EBP-00 _ebp EBP-04 trylevel EBP-08 scopetable數組指針 EBP-0C handler函數地址 EBP-10指向前一個EXCEPTION_REGISTRATION結構 EBP-14 GetExceptionInformation EBP-18 棧幀中的標準ESP
在操作系統看來,只存在組成原始 EXCEPTION_REGISTRATION 結構的兩個域:即[EBP-10h]處的prev指針和[EBP-0Ch]處的handler函數指針。棧幀中的其它所有內容是針對于Visual C++的。把這個Visual C++生成的標準異常堆棧幀記到腦子里之后,讓我們來看一下真正實現編譯器層面SEH的這個Visual C++運行時庫例程——__except_handler3。
__except_handler3 和 scopetable
我真的很希望讓你看一看Visual C++運行時庫源代碼,讓你自己好好研究一下__except_handler3函數,但是我辦不到。因為 Microsoft并沒有提供。在這里你就將就著看一下我為__except_handler3函數寫的偽代碼吧:。
圖九 __except_handler3函數的偽代碼:
int __except_handler3( struct _EXCEPTION_RECORD * pExceptionRecord, struct EXCEPTION_REGISTRATION * pRegistrationFrame, struct _CONTEXT *pContextRecord, void * pDispatcherContext ) { LONG filterFuncRet; LONG trylevel; EXCEPTION_POINTERS exceptPtrs; PSCOPETABLE pScopeTable; CLD // 將方向標志復位(不測試任何條件!) // 如果沒有設置EXCEPTION_UNWINDING標志或EXCEPTION_EXIT_UNWIND標志 // 表明這是第一次調用這個處理程序(也就是說,并非處于異常展開階段) if ( ! (pExceptionRecord->ExceptionFlags & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) ) { // 在堆棧上創建一個EXCEPTION_POINTERS結構 exceptPtrs.ExceptionRecord = pExceptionRecord; exceptPtrs.ContextRecord = pContextRecord; // 把前面定義的EXCEPTION_POINTERS結構的地址放在比 // establisher棧幀低4個字節的位置上。參考前面我講 // 的編譯器為GetExceptionInformation生成的匯編代 // 碼*(PDWORD)((PBYTE)pRegistrationFrame - 4) = &exceptPtrs; // 獲取初始的“trylevel”值 trylevel = pRegistrationFrame->trylevel; // 獲取指向scopetable數組的指針 scopeTable = pRegistrationFrame->scopetable; search_for_handler: if ( pRegistrationFrame->trylevel != TRYLEVEL_NONE ) { if ( pRegistrationFrame->scopetable[trylevel].lpfnFilter ) { PUSH EBP // 保存這個棧幀指針 // !!!非常重要!!!切換回原來的EBP。正是這個操作才使得 // 棧幀上的所有局部變量能夠在異常發生后仍然保持它的值不變。 EBP = &pRegistrationFrame->_ebp; // 調用過濾器函數 filterFuncRet = scopetable[trylevel].lpfnFilter(); POP EBP // 恢復異常處理程序的棧幀指針 if ( filterFuncRet != EXCEPTION_CONTINUE_SEARCH ) { if ( filterFuncRet < 0 ) // EXCEPTION_CONTINUE_EXECUTION return ExceptionContinueExecution; // 如果能夠執行到這里,說明返回值為EXCEPTION_EXECUTE_HANDLER scopetable = pRegistrationFrame->scopetable; // 讓操作系統清理已經注冊的棧幀,這會使本函數被遞歸調用 __global_unwind2( pRegistrationFrame ); // 一旦執行到這里,除最后一個棧幀外,所有的棧幀已經 // 被清理完畢,流程要從最后一個棧幀繼續執行 EBP = &pRegistrationFrame->_ebp; __local_unwind2( pRegistrationFrame, trylevel ); // NLG = "non-local-goto" (setjmp/longjmp stuff) __NLG_Notify( 1 ); // EAX = scopetable->lpfnHandler // 把當前的trylevel設置成當找到一個異常處理程序時 // SCOPETABLE中當前正在被使用的那一個元素的內容 pRegistrationFrame->trylevel = scopetable->previousTryLevel; // 調用__except {}塊,這個調用并不會返回 pRegistrationFrame->scopetable[trylevel].lpfnHandler(); } } scopeTable = pRegistrationFrame->scopetable; trylevel = scopeTable->previousTryLevel; goto search_for_handler; } else // trylevel == TRYLEVEL_NONE { return ExceptionContinueSearch; } } else // 設置了EXCEPTION_UNWINDING標志或EXCEPTION_EXIT_UNWIND標志 { PUSH EBP // 保存EBP EBP = &pRegistrationFrame->_ebp; // 為調用__local_unwind2設置EBP __local_unwind2( pRegistrationFrame, TRYLEVEL_NONE ) POP EBP // 恢復EBP return ExceptionContinueSearch; } }
雖然__except_handler3的代碼看起來很多,但是記住一點:它只是一個我在文章開頭講過的異常處理回調函數。它同MYSEH.EXE和 MYSEH2.EXE中的異常回調函數都帶有同樣的四個參數。__except_handler3大體上可以由第一個if語句分為兩部分。這是由于這個函數可以在兩種情況下被調用,一次是正常調用,另一次是在展開階段。其中大部分是在非展開階段的回調。
__except_handler3一開始就在堆棧上創建了一個EXCEPTION_POINTERS結構,并用它的兩個參數來對這個結構進行初始化。我在偽代碼中把這個結構稱為 exceptPrts,它的地址被放在[EBP-14h]處。你回憶一下前面我講的編譯器為 GetExceptionInformation和 GetExceptionCode 函數生成的匯編代碼就會意識到,這實際上初始化了這兩個函數使用的指針。
接著,__except_handler3從EXCEPTION_REGISTRATION幀中獲取當前的trylevel(在[EBP-04h]處)。 trylevel變量實際是scopetable數組的索引,而正是這個數組才使得一個函數中的多個__try塊和嵌套的__try塊能夠僅使用一個 EXCEPTION_REGISTRATION結構。每個scopetable元素結構如下:
typedef struct _SCOPETABLE { DWORD previousTryLevel; DWORD lpfnFilter; DWORD lpfnHandler; } SCOPETABLE, *PSCOPETABLE;
SCOPETABLE結構中的第二個成員和第三個成員比較容易理解。它們分別是過濾器表達式代碼的地址和相應的__except塊的地址。但是prviousTryLevel成員有點復雜。總之一句話,它用于嵌套的__try塊。這里的關鍵是函數中的每個__try塊都有一個相應的SCOPETABLE結構。
正如我前面所說,當前的 trylevel 指定了要使用的scopetable數組的哪一個元素,最終也就是指定了過濾器表達式和__except塊的地址。現在想像一下兩個__try塊嵌套的情形。如果內層__try塊的過濾器表達式不處理某個異常,那外層__try塊的過濾器表達式就必須處理它。那現在要問,__except_handler3是如何知道SCOPETABLE數組的哪個元素相應于外層的__try塊的呢?答案是:外層__try塊的索引由 SCOPETABLE結構的previousTryLevel域給出。利用這種機制,你可以嵌套任意層的__try塊。previousTryLevel 域就好像是一個函數中所有可能的異常處理程序構成的線性鏈表中的結點一樣。如果trylevel的值為0xFFFFFFFF(實際上就是-1,這個值在 EXSUP.INC中被定義為TRYLEVEL_NONE),標志著這個鏈表結束。
回到__except_handler3的代碼中。在獲取了當前的trylevel之后,它就調用相應的SCOPETABLE結構中的過濾器表達式代碼。如果過濾器表達式返回EXCEPTION_CONTINUE_SEARCH,__exception_handler3 移向SCOPETABLE數組中的下一個元素,這個元素的索引由previousTryLevel域給出。如果遍歷完整個線性鏈表(還記得嗎?這個鏈表是由于在一個函數內部嵌套使用__try塊而形成的)都沒有找到處理這個異常的代碼,__except_handler3返回DISPOSITION_CONTINUE_SEARCH(原文如此,但根據_except_handler函數的定義,這個返回值應該為ExceptionContinueSearch。實際上這兩個常量的值是一樣的。我在偽代碼中已經將其改正過來了),這導致系統移向下一個EXCEPTION_REGISTRATION幀(這個鏈表是由于函數嵌套調用而形成的)。
如果過濾器表達式返回EXCEPTION_EXECUTE_HANDLER,這意味著異常應該由相應的__except塊處理。它同時也意味著所有前面的EXCEPTION_REGISTRATION幀都應該從鏈表中移除,并且相應的__except塊都應該被執行。第一個任務通過調用__global_unwind2來完成的,后面我會講到這個函數。跳過這中間的一些清理代碼,流程離開__except_handler3轉向__except塊。令人奇怪的是,流程并不從__except塊中返回,雖然是 __except_handler3使用CALL指令調用了它。
當前的trylevel值是如何被設置的呢?它實際上是由編譯器隱含處理的。編譯器非常機靈地修改這個擴展的EXCEPTION_REGISTRATION 結構中的trylevel域的值(實際上是生成修改這個域的值的代碼)。如果你檢查編譯器為使用SEH的函數生成的匯編代碼,就會在不同的地方都看到修改這個位于[EBP-04h]處的trylevel域的值的代碼。
__except_handler3是如何做到既通過CALL指令調用__except塊而又不讓執行流程返回呢?由于CALL指令要向堆棧中壓入了一個返回地址,你可以想象這有可能破壞堆棧。如果你檢查一下編譯器為__except塊生成的代碼,你會發現它做的第一件事就是將EXCEPTION_REGISTRATION結構下面8個字節處(即[EBP-18H]處)的一個DWORD值加載到ESP寄存器中(實際代碼為MOV ESP,DWORD PTR [EBP-18H]),這個值是在函數的 prolog 代碼中被保存在這個位置的(實際代碼為MOV DWORD PTR [EBP-18H],ESP)。
ShowSEHFrames 程序
如果你現在覺得已經被EXCEPTION_REGISTRATION、scopetable、trylevel、過濾器表達式以及展開等等之類的詞搞得暈頭轉向的話,那和我最初的感覺一樣。但是編譯器層面的結構化異常處理方面的知識并不適合一點一點的學。除非你從整體上理解它,否則有很多內容單獨看并沒有什么意義。當面對大堆的理論時,我最自然的做法就是寫一些應用我學到的理論方面的程序。如果它能夠按照預料的那樣工作,我就知道我的理解(通常)是正確的。
下面是ShowSEHFrame.EXE的源代碼。它使用__try/__except塊設置了好幾個 Visual C++ SEH 幀。然后它顯示每一個幀以及Visual C++為每個幀創建的scopetable的相關信息。這個程序本身并不生成也不依賴任何異常。相反,我使用了多個__try塊以強制Visual C++生成多個 EXCEPTION_REGISTRATION 幀以及相應的 scopetable。
//ShowSEHFrames.CPP //========================================================= // ShowSEHFrames - Matt Pietrek 1997 // Microsoft Systems Journal, February 1997 // FILE: ShowSEHFrames.CPP // 使用命令行CL ShowSehFrames.CPP進行編譯 //========================================================= #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> #pragma hdrstop //------------------------------------------------------------------- // 本程序僅適用于Visual C++,它使用的數據結構是特定于Visual C++的 //------------------------------------------------------------------- #ifndef _MSC_VER #error Visual C++ Required (Visual C++ specific information is displayed) #endif //------------------------------------------------------------------- // 結構定義 //------------------------------------------------------------------- // 操作系統定義的基本異常幀 struct EXCEPTION_REGISTRATION { EXCEPTION_REGISTRATION* prev; FARPROC handler; }; // Visual C++擴展異常幀指向的數據結構 struct scopetable_entry { DWORD previousTryLevel; FARPROC lpfnFilter; FARPROC lpfnHandler; }; // Visual C++使用的擴展異常幀 struct VC_EXCEPTION_REGISTRATION : EXCEPTION_REGISTRATION { scopetable_entry * scopetable; int trylevel; int _ebp; }; //---------------------------------------------------------------- // 原型聲明 //---------------------------------------------------------------- // __except_handler3是Visual C++運行時庫函數,我們想打印出它的地址 // 但是它的原型并沒有出現在任何頭文件中,所以我們需要自己聲明它。 extern "C" int _except_handler3(PEXCEPTION_RECORD, EXCEPTION_REGISTRATION *, PCONTEXT, PEXCEPTION_RECORD); //------------------------------------------------------------- // 代碼 //------------------------------------------------------------- // // 顯示一個異常幀及其相應的scopetable的信息 // void ShowSEHFrame( VC_EXCEPTION_REGISTRATION * pVCExcRec ) { printf( "Frame: %08X Handler: %08X Prev: %08X Scopetable: %08X/n", pVCExcRec, pVCExcRec->handler, pVCExcRec->prev, pVCExcRec->scopetable ); scopetable_entry * pScopeTableEntry = pVCExcRec->scopetable; for ( unsigned i = 0; i <= pVCExcRec->trylevel; i++ ) { printf( " scopetable[%u] PrevTryLevel: %08X " "filter: %08X __except: %08X/n", i, pScopeTableEntry->previousTryLevel, pScopeTableEntry->lpfnFilter, pScopeTableEntry->lpfnHandler ); pScopeTableEntry++; } printf( "/n" ); } // // 遍歷異常幀的鏈表,按順序顯示它們的信息 // void WalkSEHFrames( void ) { VC_EXCEPTION_REGISTRATION * pVCExcRec; // 打印出__except_handler3函數的位置 printf( "_except_handler3 is at address: %08X/n", _except_handler3 ); printf( "/n" ); // 從FS:[0]處獲取指向鏈表頭的指針 __asm mov eax, FS:[0] __asm mov [pVCExcRec], EAX // 遍歷異常幀的鏈表。0xFFFFFFFF標志著鏈表的結尾 while ( 0xFFFFFFFF != (unsigned)pVCExcRec ) { ShowSEHFrame( pVCExcRec ); pVCExcRec = (VC_EXCEPTION_REGISTRATION *)(pVCExcRec->prev); } } void Function1( void ) { // 嵌套3層__try塊以便強制為scopetable數組產生3個元素 __try { __try { __try { WalkSEHFrames(); // 現在顯示所有的異常幀的信息 } __except( EXCEPTION_CONTINUE_SEARCH ) {} } __except( EXCEPTION_CONTINUE_SEARCH ) {} } __except( EXCEPTION_CONTINUE_SEARCH ) {} } int main() { int i; // 使用兩個__try塊(并不嵌套),這導致為scopetable數組生成兩個元素 __try { i = 0x1234; } __except( EXCEPTION_CONTINUE_SEARCH ) { i = 0x4321; } __try { Function1(); // 調用一個設置更多異常幀的函數 } __except( EXCEPTION_EXECUTE_HANDLER ) { // 應該永遠不會執行到這里,因為我們并沒有打算產生任何異常 printf( "Caught Exception in main/n" ); } return 0; }
ShowSEHFrames程序中比較重要的函數是WalkSEHFrames和ShowSEHFrame。WalkSEHFrames函數首選打印出 __except_handler3的地址,打印它的原因很快就清楚了。接著,它從FS:[0]處獲取異常鏈表的頭指針,然后遍歷該鏈表。此鏈表中每個結點都是一個VC_EXCEPTION_REGISTRATION類型的結構,它是我自己定義的,用于描述Visual C++的異常處理幀。對于這個鏈表中的每個結點,WalkSEHFrames都把指向這個結點的指針傳遞給ShowSEHFrame函數。
ShowSEHFrame函數一開始就打印出異常處理幀的地址、異常處理回調函數的地址、前一個異常處理幀的地址以及scopetable的地址。接著,對于每個 scopetable數組中的元素,它都打印出其priviousTryLevel、過濾器表達式的地址以及相應的__except塊的地址。我是如何知道scopetable數組中有多少個元素的呢?其實我并不知道。但是我假定VC_EXCEPTION_REGISTRATION結構中的當前trylevel域的值比scopetable數組中的元素總數少1。
圖十一是 ShowSEHFrames 的運行結果。首先檢查以“Frame:”開頭的每一行,你會發現它們顯示的異常處理幀在堆棧上的地址呈遞增趨勢,并且在前三個幀中,它們的異常處理程序的地址是一樣的(都是004012A8)。再看輸出的開始部分,你會發現這個004012A8不是別的,它正是 Visual C++運行時庫函數__except_handler3的地址。這證明了我前面所說的單個回調函數處理所有異常這一點。
你可能想知道為什么明明 ShowSEHFrames 程序只有兩個函數使用SEH,但是卻有三個異常處理幀使用__except_handler3作為它們的異常回調函數。實際上第三個幀來自 Visual C++ 運行時庫。Visual C++ 運行時庫源代碼中的 CRT0.C 文件清楚地表明了對 main 或 WinMain 的調用也被一個__try/__except 塊封裝著。這個__try 塊的過濾器表達式代碼可以在 WINXFLTR.C文 件中找到。
回到 ShowSEHFrames 程序,注意到最后一個幀的異常處理程序的地址是 77F3AB6C,這與其它三個不同。仔細觀察一下,你會發現這個地址在 KERNEL32.DLL 中。這個特別的幀就是由 KERNEL32.DLL 中的 BaseProcessStart 函數安裝的,這在前面我已經說過。
展開
在挖掘展開(Unwinding)的實現代碼之前讓我們先來搞清楚它的意思。我在前面已經講過所有可能的異常處理程序是如何被組織在一個由線程信息塊的第一個DWORD(FS:[0])所指向的鏈表中的。由于針對某個特定異常的處理程序可能不在這個鏈表的開頭,因此就需要從鏈表中依次移除實際處理異常的那個異常處理程序之前的所有異常處理程序。
正如你在Visual C++的__except_handler3函數中看到的那樣,展開是由__global_unwind2這個運行時庫(RTL)函數來完成的。這個函數只是對RtlUnwind這個未公開的API進行了非常簡單的封裝。(現在這個API已經被公開了,但給出的信息極其簡單,詳細信息可以參考最新的Platform SDK文檔。)
__global_unwind2(void * pRegistFrame) { _RtlUnwind( pRegistFrame, &__ret_label, 0, 0 ); __ret_label: }
雖然從技術上講RtlUnwind是一個KERNEL32函數,但它只是轉發到了NTDLL.DLL中的同名函數上。下面是我為此函數寫的偽代碼。
RtlUnwind 函數的偽代碼:
void _RtlUnwind( PEXCEPTION_REGISTRATION pRegistrationFrame, PVOID returnAddr, // 并未使用!(至少是在i386機器上) PEXCEPTION_RECORD pExcptRec, DWORD _eax_value) { DWORD stackUserBase; DWORD stackUserTop; PEXCEPTION_RECORD pExcptRec; EXCEPTION_RECORD exceptRec; CONTEXT context; // 從FS:[4]和FS:[8]處獲取堆棧的界限 RtlpGetStackLimits( &stackUserBase, &stackUserTop ); if ( 0 == pExcptRec ) // 正常情況 { pExcptRec = &excptRec; pExcptRec->ExceptionFlags = 0; pExcptRec->ExceptionCode = STATUS_UNWIND; pExcptRec->ExceptionRecord = 0; pExcptRec->ExceptionAddress = [ebp+4]; // RtlpGetReturnAddress()—獲取返回地址 pExcptRec->ExceptionInformation[0] = 0; } if ( pRegistrationFrame ) pExcptRec->ExceptionFlags |= EXCEPTION_UNWINDING; else // 這兩個標志合起來被定義為EXCEPTION_UNWIND_CONTEXT pExcptRec->ExceptionFlags|=(EXCEPTION_UNWINDING|EXCEPTION_EXIT_UNWIND); context.ContextFlags =( CONTEXT_i486 | CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS); RtlpCaptureContext( &context ); context.Esp += 0x10; context.Eax = _eax_value; PEXCEPTION_REGISTRATION pExcptRegHead; pExcptRegHead = RtlpGetRegistrationHead(); // 返回FS:[0]的值 // 開始遍歷EXCEPTION_REGISTRATION結構鏈表 while ( -1 != pExcptRegHead ) { EXCEPTION_RECORD excptRec2; if ( pExcptRegHead == pRegistrationFrame ) { NtContinue( &context, 0 ); } else { // 如果存在某個異常幀在堆棧上的位置比異常鏈表的頭部還低 // 說明一定出現了錯誤 if ( pRegistrationFrame && (pRegistrationFrame <= pExcptRegHead) ) { // 生成一個異常 excptRec2.ExceptionRecord = pExcptRec; excptRec2.NumberParameters = 0; excptRec2.ExceptionCode = STATUS_INVALID_UNWIND_TARGET; excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE; RtlRaiseException( &exceptRec2 ); } } PVOID pStack = pExcptRegHead + 8; // 8 = sizeof(EXCEPTION_REGISTRATION) // 確保pExcptRegHead在堆棧范圍內,并且是4的倍數 if ( (stackUserBase <= pExcptRegHead ) && (stackUserTop >= pStack ) && (0 == (pExcptRegHead & 3)) ) { DWORD pNewRegistHead; DWORD retValue; retValue = RtlpExecutehandlerForUnwind(pExcptRec, pExcptRegHead, &context, &pNewRegistHead, pExceptRegHead->handler ); if ( retValue != DISPOSITION_CONTINUE_SEARCH ) { if ( retValue != DISPOSITION_COLLIDED_UNWIND ) { excptRec2.ExceptionRecord = pExcptRec; excptRec2.NumberParameters = 0; excptRec2.ExceptionCode = STATUS_INVALID_DISPOSITION; excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE; RtlRaiseException( &excptRec2 ); } else pExcptRegHead = pNewRegistHead; } PEXCEPTION_REGISTRATION pCurrExcptReg = pExcptRegHead; pExcptRegHead = pExcptRegHead->prev; RtlpUnlinkHandler( pCurrExcptReg ); } else // 堆棧已經被破壞!生成一個異常 { excptRec2.ExceptionRecord = pExcptRec; excptRec2.NumberParameters = 0; excptRec2.ExceptionCode = STATUS_BAD_STACK; excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE; RtlRaiseException( &excptRec2 ); } } // 如果執行到這里,說明已經到了EXCEPTION_REGISTRATION // 結構鏈表的末尾,正常情況下不應該發生這種情況。 //(因為正常情況下異常應該被處理,這樣就不會到鏈表末尾) if ( -1 == pRegistrationFrame ) NtContinue( &context, 0 ); else NtRaiseException( pExcptRec, &context, 0 ); } RtlUnwind函數的偽代碼到這里就結束了,以下是它調用的幾個函數的偽代碼: PEXCEPTION_REGISTRATION RtlpGetRegistrationHead( void ) { return FS:[0]; } RtlpUnlinkHandler( PEXCEPTION_REGISTRATION pRegistrationFrame ) { FS:[0] = pRegistrationFrame->prev; } void RtlpCaptureContext( CONTEXT * pContext ) { pContext->Eax = 0; pContext->Ecx = 0; pContext->Edx = 0; pContext->Ebx = 0; pContext->Esi = 0; pContext->Edi = 0; pContext->SegCs = CS; pContext->SegDs = DS; pContext->SegEs = ES; pContext->SegFs = FS; pContext->SegGs = GS; pContext->SegSs = SS; pContext->EFlags = flags; // 它對應的匯編代碼為__asm{ PUSHFD / pop [xxxxxxxx] } pContext->Eip = 此函數的調用者的調用者的返回地址 // 讀者看一下這個函數的 pContext->Ebp = 此函數的調用者的調用者的EBP // 匯編代碼就會清楚這一點 pContext->Esp = pContext->Ebp + 8; }
雖然 RtlUnwind 函數的規模看起來很大,但是如果你按一定方法把它分開,其實并不難理解。它首先從FS:[4]和FS:[8]處獲取當前線程堆棧的界限。它們對于后面要進行的合法性檢查非常重要,以確保所有將要被展開的異常幀都在堆棧范圍內。
RtlUnwind 接著在堆棧上創建了一個空的EXCEPTION_RECORD結構并把STATUS_UNWIND賦給它的ExceptionCode域,同時把 EXCEPTION_UNWINDING標志賦給它的 ExceptionFlags 域。指向這個結構的指針作為其中一個參數被傳遞給每個異常回調函數。然后,這個函數調用RtlCaptureContext函數來創建一個空的CONTEXT結構,這個結構也變成了在展開階段調用每個異常回調函數時傳遞給它們的一個參數。
RtlUnwind函數的其余部分遍歷EXCEPTION_REGISTRATION結構鏈表。對于其中的每個幀,它都調用 RtlpExecuteHandlerForUnwind 函數,后面我會講到這個函數。正是這個函數帶 EXCEPTION_UNWINDING 標志調用了異常處理回調函數。每次回調之后,它調用RtlpUnlinkHandler 移除相應的異常幀。
RtlUnwind 函數的第一個參數是一個幀的地址,當它遍歷到這個幀時就停止展開異常幀。上面所說的這些代碼之間還有一些安全性檢查代碼,它們用來確保不出問題。如果出現任何問題,RtlUnwind 就引發一個異常,指示出了什么問題,并且這個異常帶有EXCEPTION_NONCONTINUABLE 標志。當一個進程被設置了這個標志時,它就不允許再運行,必須終止。
未處理異常
在文章的前面,我并沒有全面描述 UnhandledExceptionFilter 這個 API。通常情況下你并不直接調用它(盡管你可以這么做)。大多數情況下它都是由 KERNEL32 中進行默認異常處理的過濾器表達式代碼調用。前面 BaseProcessStart 函數的偽代碼已經表明了這一點。
圖十三是我為 UnhandledExceptionFilter 函數寫的偽代碼。這個API有點奇怪(至少在我看來是這樣)。如果異常的類型是 EXCEPTION_ACCESS_VIOLATION,它就調用_BasepCheckForReadOnlyResource。雖然我沒有提供這個函數的偽代碼,但可以簡要描述一下。如果是因為要對 EXE 或 DLL 的資源節(.rsrc)進行寫操作而導致的異常,_BasepCurrentTopLevelFilter 就改變出錯頁面正常的只讀屬性,以便允許進行寫操作。如果是這種特殊的情況,UnhandledExceptionFilter 返回 EXCEPTION_CONTINUE_EXECUTION,使系統重新執行出錯指令。
圖十三 UnHandledExceptionFilter 函數的偽代碼
UnhandledExceptionFilter( STRUCT _EXCEPTION_POINTERS *pExceptionPtrs ) { PEXCEPTION_RECORD pExcptRec; DWORD currentESP; DWORD retValue; DWORD DEBUGPORT; DWORD dwTemp2; DWORD dwUseJustInTimeDebugger; CHAR szDbgCmdFmt[256]; // 從AeDebug這個注冊表鍵值返回的字符串 CHAR szDbgCmdLine[256]; // 實際的調試器命令行參數(已填入進程ID和事件ID) STARTUPINFO startupinfo; PROCESS_INFORMATION pi; HARDERR_STRUCT harderr; // ??? BOOL fAeDebugAuto; TIB * pTib; // 線程信息塊 pExcptRec = pExceptionPtrs->ExceptionRecord; if ( (pExcptRec->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) && (pExcptRec->ExceptionInformation[0]) ) { retValue=BasepCheckForReadOnlyResource(pExcptRec->ExceptionInformation[1]); if ( EXCEPTION_CONTINUE_EXECUTION == retValue ) return EXCEPTION_CONTINUE_EXECUTION; } // 查看這個進程是否運行于調試器下 retValue = NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort, &debugPort, sizeof(debugPort), 0 ); if ( (retValue >= 0) && debugPort ) // 通知調試器 return EXCEPTION_CONTINUE_SEARCH; // 用戶調用SetUnhandledExceptionFilter了嗎? // 如果調用了,那現在就調用他安裝的異常處理程序 if ( _BasepCurrentTopLevelFilter ) { retValue = _BasepCurrentTopLevelFilter( pExceptionPtrs ); if ( EXCEPTION_EXECUTE_HANDLER == retValue ) return EXCEPTION_EXECUTE_HANDLER; if ( EXCEPTION_CONTINUE_EXECUTION == retValue ) return EXCEPTION_CONTINUE_EXECUTION; // 只有返回值為EXCEPTION_CONTINUE_SEARCH時才會繼續執行下去 } // 調用過SetErrorMode(SEM_NOGPFAULTERRORBOX)嗎? { harderr.elem0 = pExcptRec->ExceptionCode; harderr.elem1 = pExcptRec->ExceptionAddress; if ( EXCEPTION_IN_PAGE_ERROR == pExcptRec->ExceptionCode ) harderr.elem2 = pExcptRec->ExceptionInformation[2]; else harderr.elem2 = pExcptRec->ExceptionInformation[0]; dwTemp2 = 1; fAeDebugAuto = FALSE; harderr.elem3 = pExcptRec->ExceptionInformation[1]; pTib = FS:[18h]; DWORD someVal = pTib->pProcess->0xC; if ( pTib->threadID != someVal ) { __try { char szDbgCmdFmt[256]; retValue = GetProfileStringA( "AeDebug", "Debugger", 0, szDbgCmdFmt, sizeof(szDbgCmdFmt)-1 ); if ( retValue ) dwTemp2 = 2; char szAuto[8]; retValue = GetProfileStringA( "AeDebug", "Auto", "0", szAuto, sizeof(szAuto)-1 ); if ( retValue ) if ( 0 == strcmp( szAuto, "1" ) ) if ( 2 == dwTemp2 ) fAeDebugAuto = TRUE; } __except( EXCEPTION_EXECUTE_HANDLER ) { ESP = currentESP; dwTemp2 = 1; fAeDebugAuto = FALSE; } } if ( FALSE == fAeDebugAuto ) { retValue=NtRaiseHardError(STATUS_UNHANDLED_EXCEPTION | 0x10000000, 4, 0, &harderr,_BasepAlreadyHadHardError ? 1 : dwTemp2, &dwUseJustInTimeDebugger ); } else { dwUseJustInTimeDebugger = 3; retValue = 0; } if (retValue >= 0 && (dwUseJustInTimeDebugger == 3) && (!_BasepAlreadyHadHardError)&&(!_BaseRunningInServerProcess)) { _BasepAlreadyHadHardError = 1; SECURITY_ATTRIBUTES secAttr = { sizeof(secAttr), 0, TRUE }; HANDLE hEvent = CreateEventA( &secAttr, TRUE, 0, 0 ); memset( &startupinfo, 0, sizeof(startupinfo) ); sprintf(szDbgCmdLine, szDbgCmdFmt, GetCurrentProcessId(), hEvent); startupinfo.cb = sizeof(startupinfo); startupinfo.lpDesktop = "Winsta0/Default" CsrIdentifyAlertableThread(); // ??? retValue = CreateProcessA( 0, // 應用程序名稱 szDbgCmdLine, // 命令行 0, 0, // 進程和線程安全屬性 1, // bInheritHandles 0, 0, // 創建標志、環境 0, // 當前目錄 &statupinfo, // STARTUPINFO &pi); // PROCESS_INFORMATION if ( retValue && hEvent ) { NtWaitForSingleObject( hEvent, 1, 0 ); return EXCEPTION_CONTINUE_SEARCH; } } if ( _BasepAlreadyHadHardError ) NtTerminateProcess(GetCurrentProcess(), pExcptRec->ExceptionCode); } return EXCEPTION_EXECUTE_HANDLER; } LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter( LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter ) { // _BasepCurrentTopLevelFilter是KERNEL32.DLL中的一個全局變量 LPTOP_LEVEL_EXCEPTION_FILTER previous= _BasepCurrentTopLevelFilter; // 設置為新值 _BasepCurrentTopLevelFilter = lpTopLevelExceptionFilter; return previous; // 返回以前的值 }
UnhandledExceptionFilter接下來的任務是確定進程是否運行于Win32調試器下。也就是進程的創建標志中是否帶有標志DEBUG_PROCESS或DEBUG_ONLY_THIS_PROCESS。它使用NtQueryInformationProcess函數來確定進程是否正在被調試,我在本月的Under the Hood專欄中講解了這個函數。如果正在被調試,UnhandledExceptionFilter就返回 EXCEPTION_CONTINUE_SEARCH,這告訴系統去喚醒調試器并告訴它在被調試程序(debuggee)中產生了一個異常。
UnhandledExceptionFilter接下來調用用戶安裝的未處理異常過濾器(如果存在的話)。通常情況下,用戶并沒有安裝回調函數,但是用戶可以調用 SetUnhandledExceptionFilter這個API來安裝。上面我也提供了這個API的偽代碼。這個函數只是簡單地用用戶安裝的回調函數的地址來替換一個全局變量,并返回替換前的值。
有了初步的準備之后,UnhandledExceptionFilter就開始做它的主要工作:用一個時髦的應用程序錯誤對話框來通知你犯了低級的編程錯誤。有兩種方法可以避免出現這個對話框。第一種方法是調用SetErrorMode函數并指定SEM_NOGPFAULTERRORBOX標志。另一種方法是將AeDebug子鍵下的Auto的值設為1。此時UnhandledExceptionFilter跳過應用程序錯誤對話框直接啟動AeDebug 子鍵下的Debugger的值所指定的調試器。如果你熟悉“即時調試(Just In Time Debugging,JIT)”的話,這就是操作系統支持它的地方。接下來我會詳細講。
大多數情況下,上面的兩個條件都為假。這樣UnhandledExceptionFilter就調用NTDLL.DLL中的 NtRaiseHardError函數。正是這個函數產生了應用程序錯誤對話框。這個對話框等待你單擊“確定”按鈕來終止進程,或者單擊“取消”按鈕來調試它。(單擊“取消”按鈕而不是“確定”按鈕來加載調試器好像有點顛倒了,可能這只是我個人的感覺吧。)
如果你單擊“確定”,UnhandledExceptionFilter就返回EXCEPTION_EXECUTE_HANDLER。調用UnhandledExceptionFilter 的進程通常通過終止自身來作為響應(正像你在BaseProcessStart的偽代碼中看到的那樣)。這就產生了一個有趣的問題——大多數人都認為是系統終止了產生未處理異常的進程,而實際上更準確的說法應該是,系統進行了一些設置使得產生未處理異常的進程將自身終止掉了。
UnhandledExceptionFilter執行時真正有意思的部分是當你單擊應用程序錯誤對話框中的“取消”按鈕,此時系統將調試器附加(attach)到出錯進程上。這段代碼首先調用 CreateEvent來創建一個事件內核對象,調試器成功附加到出錯進程之后會將此事件對象變成有信號狀態。這個事件句柄以及出錯進程的ID都被傳到 sprintf函數,由它將其格式化成一個命令行,用來啟動調試器。一切就緒之后,UnhandledExceptionFilter就調用 CreateProcess來啟動調試器。如果CreateProcess成功,它就調用NtWaitForSingleObject來等待前面創建的那個事件對象。此時這個調用被阻塞,直到調試器進程將此事件變成有信號狀態,以表明它已經成功附加到出錯進程上。UnhandledExceptionFilter函數中還有一些其它的代碼,我在這里只講重要的。
進入地獄
如果你已經走了這么遠,不把整個過程講完對你有點不公平。我已經講了當異常發生時操作系統是如何調用用戶定義的回調函數的。我也講了這些回調的內部情況,以及編譯器是如何使用它們來實現__try和__except的。我甚至還講了當某個異常沒有被處理時所發生的情況以及系統所做的掃尾工作。剩下的就只有異常回調過程最初是從哪里開始的這個問題了。好吧,讓我們深入系統內部來看一下結構化異常處理的開始階段吧。
圖十四是我為 KiUserExceptionDispatcher 函數和一些相關函數寫的偽代碼。這個函數在NTDLL.DLL中,它是異常處理執行的起點。為了絕對準確起見,我必須指出:剛才說的并不是絕對準確。例如在Intel平臺上,一個異常導致CPU將控制權轉到ring 0(0特權級,即內核模式)的一個處理程序上。這個處理程序由中斷描述符表(Interrupt Descriptor Table,IDT)中的一個元素定義,它是專門用來處理相應異常的。我跳過所有的內核模式代碼,假設當異常發生時CPU直接將控制權轉到了 KiUserExceptionDispatcher 函數。
圖十四 KiUserExceptionDispatcher 的偽代碼:
KiUserExceptionDispatcher( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext ) { DWORD retValue; // 注意:如果異常被處理,那么 RtlDispatchException 函數就不會返回 if ( RtlDispatchException( pExceptRec, pContext ) ) retValue = NtContinue( pContext, 0 ); else retValue = NtRaiseException( pExceptRec, pContext, 0 ); EXCEPTION_RECORD excptRec2; excptRec2.ExceptionCode = retValue; excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE; excptRec2.ExceptionRecord = pExcptRec; excptRec2.NumberParameters = 0; RtlRaiseException( &excptRec2 ); } int RtlDispatchException( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext ) { DWORD stackUserBase; DWORD stackUserTop; PEXCEPTION_REGISTRATION pRegistrationFrame; DWORD hLog; // 從FS:[4]和FS:[8]處獲取堆棧的界限 RtlpGetStackLimits( &stackUserBase, &stackUserTop ); pRegistrationFrame = RtlpGetRegistrationHead(); while ( -1 != pRegistrationFrame ) { PVOID justPastRegistrationFrame = &pRegistrationFrame + 8; if ( stackUserBase > justPastRegistrationFrame ) { pExcptRec->ExceptionFlags |= EH_STACK_INVALID; return DISPOSITION_DISMISS; // 0 } if ( stackUsertop < justPastRegistrationFrame ) { pExcptRec->ExceptionFlags |= EH_STACK_INVALID; return DISPOSITION_DISMISS; // 0 } if ( pRegistrationFrame & 3 ) // 確保堆棧按DWORD對齊 { pExcptRec->ExceptionFlags |= EH_STACK_INVALID; return DISPOSITION_DISMISS; // 0 } if ( someProcessFlag ) { hLog = RtlpLogExceptionHandler( pExcptRec, pContext, 0, pRegistrationFrame, 0x10 ); } DWORD retValue, dispatcherContext; retValue= RtlpExecuteHandlerForException(pExcptRec, pRegistrationFrame, pContext, &dispatcherContext, pRegistrationFrame->handler ); if ( someProcessFlag ) RtlpLogLastExceptionDisposition( hLog, retValue ); if ( 0 == pRegistrationFrame ) { pExcptRec->ExceptionFlags &= ~EH_NESTED_CALL; // 關閉標志 } EXCEPTION_RECORD excptRec2; DWORD yetAnotherValue = 0; if ( DISPOSITION_DISMISS == retValue ) { if ( pExcptRec->ExceptionFlags & EH_NONCONTINUABLE ) { excptRec2.ExceptionRecord = pExcptRec; excptRec2.ExceptionNumber = STATUS_NONCONTINUABLE_EXCEPTION; excptRec2.ExceptionFlags = EH_NONCONTINUABLE; excptRec2.NumberParameters = 0; RtlRaiseException( &excptRec2 ); } else return DISPOSITION_CONTINUE_SEARCH; } else if ( DISPOSITION_CONTINUE_SEARCH == retValue ) {} else if ( DISPOSITION_NESTED_EXCEPTION == retValue ) { pExcptRec->ExceptionFlags |= EH_EXIT_UNWIND; if ( dispatcherContext > yetAnotherValue ) yetAnotherValue = dispatcherContext; } else // DISPOSITION_COLLIDED_UNWIND { excptRec2.ExceptionRecord = pExcptRec; excptRec2.ExceptionNumber = STATUS_INVALID_DISPOSITION; excptRec2.ExceptionFlags = EH_NONCONTINUABLE; excptRec2.NumberParameters = 0; RtlRaiseException( &excptRec2 ); } pRegistrationFrame = pRegistrationFrame->prev; // 轉到前一個幀 } return DISPOSITION_DISMISS; } _RtlpExecuteHandlerForException: // 處理異常(第一次) MOV EDX,XXXXXXXX JMP ExecuteHandler RtlpExecutehandlerForUnwind: // 處理展開(第二次) MOV EDX,XXXXXXXX int ExecuteHandler( PEXCEPTION_RECORD pExcptRec, PEXCEPTION_REGISTRATION pExcptReg, CONTEXT * pContext, PVOID pDispatcherContext, FARPROC handler ) // 實際上是指向_except_handler()的指針 { // 安裝一個EXCEPTION_REGISTRATION幀,EDX指向相應的handler代碼 PUSH EDX PUSH FS:[0] MOV FS:[0],ESP // 調用異常處理回調函數 EAX = handler( pExcptRec, pExcptReg, pContext, pDispatcherContext ); // 移除EXCEPTION_REGISTRATION幀 MOV ESP,DWORD PTR FS:[00000000] POP DWORD PTR FS:[00000000] return EAX; } _RtlpExecuteHandlerForException使用的異常處理程序: { // 如果設置了展開標志,返回DISPOSITION_CONTINUE_SEARCH // 否則,給pDispatcherContext賦值并返回DISPOSITION_NESTED_EXCEPTION return pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT ? DISPOSITION_CONTINUE_SEARC : ( *pDispatcherContext = pRegistrationFrame->scopetable, DISPOSITION_NESTED_EXCEPTION ); } _RtlpExecuteHandlerForUnwind使用的異常處理程序: { // 如果設置了展開標志,返回DISPOSITION_CONTINUE_SEARCH // 否則,給pDispatcherContext賦值并返回DISPOSITION_COLLIDED_UNWIND return pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT ? DISPOSITION_CONTINUE_SEARCH : ( *pDispatcherContext = pRegistrationFrame->scopetable, DISPOSITION_COLLIDED_UNWIND ); }
KiUserExceptionDispatcher 的核心是對 RtlDispatchException 的調用。這拉開了搜索已注冊的異常處理程序的序幕。如果某個處理程序處理這個異常并繼續執行,那么對 RtlDispatchException 的調用就不會返回。如果它返回了,只有兩種可能:或者調用了NtContinue以便讓進程繼續執行,或者產生了新的異常。如果是這樣,那異常就不能再繼續處理了,必須終止進程。
現在把目光對準 RtlDispatchException 函數的代碼,這就是我通篇提到的遍歷異常幀的代碼。這個函數獲取一個指向EXCEPTION_REGISTRATION 結構鏈表的指針,然后遍歷此鏈表以尋找一個異常處理程序。由于堆棧可能已經被破壞了,所以這個例程非常謹慎。在調用每個EXCEPTION_REGISTRATION結構中指定的異常處理程序之前,它確保這個結構是按DWORD對齊的,并且是在線程的堆棧之中,同時在堆棧中比前一個EXCEPTION_REGISTRATION結構高。
RtlDispatchException并不直接調用EXCEPTION_REGISTRATION結構中指定的異常處理程序。相反,它調用 RtlpExecuteHandlerForException來完成這個工作。根據RtlpExecuteHandlerForException的執行情況,RtlDispatchException或者繼續遍歷異常幀,或者引發另一個異常。這第二次的異常表明異常處理程序內部出現了錯誤,這樣就不能繼續執行下去了。
RtlpExecuteHandlerForException的代碼與RtlpExecuteHandlerForUnwind的代碼極其相似。你可能會回憶起來在前面討論展開時我提到過它。這兩個“函數”都只是簡單地給EDX寄存器加載一個不同的值然后就調用ExecuteHandler函數。也就是說,RtlpExecuteHandlerForException和RtlpExecuteHandlerForUnwind都是 ExecuteHanlder這個公共函數的前端。
ExecuteHandler查找EXCEPTION_REGISTRATION結構的handler域的值并調用它。令人奇怪的是,對異常處理回調函數的調用本身也被一個結構化異常處理程序封裝著。在SEH自身中使用SEH看起來有點奇怪,但你思索一會兒就會理解其中的含義。如果在異常回調過程中引發了另外一個異常,操作系統需要知道這個情況。根據異常發生在最初的回調階段還是展開回調階段,ExecuteHandler或者返回DISPOSITION_NESTED_EXCEPTION,或者返回DISPOSITION_COLLIDED_UNWIND。這兩者都是“紅色警報!現在把一切都關掉!”類型的代碼。
如果你像我一樣,那不僅理解所有與SEH有關的函數非常困難,而且記住它們之間的調用關系也非常困難。為了幫助我自己記憶,我畫了一個調用關系圖(圖十五)。
圖十五 在SEH中是誰調用了誰
KiUserExceptionDispatcher() RtlDispatchException() RtlpExecuteHandlerForException() ExecuteHandler() // 通常到 __except_handler3 __except_handler3() scopetable filter-expression() __global_unwind2() RtlUnwind() RtlpExecuteHandlerForUnwind() scopetable __except block()
現在要問:在調用ExecuteHandler之前設置EDX寄存器的值有什么用呢?這非常簡單。如果ExecuteHandler在調用用戶安裝的異常處理程序的過程中出現了什么錯誤,它就把EDX指向的代碼作為原始的異常處理程序。它把EDX寄存器的值壓入堆棧作為原始的 EXCEPTION_REGISTRATION結構的handler域。這基本上與我在MYSEH和MYSEH2中對原始的結構化異常處理的使用情況一樣。
結論
結構化異常處理是Win32一個非常好的特性。多虧有了像Visual C++之類的編譯器的支持層對它的封裝,一般的程序員才能付出比較小的學習代價就能利用SEH所提供的便利。但是在操作系統層面上,事情遠比Win32文檔說的復雜。
不幸的是,由于人人都認為系統層面的SEH是一個非常困難的問題,因此至今這方面的資料都不多。在本文中,我已經向你指出了系統層面的SEH就是圍繞著簡單的回調在打轉。如果你理解了回調的本質,在此基礎上分層理解,系統層面的結構化異常處理也不是那么難掌握。
附錄:關于 “prolog 和 epilog ”
在 Visual C++ 文檔中,微軟對 prolog 和 epilog 的解釋是:“保護現場和恢復現場” 此附錄摘自微軟 MSDN 庫,詳細信息參見:
http://msdn.microsoft.com/en-us/library/tawsa7cb(VS.80).aspx (英文)
http://msdn.microsoft.com/zh-cn/library/tawsa7cb(VS.80).aspx (中文)
每個分配堆棧空間、調用其他函數、保存非易失寄存器或使用異常處理的函數必須具有 Prolog,Prolog 的地址限制在與各自的函數表項關聯的展開數據中予以說明(請參見異常處理 (x64))。Prolog 將執行以下操作:必要時將參數寄存器保存在其內部地址中;將非易失寄存器推入堆棧;為局部變量和臨時變量分配堆棧的固定部分;(可選)建立幀指針。關聯的展開數據必須描述 Prolog 的操作,必須提供撤消 Prolog 代碼的影響所需的信息。
如果堆棧中的固定分配超過一頁(即大于 4096 字節),則該堆棧分配的范圍可能超過一個虛擬內存頁,因此在實際分配之前必須檢查分配情況。為此,提供了一個特殊的例程,該例程可從 Prolog 調用,并且不會損壞任何參數寄存器。
保存非易失寄存器的首選方法是:在進行固定堆棧分配之前將這些寄存器移入堆棧。如果在保存非易失寄存器之前執行了固定堆棧分配,則很可能需要 32 位位移以便對保存的寄存器區域進行尋址(據說寄存器的壓棧操作與移動操作一樣快,并且在可預見的未來一段時間內都應該是這樣,盡管壓棧操作之間存在隱含的相關性)。可按任何順序保存非易失寄存器。但是,在 Prolog 中第一次使用非易失寄存器時必須對其進行保存。
典型的 Prolog 代碼可以為:
mov [RSP + 8], RCX push R15 push R14 push R13 sub RSP, fixed-allocation-size lea R13, 128[RSP] ...
此 Prolog 執行以下操作:將參數寄存器 RCX 存儲在其標識位置;保存非易失寄存器 R13、R14、R15;分配堆棧幀的固定部分;建立幀指針,該指針將 128 字節地址指向固定分配區域。使用偏移量以后,便可以通過單字節偏移量對多個固定分配區域進行尋址。
如果固定分配大小大于或等于一頁內存,則在修改 RSP 之前必須調用 helper 函數。此 __chkstk helper 函數負責探測待分配的堆棧范圍,以確保對堆棧進行正確的擴展。在這種情況下,前面的 Prolog 示例應變為:
mov [RSP + 8], RCX push R15 push R14 push R13 mov RAX, fixed-allocation-size call __chkstk sub RSP, RAX lea R13, 128[RSP] ..
.除了 R10、R11 和條件代碼以外,此 __chkstk helper 函數不會修改任何寄存器。特別是,此函數將返回未更改的 RAX,并且不會修改所有非易失寄存器和參數傳遞寄存器。
Epilog 代碼位于函數的每個出口。通常只有一個 Prolog,但可以有多個 Epilog。Epilog 代碼執行以下操作:必要時將堆棧修整為其固定分配大小;釋放固定堆棧分配;從堆棧中彈出非易失寄存器的保存值以還原這些寄存器;返回。
對于展開代碼,Epilog 代碼必須遵守一組嚴格的規則,以便通過異常和中斷進行可靠的展開。這樣可以減少所需的展開數據量,因為描述每個 Epilog 不需要額外數據。通過向前掃描整個代碼流以標識 Epilog,展開代碼可以確定 Epilog 正在執行。
如果函數中沒有使用任何幀指針,則 Epilog 必須首先釋放堆棧的固定部分,彈出非易失寄存器,然后將控制返回調用函數。例如,
add RSP, fixed-allocation-size pop R13 pop R14 pop R15 ret
如果函數中使用了幀指針,則在執行 Epilog 之前必須將堆棧修整為其固定分配。這在技術上不屬于 Epilog。例如,下面的 Epilog 可用于撤消前面使用的 Prolog:
lea RSP, -128[R13] ; epilogue proper starts here add RSP, fixed-allocation-size pop R13 pop R14 pop R15 ret
在實際應用中,使用幀指針時,沒有必要分兩個步驟調整 RSP,因此應改用以下 Epilog:
lea RSP, fixed-allocation-size – 128[R13] pop R13 pop R14 pop R15 ret
以上是 Epilog 的唯一合法形式。它必須由 add RSP,constant 或 lea RSP,constant[FPReg] 組成,后跟一系列零或多個 8 字節寄存器 pop、一個 return 或一個 jmp。(Epilog 中只允許 jmp 語句的子集。僅限于具有 ModRM 內存引用的 jmp 類,其中 ModRM mod 字段值為 00。在 ModRM mod 字段值為 01 或 10 的 Epilog 中禁止使用 jmp。有關允許使用的 ModRM 引用的更多信息,請參見“AMD x86-64 Architecture Programmer’s Manual Volume 3: General Purpose and System Instructions”(AMD x86-64 結構程序員手冊第 3 卷:通用指令和系統指令)中的表 A-15。)不能出現其他代碼。特別是,不能在 Epilog 內進行調度,包括加載返回值。
請注意,未使用幀指針時,Epilog 必須使用 add RSP,constant 釋放堆棧的固定部分,而不能使用 lea RSP,constant[RSP]。由于此限制,在搜索 Epilog 時展開代碼具有較少的識別模式。
通過遵守這些規則,展開代碼便可以確定某個 Epilog 當前正在執行,并可以模擬該 Epilog 其余部分的執行,從而允許重新創建調用函數的上下文。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
