在 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運行結果
你可能想知道為什麼明明 ShowSEHFrames 程序只有兩個函數使用SEH,但是卻有三個異常處理幀使用__except_handler3作為它們的異常回調函數。實際上第三個幀來自 Visual C++ 運行時庫。Visual C++ 運行時庫源代碼中的 CRT0.C 文件清楚地表明了對 main 或 WinMain 的調用也被一個__try/__except 塊封裝著。這個__try 塊的過濾器表達式代碼可以在 WINXFLTR.C文 件中找到。
回到 ShowSEHFrames 程序,注意到最後一個幀的異常處理程序的地址是 77F3AB6C,這與其它三個不同。仔細觀察一下,你會發現這個地址在 KERNEL32.DLL 中。這個特別的幀就是由 KERNEL32.DLL 中的 BaseProcessStart 函數安裝的,這在前面我已經說過。