C++編譯器怎麼實現異常處理4

C++和異常2

 5 顯示了函數信息(funinfo)結構的內容。請注意結構使用的名字可能和VC++編譯器使用的實際名字不一樣,而且我在圖中只顯示了有關的成員,結構中的unwind table成員我將在下一節講到。

figure5.gif

當異常產生時,異常處理不得不尋找函數中的catch塊,首先它要知道函數裏這個產生異常的語句是不是被一個try塊所包含。如果函數根本就沒有try塊,異常處理直接就從函數裏返回,否則,異常處理就從所有的try塊裏找出那個包含了這條語句的塊。

首先 ,讓我們來看看怎麼來找這個關聯的try塊。在編譯的時候,編譯器賦給每個try塊一個起始ID和一個結束ID,通過funcinfo結構,很容易找到這些ID。參看圖5。編譯器爲函數裏的每一個try塊生成tryblock數據結構。

在以前的章節裏,我已經說過了VC++擴展的EXCEPTION_REGISTRATION結構,這個結構裏就有一個成員叫ID。這個結構是放在函數幀上的,參看圖四。當異常產生的時候,異常處理從堆棧裏獲得這個結構裏的ID成員,然後檢測這個ID是不是等於try塊的兩個ID之一,或者值的範圍在起始和結束ID之間。如果上述條件滿足,那麼產生異常的語句就是在這個try塊裏的,如果不是,異常處理就查找tryblocktable裏的下一個try塊。

誰在堆棧裏寫下這些值?應該把這個值賦成多少?編譯器在函數的不同位置填寫恰當的語句來更新當前幀的ID,通過這樣的手段來反映當前的運行狀態(譯註:

比如,一段代碼:

BOOL E1(FARPROC p)

{

try{ return (*p)(); }

catch(...) { printf("exception/r/n"); return FALSE; }

}

編譯出來時這樣的

var_10 = dword ptr -10h                   ;數據定義

var_C = dword ptr -0Ch                   ;數據定義

var_4 = dword ptr -4                        ;數據定義

push ebp               ;caller's ebp

mov ebp, esp             

push 0FFFFFFFFh           ;這就是id

push offset loc_401320    ;handle

mov eax, large fs:0      

push eax                  ;prev,堆棧裏這四項組成了結構EXCEPTION_REGISTRATION

mov large fs:0, esp       ;然後將現在的EXCEPTION_REGISTRATION註冊到fs:0上

push ecx

push ebx

push esi

push edi

mov [ebp+var_4], 0        ;這是把id從 0xffffffff變成0,這就是作者說的函數中恰當的位置

mov [ebp+var_10], esp     ;保留esp,見圖四

call [ebp+arg_0]          ;調用函數

mov ecx, [ebp+var_C]     

mov large fs:0, ecx       ;恢復fs:0的值爲prev,同時調用函數(*p)()的返回值是放在EAX中的,所以用的是ECX寄存器

pop edi

pop esi

pop ebx

mov esp, ebp

pop ebp                    ;恢復EBP

retn

)

例如:編譯器會在try函數塊進入的地方添加一條語句,把try函數塊的起始ID寫到函數的堆棧幀裏。

當異常處理處理異常時找到了一個try函數塊包含產生異常的語句,它能通過trycatch表檢查這個try函數塊是否有catch塊能捕獲這個產生的異常。請注意在嵌套的try函數塊裏,內層的try函數塊產生的異常也在外層的try函數塊的範圍裏。這樣的情況下,異常處理先找內層的catch塊,如果沒有找到,纔去找外層的try函數塊。在生成tryblock表時,VC++把內層的tryblock結構放在外層的tryblock之前。

異常處理怎麼知道(從catch塊的結構中)一個catch塊能不能捕獲當前的異常呢?它是通過比較catch塊的參數,那個異常對象的種類來做到這點的。

figure6.gif

catch塊能捕獲的異常H和E是完全相同的類型,產生的異常就會被捕獲。因此,異常處理不得不動態比較參數的類型。但是,一般來說,C或者是類似C的語言並不能很容易的在運行時決定參數的類型(譯註:C就和彙編差不多,看着就知道長度,誰知道在源碼裏它是什麼類型)。爲此,定義個一個叫type_info的類,這個定義寫在標準頭文件<typeinfo> 裏,用來描述變量運行時的類型。catchblock結構的第二個成員(圖5)就是一個指向type_info結構的指針,代表了catch塊參數的運行時的類型。type_info類重載了==操作符,用來判斷兩個類型是不是完全相同的類型。因此,所有的異常處理都要做這個比較(調用==操作符重載函數)來確認catchblock參數的type_info和產生的異常的type_info是否相等,從而判斷當前的catch塊能不能捕獲當前異常。

異常處理從funcinfo結構裏知道了catchblock的參數,但是怎麼知道當前異常的type_info呢,當編譯器遇到這樣的語句

throw E();

