C編譯器如何實現(xiàn)異常處理_第1頁
C編譯器如何實現(xiàn)異常處理_第2頁
C編譯器如何實現(xiàn)異常處理_第3頁
C編譯器如何實現(xiàn)異常處理_第4頁
C編譯器如何實現(xiàn)異常處理_第5頁
已閱讀5頁,還剩14頁未讀, 繼續(xù)免費閱讀

下載本文檔

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

文檔簡介

1、C 編譯器如何實現(xiàn)異常處理C+編譯器如何實現(xiàn)異常處理轉(zhuǎn)自:原文出處:How aC+compiler implements exception handling譯者注:本文在網(wǎng)上已經(jīng)有幾個譯本,但都不完整,所以我決定自己把它翻譯過來。雖然力求信、雅、達(dá),但鑒于這是我的第一次翻譯經(jīng)歷,不足之處敬請諒解并指出。與傳統(tǒng)語言相比,C+的一項革命性創(chuàng)新就是它支持異常處理。傳統(tǒng)的錯誤處理方式經(jīng)常滿足不了要求,而異常處理則是一個極好的替代解決方案。它將正常代碼和錯誤處理代碼清晰的劃分開來,程序變得非常干凈并且容易維護(hù)。本文討論了編譯器如何實現(xiàn)異常處理。我將假定你已經(jīng)熟悉異常處理的語法和機制。本文還提供了一個用

2、于VC+的異常處理庫,要用庫中的處理程序替換掉VC+提供的那個,你只需要調(diào)用下面這個函數(shù):install_my_handler();之后,程序中的所有異常,從它們被拋出到堆棧展開(stack unwinding),再到調(diào)用catch塊,最后到程序恢復(fù)正常運行,都將由我的異常處理庫來管理。與其它C+特性一樣,C+標(biāo)準(zhǔn)并沒有規(guī)定編譯器應(yīng)該如何來實現(xiàn)異常處理。這意味著每一個編譯器的提供商都可以用它們認(rèn)為恰當(dāng)?shù)姆绞絹韺崿F(xiàn)它。下面我會描述一下VC+是怎么做的,但即使你使用其它的編譯器或操作系統(tǒng),本文也應(yīng)該會是一篇很好的學(xué)習(xí)材料。VC+的實現(xiàn)方式是以windows系統(tǒng)的結(jié)構(gòu)化異常處理(SEH)為基礎(chǔ)的。結(jié)

3、構(gòu)化異常處理-概述在本文的討論中,我認(rèn)為異常或者是被明確的拋出的,或者是由于除零溢出、空指針訪問等引起的。當(dāng)它發(fā)生時會產(chǎn)生一個中斷,接下來控制權(quán)就會傳遞到操作系統(tǒng)的手中。操作系統(tǒng)將調(diào)用異常處理程序,檢查從異常發(fā)生位置開始的函數(shù)調(diào)用序列,進(jìn)行堆棧展開和控制權(quán)轉(zhuǎn)移。Windows定義了結(jié)構(gòu)EXCEPTION_REGISTRATION,使我們能夠向操作系統(tǒng)注冊自己的異常處理程序。struct EXCEPTION_REGISTRATION EXCEPTION_REGISTRATION*prev;DWORD handler;注冊時,只需要創(chuàng)建這樣一個結(jié)構(gòu),然后把它的地址放到FS段偏移0的位置上去就行了。

4、下面這句匯編代碼演示了這一操作:mov FS:0,exc_regp prev字段用于建立一個EXCEPTION_REGISTRATION結(jié)構(gòu)的鏈表,每次注冊新的EXCEPTION_REGISTRATION時,我們都要把原來注冊的那個的地址存到prev中。那么,那個異?;卣{(diào)函數(shù)長什么樣呢?在excpt.h中,windows定義了它的原形:EXCEPTION_DISPOSITION(*handler)(_EXCEPTION_RECORD*ExcRecord,void*EstablisherFrame,_CONTEXT*ContextRecord,void*DispatcherContext);不要

5、管它的參數(shù)和返回值,我們先來看一個簡單的例子。下面的程序注冊了一個異常處理程序,然后通過除以零產(chǎn)生了一個異常。異常處理程序捕獲了它,打印了一條消息就完事大吉并退出了。#include iostream#include windows.h using std:cout;using std:endl;struct EXCEPTION_REGISTRATION EXCEPTION_REGISTRATION*prev;DWORD handler;EXCEPTION_DISPOSITION myHandler(_EXCEPTION_RECORD*ExcRecord,void*EstablisherFra

6、me,_CONTEXT*ContextRecord,void*DispatcherContext)coutIn the exception handlerendl;coutJust ademo.exiting.endl;exit(0);return ExceptionContinueExecution;/不會運行到這int g_div=0;void bar()/初始化一個EXCEPTION_REGISTRATION結(jié)構(gòu)EXCEPTION_REGISTRATION reg,*preg=reg.handler=(DWORD)myHandler;/取得當(dāng)前異常處理鏈的頭DWORD prev;_asm

