• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            盡管以前寫過一篇SEH相關的文章《關于SEH的簡單總結(jié)》, 但那真的只是皮毛,一直對Windows異常處理的原理似懂非懂, 看了下面的文章 ,一切都豁然開朗.
            1997年文章,Windows技術的根一直沒變: http://www.microsoft.com/msj/0197/exception/exception.aspx

            Matt Pietrek 著  
            董巖 譯

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

            下圖是在SoftICE中的結(jié)果: 

            譯者注)
            無 論正在干什么,現(xiàn)在讓我們回到手頭的問題上來。當異常發(fā)生時,操作系統(tǒng)是如何知道到哪里去調(diào)用回調(diào)函數(shù)的呢?實際 上,EXCEPTION_REGISTARTION結(jié)構(gòu)由兩個域組成,第一個你現(xiàn)在可以忽略。第二個域handler,包含一個指向 _except_handler回調(diào)函數(shù)的指針。這讓你離答案更近一點,但現(xiàn)在的問題是,操作系統(tǒng)到哪里去找 EXCEPTION_REGISTATRION結(jié)構(gòu)呢?
            要回答這個問題,記住結(jié)構(gòu)化異常處理是基于線程的這一點是非常有用的。也就是說,每個線程有它自己的異常處理回調(diào)函數(shù)。在1996年五月的Under The Hood專欄中,我介紹了一個關鍵的Win32數(shù)據(jù)結(jié)構(gòu)——線程信息塊(Thread Information/Environment Block,TIB或TEB)。這個結(jié)構(gòu)的某些域在Windows NT、Windows 95、Win32s和OS/2上是相同的。TIB的 第一個DWORD是一個指向線程的EXCEPTION_REGISTARTION結(jié)構(gòu)的指針。在基于Intel處理器的Win32平臺上,F(xiàn)S寄存器總是 指向當前的TIB。因此在FS:[0]處你可以找到一個指向EXCEPTION_REGISTARTION結(jié)構(gòu)的指針。
            到 現(xiàn)在為止,我們已經(jīng)有了足夠的認識。當異常發(fā)生時,系統(tǒng)查找出錯線程的TIB,獲取一個指向EXCEPTION_REGISTRATION結(jié)構(gòu)的指針。在 這個結(jié)構(gòu)中有一個指向_except_handler回調(diào)函數(shù)的指針。現(xiàn)在操作系統(tǒng)已經(jīng)知道了足夠的信息去調(diào)用_except_handler函數(shù),如圖 2所示。

            圖2 _except_handler函數(shù)
              把 這些小塊知識拼湊起來,我寫了一個小程序來演示上面這個對操作系統(tǒng)層面的結(jié)構(gòu)化異常處理的簡化描述,如圖3的MYSEH.CPP所示。它只有兩個函數(shù)。 main函數(shù)使用了三個內(nèi)聯(lián)匯編塊。第一個內(nèi)聯(lián)匯編塊通過兩個PUSH指令(“PUSH handler”和“PUSH FS:[0]”)在堆棧上創(chuàng)建了一個EXCEPTION_REGISTRATION結(jié)構(gòu)。PUSH FS:[0]這條指令保存了先前的FS:[0]中的值作為這個結(jié)構(gòu)的一部分,但這在此刻并不重要。重要的是現(xiàn)在堆棧上有一個8字節(jié)的 EXCEPTION_REGISTRATION結(jié)構(gòu)。緊接著的下一條指令(MOV FS:[0],ESP)使線程信息塊中的第一個DWORD指向了新的EXCEPTION_REGISTRATION結(jié)構(gòu)。(注意堆棧操作)
            圖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;
                // 指明是我們讓流程轉(zhuǎn)到我們的異常處理程序的
                printf( "Hello from an exception handler\n" );
                // 改變CONTEXT結(jié)構(gòu)中EAX的值,以便它指向可以成功進寫操作的位置
                ContextRecord->Eax = (DWORD)&scratch;
                // 告訴操作系統(tǒng)重新執(zhí)行出錯的指令
            return ExceptionContinueExecution;
            }
            int main()
            {
                DWORD handler = (DWORD)_except_handler;
                __asm
                { 
                    // 
            創(chuàng)建EXCEPTION_REGISTRATION結(jié)構(gòu):
                    push handler // handler函數(shù)的地址
                    push FS:[0] // 前一個handler函數(shù)的地址
                    mov FS:[0],ESP // 安裝新的EXECEPTION_REGISTRATION結(jié)構(gòu)
                }
                __asm
                {
                    mov eax,0     // 
            將EAX清零
                    mov [eax], 1 // 寫EAX指向的內(nèi)存從而故意引發(fā)一個錯誤
                }
                printf( "After writing!\n" );
                __asm
                { 
                    // 
            移去我們的EXECEPTION_REGISTRATION結(jié)構(gòu)
                    mov eax,[ESP]    // 獲取前一個結(jié)構(gòu)
                    mov FS:[0], EAX // 安裝前一個結(jié)構(gòu)
                    add esp, 8       // 將我們的EXECEPTION_REGISTRATION彈出堆棧
                }
                return 0;
            }
            如 果你想知道我為什么把EXCEPTION_REGISTRATION結(jié)構(gòu)創(chuàng)建在堆棧上而不是使用全局變量,我有一個很好的理由可以解釋它。實際上,當你使 用編譯器的__try/__except語法結(jié)構(gòu)時,編譯器自己也把EXCEPTION_REGISTRATION結(jié)構(gòu)創(chuàng)建在堆棧上。我只是簡單地向你展 示了如果使用__try/__except時編譯器做法的簡化版。
            回 到main函數(shù),第二個__asm塊通過先把EAX寄存器清零(MOV EAX,0)然后把此寄存器的值作為內(nèi)存地址讓下一條指令(MOV [EAX],1)向此地址寫入數(shù)據(jù)而故意引發(fā)一個錯誤。最后的__asm塊移除這個簡單的異常處理程序:它首先恢復了FS:[0]中先前的內(nèi)容,然后把 EXCEPTION_REGISTRATION結(jié)構(gòu)彈出堆棧(ADD ESP,8)。
            現(xiàn) 在假若你運行MYSEH.EXE,就會看到整個過程。當MOV [EAX],1這條指令執(zhí)行時,它引發(fā)一個訪問違規(guī)。系統(tǒng)在FS:[0]處的TIB中查找,然后發(fā)現(xiàn)了一個指向 EXCEPTION_REGISTRATION結(jié)構(gòu)的指針。在MYSEH.CPP中,在這個結(jié)構(gòu)中有一個指向_except_handler函數(shù)的指針。 系統(tǒng)然后把所需的四個參數(shù)(我在前面已經(jīng)說過)壓入堆棧,接著調(diào)用_except_handler函數(shù)。
            一 旦進入_except_handler,這段代碼首先通過一個printf語句表明“哈!是我讓它轉(zhuǎn)到這里的!”。接著,_except_handler 修復了引發(fā)錯誤的問題——即EAX寄存器指向了一個不能寫的內(nèi)存地址(地址0)。修復方法就是改變CONTEXT結(jié)構(gòu)中的EAX的值使它指向一個允許寫的 位置。在這個簡單的程序中,我專門為此設置了一個DWORD變量(scratch)。_except_handler函數(shù)最后的動作是返回 ExceptionContinueExecution這個值,它在EXCPT.H文件中定義。
            當 操作系統(tǒng)看到返回值為ExceptionContinueExecution時,它將其理解為你已經(jīng)修復了問題,而引起錯誤的那條指令應該被重新執(zhí)行。由 于我的_except_handler函數(shù)已經(jīng)讓EAX寄存器指向一個合法的內(nèi)存,MOV [EAX],1指令再次執(zhí)行,這次main函數(shù)一切正常。看,這也并不復雜,不是嗎?
            移向更深處
            有 了這個最簡單的情景之后,讓我們回去填補那些空白。雖然這個異常回調(diào)機制很好,但它并不是一個完美的解決方案。對于稍微復雜一些的應用程序來說,僅用一個 函數(shù)就能處理程序中任何地方都可能發(fā)生的異常是相當困難的。一個更實用的方案應該是有多個異常處理例程,每個例程針對程序中的一部分。實際上,操作系統(tǒng)提 供的正是這個功能。
            還 記得系統(tǒng)用來查找異常回調(diào)函數(shù)的EXCEPTION_REGISTRATION結(jié)構(gòu)嗎?這個結(jié)構(gòu)的第一個成員,稱為prev,前面我們暫時把它忽略了。它 實際上是一個指向另外一個EXCEPTION_REGISTRATION結(jié)構(gòu)的指針。這第二個EXCEPTION_REGISTRATION結(jié)構(gòu)可以有一 個完全不同的處理函數(shù)。它的prev域可以指向第三個EXCEPTION_REGISTRATION結(jié)構(gòu),依次類推。簡單地說,就是有一個EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表。線程信息塊的第一個DWORD(在基于Intel CPU的機器上是FS:[0])指向這個鏈表的頭部。
            操作系統(tǒng)要這個EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表做 什么呢?原來,當異常發(fā)生時,系統(tǒng)遍歷這個鏈表以查找一個(其異常處理程序)同意處理這個異常的EXCEPTION_REGISTRATION結(jié)構(gòu)。在 MYSEH.CPP中,異常處理程序通過返回ExceptionContinueExecution表示它同意處理這個異常。異常回調(diào)函數(shù)也可以拒絕處理 這個異常。在這種情況下,系統(tǒng)移向鏈表的下一個EXCEPTION_REGISTRATION結(jié)構(gòu)并詢問它的異常回調(diào)函數(shù),看它是否同意處理這個異常。圖 4顯示了這個過程。一旦系統(tǒng)找到一個處理這個異常的回調(diào)函數(shù),它就停止遍歷鏈表。

            圖4 查找一個處理異常的EXCEPTION_REGISTRATION結(jié)構(gòu)
            圖 5的MYSEH2.CPP就是一個異常處理函數(shù)不處理某個異常的例子。為了使代碼盡量簡單,我使用了編譯器層面的異常處理。main函數(shù)只設置了一個 __try/__except塊。在__try塊內(nèi)部調(diào)用了HomeGrownFrame函數(shù)。這個函數(shù)與前面的MYSEH程序非常相似。它也是在堆棧上 創(chuàng)建一個EXCEPTION_REGISTRATION結(jié)構(gòu),并且讓FS:[0]指向此結(jié)構(gòu)。在建立了新的異常處理程序之后,這個函數(shù)通過向一個NULL 指針所指向的內(nèi)存處寫入數(shù)據(jù)而故意引發(fā)一個錯誤:
            *(PDWORD)0 = 0;
            這個異常處理回調(diào)函 數(shù),同樣被稱為_except_handler,卻與前面的那個截然不同。它首先打印出ExceptionRecord結(jié)構(gòu)中的異常代碼和標志,這個結(jié)構(gòu) 的地址是作為一個指針參數(shù)被這個函數(shù)接收的。打印出異常標志的原因一會兒就清楚了。因為_except_handler函數(shù)并沒有打算修復出錯的代碼,因 此它返回ExceptionContinueSearch。這導致操作系統(tǒng)繼續(xù)在EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表中搜索下一個 EXCEPTION_REGISTRATION結(jié)構(gòu)。接下來安裝的異常回調(diào)函數(shù)是針對main函數(shù)中的__try/__except塊的。 __except塊簡單地打印出“Caught the exception in main()”。此時我們只是簡單地忽略這個異常來表明我們已經(jīng)處理了它。
            圖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" );
            // 我們不想處理這個異常,讓其它函數(shù)處理吧
            return ExceptionContinueSearch;
            }
            void HomeGrownFrame( void )
            {
            DWORD handler = (DWORD)_except_handler;
            __asm
            {
               // 
            創(chuàng)建EXCEPTION_REGISTRATION結(jié)構(gòu):
               push handler       // handler函數(shù)的地址
               push FS:[0]        // 前一個handler函數(shù)的地址
               mov FS:[0],ESP     // 安裝新的EXECEPTION_REGISTRATION結(jié)構(gòu)
            }
            *(PDWORD)0 = 0; // 寫入地址0,從而引發(fā)一個錯誤
            printf( "I should never get here!\n" );
            __asm
            {
               // 
            移去我們的EXECEPTION_REGISTRATION結(jié)構(gòu)
               mov eax,[ESP]     // 獲取前一個結(jié)構(gòu)
               mov FS:[0], EAX // 安裝前一個結(jié)構(gòu)
               add esp, 8        // 把我們EXECEPTION_REGISTRATION結(jié)構(gòu)彈出堆棧
            }
            }
            int main()
            {
            __try
            {
                HomeGrownFrame();
            }
            __except( EXCEPTION_EXECUTE_HANDLER )
            {
                printf( "Caught the exception in main()\n" );
            }
            return 0;
            }
            這里的關鍵是執(zhí)行流程。當一個異常處理程序拒絕處理某個異常時,它實際上也就拒絕決定流程最終將從何處恢復。只有處理某個異常的異常處理程序才能決定待所有異常處理代碼執(zhí)行完畢之后流程將從何處恢復。這個規(guī)則的意義非常重大,雖然現(xiàn)在還不明顯。
            當 使用結(jié)構(gòu)化異常處理時,如果一個函數(shù)有一個異常處理程序但它卻不處理某個異常,這個函數(shù)就有可能非正常退出。例如在MYSEH2中 HomeGrownFrame函數(shù)就不處理異常。由于在鏈表中后面的某個異常處理程序(這里是main函數(shù)中的)處理了這個異常,因此出錯指令后面的 printf就永遠不會執(zhí)行。從某種程度上說,使用結(jié)構(gòu)化異常處理與使用setjmp和longjmp運行時庫函數(shù)有些類似。
            如果你運行MYSEH2,會發(fā)現(xiàn)其輸出有些奇怪。看起來好像調(diào)用了兩次_except_handler函數(shù)。根據(jù)你現(xiàn)有的知識,第一次調(diào)用當然可以完全理解。但是為什么會有第二次呢?
            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”開頭的兩行,就會看出它們之間有明顯的區(qū)別。第一次異常標志是0,而第二次是2。這把我們帶入到了展開(Unwinding)的世界中。實際上,當一個異常處理回調(diào)函數(shù)拒絕處理某個異常時,它會被再一次調(diào)用。但是這次回調(diào)并不是立即發(fā)生的。這有點復雜。我需要把異常發(fā)生時的情形好好梳理一下。
            當 異常發(fā)生時,系統(tǒng)遍歷EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表,直到它找到一個處理這個異常的處理程序。一旦找到,系統(tǒng)就再次遍歷這個鏈 表,直到處理這個異常的結(jié)點為止。在這第二次遍歷中,系統(tǒng)將再次調(diào)用每個異常處理函數(shù)。關鍵的區(qū)別是,在第二次調(diào)用中,異常標志被設置為2。這個值被定義 為EH_UNWINDING。(EH_UNWINDING的定義在Visual C++ 運行時庫源代碼文件EXCEPT.INC中,但Win32 SDK中并沒有與之等價的定義。)
            EH_UNWINDING表 示什么意思呢?原來,當一個異常處理回調(diào)函數(shù)被第二次調(diào)用時(帶EH_UNWINDING標志),操作系統(tǒng)給這個函數(shù)一個最后清理的機會。什么樣的清理 呢?一個絕好的例子是C++類的析構(gòu)函數(shù)。當一個函數(shù)的異常處理程序拒絕處理某個異常時,通常執(zhí)行流程并不會正常地從那個函數(shù)退出。現(xiàn)在,想像一個定義了 一個C++類的實例作為局部變量的函數(shù)。C++規(guī)范規(guī)定析構(gòu)函數(shù)必須被調(diào)用。這帶EH_UNWINDING標志的第二次回調(diào)就給這個函數(shù)一個機會去做一些 類似于調(diào)用析構(gòu)函數(shù)和__finally塊之類的清理工作。
            在 異常已經(jīng)被處理完畢,并且所有前面的異常幀都已經(jīng)被展開之后,流程從處理異常的那個回調(diào)函數(shù)決定的地方開始繼續(xù)執(zhí)行。一定要記住,僅僅把指令指針設置到所 需的代碼處就開始執(zhí)行是不行的。流程恢復執(zhí)行處的代碼的堆棧指針和棧幀指針(在Intel CPU上是ESP和EBP)也必須被恢復成它們在處理這個異常的函數(shù)的棧幀上的值。因此,這個處理異常的回調(diào)函數(shù)必須負責把堆棧指針和棧幀指針恢復成它們 在包含處理這個異常的SEH代碼的函數(shù)的堆棧上的值。
            通 常,展開操作導致堆棧上處理異常的幀以下的堆棧區(qū)域上的所有內(nèi)容都被移除了,就好像我們從來沒有調(diào)用過這些函數(shù)一樣。展開的另外一個效果就是 EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表上處理異常的那個結(jié)構(gòu)之前的所有EXCEPTION_REGISTRATION結(jié)構(gòu)都被移除了。這 很好理解,因為這些EXCEPTION_REGISTRATION結(jié)構(gòu)通常都被創(chuàng)建在堆棧上。在異常被處理后,堆棧指針和棧幀指針在內(nèi)存中比那些從 EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表上移除的EXCEPTION_REGISTRATION結(jié)構(gòu)高。圖6顯示了我說的情況。

            圖6 從異常展開
            迄 今為止,我實際上一直在假設操作系統(tǒng)總是能在EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表中找到一個異常處理程序。如果找不到怎么辦呢?實際 上,這幾乎不可能發(fā)生。因為操作系統(tǒng)暗中已經(jīng)為每個線程都提供了一個默認的異常處理程序。這個默認的異常處理程序總是鏈表的最后一個結(jié)點,并且它總是選擇 處理異常。它進行的操作與其它正常的異常處理回調(diào)函數(shù)有些不同,下面我會說明。
            讓我們來看一下系統(tǒng)是在什么時候插入了這個默認的、最后一個異常處理程序。很明顯它需要在線程執(zhí)行的早期,在任何用戶代碼開始執(zhí)行之前。圖7是我為BaseProcessStart函數(shù)寫的偽代碼,它是Windows NT KERNEL32.DLL的一個內(nèi)部例程。這個函數(shù)帶一個參數(shù)——線程入口點函數(shù)的地址。BaseProcessStart運行在新進程的環(huán)境中,并且它調(diào)用這個進程的第一個線程的入口點函數(shù)。
            圖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的調(diào)用被一個__try和__except塊封裝著。就是這個__try塊安裝了默認的、異常處理程 序鏈表上的最后一個異常處理程序。所有后來注冊的異常處理程序都被安裝在鏈表中這個結(jié)點的前面。如果lpfnEntryPoint函數(shù)返回,那么表明線程 一直運行到完成并且沒有引發(fā)異常。這時BaseProcessStart調(diào)用ExitThread使線程退出。
            如果線程引發(fā)了一個異常但是沒有異常處理程序來處理它時怎么辦呢?這時,執(zhí)行流程轉(zhuǎn)到__except關鍵字后面的括號中。在BaseProcessStart中,這段代碼調(diào)用UnhandledExceptionFilter這個API,后面我會講到它。現(xiàn)在對于我們來說,重要的是UnhandledExceptionFilter這個API包含了默認的異常處理程序。
            如 果UnhandledExceptionFilter返回EXCEPTION_EXECUTE_HANDLER,這時BaseProcessStart中 的__except塊開始執(zhí)行。而__except塊所做的只是調(diào)用ExitProcess函數(shù)去終止當前進程。稍微想一下你就會理解了。常識告訴我們, 如果一個進程引發(fā)了一個錯誤而沒有異常處理程序去處理它,這個進程就會被系統(tǒng)終止。你在偽代碼中看到的正是這些。
            對于上面所說的我還有一點要補充。如果引發(fā)錯誤的線程是作為服務來運行的,并且是基于線程的服務,那么__except塊并不調(diào)用ExitProcess,相反,它調(diào)用ExitThread。不能僅僅因為一個服務出錯就終止整個服務進程。
            UnhandledExceptionFilter中的默認異常處理程序都做了什么呢?當我在一個技術講座上問起這個問題時,響應者寥寥無幾。幾乎沒有人知道當未處理異常發(fā)生時,到底操作系統(tǒng)的默認行為是什么。簡單地演示一下這個默認的行為也許會讓很多人豁然開朗。我運行一個故意引發(fā)錯誤的程序,其結(jié)果如下(見圖8)。

            圖8 未處理異常對話框
               表面上看,UnhandledExceptionFilter顯示了一個對話框告訴你發(fā)生了一個錯誤。這時,你被給予了一個機會或者終止出錯進程,或者調(diào)試它。但是幕后發(fā)生了許多事情,我會在文章最后詳細講述它。
            正如我讓你看到的那樣,當異常發(fā)生時,用戶寫的代碼可以(并且通常是這樣)獲得機會執(zhí)行。同樣,在展開操作期間,用戶寫的代碼也可以執(zhí)行。這個用戶寫的代碼可能也有錯誤,并且可能引發(fā)另一個異常。由于這個原因,異常處理回調(diào)函數(shù)也可以返回另外兩個值:ExceptionNestedExceptionExceptionCollidedUnwind。很明顯,它們很重要。但這是非常復雜的問題,我并不打算在這里涉及它們。要想理解其中的一些基本問題太困難了。
            編譯器層面的SEH
            雖 然我在前面偶爾也使用了__try和__except,但迄今為止幾乎我寫的所有內(nèi)容都是關于操作系統(tǒng)方面對SEH的實現(xiàn)。然而看一下我那兩個使用操作系 統(tǒng)的原始SEH的小程序別扭的樣子,編譯器對這個功能進行封裝實在是非常有必要的。現(xiàn)在讓我們來看一下Visual C++是如何在操作系統(tǒng)對SEH功能實現(xiàn)的基礎上來創(chuàng)建它自己的結(jié)構(gòu)化異常處理支持的。
            在 我們繼續(xù)下去之前,記住其它編譯器可以使用原始的系統(tǒng)SEH來做一些完全不同的事情這一點是非常重要的。并沒有什么規(guī)定編譯器必須實現(xiàn)Win32 SDK文檔中描述的__try/__except模型。例如Visual Basic 5.0在它的運行時代碼中使用了結(jié)構(gòu)化異常處理,但是那里的數(shù)據(jù)結(jié)構(gòu)和算法與我這里要講的完全不同。
            如果你把Win32 SDK文檔中關于結(jié)構(gòu)化異常處理方面的內(nèi)容從頭到尾讀一遍,一定會遇到下面所謂的“基于幀”的異常處理程序模型
            __try {
                // 
            這里是被保護的代碼
            }
            __except (過濾器表達式) { 
               // 這里是異常處理程序代碼
            }
                簡 單地說,在一個函數(shù)中,一個__try塊中的所有代碼就通過創(chuàng)建在這個函數(shù)的堆棧幀上的一個EXCEPTION_REGISTRATION結(jié)構(gòu)來保護。在 函數(shù)的入口處,這個新的EXCEPTION_REGISTRATION結(jié)構(gòu)被放在異常處理程序鏈表的頭部。在__try塊結(jié)束后,相應的 EXCEPTION_REGISTRATION結(jié)構(gòu)從這個鏈表的頭部被移除。正如我前面所說,異常處理程序鏈表的頭部被保存在FS:[0]處。因此,如果 你在調(diào)試器中單步跟蹤時看到類似下面的指令時
            MOV DWORD PTR FS:[00000000],ESP
            或者
                       MOV DWORD PTR FS:[00000000],ECX
            就能非常確定這段代碼正在進入或退出一個__try/__except塊。
            既然一個__try塊相當于堆棧上的一個EXCEPTION_REGISTRATION結(jié)構(gòu),那么EXCEPTION_REGISTRATION結(jié)構(gòu)中的回調(diào)函數(shù)相當于什么呢?使用Win32的術語來說,異常處理回調(diào)函數(shù)相當于過濾器表達式(filter-expression)代碼。實際上,過濾器表達式就是__except關鍵字后面的小括號中的代碼。就是這個過濾器表達式代碼決定了后面的大括號中的代碼是否執(zhí)行。
            由于過濾器表達式代碼是你自己寫的,你當然可以決定在你的代碼中的某個地方是否處理某個特定的異常。它可以簡單的只是一句“EXCEPTION_EXECUTE_HANDLER”,也可以先調(diào)用一個把p計算到20,000,000的函數(shù),然后再返回一個值來告訴操作系統(tǒng)下一步做什么。隨你的便。關鍵是你的過濾器表達式代碼必須是我前面講的有效的異常處理回調(diào)函數(shù)。
            我剛才講的雖然相當簡單,但那只不過是隔著有色玻璃看世界罷了。實際它是非常復雜的。首先,你的過濾器表達式代碼并不是被操作系統(tǒng)直接調(diào)用的。事實上,各個EXCEPTION_REGISTRATION結(jié)構(gòu)的handler域都指向了同一個函數(shù)。這個函數(shù)在Visual C++的運行時庫中,它被稱為__except_handler3。正是這個__except_handler3調(diào)用了你的過濾器表達式代碼,我一會兒再接著說它。
            對 我前面的簡單描述需要修正的另一個地方是,并不是每次進入或退出一個__try塊時就創(chuàng)建或撤銷一個EXCEPTION_REGISTRATION結(jié)構(gòu)。 相反,在使用SEH的任何函數(shù)中只創(chuàng)建一個EXCEPTION_REGISTRATION結(jié)構(gòu)。換句話說,你可以在一個函數(shù)中使用多個 __try/__except塊,但是在堆棧上只創(chuàng)建一個EXCEPTION_REGISTRATION結(jié)構(gòu)。同樣,你可以在一個函數(shù)中嵌套使用 __try塊,但Visual C++仍舊只是創(chuàng)建一個EXCEPTION_REGISTRATION結(jié)構(gòu)。
            如 果整個EXE或DLL只需要單個的異常處理程序(__except_handler3),同時,如果單個的EXCEPTION_REGISTRATION 結(jié)構(gòu)就能處理多個__try塊的話,很明顯,這里面還有很多東西我們不知道。這個技巧是通過一個通常情況下看不到的表中的數(shù)據(jù)來完成的。由于本文的目的就 是要深入探索結(jié)構(gòu)化異常處理,那就讓我們來看一看這些數(shù)據(jù)結(jié)構(gòu)吧。
            擴展的異常處理幀
                Visual C++的 SEH實現(xiàn)并沒有使用原始的EXCEPTION_REGISTRATION結(jié)構(gòu)。它在這個結(jié)構(gòu)的末尾添加了一些附加數(shù)據(jù)。這些附加數(shù)據(jù)正是允許單個函數(shù) (__except_handler3)處理所有異常并將執(zhí)行流程傳遞到相應的過濾器表達式和__except塊的關鍵。我在Visual C++運行時庫源代碼中的EXSUP.INC文件中找到了有關Visual C++擴展的EXCEPTION_REGISTRATION結(jié)構(gòu)格式的線索。在這個文件中,你會看到以下定義(已經(jīng)被注釋掉了):
            ;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;
            ;};
                 在 前面你已經(jīng)見過前兩個域:prev和handler。它們組成了基本的EXCEPTION_REGISTRATION結(jié)構(gòu)。后面三個 域:scopetable(作用域表)、trylevel和_ebp是新增加的。scopetable域指向一個scopetable_entry結(jié)構(gòu)數(shù)組,而trylevel域?qū)嶋H上是這個數(shù)組的索引。最后一個域_ebp,是EXCEPTION_REGISTRATION結(jié)構(gòu)創(chuàng)建之前棧幀指針(EBP)的值。
            _ebp域成為擴展的EXCEPTION_REGISTRATION結(jié)構(gòu)的一部分并非偶然。它是通過PUSH EBP這條指令被包含進這個結(jié)構(gòu)中的,而大多數(shù)函數(shù)開頭都是這條指令(通常編譯器并不為使用FPO優(yōu)化的函數(shù)生成標準的堆棧幀,這樣其第一條指令可能不是PUSH EBP。但是如果使用了SEH的話,那么無論你是否使用了FPO優(yōu)化,編譯器一定生成標準的堆棧幀)。 這條指令可以使EXCEPTION_REGISTRATION結(jié)構(gòu)中所有其它的域都可以用一個相對于棧幀指針(EBP)的負偏移來訪問。例如 trylevel域在[EBP-04]處,scopetable指針在[EBP-08]處,等等。(也就是說,這個結(jié)構(gòu)是從[EBP-10H]處開始 的。)
            緊跟著擴展的EXCEPTION_REGISTRATION結(jié)構(gòu)下面,Visual C++壓入了另外兩個值。緊跟著(即[EBP-14H]處)的一個DWORD,是為一個指向EXCEPTION_POINTERS結(jié)構(gòu)(一個標準的Win32 結(jié)構(gòu))的指針所保留的空間。這個指針就是你調(diào)用GetExceptionInformation這個API時返回的指針。盡管SDK文檔暗示GetExceptionInformation是一個標準的Win32 API,但事實上它是一個編譯器內(nèi)聯(lián)函數(shù)。當你調(diào)用這個函數(shù)時,Visual C++生成以下代碼:
            MOV EAX,DWORD PTR [EBP-14]
            GetExceptionInformation是一個編譯器內(nèi)聯(lián)函數(shù),與它相關的GetExceptionCode函 數(shù)也是如此。此函數(shù)實際上只是返回GetExceptionInformation返回的數(shù)據(jù)結(jié)構(gòu)(EXCEPTION_POINTERS)中的一個結(jié)構(gòu) (EXCEPTION_RECORD)中的一個域(ExceptionCode)的值。當Visual C++為GetExceptionCode函數(shù)生成下面的指令時,它到底是想干什么?我把這個問題留給讀者。(現(xiàn)在就能理解為什么SDK文檔提醒我們要注 意這兩個函數(shù)的使用范圍了。)
            MOV EAX,DWORD PTR [EBP-14] ; 執(zhí)行完畢,EAX指向EXCEPTION_POINTERS結(jié)構(gòu)
            MOV EAX,DWORD PTR [EAX]     ; 
            執(zhí)行完畢,EAX指向EXCEPTION_RECORD結(jié)構(gòu)
            MOV EAX,DWORD PTR [EAX]     ; 執(zhí)行完畢,EAX中是ExceptionCode的值
            現(xiàn)在回到擴展的EXCEPTION_REGISTRATION結(jié)構(gòu)上來。在這個結(jié)構(gòu)開始前的8個字節(jié)處(即[EBP-18H]處),Visual C++保留了一個DWORD來保存所有prolog代 碼執(zhí)行完畢之后的堆棧指針(ESP)的值(實際生成的指令為MOV DWORD PTR [EBP-18H],ESP)。這個DWORD中保存的值是函數(shù)執(zhí)行時ESP寄存器的正常值(除了在準備調(diào)用其它函數(shù)時把參數(shù)壓入堆棧這個過程會改變 ESP寄存器的值并在函數(shù)返回時恢復它的值外,函數(shù)在執(zhí)行過程中一般不改變ESP寄存器的值)。
            看起來好像我一下子給你灌輸了太多的信息,這點我承認。在繼續(xù)下去之前,讓我們先暫停,來回顧一下Visual C++為使用結(jié)構(gòu)化異常處理的函數(shù)生成的標準異常堆棧幀,它看起來像下面這個樣子:
            EBP-00 _ebp
            EBP-04 trylevel
            EBP-08 scopetable
            數(shù)組指針
            EBP-0C handler函數(shù)地址
            EBP-10指向前一個EXCEPTION_REGISTRATION結(jié)構(gòu)
            EBP-14 GetExceptionInformation
            EBP-18 
            棧幀中的標準ESP
            在操作系統(tǒng)看來,只存在組成原始EXCEPTION_REGISTRATION結(jié)構(gòu)的兩個域:即[EBP-10h]處的prev指針和[EBP-0Ch]處的handler函數(shù)指針。棧幀中的其它所有內(nèi)容是針對于Visual C++的。把這個Visual C++生成的標準異常堆棧幀記到腦子里之后,讓我們來看一下真正實現(xiàn)編譯器層面SEH的這個Visual C++運行時庫例程——__except_handler3。
            __except_handler3和scopetable
            我 真的很希望讓你看一看Visual C++運行時庫源代碼,讓你自己好好研究一下__except_handler3函數(shù),但是我辦不到。因為Microsoft并沒有提供。在這里你就將就 著看一下我為__except_handler3函數(shù)寫的偽代碼吧(如圖9所示)。
            圖9 __except_handler3函數(shù)的偽代碼
            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標志
            // 表明這是第一次調(diào)用這個處理程序(也就是說,并非處于異常展開階段)
            if ( ! (pExceptionRecord->ExceptionFlags
                    & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) )
            {
                // 
            在堆棧上創(chuàng)建一個EXCEPTION_POINTERS結(jié)構(gòu)
                exceptPtrs.ExceptionRecord = pExceptionRecord;
                exceptPtrs.ContextRecord = pContextRecord;
            // 把前面定義的EXCEPTION_POINTERS結(jié)構(gòu)的地址放在比
            // establisher棧幀低4個字節(jié)的位置上。參考前面我講
            // 的編譯器為GetExceptionInformation生成的匯編代碼*(PDWORD)((PBYTE)pRegistrationFrame - 4) = &exceptPtrs;
            // 獲取初始的“trylevel”值
            trylevel = pRegistrationFrame->trylevel;
            // 獲取指向scopetable數(shù)組的指針 
            scopeTable = pRegistrationFrame->scopetable;
            search_for_handler:
            if ( pRegistrationFrame->trylevel != TRYLEVEL_NONE )
            {
                if ( pRegistrationFrame->scopetable[trylevel].lpfnFilter )
                {
                    PUSH EBP // 
            保存這個棧幀指針
            // !!!非常重要!!!切換回原來的EBP。正是這個操作才使得
            // 棧幀上的所有局部變量能夠在異常發(fā)生后仍然保持它的值不變。
            EBP = &pRegistrationFrame->_ebp;
            // 調(diào)用過濾器函數(shù)
            filterFuncRet = scopetable[trylevel].lpfnFilter();
            POP EBP // 恢復異常處理程序的棧幀指針
            if ( filterFuncRet != EXCEPTION_CONTINUE_SEARCH )
            {
            if ( filterFuncRet < 0 ) // EXCEPTION_CONTINUE_EXECUTION
                   return 
            ExceptionContinueExecution;
            // 如果能夠執(zhí)行到這里,說明返回值為EXCEPTION_EXECUTE_HANDLER
            scopetable = pRegistrationFrame->scopetable;
            // 讓操作系統(tǒng)清理已經(jīng)注冊的棧幀,這會使本函數(shù)被遞歸調(diào)用
            __global_unwind2( pRegistrationFrame );
            // 一旦執(zhí)行到這里,除最后一個棧幀外,所有的棧幀已經(jīng)
            // 被清理完畢,流程要從最后一個棧幀繼續(xù)執(zhí)行
            EBP = &pRegistrationFrame->_ebp;
            __local_unwind2( pRegistrationFrame, trylevel );
            // NLG = "non-local-goto" (setjmp/longjmp stuff)
            __NLG_Notify( 1 ); // EAX = scopetable->lpfnHandler
            // 把當前的trylevel設置成當找到一個異常處理程序時
            // SCOPETABLE中當前正在被使用的那一個元素的內(nèi)容
            pRegistrationFrame->trylevel = scopetable->previousTryLevel;
            // 調(diào)用__except {}塊,這個調(diào)用并不會返回
            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; // 為調(diào)用__local_unwind2設置EBP
               __local_unwind2( pRegistrationFrame, TRYLEVEL_NONE )
               POP EBP // 
            恢復EBP
               return ExceptionContinueSearch;
            }
            }
            雖 然__except_handler3的代碼看起來很多,但是記住一點:它只是一個我在文章開頭講過的異常處理回調(diào)函數(shù)。它同MYSEH.EXE和 MYSEH2.EXE中的異常回調(diào)函數(shù)都帶有同樣的四個參數(shù)。__except_handler3大體上可以由第一個if語句分為兩部分。這是由于這個函 數(shù)可以在兩種情況下被調(diào)用,一次是正常調(diào)用,另一次是在展開階段。其中大部分是在非展開階段的回調(diào)。
            __except_handler3一 開始就在堆棧上創(chuàng)建了一個EXCEPTION_POINTERS結(jié)構(gòu),并用它的兩個參數(shù)來對這個結(jié)構(gòu)進行初始化。我在偽代碼中把這個結(jié)構(gòu)稱為 exceptPrts,它的地址被放在[EBP-14h]處。你回憶一下前面我講的編譯器為GetExceptionInformation和 GetExceptionCode函數(shù)生成的匯編代碼就會意識到,這實際上初始化了這兩個函數(shù)使用的指針。
            接 著,__except_handler3從EXCEPTION_REGISTRATION幀中獲取當前的trylevel(在[EBP-04h]處)。 trylevel變量實際是scopetable數(shù)組的索引,而正是這個數(shù)組才使得一個函數(shù)中的多個__try塊和嵌套的__try塊能夠僅使用一個 EXCEPTION_REGISTRATION結(jié)構(gòu)。每個scopetable元素結(jié)構(gòu)如下:
            typedef struct _SCOPETABLE
            {
               DWORD previousTryLevel;
               DWORD lpfnFilter;
               DWORD lpfnHandler;
            } SCOPETABLE, *PSCOPETABLE;
            SCOPETABLE結(jié)構(gòu)中的第二個成員和第三個成員比較容易理解。它們分別是過濾器表達式代碼的地址和相應的__except塊的地址。但是prviousTryLevel成員有點復雜。總之一句話,它用于嵌套的__try塊。這里的關鍵是函數(shù)中的每個__try塊都有一個相應的SCOPETABLE結(jié)構(gòu)

            正 如我前面所說,當前的trylevel指定了要使用的scopetable數(shù)組的哪一個元素,最終也就是指定了過濾器表達式和__except塊的地址。 現(xiàn)在想像一下兩個__try塊嵌套的情形。如果內(nèi)層__try塊的過濾器表達式不處理某個異常,那外層__try塊的過濾器表達式就必須處理它。那現(xiàn)在要 問,__except_handler3是如何知道SCOPETABLE數(shù)組的哪個元素相應于外層的__try塊的呢?答案是:外層__try塊的索引由 SCOPETABLE結(jié)構(gòu)的previousTryLevel域給出。利用這種機制,你可以嵌套任意層的__try塊。previousTryLevel 域就好像是一個函數(shù)中所有可能的異常處理程序構(gòu)成的線性鏈表中的結(jié)點一樣。如果trylevel的值為0xFFFFFFFF(實際上就是-1,這個值在 EXSUP.INC中被定義為TRYLEVEL_NONE),標志著這個鏈表結(jié)束。
            回到__except_handler3的代碼中。在獲取了當前的trylevel之后,它就調(diào)用相應的SCOPETABLE結(jié)構(gòu)中的過濾器表達式代碼。如果過濾器表達式返回EXCEPTION_CONTINUE_SEARCH,__exception_handler3 移向SCOPETABLE數(shù)組中的下一個元素,這個元素的索引由previousTryLevel域給出。如果遍歷完整個線性鏈表(還記得嗎?這個鏈表是 由于在一個函數(shù)內(nèi)部嵌套使用__try塊而形成的)都沒有找到處理這個異常的代碼,__except_handler3返回DISPOSITION_CONTINUE_SEARCH(原文如此,但根據(jù)_except_handler函數(shù)的定義,這個返回值應該為ExceptionContinueSearch。實際上這兩個常量的值是一樣的。我在偽代碼中已經(jīng)將其改正過來了),這導致系統(tǒng)移向下一個EXCEPTION_REGISTRATION幀(這個鏈表是由于函數(shù)嵌套調(diào)用而形成的)。
            如果過濾器表達式返回EXCEPTION_EXECUTE_HANDLER, 這意味著異常應該由相應的__except塊處理。它同時也意味著所有前面的EXCEPTION_REGISTRATION幀都應該從鏈表中移除,并且相 應的__except塊都應該被執(zhí)行。第一個任務通過調(diào)用__global_unwind2來完成的,后面我會講到這個函數(shù)。跳過這中間的一些清理代碼, 流程離開__except_handler3轉(zhuǎn)向__except塊。令人奇怪的是,流程并不從__except塊中返回,雖然是 __except_handler3使用CALL指令調(diào)用了它。
            當 前的trylevel值是如何被設置的呢?它實際上是由編譯器隱含處理的。編譯器非常機靈地修改這個擴展的EXCEPTION_REGISTRATION 結(jié)構(gòu)中的trylevel域的值(實際上是生成修改這個域的值的代碼)。如果你檢查編譯器為使用SEH的函數(shù)生成的匯編代碼,就會在不同的地方都看到修改 這個位于[EBP-04h]處的trylevel域的值的代碼。
            __except_handler3是 如何做到既通過CALL指令調(diào)用__except塊而又不讓執(zhí)行流程返回呢?由于CALL指令要向堆棧中壓入了一個返回地址,你可以想象這有可能破壞堆 棧。如果你檢查一下編譯器為__except塊生成的代碼,你會發(fā)現(xiàn)它做的第一件事就是將EXCEPTION_REGISTRATION結(jié)構(gòu)下面8個字節(jié) 處(即[EBP-18H]處)的一個DWORD值加載到ESP寄存器中(實際代碼為MOV ESP,DWORD PTR [EBP-18H]),這個值是在函數(shù)的prolog代碼中被保存在這個位置的(實際代碼為MOV DWORD PTR [EBP-18H],ESP)。
            ShowSEHFrames程序
            如 果你現(xiàn)在覺得已經(jīng)被EXCEPTION_REGISTRATION、scopetable、trylevel、過濾器表達式以及展開等等之類的詞搞得暈頭 轉(zhuǎn)向的話,那和我最初的感覺一樣。但是編譯器層面的結(jié)構(gòu)化異常處理方面的知識并不適合一點一點的學。除非你從整體上理解它,否則有很多內(nèi)容單獨看并沒有什 么意義。當面對大堆的理論時,我最自然的做法就是寫一些應用我學到的理論方面的程序。如果它能夠按照預料的那樣工作,我就知道我的理解(通常)是正確的。
            圖 10是ShowSEHFrame.EXE的源代碼。它使用__try/__except塊設置了好幾個Visual C++ SEH幀。然后它顯示每一個幀以及Visual C++為每個幀創(chuàng)建的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++,它使用的數(shù)據(jù)結(jié)構(gòu)是特定于Visual C++的
            //-------------------------------------------------------------------
            #ifndef _MSC_VER
            #error Visual C++ Required (Visual C++ specific information is displayed)
            #endif
            //-------------------------------------------------------------------
            // 
            結(jié)構(gòu)定義
            //-------------------------------------------------------------------
            // 操作系統(tǒng)定義的基本異常幀
            struct EXCEPTION_REGISTRATION

            {
                 EXCEPTION_REGISTRATION* prev;
                 FARPROC handler;
            };
            // Visual C++擴展異常幀指向的數(shù)據(jù)結(jié)構(gòu)
            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++運行時庫函數(shù),我們想打印出它的地址
            // 但是它的原型并沒有出現(xiàn)在任何頭文件中,所以我們需要自己聲明它。
            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函數(shù)的位置
            printf( "_except_handler3 is at address: %08X\n", _except_handler3 );
            printf( "\n" );
            // 從FS:[0]處獲取指向鏈表頭的指針
            __asm mov eax, FS:[0]
            __asm mov [pVCExcRec], EAX
            // 遍歷異常幀的鏈表。0xFFFFFFFF標志著鏈表的結(jié)尾
            while ( 0xFFFFFFFF != (unsigned)pVCExcRec )
            {
               ShowSEHFrame( pVCExcRec );
               pVCExcRec = (VC_EXCEPTION_REGISTRATION *)(pVCExcRec->prev);
            }
            }
            void Function1( void )
            {
               // 
            嵌套3層__try塊以便強制為scopetable數(shù)組產(chǎn)生3個元素
               __try
               {
                     __try
                     {
                         __try
                         {
                             WalkSEHFrames(); // 
            現(xiàn)在顯示所有的異常幀的信息
                         } __except( EXCEPTION_CONTINUE_SEARCH )
                          {}
                    } __except( EXCEPTION_CONTINUE_SEARCH )
                      {}
                } __except( EXCEPTION_CONTINUE_SEARCH )
                  {}
            }
            int main()
            {
            int i;
            // 使用兩個__try塊(并不嵌套),這導致為scopetable數(shù)組生成兩個元素
            __try
            {
                i = 0x1234;
            } __except( EXCEPTION_CONTINUE_SEARCH )
            {
                  i = 0x4321;
            }
            __try
            {
                Function1(); // 
            調(diào)用一個設置更多異常幀的函數(shù)
            } __except( EXCEPTION_EXECUTE_HANDLER )
            {
                   // 應該永遠不會執(zhí)行到這里,因為我們并沒有打算產(chǎn)生任何異常
                   printf( "Caught Exception in main\n" );
            }
            return 0;
            }
            ShowSEHFrames程 序中比較重要的函數(shù)是WalkSEHFrames和ShowSEHFrame。WalkSEHFrames函數(shù)首選打印出 __except_handler3的地址,打印它的原因很快就清楚了。接著,它從FS:[0]處獲取異常鏈表的頭指針,然后遍歷該鏈表。此鏈表中每個結(jié) 點都是一個VC_EXCEPTION_REGISTRATION類型的結(jié)構(gòu),它是我自己定義的,用于描述Visual C++的異常處理幀。對于這個鏈表中的每個結(jié)點,WalkSEHFrames都把指向這個結(jié)點的指針傳遞給ShowSEHFrame函數(shù)。
            ShowSEHFrame函 數(shù)一開始就打印出異常處理幀的地址、異常處理回調(diào)函數(shù)的地址、前一個異常處理幀的地址以及scopetable的地址。接著,對于每個 scopetable數(shù)組中的元素,它都打印出其priviousTryLevel、過濾器表達式的地址以及相應的__except塊的地址。我是如何知 道scopetable數(shù)組中有多少個元素的呢?其實我并不知道。但是我假定VC_EXCEPTION_REGISTRATION結(jié)構(gòu)中的當前trylevel域的值比scopetable數(shù)組中的元素總數(shù)少1。
            圖 11是ShowSEHFrames的運行結(jié)果。首先檢查以“Frame:”開頭的每一行,你會發(fā)現(xiàn)它們顯示的異常處理幀在堆棧上的地址呈遞增趨勢,并且在 前三個幀中,它們的異常處理程序的地址是一樣的(都是004012A8)。再看輸出的開始部分,你會發(fā)現(xiàn)這個004012A8不是別的,它正是 Visual C++運行時庫函數(shù)__except_handler3的地址。這證明了我前面所說的單個回調(diào)函數(shù)處理所有異常這一點。

            圖11 ShowSEHFrames運行結(jié)果
            你 可能想知道為什么明明ShowSEHFrames程序只有兩個函數(shù)使用SEH,但是卻有三個異常處理幀使用__except_handler3作為它們的 異常回調(diào)函數(shù)。實際上第三個幀來自Visual C++運行時庫。Visual C++運行時庫源代碼中的CRT0.C文件清楚地表明了對main或WinMain的調(diào)用也被一個__try/__except塊封裝著。這個__try 塊的過濾器表達式代碼可以在WINXFLTR.C文件中找到。
            回 到ShowSEHFrames程序,注意到最后一個幀的異常處理程序的地址是77F3AB6C,這與其它三個不同。仔細觀察一下,你會發(fā)現(xiàn)這個地址在 KERNEL32.DLL中。這個特別的幀就是由KERNEL32.DLL中的BaseProcessStart函數(shù)安裝的,這在前面我已經(jīng)說過。
            展開
            在 挖掘展開(Unwinding)的實現(xiàn)代碼之前讓我們先來搞清楚它的意思。我在前面已經(jīng)講過所有可能的異常處理程序是如何被組織在一個由線程信息塊的第一 個DWORD(FS:[0])所指向的鏈表中的。由于針對某個特定異常的處理程序可能不在這個鏈表的開頭,因此就需要從鏈表中依次移除實際處理異常的那個 異常處理程序之前的所有異常處理程序。
            正如你在Visual C++的__except_handler3函數(shù)中看到的那樣,展開是由__global_unwind2這個運行時庫(RTL)函數(shù)來完成的。這個函數(shù)只是對RtlUnwind這個未公開的API進行了非常簡單的封裝。(現(xiàn)在這個API已經(jīng)被公開了,但給出的信息極其簡單,詳細信息可以參考最新的Platform SDK文檔。)
            __global_unwind2(void * pRegistFrame)
            {
                _RtlUnwind( pRegistFrame, &__ret_label, 0, 0 );
                __ret_label:
            }
              雖然從技術上講RtlUnwind是一個KERNEL32函數(shù),但它只是轉(zhuǎn)發(fā)到了NTDLL.DLL中的同名函數(shù)上。圖12是我為此函數(shù)寫的偽代碼。
            圖12 RtlUnwind函數(shù)的偽代碼
            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結(jié)構(gòu)鏈表
            while ( -1 != pExcptRegHead )
            {
               EXCEPTION_RECORD excptRec2;
            if ( pExcptRegHead == pRegistrationFrame )
            {
               NtContinue( &context, 0 );
            }
            else
            {
               // 
            如果存在某個異常幀在堆棧上的位置比異常鏈表的頭部還低
               // 說明一定出現(xiàn)了錯誤
               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在堆棧范圍內(nèi),并且是4的倍數(shù)
            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 // 
            堆棧已經(jīng)被破壞!生成一個異常
            {
               excptRec2.ExceptionRecord = pExcptRec;
               excptRec2.NumberParameters = 0;
               excptRec2.ExceptionCode = STATUS_BAD_STACK;
               excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
               RtlRaiseException( &excptRec2 );
            }
            }
            // 如果執(zhí)行到這里,說明已經(jīng)到了EXCEPTION_REGISTRATION
                 // 結(jié)構(gòu)鏈表的末尾,正常情況下不應該發(fā)生這種情況。
                 //(因為正常情況下異常應該被處理,這樣就不會到鏈表末尾)
                 if ( -1 == pRegistrationFrame )
                     NtContinue( &context, 0 );
                 else
                     NtRaiseException( pExcptRec, &context, 0 );
            }
            RtlUnwind函數(shù)的偽代碼到這里就結(jié)束了,以下是它調(diào)用的幾個函數(shù)的偽代碼:
            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 = 此函數(shù)的調(diào)用者的調(diào)用者的返回地址    // 讀者看一下這個函數(shù)的
               pContext->Ebp = 此函數(shù)的調(diào)用者的調(diào)用者的EBP        // 匯編代碼就會清楚這一點
               pContext->Esp = pContext->Ebp + 8;
            }
            雖然RtlUnwind函數(shù)的規(guī)模看起來很大,但是如果你按一定方法把它分開,其實并不難理解。它首先從FS:[4]和FS:[8]處獲取當前線程堆棧的界限。它們對于后面要進行的合法性檢查非常重要,以確保所有將要被展開的異常幀都在堆棧范圍內(nèi)。
            RtlUnwind接 著在堆棧上創(chuàng)建了一個空的EXCEPTION_RECORD結(jié)構(gòu)并把STATUS_UNWIND賦給它的ExceptionCode域,同時把 EXCEPTION_UNWINDING標志賦給它的ExceptionFlags域。指向這個結(jié)構(gòu)的指針作為其中一個參數(shù)被傳遞給每個異常回調(diào)函數(shù)。然 后,這個函數(shù)調(diào)用RtlCaptureContext函數(shù)來創(chuàng)建一個空的CONTEXT結(jié)構(gòu),這個結(jié)構(gòu)也變成了在展開階段調(diào)用每個異常回調(diào)函數(shù)時傳遞給它 們的一個參數(shù)。
            RtlUnwind函 數(shù)的其余部分遍歷EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表。對于其中的每個幀,它都調(diào)用 RtlpExecuteHandlerForUnwind函數(shù),后面我會講到這個函數(shù)。正是這個函數(shù)帶EXCEPTION_UNWINDING標志調(diào)用了 異常處理回調(diào)函數(shù)。每次回調(diào)之后,它調(diào)用RtlpUnlinkHandler移除相應的異常幀。
            RtlUnwind函 數(shù)的第一個參數(shù)是一個幀的地址,當它遍歷到這個幀時就停止展開異常幀。上面所說的這些代碼之間還有一些安全性檢查代碼,它們用來確保不出問題。如果出現(xiàn)任 何問題,RtlUnwind就引發(fā)一個異常,指示出了什么問題,并且這個異常帶有EXCEPTION_NONCONTINUABLE標志。當一個進程被設 置了這個標志時,它就不允許再運行,必須終止。
            未處理異常
            在 文章的前面,我并沒有全面描述UnhandledExceptionFilter這個API。通常情況下你并不直接調(diào)用它(盡管你可以這么做)。大多數(shù)情 況下它都是由KERNEL32中進行默認異常處理的過濾器表達式代碼調(diào)用。前面BaseProcessStart函數(shù)的偽代碼已經(jīng)表明了這一點。
            圖 13是我為UnhandledExceptionFilter函數(shù)寫的偽代碼。這個API有點奇怪(至少在我看來是這樣)。如果異常的類型是 EXCEPTION_ACCESS_VIOLATION,它就調(diào)用_BasepCheckForReadOnlyResource。雖然我沒有提供這個函 數(shù)的偽代碼,但可以簡要描述一下。如果是因為要對EXE或DLL的資源節(jié)(.rsrc)進行寫操作而導致的異 常,_BasepCurrentTopLevelFilter就改變出錯頁面正常的只讀屬性,以便允許進行寫操作。如果是這種特殊的情 況,UnhandledExceptionFilter返回EXCEPTION_CONTINUE_EXECUTION,使系統(tǒng)重新執(zhí)行出錯指令。
            圖13 UnHandledExceptionFilter函數(shù)的偽代碼
            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]; // 實際的調(diào)試器命令行參數(shù)(已填入進程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;
            }
            // 查看這個進程是否運行于調(diào)試器下
            retValue = NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort,
                                                 &debugPort, sizeof(debugPort), 0 );
            if ( (retValue >= 0) && debugPort ) // 通知調(diào)試器
            return EXCEPTION_CONTINUE_SEARCH;
            // 用戶調(diào)用SetUnhandledExceptionFilter了嗎?
            // 如果調(diào)用了,那現(xiàn)在就調(diào)用他安裝的異常處理程序
            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時才會繼續(xù)執(zhí)行下去
            }
            // 調(diào)用過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,          // 
            創(chuàng)建標志、環(huán)境
            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調(diào)試器下。也就是進程的創(chuàng)建標志中是否帶有標志DEBUG_PROCESSDEBUG_ONLY_THIS_PROCESS。 它使用NtQueryInformationProcess函數(shù)來確定進程是否正在被調(diào)試,我在本月的Under the Hood專欄中講解了這個函數(shù)。如果正在被調(diào)試,UnhandledExceptionFilter就返回 EXCEPTION_CONTINUE_SEARCH,這告訴系統(tǒng)去喚醒調(diào)試器并告訴它在被調(diào)試程序(debuggee)中產(chǎn)生了一個異常。
            UnhandledExceptionFilter接 下來調(diào)用用戶安裝的未處理異常過濾器(如果存在的話)。通常情況下,用戶并沒有安裝回調(diào)函數(shù),但是用戶可以調(diào)用 SetUnhandledExceptionFilter這個API來安裝。上面我也提供了這個API的偽代碼。這個函數(shù)只是簡單地用用戶安裝的回調(diào)函數(shù) 的地址來替換一個全局變量,并返回替換前的值。
            有 了初步的準備之后,UnhandledExceptionFilter就開始做它的主要工作:用一個時髦的應用程序錯誤對話框來通知你犯了低級的編程錯 誤。有兩種方法可以避免出現(xiàn)這個對話框。第一種方法是調(diào)用SetErrorMode函數(shù)并指定SEM_NOGPFAULTERRORBOX標志。另一種方 法是將AeDebug子鍵下的Auto的值設為1。此時UnhandledExceptionFilter跳過應用程序錯誤對話框直接啟動AeDebug 子鍵下的Debugger的值所指定的調(diào)試器。如果你熟悉“即時調(diào)試(Just In Time Debugging,JIT)”的話,這就是操作系統(tǒng)支持它的地方。接下來我會詳細講。
            大 多數(shù)情況下,上面的兩個條件都為假。這樣UnhandledExceptionFilter就調(diào)用NTDLL.DLL中的 NtRaiseHardError函數(shù)。正是這個函數(shù)產(chǎn)生了應用程序錯誤對話框。這個對話框等待你單擊“確定”按鈕來終止進程,或者單擊“取消”按鈕來調(diào) 試它。(單擊“取消”按鈕而不是“確定”按鈕來加載調(diào)試器好像有點顛倒了,可能這只是我個人的感覺吧。)
            如果你單擊“確定”,UnhandledExceptionFilter就返回EXCEPTION_EXECUTE_HANDLER。調(diào)用UnhandledExceptionFilter 的進程通常通過終止自身來作為響應(正像你在BaseProcessStart的偽代碼中看到的那樣)。這就產(chǎn)生了一個有趣的問題——大多數(shù)人都認為是系 統(tǒng)終止了產(chǎn)生未處理異常的進程,而實際上更準確的說法應該是,系統(tǒng)進行了一些設置使得產(chǎn)生未處理異常的進程將自身終止掉了。
            UnhandledExceptionFilter執(zhí) 行時真正有意思的部分是當你單擊應用程序錯誤對話框中的“取消”按鈕,此時系統(tǒng)將調(diào)試器附加(attach)到出錯進程上。這段代碼首先調(diào)用 CreateEvent來創(chuàng)建一個事件內(nèi)核對象,調(diào)試器成功附加到出錯進程之后會將此事件對象變成有信號狀態(tài)。這個事件句柄以及出錯進程的ID都被傳到 sprintf函數(shù),由它將其格式化成一個命令行,用來啟動調(diào)試器。一切就緒之后,UnhandledExceptionFilter就調(diào)用 CreateProcess來啟動調(diào)試器。如果CreateProcess成功,它就調(diào)用NtWaitForSingleObject來等待前面創(chuàng)建的那 個事件對象。此時這個調(diào)用被阻塞,直到調(diào)試器進程將此事件變成有信號狀態(tài),以表明它已經(jīng)成功附加到出錯進程上。 UnhandledExceptionFilter函數(shù)中還有一些其它的代碼,我在這里只講重要的。
            進入地獄
            如 果你已經(jīng)走了這么遠,不把整個過程講完對你有點不公平。我已經(jīng)講了當異常發(fā)生時操作系統(tǒng)是如何調(diào)用用戶定義的回調(diào)函數(shù)的。我也講了這些回調(diào)的內(nèi)部情況,以 及編譯器是如何使用它們來實現(xiàn)__try和__except的。我甚至還講了當某個異常沒有被處理時所發(fā)生的情況以及系統(tǒng)所做的掃尾工作。剩下的就只有異 常回調(diào)過程最初是從哪里開始的這個問題了。好吧,讓我們深入系統(tǒng)內(nèi)部來看一下結(jié)構(gòu)化異常處理的開始階段吧。
            圖 14是我為KiUserExceptionDispatcher函數(shù)和一些相關函數(shù)寫的偽代碼。這個函數(shù)在NTDLL.DLL中,它是異常處理執(zhí)行的起 點。為了絕對準確起見,我必須指出:剛才說的并不是絕對準確。例如在Intel平臺上,一個異常導致CPU將控制權(quán)轉(zhuǎn)到ring 0(0特權(quán)級,即內(nèi)核模式)的一個處理程序上。這個處理程序由中斷描述符表(Interrupt Descriptor Table,IDT)中的一個元素定義,它是專門用來處理相應異常的。我跳過所有的內(nèi)核模式代碼,假設當異常發(fā)生時CPU直接將控制權(quán)轉(zhuǎn)到了 KiUserExceptionDispatcher函數(shù)。
            圖14 KiUserExceptionDispatcher的偽代碼
            KiUserExceptionDispatcher( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext )
            {
               DWORD retValue;
            // 注意:如果異常被處理,那么RtlDispatchException函數(shù)就不會返回
            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; // 轉(zhuǎn)到前一個幀
            }
            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
               // 調(diào)用異常處理回調(diào)函數(shù)
            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的調(diào)用。這拉開了搜索已注冊的異常處理程序的序幕。如果某個處理程序處理這個異常并繼續(xù)執(zhí)行,那么對 RtlDispatchException的調(diào)用就不會返回。如果它返回了,只有兩種可能:或者調(diào)用了NtContinue以便讓進程繼續(xù)執(zhí)行,或者產(chǎn)生 了新的異常。如果是這樣,那異常就不能再繼續(xù)處理了,必須終止進程。
            現(xiàn)在把目光對準RtlDispatchException函 數(shù)的代碼,這就是我通篇提到的遍歷異常幀的代碼。這個函數(shù)獲取一個指向EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表的指針,然后遍歷此鏈表以尋 找一個異常處理程序。由于堆棧可能已經(jīng)被破壞了,所以這個例程非常謹慎。在調(diào)用每個EXCEPTION_REGISTRATION結(jié)構(gòu)中指定的異常處理程 序之前,它確保這個結(jié)構(gòu)是按DWORD對齊的,并且是在線程的堆棧之中,同時在堆棧中比前一個EXCEPTION_REGISTRATION結(jié)構(gòu)高。
            RtlDispatchException并 不直接調(diào)用EXCEPTION_REGISTRATION結(jié)構(gòu)中指定的異常處理程序。相反,它調(diào)用 RtlpExecuteHandlerForException來完成這個工作。根據(jù)RtlpExecuteHandlerForException的執(zhí) 行情況,RtlDispatchException或者繼續(xù)遍歷異常幀,或者引發(fā)另一個異常。這第二次的異常表明異常處理程序內(nèi)部出現(xiàn)了錯誤,這樣就不能 繼續(xù)執(zhí)行下去了。
            RtlpExecuteHandlerForException的 代碼與RtlpExecuteHandlerForUnwind的代碼極其相似。你可能會回憶起來在前面討論展開時我提到過它。這兩個“函數(shù)”都只是簡單 地給EDX寄存器加載一個不同的值然后就調(diào)用ExecuteHandler函數(shù)。也就是 說,RtlpExecuteHandlerForException和RtlpExecuteHandlerForUnwind都是 ExecuteHanlder這個公共函數(shù)的前端。
            ExecuteHandler查 找EXCEPTION_REGISTRATION結(jié)構(gòu)的handler域的值并調(diào)用它。令人奇怪的是,對異常處理回調(diào)函數(shù)的調(diào)用本身也被一個結(jié)構(gòu)化異常處 理程序封裝著。在SEH自身中使用SEH看起來有點奇怪,但你思索一會兒就會理解其中的含義。如果在異常回調(diào)過程中引發(fā)了另外一個異常,操作系統(tǒng)需要知道 這個情況。根據(jù)異常發(fā)生在最初的回調(diào)階段還是展開回調(diào)階段,ExecuteHandler或者返回DISPOSITION_NESTED_EXCEPTION,或者返回DISPOSITION_COLLIDED_UNWIND。這兩者都是“紅色警報!現(xiàn)在把一切都關掉!”類型的代碼。
            如果你像我一樣,那不僅理解所有與SEH有關的函數(shù)非常困難,而且記住它們之間的調(diào)用關系也非常困難。為了幫助我自己記憶,我畫了一個調(diào)用關系圖(圖15)。
            現(xiàn) 在要問:在調(diào)用ExecuteHandler之前設置EDX寄存器的值有什么用呢?這非常簡單。如果ExecuteHandler在調(diào)用用戶安裝的異常處 理程序的過程中出現(xiàn)了什么錯誤,它就把EDX指向的代碼作為原始的異常處理程序。它把EDX寄存器的值壓入堆棧作為原始的 EXCEPTION_REGISTRATION結(jié)構(gòu)的handler域。這基本上與我在MYSEH和MYSEH2中對原始的結(jié)構(gòu)化異常處理的使用情況一 樣。

            圖15 在SEH中是誰調(diào)用了誰
            結(jié)論
            結(jié)構(gòu)化異常處理是Win32一個非常好的特性。多虧有了像Visual C++之類的編譯器的支持層對它的封裝,一般的程序員才能付出比較小的學習代價就能利用SEH所提供的便利。但是在操作系統(tǒng)層面上,事情遠比Win32文檔說的復雜。
            不幸的是,由于人人都認為系統(tǒng)層面的SEH是一個非常困難的問題,因此至今這方面的資料都不多。在本文中,我已經(jīng)向你指出了系統(tǒng)層面的SEH就是圍繞著簡單的回調(diào)在打轉(zhuǎn)。如果你理解了回調(diào)的本質(zhì),在此基礎上分層理解,系統(tǒng)層面的結(jié)構(gòu)化異常處理也不是那么難掌握。


            附錄:關于prolog和epilog
            美 國英語中的“prolog”實際上就是“prologue”。從這個詞的意思“序幕、序言”就能大致猜出它的作用。一個函數(shù)的prolog代碼主要是為這 個函數(shù)的執(zhí)行做一些準備工作,例如設置堆棧幀、設置局部變量所使用的堆棧空間以及保存相關的寄存器等。標準的prolog代碼開頭一般為以下三條指令:
            PUSH        EBP
            MOV     EBP, ESP
            SUB     ESP, XXX
            上 面的三條指令為使用EBP寄存器來訪問函數(shù)的參數(shù)(正偏移)和局部變量(負偏移)做好了準備。例如按照__stdcall調(diào)用約定,調(diào)用者 (caller)將被調(diào)函數(shù)(callee)的參數(shù)從右向左壓入堆棧,然后用CALL指令調(diào)用這個函數(shù)。CALL指令將返回地址壓入堆棧,然后流程就轉(zhuǎn)到 了被調(diào)函數(shù)的prolog代碼。此時[ESP]中是返回地址,[ESP+4]中是函數(shù)的第一個參數(shù)。本來可以就這樣使用ESP寄存器來訪問參數(shù),但由于 PUSH和POP指令會隱含修改ESP寄存器的值,這樣同一個參數(shù)在不同時刻可能需要通過不同的指令形式來訪問(例如,如果現(xiàn)在向堆棧中壓入一個值的話, 那訪問第一個參數(shù)就需要使用[ESP+8]了)。為了解決這個問題,所以使用EBP寄存器。EBP寄存器被稱為棧幀(frame)指針,它正是用于此目 的。當上述prolog指令中的前兩條指令執(zhí)行后,就可以使用EBP來訪問參數(shù)了,并且在整個函數(shù)中都不會改變此寄存器的值。在前面的例子中, [EBP+8]處就是第一個參數(shù)的值,[EBP+0Ch]處是第二個參數(shù)的值,依次類推。
            大多數(shù)C/C++編譯器都有“棧幀指針省略(Frame-Pointer Omission)”這 個選項(在Microsoft C/C++編譯器中為/Oy),它導致函數(shù)使用ESP來訪問參數(shù),從而可以空閑出一個寄存器(EBP)用于其它目的,并且由于不需要設置堆棧幀,從而會稍 微提高運行速度。但是在某些情況下必須使用堆棧幀。作者在前面也提到過,Microsoft已經(jīng)在其MSDN文檔中指明:結(jié)構(gòu)化異常處理是基于幀的異常處理。也就是說,它必須使用堆棧幀。當你查看編譯器為使用SEH的函數(shù)生成的匯編代碼時就會清楚這一點。無論你是否使用/Oy選項,它都設置堆棧幀。
            可 能有的讀者在調(diào)試應用程序時偶然進入到了系統(tǒng)DLL(例如NTDLL.DLL)中,但是卻意外地發(fā)現(xiàn)許多函數(shù)的prolog代碼的第一條指令并不是上面所 說的“PUSH EBP”,而是一條“垃圾”指令——“MOV EDI, EDI”(這條指令占兩個字節(jié))。Microsoft C/C++編譯器被稱為優(yōu)化編譯器,它怎么可能生成這么一條除了占用空間之外別無它用的指令呢?實際上,如果你比較細心的話,會發(fā)現(xiàn)以這條指令開頭的函數(shù) 的前面有5條NOP指令(它們一共占5個字節(jié)),如下圖所示。
            考 慮一下使用JMP指令進行近跳轉(zhuǎn)和遠跳轉(zhuǎn)分別需要幾個字節(jié)?他們正好分別是2個字節(jié)和5個字節(jié)!這難道是巧合?熟悉API攔截的讀者可能已經(jīng)猜到了,它們 是供攔截API時使用的。實際上,這是Microsoft對系統(tǒng)打“熱補丁”(Hot Patching)時攔截API用的。在打“熱補丁“時,修補程序在5條NOP指令處寫入一個遠跳轉(zhuǎn)指令,以跳轉(zhuǎn)到被修補過的代碼處。而“MOV EDI, EDI”處用一個近跳轉(zhuǎn)指令覆蓋,它跳轉(zhuǎn)到5個NOP指令所在的位置。使用“MOV EDI, EDI”而不是直接使用兩個NOP指令是出于性能考慮。
            第 三條指令用于為局部變量保留空間,其中的XXX就是需要保留的字節(jié)數(shù)。不使用局部變量的函數(shù)沒有這條指令。另外,如果局部變量比較少的話——例如2個,為 了性能考慮,編譯器往往會使用類似于兩條“PUSH ECX”這樣的指令來為局部變量保留空間。這三條指令后面一般還有幾條PUSH指令用于保存函數(shù)使用的寄存器(一般是EBX、ESI和EDI)。
            與prolog代碼相對的就是epilog代碼。與prolog類似,從它的意思“尾聲、結(jié)尾”也能猜出它的作用。它主要做一些清理工作。標準的epilog代碼如下:
            MOV     ESP, EBP
                POP     EBP
                RET     XXX
            這 三條指令前面可能還有幾條POP指令用于恢復在prolog代碼中保存的寄存器(如果存在的話)。有了前面的分析,epilog代碼不言自明。需要說明的 一點是,最后的RET指令用于返回調(diào)用者,并從堆棧中彈出無用信息,XXX指定了彈出的字節(jié)數(shù)。它一般用于將參數(shù)彈出堆棧。因此從這個值就可以知道函數(shù)的 參數(shù)個數(shù)(每個參數(shù)均為4字節(jié))。
            為 了簡化這種操作,Intel引入了ENTER和LEAVE指令。其中ENTER相當于前面所說的prolog代碼的前兩條指令,而LEAVE相當于上面的 epilog代碼的前兩條指令。但由于實現(xiàn)上ENTER指令比前面所說的兩條指令執(zhí)行速度慢,因此編譯器都不使用這條指令。這樣,你實際看到的情況就 是:prolog代碼就是前面所說的那樣,但epilog代碼使用了LEAVE指令。
            posted on 2015-02-27 21:40 Richard Wei 閱讀(16595) 評論(0)  編輯 收藏 引用 所屬分類: windows desktop
            波多野结衣中文字幕久久| 久久精品国产精品青草app| 成人免费网站久久久| 亚洲一区精品伊人久久伊人| 欧美喷潮久久久XXXXx| 国产AV影片久久久久久| 亚洲级αV无码毛片久久精品| 精品久久人人妻人人做精品| 久久午夜羞羞影院免费观看| 一级a性色生活片久久无少妇一级婬片免费放| 国产麻豆精品久久一二三| 久久伊人精品一区二区三区| 久久人搡人人玩人妻精品首页| 成人久久精品一区二区三区| 777午夜精品久久av蜜臀| 久久夜色撩人精品国产小说| 久久亚洲国产欧洲精品一| 久久精品国产亚洲AV高清热| 久久精品国产99久久久古代| 亚洲精品国精品久久99热| 国产亚洲成人久久| 久久99热国产这有精品| 99久久99久久精品免费看蜜桃| 亚洲成色WWW久久网站| 久久婷婷色综合一区二区| 亚洲а∨天堂久久精品9966| 精品久久人人做人人爽综合 | 国产国产成人精品久久| 亚洲国产欧洲综合997久久| 免费精品久久天干天干| 模特私拍国产精品久久| 国产精品久久久久免费a∨| 久久无码国产专区精品| 国产aⅴ激情无码久久| 亚洲精品白浆高清久久久久久| 亚洲午夜久久久久久噜噜噜| 亚洲精品乱码久久久久久久久久久久| 伊人久久大香线蕉av一区| 久久精品国产亚洲av水果派 | 久久AⅤ人妻少妇嫩草影院| 久久精品国产只有精品66|