[C++]深刻解析結構化異常處理(SEH)

http://www.cppblog.com/weiym/archive/2015/02/27/209884.htmlhtml

 

儘管之前寫過一篇SEH相關的文章《 關於SEH的簡單總結》, 但那真的只是皮毛,一直對Windows異常處理的原理似懂非懂, 看了下面的文章 ,一切都豁然開朗. 
1997年文章,Windows技術的根一直沒變:  http://www.microsoft.com/msj/0197/exception/exception.aspx

Matt Pietrek 著  
董巖 譯

在Win32操做系統提供的全部功能中,使用最普遍而又沒有公開的恐怕要數結構化異常處理(Structured Exception Handling,SEH) 了。當你考慮Win32結構化異常處理時,也許會想到__try、__finally和__except等術語。可能你在任何一本講解Win32的好書上 都能找到關於SEH較爲詳細的描述,甚至Win32 SDK文檔也對使用__try、__finally和__except進行結構化異常處理進行了至關完整的描述。
既 然已經有了這些文檔,那爲何我還說SEH並未公開呢?本質上來講,Win32結構化異常處理是操做系統提供的服務。你可能找到的全部關於SEH方面的文 檔都只是描述了某個特別的編譯器的運行時庫對操做系統實現的封裝。關鍵字__try、__finally或者__except並無什麼神奇的。 Microsoft的操做系統和編譯器開發小組定義了這些關鍵字和它們的做用。其它C++編譯器廠商徹底按照它們的語義來就能夠了。當編譯器的SEH支持 層把原始的操做系統SEH的複雜性封裝起來的時候,它同時也把原始的操做系統SEH的細節隱藏了起來。
我 曾經接到大量來自想本身實現編譯器層面SEH的人發來的電子郵件,他們苦於找不到關於操做系統SEH實現方面的任何文檔。按說,我應該可以告訴他們 Visual C++或Borland C++的運行時庫源代碼就是他們想要的。可是不知出於什麼緣由,編譯器層面的SEH看起來好像是個大祕密。不管是Microsoft仍是Borland都 沒有提供他們的SEH支持層最底層的源代碼。(如今Microsoft仍然沒有提供這些源代碼,它提供的是編譯過的目標文件,而Borland則提供了相 應的源代碼。)
在 本文中,我會剝掉結構化異常處理外面的包裝直至其最基本的概念。在此過程當中,我會把操做系統提供的支持與編譯器經過代碼生成和運行時庫提供的支持分開來 說。當我挖掘到關鍵的操做系統例程時,我使用的是運行於Intel處理器上的Windows NT 4.0。可是我這裏講的大部份內容一樣也適用於其它處理器。
我會避免涉及到真實的C++異常處理,它使用的是catch()而不是__except。從內部來說,真實的C++異常處理的實現與我這裏要講的很是類似。可是真實的C++異常處理有一些其它的複雜問題,它會混淆我這裏要講的一些概念。
在 挖掘組成Win32 SEH的晦澀的.H和.INC文件的過程當中,我發現最好的信息來源之一是IBM OS/2頭文件(特別是BSEXCPT.H)。若是你涉足這方面已經有一段時間了,就不會感到太奇怪。這裏描述的SEH機制是早在Microsoft還工 做在OS/2上時就已經定義好的。因爲這個緣由,你會發現Win32下的SEH和OS/2下的SEH極其類似。(如今咱們可能已經沒有機會體驗這一點 了,OS/2已經永遠成爲歷史了。)
 
