C++異常實現機制

一、C函數的調用和返回

  要理解C++異常機制實現以前,首先要了解一個函數的調用和返回機制,這裏面就要涉及到ESP和EBP寄存器。咱們先看一下函數調用和返回的流程。數據結構

下面是按調用約定__stdcall 調用函數test(int p1,int p2)的彙編代碼
假設執行函數前堆棧指針ESP爲NN
push   p2    ;參數2入棧, ESP -= 4h , ESP = NN - 4h
push   p1    ;參數1入棧, ESP -= 4h , ESP = NN - 8h
call test    ;壓入返回地址 ESP -= 4h, ESP = NN - 0Ch 
{
push   ebp                        ;保護先前EBP指針, EBP入棧, ESP-=4h, ESP = NN - 10h
mov    ebp, esp                   ;設置EBP指針指向棧頂 NN-10h
mov    eax, dword ptr  [ebp+0ch]  ;ebp+0ch爲NN-4h,即參數2的位置
mov    ebx, dword ptr  [ebp+08h]  ;ebp+08h爲NN-8h,即參數1的位置
sub    esp, 8                     ;局部變量所佔空間ESP-=8, ESP = NN-18h
...
add    esp, 8                     ;釋放局部變量, ESP+=8, ESP = NN-10h
pop    ebp                        ;出棧,恢復EBP, ESP+=4, ESP = NN-0Ch
ret    8                          ;ret返回,彈出返回地址,ESP+=4, ESP=NN-08h, 後面加操做數8爲平衡堆棧,ESP+=8,ESP=NN, 恢復進入函數前的堆棧.
}

   函數棧架構主要承載着如下幾個部分:架構

    一、傳遞參數:一般,函數的調用參數老是在這個函數棧框架的最頂端。框架

    二、傳遞返回地址:告訴被調用者的 return 語句應該 return 到哪裏去,一般指向該函數調用的下一條語句(代碼段中的偏移)。函數

    三、存放調用者的當前棧指針:便於清理被調用者的全部局部變量、並恢復調用者的現場。字體

    四、存放當前函數內的全部局部變量:記得嗎?剛纔說過全部局部和臨時變量都是存儲在棧上的。this

    

二、C++函數調用

  首先澄清一點,這裏說的 「C++ 函數」是指:spa

    一、該函數可能會直接或間接地拋出一個異常:即該函數的定義存放在一個 C++ 編譯(而不是傳統 C)單元內,而且該函數沒有使用「throw()」異常過濾器線程

    二、該函數的定義內使用了 try 塊。翻譯

  以上二者知足其一便可。爲了可以成功地捕獲異常和正確地完成棧回退(stack unwind),編譯器必需要引入一些額外的數據結構和相應的處理機制。咱們首先來看看引入了異常處理機制的棧框架大概是什麼樣子:3d

  

  由圖2可見,在每一個 C++ 函數的棧框架中都多了一些東西。仔細觀察的話,你會發現,多出來的東西正好是一個 EXP 類型的結構體。進一步分析就會發現,這是一個典型的單向鏈表式結構:

    piPrev 成員指向鏈表的上一個節點,它主要用於在函數調用棧中逐級向上尋找匹配的 catch 塊,並完成棧回退工做。

    piHandler 成員指向完成異常捕獲和棧回退所必須的數據結構(主要是兩張記載着關鍵數據的表:「try」塊表:tblTryBlocks 及「棧回退表」:tblUnwind)。

    nStep 成員用來定位 try 塊,以及在棧回退表中尋找正確的入口。

  須要說明的是:編譯器會爲每個「C++ 函數」定義一個 EHDL 結構,不過只會爲包含了「try」塊的函數定義 tblTryBlocks 成員。此外,異常處理器還會爲每一個線程維護一個指向當前異常處理框架的指針。該指針指向異常處理器鏈表的鏈尾,一般存放在某個 TLS 槽或能起到相似做用的地方。

三、棧回退(stack unwind)

  「棧回退」是伴隨異常處理機制引入 C++ 中的一個新概念,主要用來確保在異常被拋出、捕獲並處理後,全部生命期已結束的對象都會被正確地析構,它們所佔用的空間會被正確地回收。下面咱們就來具體看看編譯器是如何實現棧回退機制的:

  

  圖中的「FuncUnWind」函數內,全部真實代碼均以黑色和藍色字體標示,編譯器生成的代碼則由灰色和橙色字體標明。此時,在圖裏給出的 nStep 變量和 tblUnwind 成員做用就十分明顯了。

  nStep 變量用於跟蹤函數內局部對象的構造、析構階段。再配合編譯器爲每一個函數生成的 tblUnwind 表,就能夠完成退棧機制。表中的 pfnDestroyer 字段記錄了對應階段應當執行的析構操做(析構函數指針);pObj 字段則記錄了與之相對應的對象 this 指針偏移。將 pObj 所指的偏移值加上當前棧框架基址(EBP),就是要代入 pfnDestroyer 所指析構函數的 this 指針,這樣便可完成對該對象的析構工做。而 nNextIdx 字段則指向下一個須要析構對象所在的行(下標)。

  在發生異常時,異常處理器首先檢查當前函數棧框架內的 nStep 值,並經過 piHandler 取得 tblUnwind[] 表。而後將 nStep 做爲下標帶入表中,執行該行定義的析構操做,而後轉向由 nNextIdx 指向的下一行,直到 nNextIdx 爲 -1 爲止。在當前函數的棧回退工做結束後,異常處理器可沿當前函數棧框架內 piPrev 的值回溯到異常處理鏈中的上一節點重複上述操做,直到全部回退工做完成爲止。

  值得一提的是,nStep 的值徹底在編譯時決定,運行時僅需執行若干次簡單的整形當即數賦值(一般是直接賦值給CPU裏的某個寄存器)。此外,對於全部內部類型以及使用了默認構造、析構方法(而且它的全部成員和基類也使用了默認方法)的類型,其建立和銷燬均不影響 nStep 的值。

  注意:若是在棧回退的過程當中,因爲析構函數的調用而再次引起了異常(異常中的異常),則被認爲是一次異常處理機制的嚴重失敗。此時進程將被強行禁止。爲防止出現這種狀況,應在全部可能拋出異常的析構函數中使用「std::uncaught_exception()」方法判斷當前是否正在進行棧回退(即:存在一個未捕獲或未徹底處理完畢的異常)。如是,則應抑制異常的再次拋出。

