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

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

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

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

圖4 查找一個(gè)處理異常的EXCEPTION_REGISTRATION結(jié)構(gòu)
圖 5的MYSEH2.CPP就是一個(gè)異常處理函數(shù)不處理某個(gè)異常的例子。為了使代碼盡量簡(jiǎn)單,我使用了編譯器層面的異常處理。main函數(shù)只設(shè)置了一個(gè) __try/__except塊。在__try塊內(nèi)部調(diào)用了HomeGrownFrame函數(shù)。這個(gè)函數(shù)與前面的MYSEH程序非常相似。它也是在堆棧上 創(chuàng)建一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu),并且讓FS:[0]指向此結(jié)構(gòu)。在建立了新的異常處理程序之后,這個(gè)函數(shù)通過(guò)向一個(gè)NULL 指針?biāo)赶虻膬?nèi)存處寫(xiě)入數(shù)據(jù)而故意引發(fā)一個(gè)錯(cuò)誤:
*(PDWORD)0 = 0;
這個(gè)異常處理回調(diào)函 數(shù),同樣被稱(chēng)為_(kāi)except_handler,卻與前面的那個(gè)截然不同。它首先打印出ExceptionRecord結(jié)構(gòu)中的異常代碼和標(biāo)志,這個(gè)結(jié)構(gòu) 的地址是作為一個(gè)指針參數(shù)被這個(gè)函數(shù)接收的。打印出異常標(biāo)志的原因一會(huì)兒就清楚了。因?yàn)開(kāi)except_handler函數(shù)并沒(méi)有打算修復(fù)出錯(cuò)的代碼,因 此它返回ExceptionContinueSearch。這導(dǎo)致操作系統(tǒng)繼續(xù)在EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表中搜索下一個(gè) EXCEPTION_REGISTRATION結(jié)構(gòu)。接下來(lái)安裝的異常回調(diào)函數(shù)是針對(duì)main函數(shù)中的__try/__except塊的。 __except塊簡(jiǎn)單地打印出“Caught the exception in main()”。此時(shí)我們只是簡(jiǎn)單地忽略這個(gè)異常來(lái)表明我們已經(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 ) // 注意這個(gè)標(biāo)志
printf( " EH_STACK_INVALID" );
if ( ExceptionRecord->ExceptionFlags & 0x10 ) // 注意這個(gè)標(biāo)志
printf( " EH_NESTED_CALL" );
printf( "\n" );
// 我們不想處理這個(gè)異常,讓其它函數(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] // 前一個(gè)handler函數(shù)的地址
mov FS:[0],ESP // 安裝新的EXECEPTION_REGISTRATION結(jié)構(gòu)
}
*(PDWORD)0 = 0; // 寫(xiě)入地址0,從而引發(fā)一個(gè)錯(cuò)誤
printf( "I should never get here!\n" );
__asm
{
// 移去我們的EXECEPTION_REGISTRATION結(jié)構(gòu)
mov eax,[ESP] // 獲取前一個(gè)結(jié)構(gòu)
mov FS:[0], EAX // 安裝前一個(gè)結(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;
}
這里的關(guān)鍵是執(zhí)行流程。當(dāng)一個(gè)異常處理程序拒絕處理某個(gè)異常時(shí),它實(shí)際上也就拒絕決定流程最終將從何處恢復(fù)。只有處理某個(gè)異常的異常處理程序才能決定待所有異常處理代碼執(zhí)行完畢之后流程將從何處恢復(fù)。這個(gè)規(guī)則的意義非常重大,雖然現(xiàn)在還不明顯。
當(dāng) 使用結(jié)構(gòu)化異常處理時(shí),如果一個(gè)函數(shù)有一個(gè)異常處理程序但它卻不處理某個(gè)異常,這個(gè)函數(shù)就有可能非正常退出。例如在MYSEH2中 HomeGrownFrame函數(shù)就不處理異常。由于在鏈表中后面的某個(gè)異常處理程序(這里是main函數(shù)中的)處理了這個(gè)異常,因此出錯(cuò)指令后面的 printf就永遠(yuǎn)不會(huì)執(zhí)行。從某種程度上說(shuō),使用結(jié)構(gòu)化異常處理與使用setjmp和longjmp運(yùn)行時(shí)庫(kù)函數(shù)有些類(lèi)似。
如果你運(yùn)行MYSEH2,會(huì)發(fā)現(xiàn)其輸出有些奇怪。看起來(lái)好像調(diào)用了兩次_except_handler函數(shù)。根據(jù)你現(xiàn)有的知識(shí),第一次調(diào)用當(dāng)然可以完全理解。但是為什么會(huì)有第二次呢?
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”開(kāi)頭的兩行,就會(huì)看出它們之間有明顯的區(qū)別。第一次異常標(biāo)志是0,而第二次是2。這把我們帶入到了展開(kāi)(Unwinding)的世界中。實(shí)際上,當(dāng)一個(gè)異常處理回調(diào)函數(shù)拒絕處理某個(gè)異常時(shí),它會(huì)被再一次調(diào)用。但是這次回調(diào)并不是立即發(fā)生的。這有點(diǎn)復(fù)雜。我需要把異常發(fā)生時(shí)的情形好好梳理一下。
當(dāng) 異常發(fā)生時(shí),系統(tǒng)遍歷EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表,直到它找到一個(gè)處理這個(gè)異常的處理程序。一旦找到,系統(tǒng)就再次遍歷這個(gè)鏈 表,直到處理這個(gè)異常的結(jié)點(diǎn)為止。在這第二次遍歷中,系統(tǒng)將再次調(diào)用每個(gè)異常處理函數(shù)。關(guān)鍵的區(qū)別是,在第二次調(diào)用中,異常標(biāo)志被設(shè)置為2。這個(gè)值被定義 為EH_UNWINDING。(EH_UNWINDING的定義在Visual C++ 運(yùn)行時(shí)庫(kù)源代碼文件EXCEPT.INC中,但Win32 SDK中并沒(méi)有與之等價(jià)的定義。)
EH_UNWINDING表 示什么意思呢?原來(lái),當(dāng)一個(gè)異常處理回調(diào)函數(shù)被第二次調(diào)用時(shí)(帶EH_UNWINDING標(biāo)志),操作系統(tǒng)給這個(gè)函數(shù)一個(gè)最后清理的機(jī)會(huì)。什么樣的清理 呢?一個(gè)絕好的例子是C++類(lèi)的析構(gòu)函數(shù)。當(dāng)一個(gè)函數(shù)的異常處理程序拒絕處理某個(gè)異常時(shí),通常執(zhí)行流程并不會(huì)正常地從那個(gè)函數(shù)退出。現(xiàn)在,想像一個(gè)定義了 一個(gè)C++類(lèi)的實(shí)例作為局部變量的函數(shù)。C++規(guī)范規(guī)定析構(gòu)函數(shù)必須被調(diào)用。這帶EH_UNWINDING標(biāo)志的第二次回調(diào)就給這個(gè)函數(shù)一個(gè)機(jī)會(huì)去做一些 類(lèi)似于調(diào)用析構(gòu)函數(shù)和__finally塊之類(lèi)的清理工作。
在 異常已經(jīng)被處理完畢,并且所有前面的異常幀都已經(jīng)被展開(kāi)之后,流程從處理異常的那個(gè)回調(diào)函數(shù)決定的地方開(kāi)始繼續(xù)執(zhí)行。一定要記住,僅僅把指令指針設(shè)置到所 需的代碼處就開(kāi)始執(zhí)行是不行的。流程恢復(fù)執(zhí)行處的代碼的堆棧指針和棧幀指針(在Intel CPU上是ESP和EBP)也必須被恢復(fù)成它們?cè)谔幚磉@個(gè)異常的函數(shù)的棧幀上的值。因此,這個(gè)處理異常的回調(diào)函數(shù)必須負(fù)責(zé)把堆棧指針和棧幀指針恢復(fù)成它們 在包含處理這個(gè)異常的SEH代碼的函數(shù)的堆棧上的值。
通 常,展開(kāi)操作導(dǎo)致堆棧上處理異常的幀以下的堆棧區(qū)域上的所有內(nèi)容都被移除了,就好像我們從來(lái)沒(méi)有調(diào)用過(guò)這些函數(shù)一樣。展開(kāi)的另外一個(gè)效果就是 EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表上處理異常的那個(gè)結(jié)構(gòu)之前的所有EXCEPTION_REGISTRATION結(jié)構(gòu)都被移除了。這 很好理解,因?yàn)檫@些EXCEPTION_REGISTRATION結(jié)構(gòu)通常都被創(chuàng)建在堆棧上。在異常被處理后,堆棧指針和棧幀指針在內(nèi)存中比那些從 EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表上移除的EXCEPTION_REGISTRATION結(jié)構(gòu)高。圖6顯示了我說(shuō)的情況。
圖6 從異常展開(kāi)
迄 今為止,我實(shí)際上一直在假設(shè)操作系統(tǒng)總是能在EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表中找到一個(gè)異常處理程序。如果找不到怎么辦呢?實(shí)際 上,這幾乎不可能發(fā)生。因?yàn)椴僮飨到y(tǒng)暗中已經(jīng)為每個(gè)線(xiàn)程都提供了一個(gè)默認(rèn)的異常處理程序。這個(gè)默認(rèn)的異常處理程序總是鏈表的最后一個(gè)結(jié)點(diǎn),并且它總是選擇 處理異常。它進(jìn)行的操作與其它正常的異常處理回調(diào)函數(shù)有些不同,下面我會(huì)說(shuō)明。
讓我們來(lái)看一下系統(tǒng)是在什么時(shí)候插入了這個(gè)默認(rèn)的、最后一個(gè)異常處理程序。很明顯它需要在線(xiàn)程執(zhí)行的早期,在任何用戶(hù)代碼開(kāi)始執(zhí)行之前。圖7是我為BaseProcessStart函數(shù)寫(xiě)的偽代碼,它是Windows NT KERNEL32.DLL的一個(gè)內(nèi)部例程。這個(gè)函數(shù)帶一個(gè)參數(shù)——線(xiàn)程入口點(diǎn)函數(shù)的地址。BaseProcessStart運(yùn)行在新進(jìn)程的環(huán)境中,并且它調(diào)用這個(gè)進(jìn)程的第一個(gè)線(xiàn)程的入口點(diǎn)函數(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( //過(guò)濾器表達(dá)式代碼
exceptionCode = GetExceptionInformation(),
UnhandledExceptionFilter( GetExceptionInformation() ) )
{
ESP = currentESP;
if ( !_BaseRunningInServerProcess ) // 普通進(jìn)程
ExitProcess( exceptionCode );
else // 服務(wù)
ExitThread( exceptionCode );
}
}
在 上面的偽代碼中,注意對(duì)lpfnEntryPoint的調(diào)用被一個(gè)__try和__except塊封裝著。就是這個(gè)__try塊安裝了默認(rèn)的、異常處理程 序鏈表上的最后一個(gè)異常處理程序。所有后來(lái)注冊(cè)的異常處理程序都被安裝在鏈表中這個(gè)結(jié)點(diǎn)的前面。如果lpfnEntryPoint函數(shù)返回,那么表明線(xiàn)程 一直運(yùn)行到完成并且沒(méi)有引發(fā)異常。這時(shí)BaseProcessStart調(diào)用ExitThread使線(xiàn)程退出。
如果線(xiàn)程引發(fā)了一個(gè)異常但是沒(méi)有異常處理程序來(lái)處理它時(shí)怎么辦呢?這時(shí),執(zhí)行流程轉(zhuǎn)到__except關(guān)鍵字后面的括號(hào)中。在BaseProcessStart中,這段代碼調(diào)用UnhandledExceptionFilter這個(gè)API,后面我會(huì)講到它。現(xiàn)在對(duì)于我們來(lái)說(shuō),重要的是UnhandledExceptionFilter這個(gè)API包含了默認(rèn)的異常處理程序。
如 果UnhandledExceptionFilter返回EXCEPTION_EXECUTE_HANDLER,這時(shí)BaseProcessStart中 的__except塊開(kāi)始執(zhí)行。而__except塊所做的只是調(diào)用ExitProcess函數(shù)去終止當(dāng)前進(jìn)程。稍微想一下你就會(huì)理解了。常識(shí)告訴我們, 如果一個(gè)進(jìn)程引發(fā)了一個(gè)錯(cuò)誤而沒(méi)有異常處理程序去處理它,這個(gè)進(jìn)程就會(huì)被系統(tǒng)終止。你在偽代碼中看到的正是這些。
對(duì)于上面所說(shuō)的我還有一點(diǎn)要補(bǔ)充。如果引發(fā)錯(cuò)誤的線(xiàn)程是作為服務(wù)來(lái)運(yùn)行的,并且是基于線(xiàn)程的服務(wù),那么__except塊并不調(diào)用ExitProcess,相反,它調(diào)用ExitThread。不能僅僅因?yàn)橐粋€(gè)服務(wù)出錯(cuò)就終止整個(gè)服務(wù)進(jìn)程。
UnhandledExceptionFilter中的默認(rèn)異常處理程序都做了什么呢?當(dāng)我在一個(gè)技術(shù)講座上問(wèn)起這個(gè)問(wèn)題時(shí),響應(yīng)者寥寥無(wú)幾。幾乎沒(méi)有人知道當(dāng)未處理異常發(fā)生時(shí),到底操作系統(tǒng)的默認(rèn)行為是什么。簡(jiǎn)單地演示一下這個(gè)默認(rèn)的行為也許會(huì)讓很多人豁然開(kāi)朗。我運(yùn)行一個(gè)故意引發(fā)錯(cuò)誤的程序,其結(jié)果如下(見(jiàn)圖8)。