7、 mov EAX,FS:0mov prev,EAX reg.prev=(EXCEPTION_REGISTRATION*)prev;/注冊!_asm mov EAX,preg mov FS:0,EAX/產(chǎn)生一個異常int j=10/g_div;/異常,除零溢出int main()bar();return 0;/*-輸出-In the exception handler Just ademo.exiting.-*/注意EXCEPTION_REGISTRATION必須定義在棧上,并且必須位于比上一個結(jié)點更低的內(nèi)存地址上,Windows對此有嚴(yán)格要求,達(dá)不到的話,它就會立刻終止進(jìn)程。函數(shù)和堆棧堆棧是用

8、來保存局部對象的連續(xù)內(nèi)存區(qū)。更明確的說,每個函數(shù)都有一個相關(guān)的棧楨(stack frame)來保存它所有的局部對象和表達(dá)式計算過程中用到的臨時對象,至少理論上是這樣的。但現(xiàn)實中,編譯器經(jīng)常會把一些對象放到寄存器中以便能以更快的速度訪問。堆棧是一個處理器(CPU)層次的概念,為了操縱它,處理器提供了一些專用的寄存器和指令。圖1是一個典型的堆棧,它示出了函數(shù)foo調(diào)用bar,bar又調(diào)用widget時的情景。請注意堆棧是向下增長的,這意味著新壓入的項的地址低于原有項的地址。通常編譯器使用EBP寄存器來指示當(dāng)前活動的棧楨。本例中,CPU正在運行widget,所以圖中的EBP指向了widget的棧楨。

9、編譯器在編譯時將所有局部對象解析成相對于棧楨指針(EBP)的固定偏移,函數(shù)則通過棧楨指針來間接訪問局部對象。舉個例子,典型的,widget訪問它的局部變量時就是通過訪問棧楨指針以下的、有著確定位置的幾個字節(jié)來實現(xiàn)的,比如說EBP-24。上圖中也畫出了ESP寄存器,它叫棧指針,指向棧的最后一項。在本例中,ESP指著widget的棧楨的末尾,這也是下一個棧楨(如果它被創(chuàng)建的話)的開始位置。處理器支持兩種類型的棧操作:壓棧(push)和彈棧(pop)。比如,pop EAX的作用是從ESP所指的位置讀出4字節(jié)放到EAX寄存器中,并把ESP加上(記住,棧是向下增長的)4(在32位處理器上);類似的,pu

10、sh EBP的作用是把ESP減去4,然后將EBP的值放到ESP指向的位置中去。編譯器編譯一個函數(shù)時,會在它的開頭添加一些代碼來為其創(chuàng)建并初始化棧楨,這些代碼被稱為序言(prologue);同樣,它也會在函數(shù)的結(jié)尾處放上代碼來清除棧楨,這些代碼叫做尾聲(epilogue)。一般情況下,序言是這樣的:Push EBP;把原來的棧楨指針保存到棧上Mov EBP,ESP;激活新的棧楨Sub ESP,10;減去一個數(shù)字,讓ESP指向棧楨的末尾第一條指令把原來的棧楨指針EBP保存到棧上;第二條指令通過讓EBP指向主調(diào)函數(shù)的EBP的保存位置來激活被調(diào)函數(shù)的棧楨;第三條指令把ESP減去了一個數(shù)字,這樣ESP就

11、指向了當(dāng)前棧楨的末尾,而這個數(shù)字是函數(shù)要用到的所有局部對象和臨時對象的大小。編譯時,編譯器知道函數(shù)的所有局部對象的類型和體積,所以,它能很容易的計算出棧楨的大小。尾聲所做的正好和序言相反,它必須把當(dāng)前棧楨從棧上清除掉:Mov ESP,EBP Pop EBP;激活主調(diào)函數(shù)的棧楨Ret;返回主調(diào)函數(shù)它讓ESP指向主調(diào)函數(shù)的棧楨指針的保存位置(也就是被調(diào)函數(shù)的棧楨指針指向的位置),彈出EBP從而激活主調(diào)函數(shù)的棧楨,然后返回主調(diào)函數(shù)。一旦CPU遇到返回指令,它就要做以下兩件事:把返回地址從棧中彈出,然后跳轉(zhuǎn)到那個地址去。返回地址是主調(diào)函數(shù)執(zhí)行call指令調(diào)用被調(diào)函數(shù)時自動壓棧的。Call指令執(zhí)行時,會

