深入摸索Win32結(jié)構(gòu)化異常處理_第1頁
深入摸索Win32結(jié)構(gòu)化異常處理_第2頁
深入摸索Win32結(jié)構(gòu)化異常處理_第3頁
深入摸索Win32結(jié)構(gòu)化異常處理_第4頁
深入摸索Win32結(jié)構(gòu)化異常處理_第5頁
已閱讀5頁,還剩66頁未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡介

本文格式為Word版,下載可任意編輯——深入摸索Win32結(jié)構(gòu)化異常處理深入摸索Win32結(jié)構(gòu)化異常處理

在Win32操作系統(tǒng)提供的所有功能中,使用最廣泛而又沒有公開的生怕要數(shù)結(jié)構(gòu)化異常處理(StructuredExceptionHandling,SEH)了。當(dāng)你考慮Win32結(jié)構(gòu)化異常處理時(shí),可能會想到__try、__finally和__except等術(shù)語??赡苣阍谌魏我槐局v解Win32的好書上都能找到關(guān)于SEH較為詳細(xì)的描述,甚至Win32SDK文檔也對使用__try、__finally和__except進(jìn)行結(jié)構(gòu)化異常處理進(jìn)行了相當(dāng)完整的描述。

既然已經(jīng)有了這些文檔,那為什么我還說SEH并未公開呢?本質(zhì)上來說,Win32結(jié)構(gòu)化異常處理是操作系統(tǒng)提供的服務(wù)。你可能找到的所有關(guān)于SEH方面的文檔都只是描述了某個(gè)特別的編譯器的運(yùn)行時(shí)庫對操作系統(tǒng)實(shí)現(xiàn)的封裝。關(guān)鍵字__try、__finally或者_(dá)_except并沒有什么巧妙的。Microsoft的操作系統(tǒng)和編譯器開發(fā)小組定義了這些關(guān)鍵字和它們的作用。其它C++編譯器廠商完全依照它們的語義來就可以了。當(dāng)編譯器的SEH支持層把原始的操作系統(tǒng)SEH的繁雜性封裝起來的時(shí)候,它同時(shí)也把原始的操作系統(tǒng)SEH的細(xì)節(jié)隱蔽了起來。

我曾經(jīng)接到大量來自想自己實(shí)現(xiàn)編譯器層面SEH的人發(fā)來的電子郵件,他們苦于找不到關(guān)于操作系統(tǒng)SEH實(shí)現(xiàn)方面的任何文檔。按說,我應(yīng)當(dāng)能夠告訴他們VisualC++或BorlandC++的運(yùn)行時(shí)庫源代碼就是他們想要的。但是不知出于什么原因,編譯器層面的SEH看起來好像是個(gè)大機(jī)要。無論是Microsoft還是Borland都沒有提供他們的SEH支持層最底層的源代碼。(現(xiàn)在Microsoft依舊沒有提供這些源代碼,它提供的是編譯過的目標(biāo)文件,而Borland則提供了相應(yīng)的源代碼。)

在本文中,我會剝掉結(jié)構(gòu)化異常處理外面的包裝直至其最基本的概念。在此過程中,我會把操作系統(tǒng)提供的支持與編譯器通過代碼生成和運(yùn)行時(shí)庫提供的支持分開來說。當(dāng)我挖掘到關(guān)鍵的操作系統(tǒng)例程時(shí),我使用的是運(yùn)行于Intel處理器上的WindowsNT4.0。但是我這里講的大部分內(nèi)容同樣也適用于其它處理器。

我會避免涉及到真實(shí)的C++異常處理,它使用的是catch()而不是__except。從內(nèi)部來講,真實(shí)的C++異常處理的實(shí)現(xiàn)與我這里要講的十分相像。但是真實(shí)的C++異常處理有一些其它的繁雜問題,它會混淆我這里要講的一些概念。

在挖掘組成Win32SEH的晦澀的.H和.INC文件的過程中,我發(fā)現(xiàn)最好的信息來源之一是IBMOS/2頭文件(特別是BSEXCPT.H)。假使你涉足這方面已經(jīng)有一段時(shí)間了,就不會感到太奇怪。這里描述的SEH機(jī)制是早在Microsoft還工作在OS/2上時(shí)就已經(jīng)定義好的。由于這個(gè)原因,你會發(fā)現(xiàn)Win32下的SEH和OS/2下的SEH極其相像。(現(xiàn)在我們可能已經(jīng)沒有機(jī)遇體驗(yàn)這一點(diǎn)了,OS/2已經(jīng)永遠(yuǎn)成為歷史了。)

淺析SEH

如果我把SEH的所有細(xì)節(jié)一股腦兒全倒給你,你可能無法接受。因此我先從一小部分開始,然后層層深入。假使你以前從未接觸過結(jié)構(gòu)化異常處理,那正好,由于你頭腦中沒有一些自己設(shè)想的概念。假使你以前接觸過SEH,最好把頭腦中有關(guān)__try、GetExceptionCode和EXCEPTION_EXECUTE_HANDLER之類的詞統(tǒng)統(tǒng)忘掉,假設(shè)它對你來說是全新的。深呼吸。準(zhǔn)備好了嗎?讓我們開始吧!

設(shè)想我告訴過你,當(dāng)一個(gè)線程出現(xiàn)錯(cuò)誤時(shí),操作系統(tǒng)給你一個(gè)機(jī)遇被告知這個(gè)錯(cuò)誤。說得更明白一些就是,當(dāng)一個(gè)線程出現(xiàn)錯(cuò)誤時(shí),操作系統(tǒng)調(diào)用用戶定義的一個(gè)回調(diào)函數(shù)。這個(gè)回調(diào)函數(shù)可以做它想做的一切。例如它可以修復(fù)錯(cuò)誤,或者它也可以播放一段音樂。無論回調(diào)函數(shù)做什么,它最終都要返回一個(gè)值來告訴系統(tǒng)下一步做什么。(這不是十分確鑿,但就此刻來說十分接近。)

當(dāng)你的某一部分代碼出錯(cuò)時(shí),系統(tǒng)再回調(diào)你的其它代碼,那么這個(gè)回調(diào)函數(shù)看起來是什么樣子呢?換句話說,你想知道關(guān)于異常什么類型的信息呢?實(shí)際上這并不重要,由于Win32已經(jīng)替你做了決定。異常回調(diào)函數(shù)看起來像下面這個(gè)樣子:EXCEPTION_DISPOSITION

__cdecl_except_handler(struct_EXCEPTION_RECORD*ExceptionRecord,void*EstablisherFrame,struct_CONTEXT*ContextRecord,void*DispatcherContext);

這個(gè)原型來自標(biāo)準(zhǔn)的Win32頭文件EXCPT.H,乍看起來有些費(fèi)解。但假使你細(xì)心看,它并不是很難理解。首先,忽略掉返回值的類型(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中定義,如下所示:

typedefstruct_EXCEPTION_RECORD{DWORDExceptionCode;DWORDExceptionFlags;

struct_EXCEPTION_RECORD*ExceptionRecord;PVOIDExceptionAddress;DWORDNumberParameters;

DWORDExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];}EXCEPTION_RECORD;