淺析SEH
如 果我把SEH的全部細節一古腦兒全倒給你,你可能沒法接受。所以我先從一小部分開始,而後層層深刻。若是你之前從未接觸過結構化異常處理,那正好,由於你 頭腦中沒有一些本身設想的概念。若是你之前接觸過SEH,最好把頭腦中有關__try、GetExceptionCode和 EXCEPTION_EXECUTE_HANDLER之類的詞通通忘掉,假設它對你來講是全新的。深呼吸。準備好了嗎?讓咱們開始吧!
設想我告訴過你,當一個線程出現錯誤時,操做系統給你一個機會被告知這個錯誤。說得更明白一些就是, 當一個線程出現錯誤時,操做系統調用用戶定義的一個回調函數。這個回調函數能夠作它想作的一切。例如它能夠修復錯誤,或者它也能夠播放一段音樂。不管回調函數作什麼,它最後都要返回一個值來告訴系統下一步作什麼。(這不是十分準確,但就此刻來講很是接近。)
當你的某一部分代碼出錯時,系統再回調你的其它代碼,那麼這個回調函數看起來是什麼樣子呢?換句話說,你想知道關於異常什麼類型的信息呢?實際上這並不重要,由於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;
這個結構中的 ExcepitonCode成員是賦予異常的代碼。經過在WINNT.H中搜索以「STATUS_」開頭的#define定義,你能夠獲得一個異常代碼列 表。例如全部人都很是熟悉的STATUS_ACCESS_VIOLATION的代碼是0xC0000005。一個更全面的異常代碼列表能夠在 Windows NT DDK的NTSTATUS.H中找到。此結構的第四個成員是異常發生的地址。其它成員暫時能夠忽略。
_except_handler函數的第二個參數是一個指向establisher幀結構的指針。它是SEH中一個相當重要的參數,可是如今你能夠忽略它。
_except_handler回調函數的第三個參數是一個指向 CONTEXT結 構的指針。此結構在WINNT.H中定義,它表明某個特定線程的寄存器值。圖1顯示了CONTEXT結構的成員。當用於SEH時,CONTEXT結構表示 異常發生時寄存器的值。順便說一下,這個CONTEXT結構就是GetThreadContext和SetThreadContext這兩個API中使用 的那個CONTEXT結構。
圖1 CONTEXT結構
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;
_except_handler回調函數的第四個參數被稱爲DispatcherContext。它暫時也能夠被忽略。
到如今爲止,你頭腦中已經有了一個當異常發生時會被操做系統調用的回調函數的模型了。這個回調函數帶四個參數,其中三個指向其它結構。在這些結構中,一些域比較重要,其它的就不那麼重要。這裏的關鍵是 _exept_handler回調函數接收到操做系統傳遞過來的許多有價值的信息,例如異常的類型和發生的地址。使用這些信息,異常回調函數就能決定下一步作什麼。
對 我來講,如今就寫一個可以顯示_except_handler做用的樣例程序是再誘人不過的了。可是咱們還缺乏一些關鍵信息。特別是,當錯誤發生時操做系 統是怎麼知道到哪裏去調用這個回調函數的呢?答案是還有一個稱爲EXCEPTION_REGISTRATION的結構。通篇你都會看到這個結構,因此不要 跳過這一部分。我惟一能找到的 EXCEPTION_REGISTRATION結構的正式定義是在Visual C++運行時庫源代碼中的EXSUP.INC文件中:
_EXCEPTION_REGISTRATION struc
    prev                dd       ?
    handler             dd       ?