四、異常捕獲

  一個異常被拋出時,就會當即引起 C++ 的異常捕獲機制:

  

  

  在上一小節中,咱們已經看到了 nStep 變量在跟蹤對象構造、析構方面的做用。實際上 nStep 除了可以跟蹤對象建立、銷燬階段之外,還可以標識當前執行點是否在 try 塊中,以及(若是當前函數有多個 try 塊的話)究竟在哪一個 try 塊中。這是經過在每個 try 塊的入口和出口各爲 nStep 賦予一個惟一 ID 值,並確保 nStep 在對應 try 塊內的變化恰在此範圍以內來實現的。

  在具體實現異常捕獲時,首先,C++ 異常處理器檢查發生異常的位置是否在當前函數的某個 try 塊以內。這項工做能夠經過將當前函數的 nStep 值依次在 piHandler 指向tblTryBlocks[] 表的條目中進行範圍爲 [nBeginStep, nEndStep) 的比對來完成。

  例如:若圖4 中的 FuncB 在 nStep == 2 時發生了異常,則經過比對 FuncB 的 tblTryBlocks[] 表發現 2∈[1, 3),故該異常發生在 FuncB 內的第一個 try 塊中。

  其次,若是異常發生的位置在當前函數中的某個 try 塊內,則嘗試匹配該 tblTryBlocks[] 相應條目中的 tblCatchBlocks[] 表。tblCatchBlocks[] 表中記錄了與指定 try 塊配套出現的全部 catch 塊相關信息,包括這個 catch 塊所能捕獲的異常類型及其起始地址等信息。

  若找到了一個匹配的 catch 塊,則複製當前異常對象到此 catch 塊,而後跳轉到其入口地址執行塊內代碼。

  不然,則說明異常發生位置不在當前函數的 try 塊內,或者這個 try 塊中沒有與當前異常相匹配的 catch 塊,此時則沿着函數棧框架中 piPrev 所指地址(即:異常處理鏈中的上一個節點)逐級重複以上過程,直至找到一個匹配的 catch 塊或到達異常處理鏈的首節點。對於後者,咱們稱爲發生了未捕獲的異常,對於 C++ 異常處理器而言,未捕獲的異常是一個嚴重錯誤,將致使當前進程被強制結束。

五、拋出異常

  接下來討論整個 C++ 異常處理機制中的最後一個環節,異常的拋出:

  

  

  在編譯一段 C++ 代碼時,編譯器會將全部 throw 語句替換爲其 C++ 運行時庫中的某一指定函數,這裏咱們叫它 __CxxRTThrowExp(與本文提到的全部其它數據結構和屬性名同樣,在實際應用中它能夠是任意名稱)。該函數接收一個編譯器承認的內部結構(咱們叫它 EXCEPTION 結構)。這個結構中包含了待拋出異常對象的起始地址、用於銷燬它的析構函數,以及它的 type_info 信息。對於沒有啓用 RTTI 機制(編譯器禁用了 RTTI 機制或沒有在類層次結構中使用虛表)的異常類層次結構,可能還要包含其全部基類的 type_info 信息,以便與相應的 catch 塊進行匹配。

  在圖中的深灰色框圖內,咱們使用 C++ 僞代碼展現了函數 FuncA 中的 「throw myExp(1);」 語句將被編譯器最終翻譯成的樣子。實際上在多數狀況下,__CxxRTThrowExp 函數即咱們前面曾屢次提到的「異常處理器」,異常捕獲和棧回退等各項重要工做都由它來完成。

__CxxRTThrowExp 首先接收(並保存)EXCEPTION 對象;而後從 TLS:Current ExpHdl 處找到與當前函數對應的 piHandler、nStep 等異常處理相關數據;並按照前文所述的機制完成異常捕獲和棧回退。由此完成了包括「拋出」->「捕獲」->「回退」等步驟的整套異常處理機制。

 六、總結

  以上就是C++異常的實現原理,固然其餘語言的異常捕獲機制也是一樣的思想實現異常處理的。

相關文章
相關標籤/搜索