這個(gè)結(jié)構(gòu)中的ExcepitonCode成員是賦予異常的代碼。通過在WINNT.H中探尋以“STATUS_〞開頭的#define定義,你可以得到一個(gè)異常代碼列表。例如所有人都十分熟悉的

STATUS_ACCESS_VIOLATION的代碼是0xC0000005。一個(gè)更全面的異常代碼列表可以在WindowsNTDDK的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è)特定線程的寄放器值。圖1顯示了CONTEXT結(jié)構(gòu)的成員。當(dāng)用于SEH時(shí),CONTEXT結(jié)構(gòu)表示異常發(fā)生時(shí)寄放器的值。順便說一下,這個(gè)CONTEXT結(jié)構(gòu)就是GetThreadContext和SetThreadContext這兩個(gè)API中使用的那個(gè)CONTEXT結(jié)構(gòu)。圖1CONTEXT結(jié)構(gòu)typedefstruct_CONTEXT{

DWORDContextFlags;DWORDDr0;DWORDDr1;DWORDDr2;DWORDDr3;DWORDDr6;DWORDDr7;

FLOATING_SAVE_AREAFloatSave;DWORDSegGs;DWORDSegFs;

DWORDSegEs;DWORDSegDs;DWORDEdi;DWORDEsi;DWORDEbx;DWORDEdx;DWORDEcx;DWORDEax;DWORDEbp;DWORDEip;DWORDSegCs;DWORDEFlags;DWORDEsp;DWORDSegSs;}CONTEXT;

_except_handler回調(diào)函數(shù)的第四個(gè)參數(shù)被稱為DispatcherContext。它暫時(shí)也可以被忽略。到現(xiàn)在為止,你頭腦中已經(jīng)有了一個(gè)當(dāng)異常發(fā)生時(shí)會被操作系統(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)傳遞過來的大量有價(jià)值的信息,例如異常的類型和發(fā)生的地址。使用這些信息,異?;卣{(diào)函數(shù)就能決定下一步做什么。

對我來說,現(xiàn)在就寫一個(gè)能夠顯示_except_handler作用的樣例程序是再迷人不過的了。但是我們還缺少一些關(guān)鍵信息。特別是,當(dāng)錯(cuò)誤發(fā)生時(shí)操作系統(tǒng)是怎么知道到哪里去調(diào)用這個(gè)回調(diào)函數(shù)的呢?答案是還有一個(gè)稱為EXCEPTION_REGISTRATION的結(jié)構(gòu)。通篇你都會看到這個(gè)結(jié)構(gòu),所以不要跳過這一部分。我唯一能找到的EXCEPTION_REGISTRATION結(jié)構(gòu)的正式定義是在VisualC++運(yùn)行時(shí)庫源代碼中的EXSUP.INC文件中:_EXCEPTION_REGISTRATIONstrucprevdd?handlerdd?_EXCEPTION_REGISTRATIONends

這個(gè)結(jié)構(gòu)在WINNT.H的NT_TIB結(jié)構(gòu)的定義中被稱為_EXCEPITON_REGISTARTION_RECORD。唉,沒有一個(gè)地方能夠找到_EXCEPTION_REGISTRATION_RECORD的定義,所以我不得不使用EXSUP.INC中這個(gè)匯編語言的結(jié)構(gòu)定義。這是我前面所說SEH未公開的一個(gè)證據(jù)。(讀者可以使用內(nèi)核調(diào)試器,如KD或SoftICE并加載調(diào)試符號來查看這個(gè)結(jié)構(gòu)的定義。下圖是在KD中的結(jié)果:

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

譯者注)

無論正在干什么,現(xiàn)在讓我們回到手頭的問題上來。當(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)在的問題是,操作系統(tǒng)到哪里去找EXCEPTION_REGISTATRION結(jié)構(gòu)呢?

要回復(fù)這個(gè)問題,記住結(jié)構(gòu)化異常處理是基于線程的這一點(diǎn)是十分有用的。也就是說,每個(gè)線程有它自己的異常處理回調(diào)函數(shù)。在1996年五月的UnderTheHood專欄中,我介紹了一個(gè)關(guān)鍵的Win32數(shù)據(jù)結(jié)構(gòu)——線程信息塊(ThreadInformation/EnvironmentBlock,TIB或TEB)。這個(gè)結(jié)構(gòu)的某些域在WindowsNT、Windows95、Win32s和OS/2上是一致的。TIB的第一個(gè)DWORD是一個(gè)指向線程的EXCEPTION_REGISTARTION結(jié)構(gòu)的指針。在基于Intel處理器的Win32平臺上,F(xiàn)S寄放器總是指向當(dāng)前的TIB。因此在FS:[0]處你可以找到一個(gè)指向EXCEPTION_REGISTARTION結(jié)構(gòu)的指針。(注:此結(jié)構(gòu)體并非_EXCEPTION_RECORD結(jié)構(gòu)體指針,而是

EXCEPTION_REGISTARTION異常鏈結(jié)構(gòu)體指針!此時(shí)棧頂ESP所指位置地址下的4個(gè)為_EXCEPTION_RECORD結(jié)構(gòu))

到現(xiàn)在為止,我們已經(jīng)有了足夠的認(rèn)識。當(dāng)異常發(fā)生時(shí),系統(tǒng)查找出錯(cuò)線程的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ù)

把這些小塊知識拼湊起來,我寫了一個(gè)小程序來演示上面這個(gè)對操作系統(tǒng)層面的結(jié)構(gòu)化異常處理的簡化描述,如圖3的MYSEH.CPP所示。它只有兩個(gè)函數(shù)。main函數(shù)使用了三個(gè)內(nèi)聯(lián)匯編塊。第一個(gè)內(nèi)聯(lián)匯編塊通過兩個(gè)PUSH指令(“PUSHhandler〞和“PUSHFS:[0]〞)在堆棧上創(chuàng)立了一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)。PUSHFS:[0]這條指令保存了從前的FS:[0]中的值作為這個(gè)結(jié)構(gòu)的一部分,但這在此刻并不重要。重要的是現(xiàn)在堆棧上有一個(gè)8字節(jié)的

EXCEPTION_REGISTRATION結(jié)構(gòu)。緊接著的下一條指令(MOVFS:[0],ESP)使線程信息塊中的第一個(gè)DWORD指向了新的EXCEPTION_REGISTRATION結(jié)構(gòu)。(注意堆棧操作)圖3MYSEH.CPP

//==================================================//MYSEH-MattPietrek1997

//MicrosoftSystemsJournal,January1997//FILE:MYSEH.CPP

//用命令行CLMYSEH.CPP編譯

//==================================================#defineWIN32_LEAN_AND_MEAN#include#includeDWORDscratch;EXCEPTION_DISPOSITION__cdecl

_except_handler(struct_EXCEPTION_RECORD*ExceptionRecord,void*EstablisherFrame,struct_CONTEXT*ContextRecord,void*DispatcherContext){

unsignedi;

//指明是我們讓流程轉(zhuǎn)到我們的異常處理程序的printf(\

//改變CONTEXT結(jié)構(gòu)中EAX的值,以便它指向可以成功進(jìn)寫操作的位置ContextRecord->Eax=(DWORD)//告訴操作系統(tǒng)重新執(zhí)行出錯(cuò)的指令

returnExceptionContinueExecution;}