_EXCEPTION_REGISTRATION ends
這 個結構在WINNT.H的NT_TIB結構的定義中被稱爲_EXCEPITON_REGISTARTION_RECORD。唉,沒有一個地方可以找到 _EXCEPTION_REGISTRATION_RECORD的定義,因此我不得不使用EXSUP.INC中這個彙編語言的結構定義。這是我前面所說 SEH未公開的一個證據。(讀者可使用內核調試器,如KD或SoftICE並加載調試符號來查看這個結構的定義。
下圖是在KD中的結果:

下圖是在SoftICE中的結果:  

譯者注)
無 論正在幹什麼,如今讓咱們回到手頭的問題上來。當異常發生時,操做系統是如何知道到哪裏去調用回調函數的呢?實際 上,EXCEPTION_REGISTARTION結構由兩個域組成,第一個你如今能夠忽略。第二個域handler,包含一個指向 _except_handler回調函數的指針。這讓你離答案更近一點,但如今的問題是,操做系統到哪裏去找 EXCEPTION_REGISTATRION結構呢?
要回答這個問題,記住 結構化異常處理是基於線程的這一點是很是有用的。也就是說,每一個線程有它本身的異常處理回調函數。在1996年五月的Under The Hood專欄中,我介紹了一個關鍵的Win32數據結構—— 線程信息塊(Thread Information/Environment Block,TIB或TEB)。這個結構的某些域在Windows NT、Windows 9五、Win32s和OS/2上是相同的。 TIB的 第一個DWORD是一個指向線程的EXCEPTION_REGISTARTION結構的指針。在基於Intel處理器的Win32平臺上,FS寄存器老是 指向當前的TIB。所以在FS:[0]處你能夠找到一個指向EXCEPTION_REGISTARTION結構的指針。
到 如今爲止,咱們已經有了足夠的認識。當異常發生時,系統查找出錯線程的TIB,獲取一個指向EXCEPTION_REGISTRATION結構的指針。在 這個結構中有一個指向_except_handler回調函數的指針。如今操做系統已經知道了足夠的信息去調用_except_handler函數,如圖 2所示。

圖2 _except_handler函數
  把 這些小塊知識拼湊起來,我寫了一個小程序來演示上面這個對操做系統層面的結構化異常處理的簡化描述,如圖3的MYSEH.CPP所示。它只有兩個函數。 main函數使用了三個內聯彙編塊。第一個內聯彙編塊經過兩個PUSH指令(「PUSH handler」和「PUSH FS:[0]」)在堆棧上建立了一個EXCEPTION_REGISTRATION結構。PUSH FS:[0]這條指令保存了先前的FS:[0]中的值做爲這個結構的一部分,但這在此刻並不重要。重要的是如今堆棧上有一個8字節的 EXCEPTION_REGISTRATION結構。緊接着的下一條指令(MOV FS:[0],ESP)使線程信息塊中的第一個DWORD指向了新的EXCEPTION_REGISTRATION結構。(注意堆棧操做)
圖3 MYSEH.CPP
//==================================================
// MYSEH - Matt Pietrek 1997
// Microsoft Systems Journal, January 1997
// FILE: MYSEH.CPP
//  用命令行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;
    //  指明是咱們讓流程轉到咱們的異常處理程序的
    printf( "Hello from an exception handler\n" );
    //  改變CONTEXT結構中EAX的值,以便它指向能夠成功進寫操做的位置
    ContextRecord->Eax = (DWORD)&scratch;
    //  告訴操做系統從新執行出錯的指令
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;
}
如 果你想知道我爲何把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這條指令執行時,它引起一個訪問違規。系統在FS:[0]處的TIB中查找,而後發現了一個指向 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結構並詢問它的異常回調函數,看它是否贊成處理這個異常。圖 4顯示了這個過程。一旦系統找到一個處理這個異常的回調函數,它就中止遍歷鏈表。

圖4 查找一個處理異常的EXCEPTION_REGISTRATION結構
圖 5的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()」。此時咱們只是簡單地忽略這個異常來代表咱們已經處理了它。
圖5 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。這把咱們帶入到了 展開(Unwinding)的世界中。實際上,當一個異常處理回調函數拒絕處理某個異常時,它會被再一次調用。可是此次回調並非當即發生的。這有點複雜。我須要把異常發生時的情形好好梳理一下。
當 異常發生時,系統遍歷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結構高。圖6顯示了我說的狀況。

