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

C++和異常

再回頭來說我們在第一節裏說到的 EXCEPTION_REGISTRATION結構,這個結構是用來註冊操作系統的異常回調函數的,當異常發生時,該函數將被調用。

VC++擴展了異常回調函數得語法,增加了兩個新的參數:

struct EXCEPTION_REGISTRATION
{
   EXCEPTION_REGISTRATION *prev;
   DWORD handler;
   int   id;
   DWORD ebp;
};

VC ++裏,除了一些例外,在每個函數的頭部生成EXCEPTION_REGISTRATION結構的局部變量(作者注:編譯器可能在一個函數里根本不生成任何異常相關的代碼,如果函數裏沒有try或者所有的局部對象都沒有可供調用的析構函數(譯註:當異常產生時,要把堆棧頂到catch到異常那點之間的局部變量都釋放掉,這是異常處理一個重要責任。如果這些局部變量都不需要去特別釋放,比如int變量或簡單結構變量,只要把堆棧指針指過來就可以了,異常處理在這一段裏就是沒有必要的,因此編譯器識別了這樣的情況,作個一定的優化))。上面結構的ebp和前面說的堆棧幀裏的ebp指針是重疊在一起的(譯註:請參看C++編譯器怎麼實現異常處理2),函數在開始時把這個結構創建在它的堆棧上,同時把這個結構註冊到操作系統(譯註:就是把FS:[0]的位置改成這個結構的指針);在函數的結尾恢復調用者在系統註冊的EXCEPTION_REGISTRATION結構(譯註:就是把FS:[0]改成結構裏的prev),我將在下一節討論EXCEPTION_REGISTRATION結構裏的id域的重要性。

當VC++編譯一個函數,它生成函數的兩套數據:

a) 異常的回調函數

b) 一個包含重要信息的數據結構,這些重要信息比如,各個catch塊,它們的地址,關心的異常的種類等等,我將在下一節更多的談論它

 4 顯示瞭如果考慮異常處理,程序運行時堆棧的情況。Widget的異常回調是在異常鏈的頭部(異常鏈的頭部就是FS:[0]指向的內容),FS:[0]的內容就是在Widget頭部定義的EXCEPTION_REGISTRATION結構的指針。當異常產生時,異常處理把Widget的函數信息結構的地址(就是EXCEPTION_REGISTRATION的指針)傳遞給__CxxFrameHandler函數,__CxxFrameHandler函數檢查這個數據結構,察看函數裏是否有catch塊對當前的異常感興趣,如果沒有找到,函數返回ExceptionContinueSearch給操作系統,操作系統獲得異常處理鏈裏的下一個節點,再調用這個節點的異常處理函數(這個節點應該是在本函數的調用者定義的)

figure4.gif

這個動作一直持續到異常處理找到一個對這個異常感興趣的catch塊,找到了匹配的catch塊以後,程序不再返回到操作系統。但是在它調用catch塊前(在函數信息結構裏有這個異常回調函數的指針,看圖4),異常處理必須執行堆棧釋放:清除這個函數之前的所有函數的堆棧幀。清除堆棧幀的動作有點複雜,異常處理必須找到所有在異常產生時還沒有結束的函數,找到每個函數所有的局部變量,調用每個變量的析構函數。我將在以後的章節裏更詳細的討論這點。

異常處理是這樣一個任務,當異常處理時清除異常處理所在的函數幀。這個操作是從FS:[0]指向的位置,也就是異常處理鏈頭開始的, 然後沿着鏈一個節點一個節點的通知它所在的堆棧將要被回收,收到通知的節點,調用所在幀的所有局部對象的析構然後返回,這個過程一直延續到拋出的異常被捕獲到。

因爲catch塊也是某個函數的一部分,所以它也用了所在函數的函數幀來保存數據。因此異常處理的一個catch塊進入的時候會激活所在函數的那一幀(譯註:就是會把ebp改到那個函數去,而esp是不動的,簡單的說,在catch函數塊裏執行的時候,ebp是指向所在函數的幀頂,而esp可能在非常遠的堆棧頂端)。同時,每個能能捕獲異常的catch塊(就是異常種類和catch塊要捕獲的異常種類一致)實際上只有一個參數,就是這個異常對象的拷貝或者是異常對象的引用拷貝。catch塊知道怎麼從函數信息結構中拷貝這個異常對象,編譯器已經產生了足夠多的信息(譯註:就是根據傳入的id,來判斷怎麼複製異常)。

當拷貝了異常對象同時激活了函數幀以後,異常處理就開始調用catch塊,catch塊的返回告訴異常處理在try-catch後面跟着執行(譯註:當然也可以在catch塊裏接着throw)。注意,在這一時刻,即使堆棧恢復已經存在,而且函數幀都已經清理,但是這些地址仍然在堆棧上存在(譯註:就是說在進入catch塊函數時,仍然是用程序的堆棧,不過多壓了好多東西進去,而在棧頂之前的一些東西都已經被清理了,但是它們仍然在堆棧上佔據着空間,這個時間是檢查錯誤來源的好時機,這也是我研究異常處理詳細機制的目的),這是因爲異常處理仍然像其他函數一樣的執行,它也使用程序堆棧來存放它的局部變量,cantch塊存放局部變量的幀在異常產生時調用的最後一個函數的堆棧幀的上邊(地址更低),當catch塊返回時,需要刪除這個異常對象的拷貝(譯註:如果異常對象是在棧裏,自動就刪除了,如果是在堆了,需要用delete或者free明確刪除)。在catch塊的結尾,異常處理回收了包括自己的幀在內的所有已經清理的堆棧幀,實際上就是把esp指向當前函數幀的結尾(譯註:前面說過在catch塊裏,ebp是catch所在函數的ebp比如是0x12ff58,而esp在很遠的地方比如0x12f9bc,而實際上,esp到ebp之間的大部分東西都已經被刪除了,現在把esp改回來,回到函數正常運行的狀態,比如0x12ff80,0x12ff58-0x12ff80實際上是擁有catch塊的那個函數的堆棧幀的範圍),完成這點以後,跳到try-catch塊的下一條語句繼續執行。但是catch塊是怎麼知道函數堆棧幀的尾在哪裏的?這本來是沒有辦法知道的,這就是爲什麼編譯器要在函數的堆棧幀的開頭保存一個esp的原因。參看圖4,堆棧幀指針EBP減去16就是esp所在的位置。

 catch塊自己可能會產生一個新的異常或者把當前異常重新拋出。異常不得不監視這種情況然後採取適當的操作。如果異常處理拋出一個新的異常,前一個異常就被刪除。如果catch塊決定再次拋出捕獲的異常,前一個異常就被往後傳遞。

有一個需要注意的地方:因爲每個線程都有自己的堆棧,所以都有它自己的一套異常處理鏈(由FS:[0]處指向的EXCEPTION_REGISTRATION 結構開始)

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