intmain(){

DWORDhandler=(DWORD)_except_handler;__asm{

//創(chuàng)立EXCEPTION_REGISTRATION結(jié)構(gòu):pushhandler//handler函數(shù)的地址pushFS:[0]//前一個(gè)handler函數(shù)的地址

movFS:[0],ESP//安裝新的EXECEPTION_REGISTRATION結(jié)構(gòu)}__asm{

moveax,0//將EAX清零

mov[eax],1//寫EAX指向的內(nèi)存從而有意引發(fā)一個(gè)錯(cuò)誤}

printf(\

__asm{

//移去我們的EXECEPTION_REGISTRATION結(jié)構(gòu)moveax,[ESP]//獲取前一個(gè)結(jié)構(gòu)movFS:[0],EAX//安裝前一個(gè)結(jié)構(gòu)

addesp,8//將我們的EXECEPTION_REGISTRATION彈出堆棧}return0;}

如果你想知道我為什么把EXCEPTION_REGISTRATION結(jié)構(gòu)創(chuàng)立在堆棧上而不是使用全局變量,我有一個(gè)很好的理由可以解釋它。實(shí)際上,當(dāng)你使用編譯器的__try/__except語法結(jié)構(gòu)時(shí),編譯器自己也把EXCEPTION_REGISTRATION結(jié)構(gòu)創(chuàng)立在堆棧上。我只是簡單地向你展示了假使使用__try/__except時(shí)編譯器做法的簡化版。

回到main函數(shù),其次個(gè)__asm塊通過先把EAX寄放器清零(MOVEAX,0)然后把此寄放器的值作為內(nèi)存地址讓下一條指令(MOV[EAX],1)向此地址寫入數(shù)據(jù)而有意引發(fā)一個(gè)錯(cuò)誤。最終的__asm塊移除這個(gè)簡單的異常處理程序:它首先恢復(fù)了FS:[0]中從前的內(nèi)容,然后把EXCEPTION_REGISTRATION結(jié)構(gòu)彈出堆棧(ADDESP,8)。

現(xiàn)在假若你運(yùn)行MYSEH.EXE,就會看到整個(gè)過程。當(dāng)MOV[EAX],1這條指令執(zhí)行時(shí),它引發(fā)一個(gè)訪問違規(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)說過)壓入堆棧,接著調(diào)用_except_handler函數(shù)。

一旦進(jìn)入_except_handler,這段代碼首先通過一個(gè)printf語句說明“哈!是我讓它轉(zhuǎn)到這里的!〞。接著,_except_handler修復(fù)了引發(fā)錯(cuò)誤的問題——即EAX寄放器指向了一個(gè)不能寫的內(nèi)存地址(地址0)。修復(fù)方法就是改變CONTEXT結(jié)構(gòu)中的EAX的值使它指向一個(gè)允許寫的位置。在這個(gè)簡單的程序中,我專門為此設(shè)置了一個(gè)DWORD變量(scratch)。_except_handler函數(shù)最終的動作是返回ExceptionContinueExecution這個(gè)值,它在EXCPT.H文件中定義。

當(dāng)操作系統(tǒng)看到返回值為ExceptionContinueExecution時(shí),它將其理解為你已經(jīng)修復(fù)了問題,而引起錯(cuò)誤的那條指令應(yīng)當(dāng)被重新執(zhí)行。由于我的_except_handler函數(shù)已經(jīng)讓EAX寄放器指向一個(gè)合法的內(nèi)存,MOV[EAX],1指令再次執(zhí)行,這次main函數(shù)一切正常???,這也并不繁雜,不是嗎?

移向更深處

有了這個(gè)最簡單的情景之后,讓我們回去填補(bǔ)那些空白。雖然這個(gè)異?;卣{(diào)機(jī)制很好,但它并不是一個(gè)完美的解決方案。對于稍微繁雜一些的應(yīng)用程序來說,僅用一個(gè)函數(shù)就能處理程序中任何地方都可能發(fā)生的異常是相當(dāng)困難的。一個(gè)更實(shí)用的方案應(yīng)當(dāng)是有多個(gè)異常處理例程,每個(gè)例程針對程序中的一部分。實(shí)際上,操作系統(tǒng)提供的正是這個(gè)功能。

還記得系統(tǒng)用來查找異?;卣{(diào)函數(shù)的EXCEPTION_REGISTRATION結(jié)構(gòu)嗎?這個(gè)結(jié)構(gòu)的第一個(gè)成員,稱為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),依次類推。簡單地說,就是有一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表。線程信息塊的第一個(gè)DWORD(在基于IntelCPU的機(jī)器上是FS:[0])指向這個(gè)鏈表的頭部。

操作系統(tǒng)要這個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表做什么呢?原來,當(dāng)異常發(fā)生時(shí),系統(tǒng)遍歷這個(gè)鏈表以查找一個(gè)(其異常處理程序)同意處理這個(gè)異常的EXCEPTION_REGISTRATION結(jié)構(gòu)。在MYSEH.CPP中,異常處理程序通過返回ExceptionContinueExecution表示它同意處理

這個(gè)異常。異?;卣{(diào)函數(shù)也可以拒絕處理這個(gè)異常。在這種狀況下,系統(tǒng)移向鏈表的下一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)并詢問它的異?;卣{(diào)函數(shù),看它是否同意處理這個(gè)異常。圖4顯示了這個(gè)過程。一旦系統(tǒng)找到一個(gè)處理這個(gè)異常的回調(diào)函數(shù),它就中止遍歷鏈表。

圖4查找一個(gè)處理異常的EXCEPTION_REGISTRATION結(jié)構(gòu)

圖5的MYSEH2.CPP就是一個(gè)異常處理函數(shù)不處理某個(gè)異常的例子。為了使代碼盡量簡單,我使用了編譯器層面的異常處理。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ù)通過向一個(gè)NULL指針?biāo)赶虻膬?nèi)存處寫入數(shù)據(jù)而有意引發(fā)一個(gè)錯(cuò)誤:

*(PDWORD)0=0;

這個(gè)異常處理回調(diào)函數(shù),同樣被稱為_except_handler,卻與前面的那個(gè)截然不同。它首先打印出ExceptionRecord結(jié)構(gòu)中的異常代碼和標(biāo)志,這個(gè)結(jié)構(gòu)的地址是作為一個(gè)指針參數(shù)被這個(gè)函數(shù)接收的。打印出異常標(biāo)志的原因一會兒就明白了。由于_except_handler函數(shù)并沒有計(jì)劃修復(fù)出錯(cuò)的代碼,因此它返回ExceptionContinueSearch。這導(dǎo)致操作系統(tǒng)繼續(xù)在

EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表中探尋下一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)。接下來安裝的異常回調(diào)函數(shù)是針對main函數(shù)中的__try/__except塊的。__except塊簡單地打印出“Caughttheexceptioninmain()〞。此時(shí)我們只是簡單地忽略這個(gè)異常來說明我們已經(jīng)處理了它。圖5MYSEH2.CPP

//=================================================//MYSEH2-MattPietrek1997

//MicrosoftSystemsJournal,January1997//FILE:MYSEH2.CPP

//使用命令行CLMYSEH2.CPP編譯

//=================================================#defineWIN32_LEAN_AND_MEAN#include#includeEXCEPTION_DISPOSITION__cdecl_except_handler(

struct_EXCEPTION_RECORD*ExceptionRecord,void*EstablisherFrame,struct_CONTEXT*ContextRecord,void*DispatcherContext){

printf(\ExceptionRecord->ExceptionCode,ExceptionRecord->ExceptionFlags);if(ExceptionRecord->ExceptionFlags}

voidHomeGrownFrame(void){

DWORDhandler=(DWORD)_except_handler;__asm{

//創(chuàng)立EXCEPTION_REGISTRATION結(jié)構(gòu):pushhandler//handler函數(shù)的地址pushFS:[0]//前一個(gè)handler函數(shù)的地址

movFS:[0],ESP//安裝新的EXECEPTION_REGISTRATION結(jié)構(gòu)}

*(PDWORD)0=0;//寫入地址0,從而引發(fā)一個(gè)錯(cuò)誤printf(\__asm{

//移去我們的EXECEPTION_REGISTRATION結(jié)構(gòu)moveax,[ESP]//獲取前一個(gè)結(jié)構(gòu)movFS:[0],EAX//安裝前一個(gè)結(jié)構(gòu)

addesp,8//把我們EXECEPTION_REGISTRATION結(jié)構(gòu)彈出堆棧}}

intmain(){__try{

HomeGrownFrame();}

__except(EXCEPTION_EXECUTE_HANDLER){

printf(\}return0;}

這里的關(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)不會執(zhí)行。從某種程度上說,使用結(jié)構(gòu)化異常處理與使用setjmp和longjmp運(yùn)行時(shí)庫函數(shù)有些類似。

假使你運(yùn)行MYSEH2,會發(fā)現(xiàn)其輸出有些奇怪。看起來好像調(diào)用了兩次_except_handler函數(shù)。根據(jù)你現(xiàn)有的知識,第一次調(diào)用當(dāng)然可以完全理解。但是為什么會有其次次呢?HomeGrownhandler:ExceptionCode:C0000005ExceptionFlags0

HomeGrownhandler:ExceptionCode:C0000027ExceptionFlags2EH_UNWINDINGCaughttheExceptioninmain()

比較一下以“HomeGrownHandler〞開頭的兩行,就會看出它們之間有明顯的區(qū)別。第一次異常標(biāo)志是0,而其次次是2。這把我們帶入到了展開(Unwinding)的世界中。實(shí)際上,當(dāng)一個(gè)異常處理回調(diào)函數(shù)拒絕處理某個(gè)異常時(shí),它會被再一次調(diào)用。但是這次回調(diào)并不是馬上發(fā)生的。這有點(diǎn)繁雜。我需要把異常發(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的定義在VisualC++運(yùn)行時(shí)庫源代碼文件EXCEPT.INC中,但Win32SDK中并沒有與之等價(jià)的定義。)

EH_UNWINDING表示什么意思呢?原來,當(dāng)一個(gè)異常處理回調(diào)函數(shù)被其次次調(diào)用時(shí)(帶EH_UNWINDING標(biāo)志),操作系統(tǒng)給這個(gè)函數(shù)一個(gè)最終清理的機(jī)遇。什么樣的清理呢?一個(gè)絕好的例子是C++類的析構(gòu)函數(shù)。當(dāng)一個(gè)函數(shù)的異常處理程序拒絕處理某個(gè)異常時(shí),尋常執(zhí)行流程并不會正常地從那個(gè)函數(shù)退出?,F(xiàn)在,想像一個(gè)定義了一個(gè)C++類的實(shí)例作為局部變量的函數(shù)。C++規(guī)范規(guī)定析構(gòu)函數(shù)必需被調(diào)用。這帶EH_UNWINDING標(biāo)志的其次次回調(diào)就給這個(gè)函數(shù)一個(gè)機(jī)遇去做一些類似于調(diào)用析構(gòu)函數(shù)和__finally塊之類的清理工作。(來2次異常的原因,標(biāo)志被設(shè)置為EH_UNWINDING(2))

在異常已經(jīng)被處理完畢,并且所有前面的異常幀都已經(jīng)被展開之后,流程從處理異常的那個(gè)回調(diào)函數(shù)決定的地方開始繼續(xù)執(zhí)行。一定要記住,僅僅把指令指針設(shè)置到所需的代碼處就開始執(zhí)行是不行的。流程恢復(fù)執(zhí)行處的代碼的堆棧指針和棧幀指針(在IntelCPU上是ESP和EBP)也必需被恢復(fù)成它們在處理這個(gè)異常的函數(shù)的棧幀上的值。因此,這個(gè)處理異常的回調(diào)函數(shù)必需負(fù)責(zé)把堆棧指針和棧幀指針恢復(fù)成它們在包含處理這個(gè)異常的SEH代碼的函數(shù)的堆棧上的值。(就是在那個(gè)異常處理函數(shù)處理了ESP和EBP也必需回復(fù)到該函數(shù)上,處理完后FS:[0]也必需指到該SEH結(jié)構(gòu)上)

通常,展開操作導(dǎo)致堆棧上處理異常的幀以下的堆棧區(qū)域上的所有內(nèi)容都被移除了,就好像我們從來沒有調(diào)用過這些函數(shù)一樣。展開的另外一個(gè)效果就是EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表上處理異常的那個(gè)結(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從異常展開

救命!沒有人處理它!

迄今為止,我實(shí)際上一直在假設(shè)操作系統(tǒng)總是能在EXCEPTION_REGISTRATION結(jié)構(gòu)鏈表中找到一個(gè)異常處理程序。假使找不到怎么辦呢?實(shí)際上,這幾乎不可能發(fā)生。由于操作系統(tǒng)暗中已經(jīng)為每個(gè)線程都提供了一個(gè)默認(rèn)的異常處理程序。這個(gè)默認(rèn)的異常處理程序總是鏈表的最終一個(gè)結(jié)點(diǎn),并且它總是選擇處理異常。它進(jìn)行的操作與其它正常的異常處理回調(diào)函數(shù)有些不同,下面我會說明。

讓我們來看一下系統(tǒng)是在什么時(shí)候插入了這個(gè)默認(rèn)的、最終一個(gè)異常處理程序。很明顯它需要在線程執(zhí)行的早期,在任何用戶代碼開始執(zhí)行之前。圖7是我為BaseProcessStart函數(shù)寫的偽代碼,它是WindowsNTKERNEL32.DLL的一個(gè)內(nèi)部例程。這個(gè)函數(shù)帶一個(gè)參數(shù)——線程入口點(diǎn)函數(shù)的地址。BaseProcessStart運(yùn)行在新進(jìn)程的環(huán)境中,并且它調(diào)用這個(gè)進(jìn)程的第一個(gè)線程的入口點(diǎn)函數(shù)。

圖7BaseProcessStart偽代碼

BaseProcessStart(PVOIDlpfnEntryPoint){

DWORDretValue;DWORDcurrentESP;DWORDexceptionCode;currentESP=ESP;__try{

ThreadQuerySetWin32StartAddress,retValue=lpfnEntryPoint();ExitThread(retValue);}

__except(//過濾器表達(dá)式代碼

exceptionCode=GetExceptionInformation(),

UnhandledExceptionFilter(GetExceptionInformation())){

ESP=currentESP;

if(!_BaseRunningInServerProcess)//普通進(jìn)程ExitProcess(exceptionCode);else//服務(wù)

ExitThread(exceptionCode);

}}

在上面的偽代碼中,注意對lpfnEntryPoint的調(diào)用被一個(gè)__try和__except塊封裝著。就是這個(gè)__try塊安裝了默認(rèn)的、異常處理程序鏈表上的最終一個(gè)異常處理程序。所有后來注冊的異常處理程序都被安裝在鏈表中這個(gè)結(jié)點(diǎn)的前面。假使lpfnEntryPoint函數(shù)返回,那么說明線程一直運(yùn)行到完成并且沒有引發(fā)異常。這時(shí)BaseProcessStart調(diào)用ExitThread使線程退出。假使線程引發(fā)了一個(gè)異常但是沒有異常處理程序來處理它時(shí)怎么辦呢?這時(shí),執(zhí)行流程轉(zhuǎn)到__except關(guān)鍵字后面的括號中。在BaseProcessStart中,這段代碼調(diào)

InformationThread(GetCurrentThread(),

用UnhandledExceptionFilter這個(gè)API,后面我會講到它?,F(xiàn)在對于我們來說,重要的是UnhandledExceptionFilter這個(gè)API包含了默認(rèn)的異常處理程序。

如果UnhandledExceptionFilter返回EXCEPTION_EXECUTE_HANDLER,這時(shí)

BaseProcessStart中的__except塊開始執(zhí)行。而__except塊所做的只是調(diào)用ExitProcess函數(shù)去終止當(dāng)前進(jìn)程。稍微想一下你就會理解了。常識告訴我們,假使一個(gè)進(jìn)程引發(fā)了一個(gè)錯(cuò)誤而沒有異常處理程序去處理它,這個(gè)進(jìn)程就會被系統(tǒng)終止。你在偽代碼中看到的正是這些。對于上面所說的我還有一點(diǎn)要補(bǔ)充。假使引發(fā)錯(cuò)誤的線程是作為服務(wù)來運(yùn)行的,并且是基于線程的服務(wù),那么__except塊并不調(diào)用ExitProcess,相反,它調(diào)用ExitThread。不能僅僅由于一個(gè)服務(wù)出錯(cuò)就終止整個(gè)服務(wù)進(jìn)程。

UnhandledExceptionFilter中的默認(rèn)異常處理程序都做了什么呢?當(dāng)我在一個(gè)技術(shù)講座上問起這個(gè)問題時(shí),響應(yīng)者寥寥無幾。幾乎沒有人知道當(dāng)未處理異常發(fā)生時(shí),終究操作系統(tǒng)的默認(rèn)行為是什么。簡單地演示一下這個(gè)默認(rèn)的行為可能會讓好多人豁然開朗。我運(yùn)行一個(gè)有意引發(fā)錯(cuò)誤的程序,其結(jié)果如下(見圖8)。

圖8未處理異常對話框

表面上看,UnhandledExceptionFilter顯示了一個(gè)對話框告訴你發(fā)生了一個(gè)錯(cuò)誤。這時(shí),你被給予了一個(gè)機(jī)遇或者終止出錯(cuò)進(jìn)程,或者調(diào)試它。但是幕后發(fā)生了大量事情,我會在文章最終詳細(xì)陳述它。

正如我讓你看到的那樣,當(dāng)異常發(fā)生時(shí),用戶寫的代碼可以(并且尋常是這樣)獲得機(jī)遇執(zhí)行。同樣,在展開操作期間,用戶寫的代碼也可以執(zhí)行。這個(gè)用戶寫的代碼可能也有錯(cuò)誤,并且可能引發(fā)另一個(gè)異常。由于這個(gè)原因,異常處理回調(diào)函數(shù)也可以返回另外兩個(gè)

值:ExceptionNestedException和ExceptionCollidedUnwind。很明顯,它們很重要。但這是十分繁雜的問題,我并不計(jì)劃在這里涉及它們。要想理解其中的一些基本問題太困難了。

編譯器層面的SEH

雖然我在前面偶爾也使用了__try和__except,但迄今為止幾乎我寫的所有內(nèi)容都是關(guān)于操作系統(tǒng)方面對SEH的實(shí)現(xiàn)。然而看一下我那兩個(gè)使用操作系統(tǒng)的原始SEH的小程序別扭的樣子,編譯器對這個(gè)功能進(jìn)行封裝實(shí)在是十分有必要的?,F(xiàn)在讓我們來看一下VisualC++是如何在操作系統(tǒng)對SEH功能實(shí)現(xiàn)的基礎(chǔ)上來創(chuàng)立它自己的結(jié)構(gòu)化異常處理支持的。

在我們繼續(xù)下去之前,記住其它編譯器可以使用原始的系統(tǒng)SEH來做一些完全不同的事情這一點(diǎn)是十分重要的。并沒有什么規(guī)定編譯器必需實(shí)現(xiàn)Win32SDK文檔中描述的__try/__except模型。例如VisualBasic5.0在它的運(yùn)行時(shí)代碼中使用了結(jié)構(gòu)化異常處理,但是那里的數(shù)據(jù)結(jié)構(gòu)和算法與我這里要講的完全不同。

假使你把Win32SDK文檔中關(guān)于結(jié)構(gòu)化異常處理方面的內(nèi)容從頭到尾讀一遍,一定會遇到下面所謂的“基于幀〞的異常處理程序模型:__try{

//這里是被保護(hù)的代碼

}

__except(過濾器表達(dá)式){//這里是異常處理程序代碼}

簡單地說,在一個(gè)函數(shù)中,一個(gè)__try塊中的所有代碼就通過創(chuàng)立在這個(gè)函數(shù)的堆棧幀上的一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)來保護(hù)。在函數(shù)的入口處,這個(gè)新的

EXCEPTION_REGISTRATION結(jié)構(gòu)被放在異常處理程序鏈表的頭部。在__try塊終止后,相應(yīng)的EXCEPTION_REGISTRATION結(jié)構(gòu)從這個(gè)鏈表的頭部被移除。正如我前面所說,異常處理程序鏈表的頭部被保存在FS:[0]處。因此,假使你在調(diào)試器中單步跟蹤時(shí)看到類似下面的指令時(shí)

MOVDWORDPTRFS:[00000000],ESP

或者

MOVDWORDPTRFS:[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ù)語來說,異常處理回調(diào)函數(shù)相當(dāng)于過濾器表達(dá)式(filter-expression)代碼。實(shí)際上,過濾器表達(dá)式就是__except關(guān)鍵字后面的小括號中的代碼。就是這個(gè)過濾器表達(dá)式代碼決定了后面的大括號中的代碼是否執(zhí)行。

由于過濾器表達(dá)式代碼是你自己寫的,你當(dāng)然可以決定在你的代碼中的某個(gè)地方是否處理某個(gè)特定的異常。它可以簡單的只是一句“EXCEPTION_EXECUTE_HANDLER〞,也可以先調(diào)用一個(gè)把?計(jì)算到20,000,000位的函數(shù),然后再返回一個(gè)值來告訴操作系統(tǒng)下一步做什么。隨你的便。關(guān)鍵是你的過濾器表達(dá)式代碼必需是我前面講的有效的異常處理回調(diào)函數(shù)。

我方才講的雖然相當(dāng)簡單,但那只不過是隔著有色玻璃看世界罷了。實(shí)際它是十分繁雜的。首先,你的過濾器表達(dá)式代碼并不是被操作系統(tǒng)直接調(diào)用的。事實(shí)上,各

個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)的handler域都指向了同一個(gè)函數(shù)。這個(gè)函數(shù)在VisualC++的運(yùn)行時(shí)庫中,它被稱為__except_handler3。正是這個(gè)__except_handler3調(diào)用了你的過濾器表達(dá)式代碼,我一會兒再接著說它。

對我前面的簡單描述需要修正的另一個(gè)地方是,并不是每次進(jìn)入或退出一個(gè)__try塊時(shí)就創(chuàng)立或撤銷一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)。相反,在使用SEH的任何函數(shù)中只創(chuàng)立一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)。換句話說,你可以在一個(gè)函數(shù)中使用多個(gè)__try/__except塊,但是在堆棧上只創(chuàng)立一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)。同樣,你可以在一個(gè)函數(shù)中嵌套使用__try塊,但VisualC++仍舊只是創(chuàng)立一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)。

如果整個(gè)EXE或DLL只需要單個(gè)的異常處理程序(__except_handler3),同時(shí),假使單個(gè)的EXCEPTION_REGISTRATION結(jié)構(gòu)就能處理多個(gè)__try塊的話,很明顯,這里面還有好多東西我們不知道。這個(gè)技巧是通過一個(gè)尋常狀況下看不到的表中的數(shù)據(jù)來完成的。由于本文的目的就是要深入摸索結(jié)構(gòu)化異常處理,那就讓我們來看一看這些數(shù)據(jù)結(jié)構(gòu)吧。

擴(kuò)展的異常處理幀VisualC++的SEH實(shí)現(xiàn)并沒有使用原始的EXCEPTION_REGISTRATION結(jié)構(gòu)。它在這個(gè)結(jié)構(gòu)的

末尾添加了一些附加數(shù)據(jù)。這些附加數(shù)據(jù)正是允許單個(gè)函數(shù)(__except_handler3)處理所有異常并將執(zhí)行流程傳遞到相應(yīng)的過濾器表達(dá)式和__except塊的關(guān)鍵。我在VisualC++運(yùn)行時(shí)庫源代碼中的EXSUP.INC文件中找到了有關(guān)VisualC++擴(kuò)展的EXCEPTION_REGISTRATION結(jié)構(gòu)格式的線索。在這個(gè)文件中,你會看到以下定義(已經(jīng)被解釋掉了):;struct_EXCEPTION_REGISTRATION{

;struct_EXCEPTION_REGISTRATION*prev;

;void(*handler)(PEXCEPTION_RECORD,;PEXCEPTION_REGISTRATION,;PCONTEXT,

;PEXCEPTION_RECORD);;structscopetable_entry*scopetable;;inttrylevel;;int_ebp;

;PEXCEPTION_POINTERSxpointers;;};

在前面你已經(jīng)見過前兩個(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)的一部分并非偶然。它是通過PUSHEBP這條指令被包含進(jìn)這個(gè)結(jié)構(gòu)中的,而大多數(shù)函數(shù)開頭都是這條指令(尋常編譯器并不為使用FPO優(yōu)化的函數(shù)生成標(biāo)準(zhǔn)的堆棧幀,這樣其第一條指令可能不是PUSHEBP。但是假使使用了SEH的話,那么無論你是否使用了FPO優(yōu)化,編譯器一定生成標(biāo)準(zhǔn)的堆棧幀)。這條指令可以使EXCEPTION_REGISTRATION結(jié)構(gòu)中所有其它的域都可以用一個(gè)相對于棧幀指針(EBP)的負(fù)偏移來訪問。例如trylevel域在[EBP-04]處,scopetable指針在[EBP-08]處,等等。(也就是說,這個(gè)結(jié)構(gòu)是從[EBP-10H]處開始的。)

緊跟著擴(kuò)展的EXCEPTION_REGISTRATION結(jié)構(gòu)下面,VisualC++壓入了另外兩個(gè)值。緊跟著(即[EBP-14H]處)的一個(gè)DWORD,是為一個(gè)指向EXCEPTION_POINTERS結(jié)構(gòu)(一個(gè)標(biāo)準(zhǔn)的Win32結(jié)構(gòu))的指針?biāo)4娴目臻g。這個(gè)指針就是你調(diào)用GetExceptionInformation這個(gè)API時(shí)返回的指針。盡管SDK文檔示意GetExceptionInformation是一個(gè)標(biāo)準(zhǔn)的Win32API,但事實(shí)上它是一個(gè)編譯器內(nèi)聯(lián)函數(shù)。當(dāng)你調(diào)用這個(gè)函數(shù)時(shí),VisualC++生成以下代碼:

MOVEAX,DWORDPTR[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)VisualC++為GetExceptionCode函數(shù)生成下面的指令時(shí),它終究是想干什么?我把這個(gè)問題留給讀者。(現(xiàn)在就能理解為什么SDK文檔提醒我們要注意這兩個(gè)函數(shù)的使用范圍了。)MOVEAX,DWORDPTR[EBP-14];執(zhí)行完畢,EAX指向EXCEPTION_POINTERS結(jié)構(gòu)MOVEAX,DWORDPTR[EAX];執(zhí)行完畢,EAX指向EXCEPTION_RECORD結(jié)構(gòu)MOVEAX,DWORDPTR[EAX];執(zhí)行完畢,EAX中是ExceptionCode的值

現(xiàn)在回到擴(kuò)展的EXCEPTION_REGISTRATION結(jié)構(gòu)上來。在這個(gè)結(jié)構(gòu)開始前的8個(gè)字節(jié)處(即[EBP-18H]處),VisualC++保存了一個(gè)DWORD來保存所有prolog代碼執(zhí)行完畢之后的堆棧指針(ESP)的值(實(shí)際生成的指令為MOVDWORDPTR[EBP-18H],ESP)。這個(gè)DWORD中保存的值是函數(shù)執(zhí)行時(shí)ESP寄放器的正常值(除了在準(zhǔn)備調(diào)用其它函數(shù)時(shí)把參數(shù)壓入堆棧這個(gè)過程會改變ESP寄放器的值并在函數(shù)返回時(shí)恢復(fù)它的值外,函數(shù)在執(zhí)行過程中一般不改變ESP寄放器的值)。