圖6 從異常展開
迄 今爲止,我實際上一直在假設操做系統老是能在EXCEPTION_REGISTRATION結構鏈表中找到一個異常處理程序。若是找不到怎麼辦呢?實際 上,這幾乎不可能發生。由於操做系統暗中已經爲每一個線程都提供了一個默認的異常處理程序。這個默認的異常處理程序老是鏈表的最後一個結點,而且它老是選擇 處理異常。它進行的操做與其它正常的異常處理回調函數有些不一樣,下面我會說明。
讓咱們來看一下系統是在何時插入了這個默認的、最後一個異常處理程序。很明顯它須要在線程執行的早期,在任何用戶代碼開始執行以前。圖7是我爲 BaseProcessStart函數寫的僞代碼,它是Windows NT KERNEL32.DLL的一個內部例程。這個函數帶一個參數——線程入口點函數的地址。BaseProcessStart運行在新進程的環境中,而且它調用這個進程的第一個線程的入口點函數。
圖7 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中的默認異常處理程序都作了什麼呢?當我在一個技術講座上問起這個問題時,響應者寥寥無幾。幾乎沒有人知道當未處理異常發生時,到底操做系統的默認行爲是什麼。簡單地演示一下這個默認的行爲也許會讓不少人豁然開朗。我運行一個故意引起錯誤的程序,其結果以下(見圖8)。

圖8 未處理異常對話框
   表面上看,UnhandledExceptionFilter顯示了一個對話框告訴你發生了一個錯誤。這時,你被給予了一個機會或者終止出錯進程,或者調試它。可是幕後發生了許多事情,我會在文章最後詳細講述它。
正如我讓你看到的那樣,當異常發生時,用戶寫的代碼能夠(而且一般是這樣)得到機會執行。一樣,在展開操做期間,用戶寫的代碼也能夠執行。這個用戶寫的代碼可能也有錯誤,而且可能引起另外一個異常。因爲這個緣由,異常處理回調函數也能夠返回另外兩個值: ExceptionNestedExceptionExceptionCollidedUnwind。很明顯,它們很重要。但這是很是複雜的問題,我並不打算在這裏涉及它們。要想理解其中的一些基本問題太困難了。
 
編譯器層面的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函數寫的僞代碼吧(如圖9所示)。
圖9 __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、過濾器表達式以及展開等等之類的詞搞得暈頭 轉向的話,那和我最初的感受同樣。可是編譯器層面的結構化異常處理方面的知識並不適合一點一點的學。除非你從總體上理解它,不然有不少內容單獨看並無什 麼意義。當面對大堆的理論時,我最天然的作法就是寫一些應用我學到的理論方面的程序。若是它可以按照預料的那樣工做,我就知道個人理解(一般)是正確的。
圖 10是ShowSEHFrame.EXE的源代碼。它使用__try/__except塊設置了好幾個Visual C++ SEH幀。而後它顯示每個幀以及Visual C++爲每一個幀建立的scopetable的相關信息。這個程序自己並不生成也不依賴任何異常。相反,我使用了多個__try塊以強制Visual C++生成多個EXCEPTION_REGISTRATION幀以及相應的scopetable。
圖10 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。
圖 11是ShowSEHFrames的運行結果。首先檢查以「Frame:」開頭的每一行,你會發現它們顯示的異常處理幀在堆棧上的地址呈遞增趨勢,而且在 前三個幀中,它們的異常處理程序的地址是同樣的(都是004012A8)。再看輸出的開始部分,你會發現這個004012A8不是別的,它正是 Visual C++運行時庫函數__except_handler3的地址。這證實了我前面所說的單個回調函數處理全部異常這一點。

圖11 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函數安裝的,這在前面我已經說過。
 