圖8 未處理異常對(duì)話(huà)框
表面上看,UnhandledExceptionFilter顯示了一個(gè)對(duì)話(huà)框告訴你發(fā)生了一個(gè)錯(cuò)誤。這時(shí),你被給予了一個(gè)機(jī)會(huì)或者終止出錯(cuò)進(jìn)程,或者調(diào)試它。但是幕后發(fā)生了許多事情,我會(huì)在文章最后詳細(xì)講述它。
正如我讓你看到的那樣,當(dāng)異常發(fā)生時(shí),用戶(hù)寫(xiě)的代碼可以(并且通常是這樣)獲得機(jī)會(huì)執(zhí)行。同樣,在展開(kāi)操作期間,用戶(hù)寫(xiě)的代碼也可以執(zhí)行。這個(gè)用戶(hù)寫(xiě)的代碼可能也有錯(cuò)誤,并且可能引發(fā)另一個(gè)異常。由于這個(gè)原因,異常處理回調(diào)函數(shù)也可以返回另外兩個(gè)值:ExceptionNestedException和ExceptionCollidedUnwind。很明顯,它們很重要。但這是非常復(fù)雜的問(wèn)題,我并不打算在這里涉及它們。要想理解其中的一些基本問(wèn)題太困難了。
編譯器層面的SEH
雖 然我在前面偶爾也使用了__try和__except,但迄今為止幾乎我寫(xiě)的所有內(nèi)容都是關(guān)于操作系統(tǒng)方面對(duì)SEH的實(shí)現(xiàn)。然而看一下我那兩個(gè)使用操作系 統(tǒng)的原始SEH的小程序別扭的樣子,編譯器對(duì)這個(gè)功能進(jìn)行封裝實(shí)在是非常有必要的。現(xiàn)在讓我們來(lái)看一下Visual C++是如何在操作系統(tǒng)對(duì)SEH功能實(shí)現(xiàn)的基礎(chǔ)上來(lái)創(chuàng)建它自己的結(jié)構(gòu)化異常處理支持的。
在 我們繼續(xù)下去之前,記住其它編譯器可以使用原始的系統(tǒng)SEH來(lái)做一些完全不同的事情這一點(diǎn)是非常重要的。并沒(méi)有什么規(guī)定編譯器必須實(shí)現(xiàn)Win32 SDK文檔中描述的__try/__except模型。例如Visual Basic 5.0在它的運(yùn)行時(shí)代碼中使用了結(jié)構(gòu)化異常處理,但是那里的數(shù)據(jù)結(jié)構(gòu)和算法與我這里要講的完全不同。
如果你把Win32 SDK文檔中關(guān)于結(jié)構(gòu)化異常處理方面的內(nèi)容從頭到尾讀一遍,一定會(huì)遇到下面所謂的“基于幀”的異常處理程序模型:
__try {
// 這里是被保護(hù)的代碼
}
__except (過(guò)濾器表達(dá)式) {
// 這里是異常處理程序代碼
}
簡(jiǎn) 單地說(shuō),在一個(gè)函數(shù)中,一個(gè)__try塊中的所有代碼就通過(guò)創(chuàng)建在這個(gè)函數(shù)的堆棧幀上的一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)來(lái)保護(hù)。在 函數(shù)的入口處,這個(gè)新的EXCEPTION_REGISTRATION結(jié)構(gòu)被放在異常處理程序鏈表的頭部。在__try塊結(jié)束后,相應(yīng)的 EXCEPTION_REGISTRATION結(jié)構(gòu)從這個(gè)鏈表的頭部被移除。正如我前面所說(shuō),異常處理程序鏈表的頭部被保存在FS:[0]處。因此,如果 你在調(diào)試器中單步跟蹤時(shí)看到類(lèi)似下面的指令時(shí)
MOV DWORD PTR FS:[00000000],ESP
或者
MOV DWORD PTR FS:[00000000],ECX
就能非常確定這段代碼正在進(jìn)入或退出一個(gè)__try/__except塊。
既然一個(gè)__try塊相當(dāng)于堆棧上的一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu),那么EXCEPTION_REGISTRATION結(jié)構(gòu)中的回調(diào)函數(shù)相當(dāng)于什么呢?使用Win32的術(shù)語(yǔ)來(lái)說(shuō),異常處理回調(diào)函數(shù)相當(dāng)于過(guò)濾器表達(dá)式(filter-expression)代碼。實(shí)際上,過(guò)濾器表達(dá)式就是__except關(guān)鍵字后面的小括號(hào)中的代碼。就是這個(gè)過(guò)濾器表達(dá)式代碼決定了后面的大括號(hào)中的代碼是否執(zhí)行。
由于過(guò)濾器表達(dá)式代碼是你自己寫(xiě)的,你當(dāng)然可以決定在你的代碼中的某個(gè)地方是否處理某個(gè)特定的異常。它可以簡(jiǎn)單的只是一句“EXCEPTION_EXECUTE_HANDLER”,也可以先調(diào)用一個(gè)把p計(jì)算到20,000,000位的函數(shù),然后再返回一個(gè)值來(lái)告訴操作系統(tǒng)下一步做什么。隨你的便。關(guān)鍵是你的過(guò)濾器表達(dá)式代碼必須是我前面講的有效的異常處理回調(diào)函數(shù)。
我剛才講的雖然相當(dāng)簡(jiǎn)單,但那只不過(guò)是隔著有色玻璃看世界罷了。實(shí)際它是非常復(fù)雜的。首先,你的過(guò)濾器表達(dá)式代碼并不是被操作系統(tǒng)直接調(diào)用的。事實(shí)上,各個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)的handler域都指向了同一個(gè)函數(shù)。這個(gè)函數(shù)在Visual C++的運(yùn)行時(shí)庫(kù)中,它被稱(chēng)為__except_handler3。正是這個(gè)__except_handler3調(diào)用了你的過(guò)濾器表達(dá)式代碼,我一會(huì)兒再接著說(shuō)它。
對(duì) 我前面的簡(jiǎn)單描述需要修正的另一個(gè)地方是,并不是每次進(jìn)入或退出一個(gè)__try塊時(shí)就創(chuàng)建或撤銷(xiāo)一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)。 相反,在使用SEH的任何函數(shù)中只創(chuàng)建一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)。換句話(huà)說(shuō),你可以在一個(gè)函數(shù)中使用多個(gè) __try/__except塊,但是在堆棧上只創(chuàng)建一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)。同樣,你可以在一個(gè)函數(shù)中嵌套使用 __try塊,但Visual C++仍舊只是創(chuàng)建一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)。
如 果整個(gè)EXE或DLL只需要單個(gè)的異常處理程序(__except_handler3),同時(shí),如果單個(gè)的EXCEPTION_REGISTRATION 結(jié)構(gòu)就能處理多個(gè)__try塊的話(huà),很明顯,這里面還有很多東西我們不知道。這個(gè)技巧是通過(guò)一個(gè)通常情況下看不到的表中的數(shù)據(jù)來(lái)完成的。由于本文的目的就 是要深入探索結(jié)構(gòu)化異常處理,那就讓我們來(lái)看一看這些數(shù)據(jù)結(jié)構(gòu)吧。
擴(kuò)展的異常處理幀
Visual C++的 SEH實(shí)現(xiàn)并沒(méi)有使用原始的EXCEPTION_REGISTRATION結(jié)構(gòu)。它在這個(gè)結(jié)構(gòu)的末尾添加了一些附加數(shù)據(jù)。這些附加數(shù)據(jù)正是允許單個(gè)函數(shù) (__except_handler3)處理所有異常并將執(zhí)行流程傳遞到相應(yīng)的過(guò)濾器表達(dá)式和__except塊的關(guān)鍵。我在Visual C++運(yùn)行時(shí)庫(kù)源代碼中的EXSUP.INC文件中找到了有關(guān)Visual C++擴(kuò)展的EXCEPTION_REGISTRATION結(jié)構(gòu)格式的線(xiàn)索。在這個(gè)文件中,你會(huì)看到以下定義(已經(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)見(jiàn)過(guò)前兩個(gè)域:prev和handler。它們組成了基本的EXCEPTION_REGISTRATION結(jié)構(gòu)。后面三個(gè) 域:scopetable(作用域表)、trylevel和_ebp是新增加的。scopetable域指向一個(gè)scopetable_entry結(jié)構(gòu)數(shù)組,而trylevel域?qū)嶋H上是這個(gè)數(shù)組的索引。最后一個(gè)域_ebp,是EXCEPTION_REGISTRATION結(jié)構(gòu)創(chuàng)建之前棧幀指針(EBP)的值。
_ebp域成為擴(kuò)展的EXCEPTION_REGISTRATION結(jié)構(gòu)的一部分并非偶然。它是通過(guò)PUSH EBP這條指令被包含進(jìn)這個(gè)結(jié)構(gòu)中的,而大多數(shù)函數(shù)開(kāi)頭都是這條指令(通常編譯器并不為使用FPO優(yōu)化的函數(shù)生成標(biāo)準(zhǔn)的堆棧幀,這樣其第一條指令可能不是PUSH EBP。但是如果使用了SEH的話(huà),那么無(wú)論你是否使用了FPO優(yōu)化,編譯器一定生成標(biāo)準(zhǔn)的堆棧幀)。 這條指令可以使EXCEPTION_REGISTRATION結(jié)構(gòu)中所有其它的域都可以用一個(gè)相對(duì)于棧幀指針(EBP)的負(fù)偏移來(lái)訪(fǎng)問(wèn)。例如 trylevel域在[EBP-04]處,scopetable指針在[EBP-08]處,等等。(也就是說(shuō),這個(gè)結(jié)構(gòu)是從[EBP-10H]處開(kāi)始 的。)
緊跟著擴(kuò)展的EXCEPTION_REGISTRATION結(jié)構(gòu)下面,Visual C++壓入了另外兩個(gè)值。緊跟著(即[EBP-14H]處)的一個(gè)DWORD,是為一個(gè)指向EXCEPTION_POINTERS結(jié)構(gòu)(一個(gè)標(biāo)準(zhǔn)的Win32 結(jié)構(gòu))的指針?biāo)A舻目臻g。這個(gè)指針就是你調(diào)用GetExceptionInformation這個(gè)API時(shí)返回的指針。盡管SDK文檔暗示GetExceptionInformation是一個(gè)標(biāo)準(zhǔn)的Win32 API,但事實(shí)上它是一個(gè)編譯器內(nèi)聯(lián)函數(shù)。當(dāng)你調(diào)用這個(gè)函數(shù)時(shí),Visual C++生成以下代碼:
MOV EAX,DWORD PTR [EBP-14]
GetExceptionInformation是一個(gè)編譯器內(nèi)聯(lián)函數(shù),與它相關(guān)的GetExceptionCode函 數(shù)也是如此。此函數(shù)實(shí)際上只是返回GetExceptionInformation返回的數(shù)據(jù)結(jié)構(gòu)(EXCEPTION_POINTERS)中的一個(gè)結(jié)構(gòu) (EXCEPTION_RECORD)中的一個(gè)域(ExceptionCode)的值。當(dāng)Visual C++為GetExceptionCode函數(shù)生成下面的指令時(shí),它到底是想干什么?我把這個(gè)問(wèn)題留給讀者。(現(xiàn)在就能理解為什么SDK文檔提醒我們要注 意這兩個(gè)函數(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)在回到擴(kuò)展的EXCEPTION_REGISTRATION結(jié)構(gòu)上來(lái)。在這個(gè)結(jié)構(gòu)開(kāi)始前的8個(gè)字節(jié)處(即[EBP-18H]處),Visual C++保留了一個(gè)DWORD來(lái)保存所有prolog代 碼執(zhí)行完畢之后的堆棧指針(ESP)的值(實(shí)際生成的指令為MOV DWORD PTR [EBP-18H],ESP)。這個(gè)DWORD中保存的值是函數(shù)執(zhí)行時(shí)ESP寄存器的正常值(除了在準(zhǔn)備調(diào)用其它函數(shù)時(shí)把參數(shù)壓入堆棧這個(gè)過(guò)程會(huì)改變 ESP寄存器的值并在函數(shù)返回時(shí)恢復(fù)它的值外,函數(shù)在執(zhí)行過(guò)程中一般不改變ESP寄存器的值)。
看起來(lái)好像我一下子給你灌輸了太多的信息,這點(diǎn)我承認(rèn)。在繼續(xù)下去之前,讓我們先暫停,來(lái)回顧一下Visual C++為使用結(jié)構(gòu)化異常處理的函數(shù)生成的標(biāo)準(zhǔn)異常堆棧幀,它看起來(lái)像下面這個(gè)樣子:
EBP-00 _ebp
EBP-04 trylevel
EBP-08 scopetable數(shù)組指針
EBP-0C handler函數(shù)地址
EBP-10指向前一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)
EBP-14 GetExceptionInformation
EBP-18 棧幀中的標(biāo)準(zhǔn)ESP
在操作系統(tǒng)看來(lái),只存在組成原始EXCEPTION_REGISTRATION結(jié)構(gòu)的兩個(gè)域:即[EBP-10h]處的prev指針和[EBP-0Ch]處的handler函數(shù)指針。棧幀中的其它所有內(nèi)容是針對(duì)于Visual C++的。把這個(gè)Visual C++生成的標(biāo)準(zhǔn)異常堆棧幀記到腦子里之后,讓我們來(lái)看一下真正實(shí)現(xiàn)編譯器層面SEH的這個(gè)Visual C++運(yùn)行時(shí)庫(kù)例程——__except_handler3。
__except_handler3和scopetable
我 真的很希望讓你看一看Visual C++運(yùn)行時(shí)庫(kù)源代碼,讓你自己好好研究一下__except_handler3函數(shù),但是我辦不到。因?yàn)镸icrosoft并沒(méi)有提供。在這里你就將就 著看一下我為_(kāi)_except_handler3函數(shù)寫(xiě)的偽代碼吧(如圖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 // 將方向標(biāo)志復(fù)位(不測(cè)試任何條件!)
// 如果沒(méi)有設(shè)置EXCEPTION_UNWINDING標(biāo)志或EXCEPTION_EXIT_UNWIND標(biāo)志
// 表明這是第一次調(diào)用這個(gè)處理程序(也就是說(shuō),并非處于異常展開(kāi)階段)
if ( ! (pExceptionRecord->ExceptionFlags
& (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) )
{
// 在堆棧上創(chuàng)建一個(gè)EXCEPTION_POINTERS結(jié)構(gòu)
exceptPtrs.ExceptionRecord = pExceptionRecord;
exceptPtrs.ContextRecord = pContextRecord;
// 把前面定義的EXCEPTION_POINTERS結(jié)構(gòu)的地址放在比
// establisher棧幀低4個(gè)字節(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 // 保存這個(gè)棧幀指針
// !!!非常重要!!!切換回原來(lái)的EBP。正是這個(gè)操作才使得
// 棧幀上的所有局部變量能夠在異常發(fā)生后仍然保持它的值不變。
EBP = &pRegistrationFrame->_ebp;
// 調(diào)用過(guò)濾器函數(shù)
filterFuncRet = scopetable[trylevel].lpfnFilter();
POP EBP // 恢復(fù)異常處理程序的棧幀指針
if ( filterFuncRet != EXCEPTION_CONTINUE_SEARCH )
{
if ( filterFuncRet < 0 ) // EXCEPTION_CONTINUE_EXECUTION
return ExceptionContinueExecution;
// 如果能夠執(zhí)行到這里,說(shuō)明返回值為EXCEPTION_EXECUTE_HANDLER
scopetable = pRegistrationFrame->scopetable;
// 讓操作系統(tǒng)清理已經(jīng)注冊(cè)的棧幀,這會(huì)使本函數(shù)被遞歸調(diào)用
__global_unwind2( pRegistrationFrame );
// 一旦執(zhí)行到這里,除最后一個(gè)棧幀外,所有的棧幀已經(jīng)
// 被清理完畢,流程要從最后一個(gè)棧幀繼續(xù)執(zhí)行
EBP = &pRegistrationFrame->_ebp;
__local_unwind2( pRegistrationFrame, trylevel );
// NLG = "non-local-goto" (setjmp/longjmp stuff)
__NLG_Notify( 1 ); // EAX = scopetable->lpfnHandler
// 把當(dāng)前的trylevel設(shè)置成當(dāng)找到一個(gè)異常處理程序時(shí)
// SCOPETABLE中當(dāng)前正在被使用的那一個(gè)元素的內(nèi)容
pRegistrationFrame->trylevel = scopetable->previousTryLevel;
// 調(diào)用__except {}塊,這個(gè)調(diào)用并不會(huì)返回
pRegistrationFrame->scopetable[trylevel].lpfnHandler();
}
}
scopeTable = pRegistrationFrame->scopetable;
trylevel = scopeTable->previousTryLevel;
goto search_for_handler;
}
else // trylevel == TRYLEVEL_NONE
{
return ExceptionContinueSearch;
}
}
else // 設(shè)置了EXCEPTION_UNWINDING標(biāo)志或EXCEPTION_EXIT_UNWIND標(biāo)志
{
PUSH EBP // 保存EBP
EBP = &pRegistrationFrame->_ebp; // 為調(diào)用__local_unwind2設(shè)置EBP
__local_unwind2( pRegistrationFrame, TRYLEVEL_NONE )
POP EBP // 恢復(fù)EBP
return ExceptionContinueSearch;
}
}
雖 然__except_handler3的代碼看起來(lái)很多,但是記住一點(diǎn):它只是一個(gè)我在文章開(kāi)頭講過(guò)的異常處理回調(diào)函數(shù)。它同MYSEH.EXE和 MYSEH2.EXE中的異常回調(diào)函數(shù)都帶有同樣的四個(gè)參數(shù)。__except_handler3大體上可以由第一個(gè)if語(yǔ)句分為兩部分。這是由于這個(gè)函 數(shù)可以在兩種情況下被調(diào)用,一次是正常調(diào)用,另一次是在展開(kāi)階段。其中大部分是在非展開(kāi)階段的回調(diào)。
__except_handler3一 開(kāi)始就在堆棧上創(chuàng)建了一個(gè)EXCEPTION_POINTERS結(jié)構(gòu),并用它的兩個(gè)參數(shù)來(lái)對(duì)這個(gè)結(jié)構(gòu)進(jìn)行初始化。我在偽代碼中把這個(gè)結(jié)構(gòu)稱(chēng)為 exceptPrts,它的地址被放在[EBP-14h]處。你回憶一下前面我講的編譯器為GetExceptionInformation和 GetExceptionCode函數(shù)生成的匯編代碼就會(huì)意識(shí)到,這實(shí)際上初始化了這兩個(gè)函數(shù)使用的指針。
接 著,__except_handler3從EXCEPTION_REGISTRATION幀中獲取當(dāng)前的trylevel(在[EBP-04h]處)。 trylevel變量實(shí)際是scopetable數(shù)組的索引,而正是這個(gè)數(shù)組才使得一個(gè)函數(shù)中的多個(gè)__try塊和嵌套的__try塊能夠僅使用一個(gè) EXCEPTION_REGISTRATION結(jié)構(gòu)。每個(gè)scopetable元素結(jié)構(gòu)如下:
typedef struct _SCOPETABLE
{
DWORD previousTryLevel;
DWORD lpfnFilter;
DWORD lpfnHandler;
} SCOPETABLE, *PSCOPETABLE;
SCOPETABLE結(jié)構(gòu)中的第二個(gè)成員和第三個(gè)成員比較容易理解。它們分別是過(guò)濾器表達(dá)式代碼的地址和相應(yīng)的__except塊的地址。但是prviousTryLevel成員有點(diǎn)復(fù)雜。總之一句話(huà),它用于嵌套的__try塊。這里的關(guān)鍵是函數(shù)中的每個(gè)__try塊都有一個(gè)相應(yīng)的SCOPETABLE結(jié)構(gòu)。
正 如我前面所說(shuō),當(dāng)前的trylevel指定了要使用的scopetable數(shù)組的哪一個(gè)元素,最終也就是指定了過(guò)濾器表達(dá)式和__except塊的地址。 現(xiàn)在想像一下兩個(gè)__try塊嵌套的情形。如果內(nèi)層__try塊的過(guò)濾器表達(dá)式不處理某個(gè)異常,那外層__try塊的過(guò)濾器表達(dá)式就必須處理它。那現(xiàn)在要 問(wèn),__except_handler3是如何知道SCOPETABLE數(shù)組的哪個(gè)元素相應(yīng)于外層的__try塊的呢?答案是:外層__try塊的索引由 SCOPETABLE結(jié)構(gòu)的previousTryLevel域給出。利用這種機(jī)制,你可以嵌套任意層的__try塊。previousTryLevel 域就好像是一個(gè)函數(shù)中所有可能的異常處理程序構(gòu)成的線(xiàn)性鏈表中的結(jié)點(diǎn)一樣。如果trylevel的值為0xFFFFFFFF(實(shí)際上就是-1,這個(gè)值在 EXSUP.INC中被定義為TRYLEVEL_NONE),標(biāo)志著這個(gè)鏈表結(jié)束。
回到__except_handler3的代碼中。在獲取了當(dāng)前的trylevel之后,它就調(diào)用相應(yīng)的SCOPETABLE結(jié)構(gòu)中的過(guò)濾器表達(dá)式代碼。如果過(guò)濾器表達(dá)式返回EXCEPTION_CONTINUE_SEARCH,__exception_handler3 移向SCOPETABLE數(shù)組中的下一個(gè)元素,這個(gè)元素的索引由previousTryLevel域給出。如果遍歷完整個(gè)線(xiàn)性鏈表(還記得嗎?這個(gè)鏈表是 由于在一個(gè)函數(shù)內(nèi)部嵌套使用__try塊而形成的)都沒(méi)有找到處理這個(gè)異常的代碼,__except_handler3返回DISPOSITION_CONTINUE_SEARCH(原文如此,但根據(jù)_except_handler函數(shù)的定義,這個(gè)返回值應(yīng)該為ExceptionContinueSearch。實(shí)際上這兩個(gè)常量的值是一樣的。我在偽代碼中已經(jīng)將其改正過(guò)來(lái)了),這導(dǎo)致系統(tǒng)移向下一個(gè)EXCEPTION_REGISTRATION幀(這個(gè)鏈表是由于函數(shù)嵌套調(diào)用而形成的)。
如果過(guò)濾器表達(dá)式返回EXCEPTION_EXECUTE_HANDLER, 這意味著異常應(yīng)該由相應(yīng)的__except塊處理。它同時(shí)也意味著所有前面的EXCEPTION_REGISTRATION幀都應(yīng)該從鏈表中移除,并且相 應(yīng)的__except塊都應(yīng)該被執(zhí)行。第一個(gè)任務(wù)通過(guò)調(diào)用__global_unwind2來(lái)完成的,后面我會(huì)講到這個(gè)函數(shù)。跳過(guò)這中間的一些清理代碼, 流程離開(kāi)__except_handler3轉(zhuǎn)向__except塊。令人奇怪的是,流程并不從__except塊中返回,雖然是 __except_handler3使用CALL指令調(diào)用了它。
當(dāng) 前的trylevel值是如何被設(shè)置的呢?它實(shí)際上是由編譯器隱含處理的。編譯器非常機(jī)靈地修改這個(gè)擴(kuò)展的EXCEPTION_REGISTRATION 結(jié)構(gòu)中的trylevel域的值(實(shí)際上是生成修改這個(gè)域的值的代碼)。如果你檢查編譯器為使用SEH的函數(shù)生成的匯編代碼,就會(huì)在不同的地方都看到修改 這個(gè)位于[EBP-04h]處的trylevel域的值的代碼。
__except_handler3是 如何做到既通過(guò)CALL指令調(diào)用__except塊而又不讓執(zhí)行流程返回呢?由于CALL指令要向堆棧中壓入了一個(gè)返回地址,你可以想象這有可能破壞堆 棧。如果你檢查一下編譯器為_(kāi)_except塊生成的代碼,你會(huì)發(fā)現(xiàn)它做的第一件事就是將EXCEPTION_REGISTRATION結(jié)構(gòu)下面8個(gè)字節(jié) 處(即[EBP-18H]處)的一個(gè)DWORD值加載到ESP寄存器中(實(shí)際代碼為MOV ESP,DWORD PTR [EBP-18H]),這個(gè)值是在函數(shù)的prolog代碼中被保存在這個(gè)位置的(實(shí)際代碼為MOV DWORD PTR [EBP-18H],ESP)。
ShowSEHFrames程序
如 果你現(xiàn)在覺(jué)得已經(jīng)被EXCEPTION_REGISTRATION、scopetable、trylevel、過(guò)濾器表達(dá)式以及展開(kāi)等等之類(lèi)的詞搞得暈頭 轉(zhuǎn)向的話(huà),那和我最初的感覺(jué)一樣。但是編譯器層面的結(jié)構(gòu)化異常處理方面的知識(shí)并不適合一點(diǎn)一點(diǎn)的學(xué)。除非你從整體上理解它,否則有很多內(nèi)容單獨(dú)看并沒(méi)有什 么意義。當(dāng)面對(duì)大堆的理論時(shí),我最自然的做法就是寫(xiě)一些應(yīng)用我學(xué)到的理論方面的程序。如果它能夠按照預(yù)料的那樣工作,我就知道我的理解(通常)是正確的。
圖 10是ShowSEHFrame.EXE的源代碼。它使用__try/__except塊設(shè)置了好幾個(gè)Visual C++ SEH幀。然后它顯示每一個(gè)幀以及Visual C++為每個(gè)幀創(chuàng)建的scopetable的相關(guān)信息。這個(gè)程序本身并不生成也不依賴(lài)任何異常。相反,我使用了多個(gè)__try塊以強(qiáng)制Visual C++生成多個(gè)EXCEPTION_REGISTRATION幀以及相應(yīng)的scopetable。
圖10 ShowSEHFrames.CPP
//=========================================================
// ShowSEHFrames - Matt Pietrek 1997
// Microsoft Systems Journal, February 1997
// FILE: ShowSEHFrames.CPP
// 使用命令行CL ShowSehFrames.CPP進(jìn)行編譯//=========================================================
#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++擴(kuò)展異常幀指向的數(shù)據(jù)結(jié)構(gòu)
struct scopetable_entry
{
DWORD previousTryLevel;
FARPROC lpfnFilter;
FARPROC lpfnHandler;
};
// Visual C++使用的擴(kuò)展異常幀
struct VC_EXCEPTION_REGISTRATION : EXCEPTION_REGISTRATION
{
scopetable_entry * scopetable;
int trylevel;
int _ebp;
};
//----------------------------------------------------------------
// 原型聲明
//----------------------------------------------------------------
// __except_handler3是Visual C++運(yùn)行時(shí)庫(kù)函數(shù),我們想打印出它的地址
// 但是它的原型并沒(méi)有出現(xiàn)在任何頭文件中,所以我們需要自己聲明它。
extern "C" int _except_handler3(PEXCEPTION_RECORD,
EXCEPTION_REGISTRATION *,
PCONTEXT,
PEXCEPTION_RECORD);
//-------------------------------------------------------------
// 代碼
//-------------------------------------------------------------
//
// 顯示一個(gè)異常幀及其相應(yīng)的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標(biāo)志著鏈表的結(jié)尾
while ( 0xFFFFFFFF != (unsigned)pVCExcRec )
{
ShowSEHFrame( pVCExcRec );
pVCExcRec = (VC_EXCEPTION_REGISTRATION *)(pVCExcRec->prev);
}
}
void Function1( void )
{
// 嵌套3層__try塊以便強(qiáng)制為scopetable數(shù)組產(chǎn)生3個(gè)元素
__try
{
__try
{
__try
{
WalkSEHFrames(); // 現(xiàn)在顯示所有的異常幀的信息
} __except( EXCEPTION_CONTINUE_SEARCH )
{}
} __except( EXCEPTION_CONTINUE_SEARCH )
{}
} __except( EXCEPTION_CONTINUE_SEARCH )
{}
}
int main()
{
int i;
// 使用兩個(gè)__try塊(并不嵌套),這導(dǎo)致為scopetable數(shù)組生成兩個(gè)元素
__try
{
i = 0x1234;
} __except( EXCEPTION_CONTINUE_SEARCH )
{
i = 0x4321;
}
__try
{
Function1(); // 調(diào)用一個(gè)設(shè)置更多異常幀的函數(shù)
} __except( EXCEPTION_EXECUTE_HANDLER )
{
// 應(yīng)該永遠(yuǎn)不會(huì)執(zhí)行到這里,因?yàn)槲覀儾](méi)有打算產(chǎn)生任何異常
printf( "Caught Exception in main\n" );
}
return 0;
}
ShowSEHFrames程 序中比較重要的函數(shù)是WalkSEHFrames和ShowSEHFrame。WalkSEHFrames函數(shù)首選打印出 __except_handler3的地址,打印它的原因很快就清楚了。接著,它從FS:[0]處獲取異常鏈表的頭指針,然后遍歷該鏈表。此鏈表中每個(gè)結(jié) 點(diǎn)都是一個(gè)VC_EXCEPTION_REGISTRATION類(lèi)型的結(jié)構(gòu),它是我自己定義的,用于描述Visual C++的異常處理幀。對(duì)于這個(gè)鏈表中的每個(gè)結(jié)點(diǎn),WalkSEHFrames都把指向這個(gè)結(jié)點(diǎn)的指針傳遞給ShowSEHFrame函數(shù)。
ShowSEHFrame函 數(shù)一開(kāi)始就打印出異常處理幀的地址、異常處理回調(diào)函數(shù)的地址、前一個(gè)異常處理幀的地址以及scopetable的地址。接著,對(duì)于每個(gè) scopetable數(shù)組中的元素,它都打印出其priviousTryLevel、過(guò)濾器表達(dá)式的地址以及相應(yīng)的__except塊的地址。我是如何知 道scopetable數(shù)組中有多少個(gè)元素的呢?其實(shí)我并不知道。但是我假定VC_EXCEPTION_REGISTRATION結(jié)構(gòu)中的當(dāng)前trylevel域的值比scopetable數(shù)組中的元素總數(shù)少1。
圖 11是ShowSEHFrames的運(yùn)行結(jié)果。首先檢查以“Frame:”開(kāi)頭的每一行,你會(huì)發(fā)現(xiàn)它們顯示的異常處理幀在堆棧上的地址呈遞增趨勢(shì),并且在 前三個(gè)幀中,它們的異常處理程序的地址是一樣的(都是004012A8)。再看輸出的開(kāi)始部分,你會(huì)發(fā)現(xiàn)這個(gè)004012A8不是別的,它正是 Visual C++運(yùn)行時(shí)庫(kù)函數(shù)__except_handler3的地址。這證明了我前面所說(shuō)的單個(gè)回調(diào)函數(shù)處理所有異常這一點(diǎn)。

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