看起來好像我一下子給你灌輸了太多的信息,這點(diǎn)我承認(rèn)。在繼續(xù)下去之前,讓我們先暫停,來回想一下VisualC++為使用結(jié)構(gòu)化異常處理的函數(shù)生成的標(biāo)準(zhǔn)異常堆棧幀,它看起來像下面這個(gè)樣子:EBP-00_ebpEBP-04trylevel

EBP-08scopetable數(shù)組指針EBP-0Chandler函數(shù)地址

EBP-10指向前一個(gè)EXCEPTION_REGISTRATION結(jié)構(gòu)EBP-14GetExceptionInformationEBP-18棧幀中的標(biāo)準(zhǔn)ESP

在操作系統(tǒng)看來,只存在組成原始EXCEPTION_REGISTRATION結(jié)構(gòu)的兩個(gè)域:

即[EBP-10h]處的prev指針和[EBP-0Ch]處的handler函數(shù)指針。棧幀中的其它所有內(nèi)容是針對于VisualC++的。把這個(gè)VisualC++生成的標(biāo)準(zhǔn)異常堆棧幀記到腦子里之后,讓我們來看一下真正實(shí)現(xiàn)編譯器層面SEH的這個(gè)VisualC++運(yùn)行時(shí)庫例程——__except_handler3。