12、先把緊隨在它后面的那條指令的地址(被調(diào)函數(shù)的返回地址)壓入棧中,然后跳轉(zhuǎn)到被調(diào)函數(shù)的開始位置。圖2更詳細(xì)的描繪了運行時的堆棧。如圖所示,主調(diào)函數(shù)把被調(diào)函數(shù)的參數(shù)也壓進(jìn)了堆棧,所以參數(shù)也是棧楨的一部分。函數(shù)返回后,主調(diào)函數(shù)需要移除這些參數(shù),它通過把所有參數(shù)的總體積加到ESP上來達(dá)到目的,而這個體積可以在編譯時知道:Add ESP,args_size當(dāng)然,也可以把參數(shù)的總體積寫在被調(diào)函數(shù)的返回指令的后面,讓被調(diào)函數(shù)去移除參數(shù),下面的指令就在返回主調(diào)函數(shù)前從棧中移去了24個字節(jié):Ret 24取決于被調(diào)函數(shù)的調(diào)用約定(call convention),這兩種方式每次只能用一個。你還要注意的是每個線程都

13、有自己獨立的堆棧。C+和異?;貞浺幌挛以诘谝还?jié)中介紹的EXCEPTION_REGISTRATION結(jié)構(gòu),我們曾用它向操作系統(tǒng)注冊了發(fā)生異常時要被調(diào)用的回調(diào)函數(shù)。VC+也是這么做的,不過它擴展了這個結(jié)構(gòu)的語義,在它的后面添加了兩個新字段:struct EXCEPTION_REGISTRATION EXCEPTION_REGISTRATION*prev;DWORD handler;int id;DWORD ebp;VC+會為絕大部分函數(shù)添加一個EXCEPTION_REGISTRATION類型的局部變量,它的最后一個字段(ebp)與棧楨指針指向的位置重疊。函數(shù)的序言創(chuàng)建這個結(jié)構(gòu)并把它注冊給操作系統(tǒng),

14、尾聲則恢復(fù)主調(diào)函數(shù)的EXCEPTION_REGISTRATION。id字段的意義我將在下一節(jié)介紹。VC+編譯函數(shù)時會為它生成兩部分?jǐn)?shù)據(jù):a)異?;卣{(diào)函數(shù)b)一個包含函數(shù)重要信息的數(shù)據(jù)結(jié)構(gòu),這些信息包括catch塊、這些塊的地址和這些塊所關(guān)心的異常的類型等等。我把這個結(jié)構(gòu)稱為funcinfo,有關(guān)它的詳細(xì)討論也在下一節(jié)。圖3是考慮了異常處理之后的運行時堆棧。widget的異常回調(diào)函數(shù)位于由FS:0指向的異常處理鏈的開始位置(這是由widget的序言設(shè)置的)。異常處理程序把widget的funcinfo結(jié)構(gòu)的地址交給函數(shù)_CxxFrameHandler,_CxxFrameHandler會檢查這個結(jié)

15、構(gòu)看函數(shù)中有沒有catch塊對當(dāng)前的異常感興趣。如果沒有的話,它就返回ExceptionContinueSearch給操作系統(tǒng),于是操作系統(tǒng)會從異常處理鏈表中取得下一個結(jié)點,并調(diào)用它的異常處理程序(也就是調(diào)用當(dāng)前函數(shù)的那個函數(shù)的異常處理程序)。這一過程將一直進(jìn)行下去-直到處理程序找到一個能處理當(dāng)前異常的catch塊為止,這時它就不再返回操作系統(tǒng)了。但是在調(diào)用catch塊之前(由于有funcinfo結(jié)構(gòu),所以知道catch塊的入口,參見圖3),必須進(jìn)行堆棧展開,也就是清理掉當(dāng)前函數(shù)的棧楨下面的所有其他的棧楨。這個操作稍微有點復(fù)雜,因為:異常處理程序必須找到異常發(fā)生時生存在這些棧楨上的所有局部對象

16、,并依次調(diào)用它們的析構(gòu)函數(shù)。后面我將對此進(jìn)行詳細(xì)介紹。異常處理程序把這項工作委托給了各個棧楨自己的異常處理程序。從FS:0指向的異常處理鏈的第一個結(jié)點開始,它依次調(diào)用每個結(jié)點的處理程序,告訴它堆棧正在展開。與之相呼應(yīng),這些處理程序會調(diào)用每個局部對象的析構(gòu)函數(shù),然后返回。此過程一直進(jìn)行到與異常處理程序自身相對應(yīng)的那個結(jié)點為止。由于catch塊是函數(shù)的一部分,所以它使用的也是函數(shù)的棧楨。因此,在調(diào)用catch塊之前,異常處理程序必須激活它所隸屬的函數(shù)的棧楨。其次,每個catch塊都只接受一個參數(shù),其類型是它希望捕獲的異常的類型。異常處理程序必須把異常對象本身或者是異常對象的引用拷貝到catch塊的