它爲這個拋出的異常創建一個excpt_info結構,參看圖6。請注意名字可能和VC++編譯器使用的有所不同,而且我只列出了有關的項。如圖所示,異常的type_info可以通過excpt_info結構來訪問。有些時候,異常處理要銷燬異常(當catch塊完成),也可能需要拷貝異常(在調用catch塊之前),爲了幫助異常處理完成這些任務,編譯器產生異常的析構函數,拷貝構造函數和取異常對象的大小的函數(通過excpt_info結構)

 

如果catch塊的參數是一個基類,產生的異常是它的派生類,異常處理仍然應該在異常產生時調用這個catch塊。然而,比較這兩個種類(基類和派生類)將返回false,因爲這兩個類型本來不是同一種類型。不管是type_info類提供的成員函數還是操作符,都不能判斷兩個類一個是不是另一個的子類。但是,在這樣的情況下,異常處理卻確實能捕獲到這樣的異常。這是怎麼做到的呢?實際上,編譯器爲這個異常產生了更多的信息。如果異常類是從別的類派生的,那麼etypeinfo_table(在結構excpt_info結構裏)包含了etype_info(擴展的type_info,我命名的)指針指向所有的父類,這樣異常處理比較catch塊的type_info和catch塊參數的所有的type_info(自己和自己的所有基類的type_info)。只要有一個匹配成功,catch塊就會被執行。

在我總結這一節之前,至少還有一個問題,就是異常處理是怎麼知道當前產生的異常的excpt_info在哪裏?我將在下面回答這個問題。

VC++把throw語句編譯成和下面類似的語句:

//throw E(); //compiler generates excpt_info structure for E.
E e = E();  //create exception on the stack
_CxxThrowException(&e, E_EXCPT_INFO_ADDR);

_CxxThrowException 把控制權傳遞給操作系統(通過軟件中斷,參看函數RaiseException),同時傳遞兩個參數。操作系統在準備調用異常回調函數時,把這兩個函數打包到結構_EXCEPTION_RECORD裏。接着操作系統找到FS:[0]處的異常處理鏈頭的第一個EXCEPTION_REGISTRATION結構,調用結構裏的handle。指向EXCEPTION_REGISTRATION 的指針也就是異常處理的第二個參數。再提一下,在VC++裏每一個函數在自己的那一幀堆棧上創建它自己的EXCEPTION_REGISTRATION 結構,同時把這個結構註冊到系統。第二個參數對異常處理很重要,通過它可以找到像ID這樣的成員(沒有ID就不能確定catch塊)。這個參數也能使異常處理知道函數的堆棧幀(這對清除本幀變量很有用),同時也能往下找出更多的EXCEPTION_REGISTRATION節點(這對清除堆棧很有用)。第一個參數是一個指向_EXCEPTION_RECORD結構的指針,通過它異常處理能找到異常對象的指針和excpt_info結構。異常處理的返回值定義在EXCPT.H裏

(譯註:

typedef enum _EXCEPTION_DISPOSITION {

ExceptionContinueExecution,

 ExceptionContinueSearch,

ExceptionNestedException,

 ExceptionCollidedUnwind }

 EXCEPTION_DISPOSITION; )

EXCEPTION_DISPOSITION (*handler)(
    _EXCEPTION_RECORD *ExcRecord,
    void * EstablisherFrame, 
    _CONTEXT *ContextRecord,
    void * DispatcherContext);

你能忽略最後的兩個參數。異常處理的返回值是一個枚舉(EXCEPTION_DISPOSITION類型)。在前面我們已經說到,如果異常處理不能找到catch塊,它就返回ExceptionContinueSearch給操作系統。其他的不太重要的信息在結構_EXCEPTION_RECORD裏,這個結構定義在WINNT.H裏:

struct _EXCEPTION_RECORD
{
    DWORD ExceptionCode;
    DWORD ExceptionFlags; 
    _EXCEPTION_RECORD *ExcRecord;
    PVOID   ExceptionAddress; 
    DWORD NumberParameters;
    DWORD ExceptionInformation[15]; 
} EXCEPTION_RECORD; 

ExceptionInformation數組的個數和入口的種類由ExceptionCode決定。如果ExceptionCode表示異常是個C++異常(ExceptionCode是0x06d7363,當通過throw來產生異常就會出現這樣的情況),那麼ExceptionInformation數組包含了指向異常對象和excpt_info結構的指針。而其他的異常,基本上都沒有入口。其他的異常有除0,訪問拒絕等,都能在WINNT.H裏找到它們對應的值。

異常處理通過EXCEPTION_RECORD結構的ExceptionFlags成員來決定異常時採取什麼動作。如果這個值是EH_UNWINDING (定義在except.inc裏),提示異常處理清除堆棧正在進行,這時,異常處理應該清除了函數堆棧幀然後返回。清除函數幀包括這樣的動作,找到所有的在異常發生時還沒有釋放的局部變量,然後調用它們的析構函數。這點下一節繼續討論。否則,異常處理不得不在函數裏繼續查找匹配的catch塊,然後調用找到的catch塊

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章