圖15 在SEH中是誰(shuí)調(diào)用了誰(shuí)
結(jié)論
結(jié)構(gòu)化異常處理是Win32一個(gè)非常好的特性。多虧有了像Visual C++之類(lèi)的編譯器的支持層對(duì)它的封裝,一般的程序員才能付出比較小的學(xué)習(xí)代價(jià)就能利用SEH所提供的便利。但是在操作系統(tǒng)層面上,事情遠(yuǎn)比Win32文檔說(shuō)的復(fù)雜。
不幸的是,由于人人都認(rèn)為系統(tǒng)層面的SEH是一個(gè)非常困難的問(wèn)題,因此至今這方面的資料都不多。在本文中,我已經(jīng)向你指出了系統(tǒng)層面的SEH就是圍繞著簡(jiǎn)單的回調(diào)在打轉(zhuǎn)。如果你理解了回調(diào)的本質(zhì),在此基礎(chǔ)上分層理解,系統(tǒng)層面的結(jié)構(gòu)化異常處理也不是那么難掌握。
附錄:關(guān)于prolog和epilog
美 國(guó)英語(yǔ)中的“prolog”實(shí)際上就是“prologue”。從這個(gè)詞的意思“序幕、序言”就能大致猜出它的作用。一個(gè)函數(shù)的prolog代碼主要是為這 個(gè)函數(shù)的執(zhí)行做一些準(zhǔn)備工作,例如設(shè)置堆棧幀、設(shè)置局部變量所使用的堆棧空間以及保存相關(guān)的寄存器等。標(biāo)準(zhǔn)的prolog代碼開(kāi)頭一般為以下三條指令:
PUSH EBP
MOV EBP, ESP
SUB ESP, XXX
上 面的三條指令為使用EBP寄存器來(lái)訪(fǎng)問(wèn)函數(shù)的參數(shù)(正偏移)和局部變量(負(fù)偏移)做好了準(zhǔn)備。例如按照__stdcall調(diào)用約定,調(diào)用者 (caller)將被調(diào)函數(shù)(callee)的參數(shù)從右向左壓入堆棧,然后用CALL指令調(diào)用這個(gè)函數(shù)。CALL指令將返回地址壓入堆棧,然后流程就轉(zhuǎn)到 了被調(diào)函數(shù)的prolog代碼。此時(shí)[ESP]中是返回地址,[ESP+4]中是函數(shù)的第一個(gè)參數(shù)。本來(lái)可以就這樣使用ESP寄存器來(lái)訪(fǎng)問(wèn)參數(shù),但由于 PUSH和POP指令會(huì)隱含修改ESP寄存器的值,這樣同一個(gè)參數(shù)在不同時(shí)刻可能需要通過(guò)不同的指令形式來(lái)訪(fǎng)問(wèn)(例如,如果現(xiàn)在向堆棧中壓入一個(gè)值的話(huà), 那訪(fǎng)問(wèn)第一個(gè)參數(shù)就需要使用[ESP+8]了)。為了解決這個(gè)問(wèn)題,所以使用EBP寄存器。EBP寄存器被稱(chēng)為棧幀(frame)指針,它正是用于此目 的。當(dāng)上述prolog指令中的前兩條指令執(zhí)行后,就可以使用EBP來(lái)訪(fǎng)問(wèn)參數(shù)了,并且在整個(gè)函數(shù)中都不會(huì)改變此寄存器的值。在前面的例子中, [EBP+8]處就是第一個(gè)參數(shù)的值,[EBP+0Ch]處是第二個(gè)參數(shù)的值,依次類(lèi)推。
大多數(shù)C/C++編譯器都有“棧幀指針省略(Frame-Pointer Omission)”這 個(gè)選項(xiàng)(在Microsoft C/C++編譯器中為/Oy),它導(dǎo)致函數(shù)使用ESP來(lái)訪(fǎng)問(wèn)參數(shù),從而可以空閑出一個(gè)寄存器(EBP)用于其它目的,并且由于不需要設(shè)置堆棧幀,從而會(huì)稍 微提高運(yùn)行速度。但是在某些情況下必須使用堆棧幀。作者在前面也提到過(guò),Microsoft已經(jīng)在其MSDN文檔中指明:結(jié)構(gòu)化異常處理是基于幀的異常處理。也就是說(shuō),它必須使用堆棧幀。當(dāng)你查看編譯器為使用SEH的函數(shù)生成的匯編代碼時(shí)就會(huì)清楚這一點(diǎn)。無(wú)論你是否使用/Oy選項(xiàng),它都設(shè)置堆棧幀。
可 能有的讀者在調(diào)試應(yīng)用程序時(shí)偶然進(jìn)入到了系統(tǒng)DLL(例如NTDLL.DLL)中,但是卻意外地發(fā)現(xiàn)許多函數(shù)的prolog代碼的第一條指令并不是上面所 說(shuō)的“PUSH EBP”,而是一條“垃圾”指令——“MOV EDI, EDI”(這條指令占兩個(gè)字節(jié))。Microsoft C/C++編譯器被稱(chēng)為優(yōu)化編譯器,它怎么可能生成這么一條除了占用空間之外別無(wú)它用的指令呢?實(shí)際上,如果你比較細(xì)心的話(huà),會(huì)發(fā)現(xiàn)以這條指令開(kāi)頭的函數(shù) 的前面有5條NOP指令(它們一共占5個(gè)字節(jié)),如下圖所示。
考 慮一下使用JMP指令進(jìn)行近跳轉(zhuǎn)和遠(yuǎn)跳轉(zhuǎn)分別需要幾個(gè)字節(jié)?他們正好分別是2個(gè)字節(jié)和5個(gè)字節(jié)!這難道是巧合?熟悉API攔截的讀者可能已經(jīng)猜到了,它們 是供攔截API時(shí)使用的。實(shí)際上,這是Microsoft對(duì)系統(tǒng)打“熱補(bǔ)丁”(Hot Patching)時(shí)攔截API用的。在打“熱補(bǔ)丁“時(shí),修補(bǔ)程序在5條NOP指令處寫(xiě)入一個(gè)遠(yuǎn)跳轉(zhuǎn)指令,以跳轉(zhuǎn)到被修補(bǔ)過(guò)的代碼處。而“MOV EDI, EDI”處用一個(gè)近跳轉(zhuǎn)指令覆蓋,它跳轉(zhuǎn)到5個(gè)NOP指令所在的位置。使用“MOV EDI, EDI”而不是直接使用兩個(gè)NOP指令是出于性能考慮。
第 三條指令用于為局部變量保留空間,其中的XXX就是需要保留的字節(jié)數(shù)。不使用局部變量的函數(shù)沒(méi)有這條指令。另外,如果局部變量比較少的話(huà)——例如2個(gè),為 了性能考慮,編譯器往往會(huì)使用類(lèi)似于兩條“PUSH ECX”這樣的指令來(lái)為局部變量保留空間。這三條指令后面一般還有幾條PUSH指令用于保存函數(shù)使用的寄存器(一般是EBX、ESI和EDI)。
與prolog代碼相對(duì)的就是epilog代碼。與prolog類(lèi)似,從它的意思“尾聲、結(jié)尾”也能猜出它的作用。它主要做一些清理工作。標(biāo)準(zhǔn)的epilog代碼如下:
MOV ESP, EBP
POP EBP
RET XXX
這 三條指令前面可能還有幾條POP指令用于恢復(fù)在prolog代碼中保存的寄存器(如果存在的話(huà))。有了前面的分析,epilog代碼不言自明。需要說(shuō)明的 一點(diǎn)是,最后的RET指令用于返回調(diào)用者,并從堆棧中彈出無(wú)用信息,XXX指定了彈出的字節(jié)數(shù)。它一般用于將參數(shù)彈出堆棧。因此從這個(gè)值就可以知道函數(shù)的 參數(shù)個(gè)數(shù)(每個(gè)參數(shù)均為4字節(jié))。
為 了簡(jiǎn)化這種操作,Intel引入了ENTER和LEAVE指令。其中ENTER相當(dāng)于前面所說(shuō)的prolog代碼的前兩條指令,而LEAVE相當(dāng)于上面的 epilog代碼的前兩條指令。但由于實(shí)現(xiàn)上ENTER指令比前面所說(shuō)的兩條指令執(zhí)行速度慢,因此編譯器都不使用這條指令。這樣,你實(shí)際看到的情況就 是:prolog代碼就是前面所說(shuō)的那樣,但epilog代碼使用了LEAVE指令。
posted on 2015-02-27 21:40
Richard Wei 閱讀(16612)
評(píng)論(0) 編輯 收藏 引用 所屬分類(lèi):
windows desktop