17、棧楨上,編譯器在funcinfo中記錄了相關(guān)信息,處理程序根據(jù)這些信息就能知道到哪去拷貝異常對象了??截愅戤惓2⒓せ顥E后,處理程序?qū)⒄{(diào)用catch塊。而catch塊將把控制權(quán)下一步要轉(zhuǎn)移到的地址返回來。請注意:雖然這時堆棧已經(jīng)展開,棧楨也都被清除了,但它們占據(jù)的內(nèi)存空間并沒有被覆蓋,所有的數(shù)據(jù)都還好好的待在棧上。這是因為異常處理程序仍在執(zhí)行,象其他函數(shù)一樣,它也需要棧來存放自己的局部對象,而其棧楨就位于發(fā)生異常的那個函數(shù)的棧楨的下面。catch塊返回以后,異常處理程序需要殺掉異常對象。此后,它讓ESP指向目標(biāo)函數(shù)(控制權(quán)要轉(zhuǎn)移到的那個函數(shù))的棧楨的末尾-這樣就把(包括它自己的在內(nèi)的)所有棧楨

18、都刪除了,然后再跳轉(zhuǎn)到catch塊返回的那個地址去,就勝利的完成整個異常處理任務(wù)了。但它怎么知道目標(biāo)函數(shù)的棧楨末尾在哪呢?事實上它沒法知道,所以編譯器把這個地址保存到了棧楨上(由前言來完成),如圖3所示,棧楨指針EBP下面第16個字節(jié)就是。當(dāng)然,catch塊也可能拋出新異常,或者是將原來的異常重新拋出。處理程序必須對此有所準(zhǔn)備。如果是拋出新異常,它必須殺掉原來的那個;而如果是重新拋出原來的異常,它必須能繼續(xù)傳播(propagate)這個異常。這里我要特別強調(diào)一點:由于每個線程有自己獨立的堆棧,所以每個線程也都有自己獨立的、由FS:0指向的EXCEPTION_REGISTRATION鏈。C+和異

19、常-2圖4是funcinfo的布局,注意這里的字段名可能與VC+編譯器實際使用的不完全一致,而且我也只給出了和我們的討論相關(guān)的字段。堆棧展開表(unwind table)的結(jié)構(gòu)留到下節(jié)再討論。異常處理程序在函數(shù)中查找catch塊時,它首先要判斷異常發(fā)生的位置是否在當(dāng)前函數(shù)(發(fā)生異常的那個函數(shù))的一個try塊中。是則查找與此try塊相關(guān)的catch塊表,否則直接返回。先來看看它怎樣找try塊。編譯時,編譯器給每個try塊都分配了start id和end id。通過funcinfo結(jié)構(gòu),異常處理程序可以訪問這兩個id,見圖4。編譯器為函數(shù)中的每個try塊都生成了相關(guān)的數(shù)據(jù)結(jié)構(gòu)。上一節(jié)中,我說過VC

20、+給EXCEPTION_REGISTRATION結(jié)構(gòu)加上了一個id字段?;貞浺幌聢D3,這個結(jié)構(gòu)位于函數(shù)的棧楨上。異常發(fā)生時,處理程序讀出這個值,看它是否在try塊的兩個id確定的區(qū)間start id,end id中。是的話,異常就發(fā)生在這個try塊中;否則繼續(xù)查看try塊表中的下一個try塊。誰負(fù)責(zé)更新id的值,它的值又應(yīng)該是什么呢?原來,編譯器會在函數(shù)的多個位置安插代碼來更新id的值,以反應(yīng)程序的實時運行狀態(tài)。比如說,編譯器會在進(jìn)入try塊的地方加上一條語句,把try塊的start id寫到棧楨上。找到try塊后,處理程序就遍歷與其關(guān)聯(lián)的catch塊表,看是否有對當(dāng)前異常感興趣的catch塊

21、。在try塊發(fā)生嵌套時,異常將既源于內(nèi)層try塊,也源于外層try塊。這種情況下,處理程序應(yīng)該按先內(nèi)后外的順序查找catch塊。但它其實沒必要關(guān)心這些,因為,在try塊表中,VC+總是把內(nèi)層try塊放在外層try塊的前面。異常處理程序還有一個難題就是如何根據(jù)catch塊的相關(guān)數(shù)據(jù)結(jié)構(gòu)判斷這個catch塊是否愿意處理當(dāng)前異常。這是通過比較異常的類型和catch塊的參數(shù)的類型來完成的。例如下面這個程序:void foo()try throw E();catch(H)/.如果H和E的類型完全相同的話,catch塊就要捕獲這個異常。這意味著處理程序必須在運行時進(jìn)行類型比較,對C等語言來說,這是不可能的

22、,因為它們無法在運行時得到對象的類型。C+則不同,它有了運行時類型識別(runtime type identification,RTTI),并提供了運行時類型比較的標(biāo)準(zhǔn)方法。C+在標(biāo)準(zhǔn)頭文件中定義了type_info類,它能在運行時代表一個類型。catch塊數(shù)據(jù)結(jié)構(gòu)的第二個字段(ptype_info,見圖4)是一個指向type_info結(jié)構(gòu)的指針,它在運行時就代表catch塊的參數(shù)類型。type_info也重載了=運算符,能夠指出兩種類型是否完全相同。這樣,異常處理程序只要比較(調(diào)用=運算符)catch塊參數(shù)的type_info(可以通過catch塊的相關(guān)數(shù)據(jù)結(jié)構(gòu)來訪問)和異常的type_in

23、fo是否相同,就能知道catch塊是不是愿意捕獲當(dāng)前異常了。catch塊的參數(shù)類型可以通過funcinfo結(jié)構(gòu)得到,但異常的type_info從哪來呢?當(dāng)編譯器碰到throw E();這條語句時,它會為異常生成一個excpt_info結(jié)構(gòu),如圖5所示。還是要提醒你注意這里用的名字可能與VC+使用的不一致,而且仍然只有與我們的討論相關(guān)的字段。從圖中可以看出,異常的type_info可以通過excpt_info結(jié)構(gòu)得到。由于異常處理程序需要拷貝異常對象(在調(diào)用catch塊之前),也需要消除掉它(在調(diào)用catch塊之后),所以編譯器在這個結(jié)構(gòu)中同時提供了異常的拷貝構(gòu)造函數(shù)、大小和析構(gòu)函數(shù)的信息。在c

24、atch塊的參數(shù)是基類,而異常是派生類時,異常處理程序也應(yīng)該調(diào)用catch塊。然而,這種情況下,比較它們的type_info絕對是不相等,因為它們本來就不是相同的類型。而且,type_info類也沒有提供任何其他函數(shù)或運算符來指出一個類是另一個類的基類。但異常處理程序還必須得去調(diào)用catch塊!為了解決這個問題,編譯器只能為處理程序提供更多的信息:如果異常是派生類,那么etypeinfo_table(通過excpt_info訪問)將包含多個指向etype_info(擴展了type_info,這個名字是我啟的)的指針,它們分別指向了各個基類的etype_info。這樣,處理程序就可以把catch

25、塊的參數(shù)和所有這些type_info比較,只要有一個相同,就調(diào)用catch塊。在結(jié)束這一部分之前,還有最后一個問題:異常處理程序是怎么知道異常和excpt_info結(jié)構(gòu)的?下面我就要回答這個問題。VC+會把throw語句翻譯成下面的樣子:/throw E();/編譯器會為E生成excpt_info結(jié)構(gòu)E e=E();/在棧上創(chuàng)建異常_CxxThrowException(&e,E_EXCPT_INFO_ADDR);_CxxThrowException會把控制權(quán)連帶它的兩個參數(shù)都交給操作系統(tǒng)(控制權(quán)轉(zhuǎn)移是通過軟件中斷實現(xiàn)的,請參見RaiseException)。而操作系統(tǒng),在為調(diào)用異?;卣{(diào)函數(shù)做準(zhǔn)

26、備時,會把這兩個參數(shù)打包到一個_EXCEPTION_RECORD結(jié)構(gòu)中。接著,它從EXCEPTION_REGISTRATION鏈表的頭結(jié)點(由FS:0指向)開始,依次調(diào)用各節(jié)點的異常處理程序。而且,指向當(dāng)前EXCEPTION_REGISTRATION結(jié)構(gòu)的指針也會作為異常處理程序的第二個參數(shù)出現(xiàn)。前面已經(jīng)說過,VC+中的每個函數(shù)都在棧上創(chuàng)建并注冊了EXCEPTION_REGISTRATION結(jié)構(gòu)。所以傳遞這個參數(shù)可以讓處理程序知道很多重要信息,比如說:EXCEPTION_REGISTRATION的id字段(用于查找catch塊)、函數(shù)的棧楨(用于清理棧楨)和EXCEPTION_REGISTRA

27、TION結(jié)點在異常鏈表中的位置(用于堆棧展開)等。第一個參數(shù)是指向_EXCEPTION_RECORD結(jié)構(gòu)的指針,通過它可以找到異常和它的excpt_info結(jié)構(gòu)。下面是excpt.h中定義的異?;卣{(diào)函數(shù)的原型:EXCEPTION_DISPOSITION(*handler)(_EXCEPTION_RECORD*ExcRecord,void*EstablisherFrame,_CONTEXT*ContextRecord,void*DispatcherContext);后兩個參數(shù)和我們的討論關(guān)系不大。函數(shù)的返回值是一個枚舉類型(也在excpt.h中定義),我前面已經(jīng)說過,如果處理程序找不到catch

28、塊,它就會向系統(tǒng)返回ExceptionContinueSearch,對本文而言,我們只要知道這一個返回值就行了。_EXCEPTION_RECORD結(jié)構(gòu)是在winnt.h中定義的:struct _EXCEPTION_RECORD DWORD ExceptionCode;DWORD ExceptionFlags;_EXCEPTION_RECORD*ExcRecord;PVOID ExceptionAddress;DWORD NumberParameters;DWORD ExceptionInformation15;EXCEPTION_RECORD;ExceptionInformation數(shù)組中元素

29、的個數(shù)和類型取決于ExceptionCode字段。如果是C+異常(異常代碼是0xe06d7363,源于throw語句),那么數(shù)組中將包含指向異常和excpt_info結(jié)構(gòu)的指針;如果是其他異常,那數(shù)組中基本上就不會有什么內(nèi)容,這些異常包括除零溢出、訪問違例等,你可以在winnt.h中找到它們的異常代碼。ExceptionFlags字段用于告訴異常處理程序應(yīng)該采取什么操作。如果它是EH_UNWINDING(見Except.inc),那是說堆棧正在展開,這時,處理程序要清理棧楨,然后返回。否則處理程序應(yīng)該在函數(shù)中查找catch塊并調(diào)用它。清理棧楨意味著必須找到異常發(fā)生時生存在棧楨上的所有局部對象,

30、并調(diào)用其析構(gòu)函數(shù),下一節(jié)我們將就此進(jìn)行詳細(xì)討論。清理棧楨C+標(biāo)準(zhǔn)明確指出:堆棧展開工作必須調(diào)用異常發(fā)生時所有生存的局部對象的析構(gòu)函數(shù)。如下面的代碼:int g_i=0;void foo()T o1,o2;T o3;10/g_i;/這里會發(fā)生異常T o4;/.foo有o1、o2、o3、o4四個局部對象,但異常發(fā)生時,o3已經(jīng)死亡,o4還未出生,所以異常處理程序應(yīng)該只調(diào)用o1和o2的析構(gòu)函數(shù)。前面已經(jīng)說過,編譯器會在函數(shù)的很多地方安插代碼來記錄當(dāng)前的運行狀態(tài)。實際上,編譯器在函數(shù)中設(shè)置了一些關(guān)鍵區(qū)域,并為它們分配了id,進(jìn)入關(guān)鍵區(qū)域時要記錄它的id,退出時恢復(fù)前一個id。try塊就是一個例子,其i

31、d就是start id。所以,在try塊的入口,編譯器會把它的start id記到棧楨上去。局部對象從創(chuàng)建到銷毀也確定了一個關(guān)鍵區(qū)域,或者,換句話說,編譯器給每個局部對象分配了唯一的id,例如下面的程序:void foo()T t1;/.編譯器會在t1的定義后面(也就是t1創(chuàng)建以后),把它的id寫到棧楨上:void foo()T t1;_id=t1_id;/編譯器插入的語句/.上面的_id是編譯器偷偷創(chuàng)建的局部變量,它的位置與EXCEPTION_REGISTRATION的id字段重疊。類似的,在調(diào)用對象的析構(gòu)函數(shù)前,編譯器會恢復(fù)前一個關(guān)鍵區(qū)域的id。清理棧楨時,異常處理程序讀出id的值(通過E

32、XCEPTION_REGISTRATION結(jié)構(gòu)的id字段或棧楨指針EBP下面的4個字節(jié)來訪問)。這個id可以表明,函數(shù)在運行到與它相關(guān)聯(lián)的那個點之前沒有發(fā)生異常。所有在這一點之前定義的對象都已初始化,應(yīng)該調(diào)用這些對象中的一部分或全部對象的析構(gòu)函數(shù)。請注意某些對象是屬于子塊(如前面代碼中的o3)的,發(fā)生異常時可能已經(jīng)銷毀了,不應(yīng)該調(diào)用它們的析構(gòu)函數(shù)。編譯器還為函數(shù)生成了另一個數(shù)據(jù)結(jié)構(gòu)-堆棧展開表(unwindtable,我啟的名字),它是一個unwind結(jié)構(gòu)的數(shù)組,可通過funcinfo來訪問,如圖4所示。函數(shù)的每個關(guān)鍵區(qū)域都有一個unwind結(jié)構(gòu),這些結(jié)構(gòu)在展開表中出現(xiàn)的次序和它們所對應(yīng)的區(qū)域

33、在函數(shù)中的出現(xiàn)次序完全相同。一般unwind結(jié)構(gòu)也會關(guān)聯(lián)一個對象(別忘了,每個對象的定義都開辟了關(guān)鍵區(qū)域,并有id與其對應(yīng)),它里面有如何銷毀這個對象的信息。每當(dāng)編譯器碰到對象定義,它就生成一小段代碼,這段代碼知道對象在棧楨上的地址(就是它相對于棧楨指針的偏移),并能銷毀它。unwind結(jié)構(gòu)中有一個字段用于保存這段代碼的入口地址:typedef void(*CLEANUP_FUNC)();struct unwind int prev;CLEANUP_FUNC cf;try塊對應(yīng)的unwind結(jié)構(gòu)的cf字段是空值NULL,因為沒有與它對應(yīng)的對象,所以也沒有東西需要它去銷毀。通過prev字段,這些

34、unwind結(jié)構(gòu)也形成了一個鏈表。異常處理程序清理棧楨時,會讀取當(dāng)前的id值,以它為索引取得展開表中對應(yīng)的項,并調(diào)用其第二個字段指向的清理代碼,這樣,那個與之關(guān)聯(lián)的對象就被銷毀了。然后,處理程序?qū)⒁援?dāng)前unwind結(jié)構(gòu)的prev字段為索引,繼續(xù)在展開表中找下一個unwind結(jié)構(gòu),調(diào)用其清理代碼。這一過程將一直重復(fù),直到鏈表的結(jié)尾(prev的值是-1)。圖6畫出了本節(jié)開始時提到的那段代碼的堆棧展開表?,F(xiàn)在把new運算符也加進(jìn)來,對于下面的代碼:T*p=new T();系統(tǒng)會首先為T分配內(nèi)存,然后調(diào)用它的構(gòu)造函數(shù)。所以,如果構(gòu)造函數(shù)拋出了異常,系統(tǒng)就必須釋放這些內(nèi)存。因此,動態(tài)創(chuàng)建那些擁有有為的構(gòu)

35、造函數(shù)的類型時,VC+也為new運算符分配了id,并且堆棧展開表中也有與其對應(yīng)的項,其清理代碼將釋放分配的內(nèi)存空間。調(diào)用構(gòu)造函數(shù)前,編譯器把new運算符的id存到EXCEPTION_REGISTRATION結(jié)構(gòu)中,構(gòu)造函數(shù)順利返回后,它再把id恢復(fù)成原來的值。更進(jìn)一步說,構(gòu)造函數(shù)拋出異常時,對象可能剛剛構(gòu)造了一部分,如果它有子成員對象或子基類對象,并且發(fā)生異常時它們中的一部分已經(jīng)構(gòu)造完成的話,就必須調(diào)用這些對象的析構(gòu)函數(shù)。和普通函數(shù)一樣,編譯器也給構(gòu)造函數(shù)生成了相關(guān)的數(shù)據(jù)來幫助完成這個任務(wù)。展開堆棧時,異常處理程序調(diào)用的是用戶定義的析構(gòu)函數(shù),這一點你必須注意,因為它也有可能拋出異常!C+標(biāo)準(zhǔn)規(guī)

36、定堆棧展開過程中,析構(gòu)函數(shù)不能拋出異常,否則系統(tǒng)將調(diào)用std:terminate。實現(xiàn)本節(jié)我們討論其他三個有待詳細(xì)解釋的問題:a)如何安裝異常處理程序b)catch塊重新拋出異?;驋伋鲂庐惓r應(yīng)該如何處理c)如何對所有線程提供異常處理支持隨同本文,有一個演示項目,查看其中的readme.txt文件可以得到一些編譯方面的幫助。第一項任務(wù)是安裝異常處理程序,也就是把VC+的處理程序替換掉。從前面的討論中,我們已經(jīng)清楚地知道_CxxFrameHandler函數(shù)是VC+所有異常處理工作的入口。編譯器為每個函數(shù)都生成一段代碼,它們在發(fā)生異常時被調(diào)用,把相應(yīng)的funcinfo結(jié)構(gòu)的指針交給_CxxFram