__except_handler3和scopetable

我真的很希望讓你看一看VisualC++運(yùn)行時(shí)庫源代碼,讓你自己好好研究一下

__except_handler3函數(shù),但是我辦不到。由于Microsoft并沒有提供。在這里你就將就著看一下我為__except_handler3函數(shù)寫的偽代碼吧(如圖9所示)。圖9__except_handler3函數(shù)的偽代碼

int__except_handler3(

struct_EXCEPTION_RECORD*pExceptionRecord,structEXCEPTION_REGISTRATION*pRegistrationFrame,struct_CONTEXT*pContextRecord,void*pDispatcherContext)

{

LONGfilterFuncRet;LONGtrylevel;

EXCEPTION_POINTERSexceptPtrs;PSCOPETABLEpScopeTable;

CLD//將方向標(biāo)志復(fù)位(不測試任何條件?。?/p>

//假使沒有設(shè)置EXCEPTION_UNWINDING標(biāo)志或EXCEPTION_EXIT_UNWIND標(biāo)志//說明這是第一次調(diào)用這個(gè)處理程序(也就是說,并非處于異常展開階段)if(!(pExceptionRecord->ExceptionFlags

exceptPtrs.ContextRecord=pContextRecord;//把前面定義的EXCEPTION_POINTERS結(jié)構(gòu)的地址放在比//establisher棧幀低4個(gè)字節(jié)的位置上。參考前面我講//的編譯器為GetExceptionInformation生成的匯編代碼*(PDWORD)((PBYTE)pRegistrationFrame-4)=//獲取初始的“trylevel〞值

trylevel=pRegistrationFrame->trylevel;//獲取指向scopetable數(shù)組的指針

scopeTable=pRegistrationFrame->scopetable;search_for_handler:

if(pRegistrationFrame->trylevel!=TRYLEVEL_NONE){

if(pRegistrationFrame->scopetable[trylevel].lpfnFilter){

PUSHEBP//保存這個(gè)棧幀指針

//!?。∈种匾。?!切換回原來的EBP。正是這個(gè)操作才使得//棧幀上的所有局部變量能夠在異常發(fā)生后依舊保持它的值不變。EBP=//調(diào)用過濾器函數(shù)

filterFuncRet=scopetable[trylevel].lpfnFilter();

POPEBP//恢復(fù)異常處理程序的棧幀指針if(filterFuncRet!=EXCEPTION_CONTINUE_SEARCH){

if(filterFuncRetscopetable;

//讓操作系統(tǒng)清理已經(jīng)注冊的棧幀,這會使本函數(shù)被遞歸調(diào)用__global_unwind2(pRegistrationFrame);

//一旦執(zhí)行到這里,除最終一個(gè)棧幀外,所有的棧幀已經(jīng)//被清理完畢,流程要從最終一個(gè)棧幀繼續(xù)執(zhí)行EBP=

__local_unwind2(pRegistrationFrame,trylevel);//NLG=\__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)用并不會返回

pRegistrationFrame->scopetable[trylevel].lpfnHandler();}

}

scopeTable=pRegistrationFrame->scopetable;trylevel=scopeTable->previousTryLevel;gotosearch_for_handler;}

else//trylevel==TRYLEVEL_NONE{

returnExceptionContinueSearch;}}