展開
在 挖掘展開(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中的同名函數上。圖12是我爲此函數寫的僞代碼。
圖12 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函數的僞代碼已經代表了這一點。
圖 13是我爲UnhandledExceptionFilter函數寫的僞代碼。這個API有點奇怪(至少在我看來是這樣)。若是異常的類型是 EXCEPTION_ACCESS_VIOLATION,它就調用_BasepCheckForReadOnlyResource。雖然我沒有提供這個函 數的僞代碼,但能夠簡要描述一下。若是是由於要對EXE或DLL的資源節(.rsrc)進行寫操做而致使的異 常,_BasepCurrentTopLevelFilter就改變出錯頁面正常的只讀屬性,以便容許進行寫操做。若是是這種特殊的情 況,UnhandledExceptionFilter返回EXCEPTION_CONTINUE_EXECUTION,使系統從新執行出錯指令。
圖13 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_PROCESSDEBUG_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的。我甚至還講了當某個異常沒有被處理時所發生的狀況以及系統所作的掃尾工做。剩下的就只有異 常回調過程最初是從哪裏開始的這個問題了。好吧,讓咱們深刻系統內部來看一下結構化異常處理的開始階段吧。
圖 14是我爲KiUserExceptionDispatcher函數和一些相關函數寫的僞代碼。這個函數在NTDLL.DLL中,它是異常處理執行的起 點。爲了絕對準確起見,我必須指出:剛纔說的並非絕對準確。例如在Intel平臺上,一個異常致使CPU將控制權轉到ring 0(0特權級,即內核模式)的一個處理程序上。這個處理程序由中斷描述符表(Interrupt Descriptor Table,IDT)中的一個元素定義,它是專門用來處理相應異常的。我跳過全部的內核模式代碼,假設當異常發生時CPU直接將控制權轉到了 KiUserExceptionDispatcher函數。
圖14 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有關的函數很是困難,並且記住它們之間的調用關係也很是困難。爲了幫助我本身記憶,我畫了一個調用關係圖(圖15)。
現 在要問:在調用ExecuteHandler以前設置EDX寄存器的值有什麼用呢?這很是簡單。若是ExecuteHandler在調用用戶安裝的異常處 理程序的過程當中出現了什麼錯誤,它就把EDX指向的代碼做爲原始的異常處理程序。它把EDX寄存器的值壓入堆棧做爲原始的 EXCEPTION_REGISTRATION結構的handler域。這基本上與我在MYSEH和MYSEH2中對原始的結構化異常處理的使用狀況一 樣。

圖15 在SEH中是誰調用了誰
 
結論
結構化異常處理是Win32一個很是好的特性。多虧有了像Visual C++之類的編譯器的支持層對它的封裝,通常的程序員才能付出比較小的學習代價就能利用SEH所提供的便利。可是在操做系統層面上,事情遠比Win32文檔說的複雜。
不幸的是,因爲人人都認爲系統層面的SEH是一個很是困難的問題,所以至今這方面的資料都很少。在本文中,我已經向你指出了系統層面的SEH就是圍繞着簡單的回調在打轉。若是你理解了回調的本質,在此基礎上分層理解,系統層面的結構化異常處理也不是那麼難掌握。
 

附錄:關於prolog和epilog
美 國英語中的「prolog」實際上就是「prologue」。從這個詞的意思「序幕、序言」就能大體猜出它的做用。一個函數的prolog代碼主要是爲這 個函數的執行作一些準備工做,例如設置堆棧幀、設置局部變量所使用的堆棧空間以及保存相關的寄存器等。標準的prolog代碼開頭通常爲如下三條指令:
PUSH        EBP
MOV     EBP, ESP
SUB     ESP, XXX
上 面的三條指令爲使用EBP寄存器來訪問函數的參數(正偏移)和局部變量(負偏移)作好了準備。例如按照__stdcall調用約定,調用者 (caller)將被調函數(callee)的參數從右向左壓入堆棧,而後用CALL指令調用這個函數。CALL指令將返回地址壓入堆棧,而後流程就轉到 了被調函數的prolog代碼。此時[ESP]中是返回地址,[ESP+4]中是函數的第一個參數。原本能夠就這樣使用ESP寄存器來訪問參數,但因爲 PUSH和POP指令會隱含修改ESP寄存器的值,這樣同一個參數在不一樣時刻可能須要經過不一樣的指令形式來訪問(例如,若是如今向堆棧中壓入一個值的話, 那訪問第一個參數就須要使用[ESP+8]了)。爲了解決這個問題,因此使用EBP寄存器。EBP寄存器被稱爲棧幀(frame)指針,它正是用於此目 的。當上述prolog指令中的前兩條指令執行後,就可使用EBP來訪問參數了,而且在整個函數中都不會改變此寄存器的值。在前面的例子中, [EBP+8]處就是第一個參數的值,[EBP+0Ch]處是第二個參數的值,依次類推。
大多數C/C++編譯器都有「棧幀指針省略(Frame-Pointer Omission)」這 個選項(在Microsoft C/C++編譯器中爲/Oy),它致使函數使用ESP來訪問參數,從而能夠空閒出一個寄存器(EBP)用於其它目的,而且因爲不須要設置堆棧幀,從而會稍 微提升運行速度。可是在某些狀況下必須使用堆棧幀。做者在前面也提到過,Microsoft已經在其MSDN文檔中指明:結構化異常處理是 基於幀的異常處理。也就是說,它必須使用堆棧幀。當你查看編譯器爲使用SEH的函數生成的彙編代碼時就會清楚這一點。不管你是否使用/Oy選項,它都設置堆棧幀。
可 能有的讀者在調試應用程序時偶然進入到了系統DLL(例如NTDLL.DLL)中,可是卻意外地發現許多函數的prolog代碼的第一條指令並非上面所 說的「PUSH EBP」,而是一條「垃圾」指令——「MOV EDI, EDI」(這條指令佔兩個字節)。Microsoft C/C++編譯器被稱爲優化編譯器,它怎麼可能生成這麼一條除了佔用空間以外別無它用的指令呢?實際上,若是你比較細心的話,會發現以這條指令開頭的函數 的前面有5條NOP指令(它們一共佔5個字節),以下圖所示。
考 慮一下使用JMP指令進行近跳轉和遠跳轉分別須要幾個字節?他們正好分別是2個字節和5個字節!這難道是巧合?熟悉API攔截的讀者可能已經猜到了,它們 是供攔截API時使用的。實際上,這是Microsoft對系統打「熱補丁」(Hot Patching)時攔截API用的。在打「熱補丁「時,修補程序在5條NOP指令處寫入一個遠跳轉指令,以跳轉到被修補過的代碼處。而「MOV EDI, EDI」處用一個近跳轉指令覆蓋,它跳轉到5個NOP指令所在的位置。使用「MOV EDI, EDI」而不是直接使用兩個NOP指令是出於性能考慮。
第 三條指令用於爲局部變量保留空間,其中的XXX就是須要保留的字節數。不使用局部變量的函數沒有這條指令。另外,若是局部變量比較少的話——例如2個,爲 了性能考慮,編譯器每每會使用相似於兩條「PUSH ECX」這樣的指令來爲局部變量保留空間。這三條指令後面通常還有幾條PUSH指令用於保存函數使用的寄存器(通常是EBX、ESI和EDI)。
與prolog代碼相對的就是epilog代碼。與prolog相似,從它的意思「尾聲、結尾」也能猜出它的做用。它主要作一些清理工做。標準的epilog代碼以下:
MOV     ESP, EBP
    POP     EBP
    RET     XXX
這 三條指令前面可能還有幾條POP指令用於恢復在prolog代碼中保存的寄存器(若是存在的話)。有了前面的分析,epilog代碼不言自明。須要說明的 一點是,最後的RET指令用於返回調用者,並從堆棧中彈出無用信息,XXX指定了彈出的字節數。它通常用於將參數彈出堆棧。所以從這個值就能夠知道函數的 參數個數(每一個參數均爲4字節)。
爲 了簡化這種操做,Intel引入了ENTER和LEAVE指令。其中ENTER至關於前面所說的prolog代碼的前兩條指令,而LEAVE至關於上面的 epilog代碼的前兩條指令。但因爲實現上ENTER指令比前面所說的兩條指令執行速度慢,所以編譯器都不使用這條指令。這樣,你實際看到的狀況就 是:prolog代碼就是前面所說的那樣,但epilog代碼使用了LEAVE指令。
相關文章
相關標籤/搜索