37、eHandler。install_my_handler()函數(shù)會改寫_CxxFrameHandler的入口處的代碼,讓程序跳轉(zhuǎn)到my_exc_handler()函數(shù)。不過,_CxxFrameHandler位于只讀的內(nèi)存頁,對它的任何寫操作都會導(dǎo)致訪問違例,所以必須首先用VirtualProtectEx把該內(nèi)存頁的保護(hù)方式改成可讀寫,等改寫完畢后,再改回只讀。寫入的數(shù)據(jù)是一個jmp_instr結(jié)構(gòu)。/install_my_handler.cpp#include windows.h#includeinstall_my_handler.h/C+默認(rèn)的異常處理程序externCEXCEPTION_DI

38、SPOSITION _CxxFrameHandler(struct _EXCEPTION_RECORD*ExceptionRecord,void*EstablisherFrame,struct _CONTEXT*ContextRecord,void*DispatcherContext);namespace char cpp_handler_instructions5;bool saved_handler_instructions=false;namespace my_handler/我的異常處理程序EXCEPTION_DISPOSITION my_exc_handler(struct _EXC

39、EPTION_RECORD*ExceptionRecord,void*EstablisherFrame,struct _CONTEXT*ContextRecord,void*DispatcherContext)throw();#pragma pack(push,1)struct jmp_instr unsigned char jmp;DWORD offset;#pragma pack(pop)bool WriteMemory(void*loc,void*buffer,int size)HANDLE hProcess=GetCurrentProcess();/把包含內(nèi)存范圍loc,loc+siz