else//設(shè)置了EXCEPTION_UNWINDING標(biāo)志或EXCEPTION_EXIT_UNWIND標(biāo)志{

PUSHEBP//保存EBP

EBP=//為調(diào)用__local_unwind2設(shè)置EBP__local_unwind2(pRegistrationFrame,TRYLEVEL_NONE)

POPEBP//恢復(fù)EBP

returnExceptionContinueSearch;}}

雖然__except_handler3的代碼看起來好多,但是記住一點(diǎn):它只是一個(gè)我在文章開頭講過的異常處理回調(diào)函數(shù)。它同MYSEH.EXE和MYSEH2.EXE中的異?;卣{(diào)函數(shù)都帶有同樣的四個(gè)參數(shù)。__except_handler3大體上可以由第一個(gè)if語句分為兩部分。這是由于這個(gè)函數(shù)可以在兩種狀況下被調(diào)用,一次是正常調(diào)用,另一次是在展開階段。其中大部分是在非展開階段的回調(diào)。

__except_handler3一開始就在堆棧上創(chuàng)立了一個(gè)EXCEPTION_POINTERS結(jié)構(gòu),并用它的兩個(gè)參數(shù)來對這個(gè)結(jié)構(gòu)進(jìn)行初始化。我在偽代碼中把這個(gè)結(jié)構(gòu)稱為exceptPrts,它的地址被放在[EBP-14h]處。你回憶一下前面我講的編譯器為GetExceptionInformation和GetExceptionCode函數(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)如下:typedefstruct_SCOPETABLE{

DWORDpreviousTryLevel;DWORDlpfnFilter;DWORDlpfnHandler;}SCOPETABLE,*PSCOPETABLE;

SCOPETABLE結(jié)構(gòu)中的其次個(gè)成員和第三個(gè)成員比較簡單理解。它們分別是過濾器表達(dá)式代碼的地址和相應(yīng)的__except塊的地址。但是prviousTryLevel成員有點(diǎn)繁雜??傊痪湓?,它用于嵌套的__try塊。這里的關(guān)鍵是函數(shù)中的每個(gè)__try塊都有一個(gè)相應(yīng)的SCOPETABLE結(jié)構(gòu)。

正如我前面所說,當(dāng)前的trylevel指定了要使用的scopetable數(shù)組的哪一個(gè)元素,最終也就是指定了過濾器表達(dá)式和__except塊的地址?,F(xiàn)在想像一下兩個(gè)__try塊嵌套的情形。假使內(nèi)層__try塊的過濾器表達(dá)式不處理某個(gè)異常,那外層__try塊的過濾器表達(dá)式就必需處理它。那現(xiàn)在要問,__except_handler3是如何知道SCOPETABLE數(shù)組的哪個(gè)元素相應(yīng)于外層的__try塊的呢?答案是:外層__try塊的索引由SCOPETABLE結(jié)構(gòu)的previousTryLevel域給出。利用這種機(jī)制,你可以嵌套任意層的__try塊。previousTryLevel域就好像是一個(gè)函數(shù)中所有可能的異常處理程序構(gòu)成的線性鏈表中的結(jié)點(diǎn)一樣。假使trylevel的值為0xFFFFFFFF(實(shí)際上就是-1,這個(gè)值在EXSUP.INC中被定義為TRYLEVEL_NONE),標(biāo)志著這個(gè)鏈表終止。

回到__except_handler3的代碼中。在獲取了當(dāng)前的trylevel之后,它就調(diào)用相應(yīng)的SCOPETABLE結(jié)構(gòu)中的過濾器表達(dá)式代碼。假使過濾器表達(dá)式返回EXCEPTION_CONTINUE_SEARCH,__exception_handler3移向SCOPETABLE數(shù)組中的下一個(gè)元素,這個(gè)元素的索引由

previousTryLevel域給出。假使遍歷完整個(gè)線性鏈表(還記得嗎?這個(gè)鏈表是由于在一個(gè)函數(shù)內(nèi)部嵌套使用__try塊而形成的)都沒有找四處理這個(gè)異常的代碼,__except_handler3返回DISPOSITION_CONTINUE_SEARCH(原文如此,但根據(jù)_except_handler函數(shù)的定義,這個(gè)返回值應(yīng)當(dāng)為ExceptionContinueSearch。實(shí)際上這兩個(gè)常量的值是一樣的。我在偽代碼中已經(jīng)將其改正過來了),這導(dǎo)致系統(tǒng)移向下一個(gè)EXCEPTION_REGISTRATION幀(這個(gè)鏈表是由于函數(shù)嵌套調(diào)用而形成的)。

假使過濾器表達(dá)式返回EXCEPTION_EXECUTE_HANDLER,這意味著異常應(yīng)當(dāng)由相應(yīng)的__except塊處理。它同時(shí)也意味著所有前面的EXCEPTION_REGISTRATION幀都應(yīng)當(dāng)從鏈表中移除,

并且相應(yīng)的__except塊都應(yīng)當(dāng)被執(zhí)行。第一個(gè)任務(wù)通過調(diào)用__global_unwind2來完成的,后面我會講到這個(gè)函數(shù)。跳過這中間的一些清理代碼,流程離開__except_handler3轉(zhuǎn)向__except塊。令人奇怪的是,流程并不從__except塊中返回,雖然是__except_handler3使用CALL指令調(diào)用了它。

當(dāng)前的trylevel值是如何被設(shè)置的呢?它實(shí)際上是由編譯器隱含處理的。編譯器十分精明地修改這個(gè)擴(kuò)展的EXCEPTION_REGISTRATION結(jié)構(gòu)中的trylevel域的值(實(shí)際上是生成修改這個(gè)域的值的代碼)。假使你檢查編譯器為使用SEH的函數(shù)生成的匯編代碼,就會在不同的地方都看到修改這個(gè)位于[EBP-04h]處的trylevel域的值的代碼。

__except_handler3是如何做到既通過CALL指令調(diào)用__except塊而又不讓執(zhí)行流程返回呢?由于CALL指令要向堆棧中壓入了一個(gè)返回地址,你可以想象這有可能破壞堆棧。假使你檢查一下編譯器為__except塊生成的代碼,你會發(fā)現(xiàn)它做的第一件事就是將

EXCEPTION_REGISTRATION結(jié)構(gòu)下面8個(gè)字節(jié)處(即[EBP-18H]處)的一個(gè)DWORD值加載到ESP寄放器中(實(shí)際代碼為MOVESP,DWORDPTR[EBP-18H]),這個(gè)值是在函數(shù)的prolog代碼中被保存在這個(gè)位置的(實(shí)際代碼為MOVDWORDPTR[EBP-18H],ESP)。

ShowSEHFrames程序

如果你現(xiàn)在覺得已經(jīng)被EXCEPTION_REGISTRATION、scopetable、trylevel、過濾器表達(dá)式以及展開等等之類的詞搞得暈頭轉(zhuǎn)向的話,那和我最初的感覺一樣。但是編譯器層面的結(jié)構(gòu)化異常處理方面的知識并不適合一點(diǎn)一點(diǎn)的學(xué)。除非你從整體上理解它,否則有好多內(nèi)容單獨(dú)看并沒有什么意義。當(dāng)面對大堆的理論時(shí),我最自然的做法就是寫一些應(yīng)用我學(xué)到的理論方面的程序。假使它能夠依照預(yù)料的那樣工作,我就知道我的理解(尋常)是正確的。

圖10是ShowSEHFrame.EXE的源代碼。它使用__try/__except塊設(shè)置了好幾個(gè)VisualC++SEH幀。然后它顯示每一個(gè)幀以及VisualC++為每個(gè)幀創(chuàng)立的scopetable的相關(guān)信息。這個(gè)程序本身并不生成也不依靠任何異常。相反,我使用了多個(gè)__try塊以強(qiáng)制VisualC++生成多個(gè)EXCEPTION_REGISTRATION幀以及相應(yīng)的scopetable。圖10ShowSEHFrames.CPP

//=========================================================//ShowSEHFrames-MattPietrek1997//MicrosoftSystemsJournal,February1997//FILE:ShowSEHFrames.CPP

//使用命令行CLShowSehFrames.CPP進(jìn)行編譯

//=========================================================#defineWIN32_LEAN_AND_MEAN#include#include#pragmahdrstop

////本程序僅適用于VisualC++,它使用的數(shù)據(jù)結(jié)構(gòu)是特定于VisualC++的//#ifndef_MSC_VER

#errorVisualC++Required(VisualC++specificinformationisdisplayed)#endif

////結(jié)構(gòu)定義

//

//操作系統(tǒng)定義的基本異常幀structEXCEPTION_REGISTRATION{

EXCEPTION_REGISTRATION*prev;FARPROChandler;};

