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

worldblog發表於2007-12-14
C++編譯器怎麼實現異常處理3 (轉)[@more@]

C++和異常

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

VC++擴充套件了異常回撥函式得語法,增加了兩個新的引數:

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

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

當VC++編譯一個函式,它生成函式的兩套資料:

a) 異常的回撥函式

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

圖 顯示瞭如果考慮異常處理,執行時堆疊的情況。Widget的異常回撥是在異常鏈的頭部(異常鏈的頭部就是FS:[0]指向的內容),FS:[0]的內容就是在Widget頭部定義的EXCEPTION_REGISTRATION結構的指標。當異常產生時,異常處理把Widget的函式資訊結構的地址(就是EXCEPTION_REGISTRATION的指標)傳遞給__CxxFrameHandler函式,__CxxFrameHandler函式檢查這個資料結構,察看函式里是否有catch塊對當前的異常感興趣,如果沒有找到,函式返回ExceptionContinueSearch給作業系統,作業系統獲得異常處理鏈裡的下一個節點,再呼叫這個節點的異常處理函式(這個節點應該是在本函式的呼叫者定義的)

這個動作一直持續到異常處理找到一個對這個異常感興趣的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 結構開始)


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-993053/,如需轉載,請註明出處,否則將追究法律責任。

相關文章