40、e的頁面的保護(hù)方式改成可讀寫DWORD old_protection;BOOL ret=VirtualProtectEx(hProcess,loc,size,PAGE_READWRITE,&old_protection);if(ret=FALSE)return false;ret=WriteProcessMemory(hProcess,loc,buffer,size,NULL);/恢復(fù)原來的保護(hù)方式DWORD o2;VirtualProtectEx(hProcess,loc,size,old_protection,&o2);return(ret=TRUE);bool ReadMemory(vo

41、id*loc,void*buffer,DWORD size)HANDLE hProcess=GetCurrentProcess();DWORD bytes_read=0;BOOL ret=ReadProcessMemory(hProcess,loc,buffer,size,&bytes_read);return(ret=TRUE&bytes_read=size);bool install_my_handler()void*my_hdlr=my_exc_handler;void*cpp_hdlr=_CxxFrameHandler;jmp_instr jmp_my_hdlr;jmp_my_hdlr

42、.jmp=0xE9;/從_CxxFrameHandler+5開始計算偏移,因為jmp指令長5字節(jié)jmp_my_hdlr.offset=reinterpret_cast(my_hdlr)-(reinterpret_cast(cpp_hdlr)+5);if(!saved_handler_instructions)if(!ReadMemory(cpp_hdlr,cpp_handler_instructions,sizeof(cpp_handler_instructions)return false;saved_handler_instructions=true;return WriteMemory(

43、cpp_hdlr,&jmp_my_hdlr,sizeof(jmp_my_hdlr);bool restore_cpp_handler()if(!saved_handler_instructions)return false;else void*loc=_CxxFrameHandler;return WriteMemory(loc,cpp_handler_instructions,sizeof(cpp_handler_instructions);編譯指令#pragma pack(push,1)告訴編譯器不要在jmp_instr結(jié)構(gòu)中填充任何用于對齊的空間。沒有這條指令,jmp_instr的大小將

44、是8字節(jié),而我們需要它是5字節(jié)?,F(xiàn)在重新回到異常處理這個主題上來。調(diào)用catch塊時,它可能重新拋出異?;驋伋鲂庐惓!G耙环N情況下,異常處理程序必須繼續(xù)傳播(propagate)當(dāng)前異常;后一種情況下,它需要在繼續(xù)之前銷毀原來的異常。此時,處理程序要面對兩個難題:如何知道異常是源于catch塊還是程序的其他部分和如何跟蹤原來的異常。我的解決方法是:在調(diào)用catch塊之前,把當(dāng)前異常保存在exception_storage對象中,并注冊一個專用于catch塊的異常處理程序-catch_block_protector。調(diào)用get_exception_storage()函數(shù),就能得到exceptio

45、n_storage對象:exception_storage*p=get_exception_storage();p-set(pexc,pexc_info);注冊catch_block_protector;調(diào)用catch塊;/.這樣,當(dāng)catch塊(重新)拋出異常時,程序?qū)?zhí)行catch_block_protector。如果是拋出了新異常,這個函數(shù)可以從exception_storage對象中分離出前一個異常并銷毀它;如果是重新拋出原來的異常(可以通過ExceptionInformation數(shù)組的前兩個元素知道是新異常還是舊異常,后一種情況下著兩個元素都是0,參見下面的代碼),就通過拷貝Exc

46、eptionInformation數(shù)組來繼續(xù)傳播它。下面的代碼就是catch_block_protector()函數(shù)的實現(xiàn)。/-/如果這個處理程序被調(diào)用了,可以斷定是catch塊(重新)拋出了異常。/異常處理程序(my_handler)在調(diào)用catch塊之前注冊了它。其任務(wù)是判斷/catch塊拋出了新異常還是重新拋出了原來的異常,并采取相應(yīng)的操作。/在前一種情況下,它需要銷毀傳遞給catch塊的前一個異常對象;在后一種/情況下,它必須找到原來的異常并將其保存到ExceptionRecord中供異常/處理程序使用。/-EXCEPTION_DISPOSITION catch_block_prote

47、ctor(_EXCEPTION_RECORD*ExceptionRecord,void*EstablisherFrame,struct _CONTEXT*ContextRecord,void*DispatcherContext)throw()EXCEPTION_REGISTRATION*pFrame;pFrame=reinterpret_cast EXCEPTION_REGISTRATION*(EstablisherFrame);if(!(ExceptionRecord-ExceptionFlags&(_EXCEPTION_UNWINDING|_EXCEPTION_EXIT_UNWIND)vo

48、id*pcur_exc=0,*pprev_exc=0;const excpt_info*pexc_info=0,*pprev_excinfo=0;exception_storage*p=get_exception_storage();pprev_exc=p-get_exception();pprev_excinfo=p-get_exception_info();p-set(0,0);bool cpp_exc=ExceptionRecord-ExceptionCode=MS_CPP_EXC;get_exception(ExceptionRecord,&pcur_exc);get_excpt_in

49、fo(ExceptionRecord,&pexc_info);if(cpp_exc&0=pcur_exc&0=pexc_info)/重新拋出ExceptionRecord-ExceptionInformation1=reinterpret_cast DWORD(pprev_exc);ExceptionRecord-ExceptionInformation2=reinterpret_cast DWORD(pprev_excinfo);else exception_helper:destroy(pprev_exc,pprev_excinfo);return ExceptionContinueSearch;下面是get_exception_storage()函數(shù)的一個實現(xiàn):exception_storage*get_exception_storage()static exception_storage es;return&es;在單線程程序中,這是一個完美的實現(xiàn)。但在多線程中,這就是個災(zāi)難了,想象一下多個線程訪問它,并把異常對象保存在里面的情景吧。由于每個線程都有自己的堆棧和

溫馨提示

  • 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)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論