要理解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
函式棧架構主要承載著以下幾個部分:
1、傳遞引數:通常,函式的呼叫引數總是在這個函式棧框架的最頂端。
3、存放呼叫者的當前棧指標:便於清理被呼叫者的所有區域性變數、並恢復呼叫者的現場。
4、存放當前函式內的所有區域性變數:記得嗎?剛才說過所有區域性和臨時變數都是儲存在棧上的。
首先澄清一點,這裡說的 「c++ 函式」是指:
1、該函式可能會直接或間接地丟擲乙個異常:即該函式的定義存放在乙個 c++ 編譯(而不是傳統 c)單元內,並且該函式沒有使用「throw()」異常過濾器
2、該函式的定義內使用了 try 塊。
以上兩者滿足其一即可。為了能夠成功地捕獲異常和正確地完成棧回退(stack unwind),編譯器必須要引入一些額外的資料結構和相應的處理機制。我們首先來看看引入了異常處理機制的棧框架大概是什麼樣子:
由圖2可見,在每個 c++ 函式的棧框架中都多了一些東西。仔細觀察的話,你會發現,多出來的東西正好是乙個 exp 型別的結構體。進一步分析就會發現,這是乙個典型的單向鍊錶式結構:
piprev 成員指向鍊錶的上乙個節點,它主要用於在函式呼叫棧中逐級向上尋找匹配的 catch 塊,並完成棧回退工作。
pihandler 成員指向完成異常捕獲和棧回退所必須的資料結構(主要是兩張記載著關鍵資料的表:「try」塊表:tbltryblocks 及「棧回退表」:tblunwind)。
nstep 成員用來定位 try 塊,以及在棧回退表中尋找正確的入口。
需要說明的是:編譯器會為每乙個「c++ 函式」定義乙個 ehdl 結構,不過只會為包含了「try」塊的函式定義 tbltryblocks 成員。此外,異常處理器還會為每個執行緒維護乙個指向當前異常處理框架的指標。該指標指向異常處理器鍊錶的鏈尾,通常存放在某個 tls 槽或能起到類似作用的地方。
「棧回退」是伴隨異常處理機制引入 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++異常的實現原理,當然其他語言的異常捕獲機制也是同樣的思想實現異常處理的。
C 異常 異常機制
c 異常是丟程式執行過程中發生的異常情況 例如被0除 的一種響應。異常提供了將控制權從程式的乙個部分傳遞到另一部分的途徑。對異常的處理有3個組成部分 引發異常 使用處理程式捕獲異常 使用try塊。程式在出現問題時將引發異常。throw語句實際上是跳轉,即命令程式跳到另一條語句。throw關鍵字表示引...
C 異常機制
在c 中的函式呼叫中,是用棧來存放其中資料物件。表1.1 我們結合這張表,來簡單介紹函式的棧結構。其中每乙個函式在入棧的時候,編譯器會自動新增額外的資料結構,這裡的exception registration就是被額外新增進來的。對於這個結構體我們稍後解釋,首先來介紹函式的基本結構。從這張圖中可以清...
C 異常機制
一 簡單例項 異常是通過丟擲物件而引發的,丟擲物件的型別決定應該啟用那個處理 即catch中的語句 被處理 是呼叫鏈中與該丟擲物件型別匹配,並且離丟擲異常位置最近的那乙個 例如 void test int main catch string e t value void print void pus...