//VisualC++擴(kuò)展異常幀指向的數(shù)據(jù)結(jié)構(gòu)structscopetable_entry{

DWORDpreviousTryLevel;FARPROClpfnFilter;FARPROClpfnHandler;};

//VisualC++使用的擴(kuò)展異常幀

structVC_EXCEPTION_REGISTRATION:EXCEPTION_REGISTRATION{

scopetable_entry*scopetable;inttrylevel;int_ebp;};

////原型聲明

////__except_handler3是VisualC++運(yùn)行時(shí)庫函數(shù),我們想打印出它的地址//但是它的原型并沒有出現(xiàn)在任何頭文件中,所以我們需要自己聲明它。extern\EXCEPTION_REGISTRATION*,PCONTEXT,

PEXCEPTION_RECORD);

////代碼

////

//顯示一個(gè)異常幀及其相應(yīng)的scopetable的信息//

voidShowSEHFrame(VC_EXCEPTION_REGISTRATION*pVCExcRec){

printf(\pVCExcRec,pVCExcRec->handler,pVCExcRec->prev,pVCExcRec->scopetable);

scopetable_entry*pScopeTableEntry=pVCExcRec->scopetable;for(unsignedi=0;itrylevel;i++){

printf(\\pScopeTableEntry->previousTryLevel,pScopeTableEntry->lpfnFilter,pScopeTableEntry->lpfnHandler);

pScopeTableEntry++;}

printf(\}

//

//遍歷異常幀的鏈表,按順序顯示它們的信息//

voidWalkSEHFrames(void){

VC_EXCEPTION_REGISTRATION*pVCExcRec;//打印出__except_handler3函數(shù)的位置

printf(\printf(\

//從FS:[0]處獲取指向鏈表頭的指針__asmmoveax,FS:[0]__asmmov[pVCExcRec],EAX

//遍歷異常幀的鏈表。0xFFFFFFFF標(biāo)志著鏈表的結(jié)尾while(0xFFFFFFFF!=(unsigned)pVCExcRec){

ShowSEHFrame(pVCExcRec);

pVCExcRec=(VC_EXCEPTION_REGISTRATION*)(pVCExcRec->prev);}}

voidFunction1(void

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

最新文檔

評論

0/150

提交評論