LLVM 中的異常處理¶
簡介¶
本文件是 LLVM 中所有與異常處理相關資訊的中心儲存庫。它描述了 LLVM 異常處理資訊採用的格式,這對於有興趣建立前端或直接處理資訊的人很有用。此外,本文件還提供了 C 和 C++ 中異常處理資訊用途的具體範例。
Itanium ABI 零成本異常處理¶
大多數程式語言的異常處理旨在從應用程式一般使用期間很少發生的情況中恢復。為此,異常處理不應透過執行檢查點任務(例如儲存目前的 pc 或暫存器狀態)來干擾應用程式演算法的主要流程。
Itanium ABI 異常處理規範定義了一種方法,可以以異常表的形式提供外圍資料,而無需在應用程式主要演算法的流程中插入推測性異常處理程式碼。因此,該規範據說為應用程式的正常執行增加了「零成本」。
可以在 Itanium C++ ABI:異常處理 中找到更完整的 Itanium ABI 異常處理執行階段支援描述。可以在 異常框架 中找到異常框架格式的描述,以及 DWARF 4 標準 中的 DWARF 4 規範詳細資訊。可以在 異常處理表 中找到 C++ 異常表格式的描述。
Setjmp/Longjmp 異常處理¶
基於 Setjmp/Longjmp (SJLJ) 的異常處理使用 LLVM 內建函式 llvm.eh.sjlj.setjmp 和 llvm.eh.sjlj.longjmp 來處理異常處理的控制流程。
對於每個執行異常處理的函數(無論是 try
/catch
區塊還是清理動作),該函數都會將自身註冊到全域框架列表中。當異常展開時,執行階段會使用此列表來識別需要處理的函數。
著陸區塊的選擇會編碼在函數上下文的呼叫站點條目中。執行階段透過 llvm.eh.sjlj.longjmp 返回函數,其中一個跳轉表會根據儲存在函數上下文中的索引,將控制權轉移到適當的著陸區塊。
與將異常區域和框架資訊編碼在行外表格中的 DWARF 異常處理形成對比,SJLJ 異常處理會在執行階段建立和移除展開框架上下文。這會導致更快的異常處理速度,但代價是在沒有拋出異常時執行速度較慢。由於異常的本質是用於不常見的程式碼路徑,因此通常會優先選擇 DWARF 異常處理而不是 SJLJ。
Windows 執行階段異常處理¶
LLVM 支援處理 Windows 執行階段產生的異常,但它需要一種非常不同的中介表示法。它不像其他兩種模型那樣基於「著陸區塊」指令,並且將在本文稍後的 使用 Windows 執行階段進行異常處理 中進行說明。
概覽¶
當 LLVM 程式碼中拋出異常時,執行階段會盡力尋找適合處理該情況的處理常式。
執行階段會先嘗試尋找對應於拋出異常之函數的 *異常框架*。如果程式語言支援異常處理(例如 C++),則異常框架會包含對描述如何處理異常的異常表的參考。如果該語言不支援異常處理(例如 C),或者需要將異常轉發到先前的激活,則異常框架會包含有關如何展開當前激活並還原先前激活狀態的資訊。重複此過程,直到異常被處理為止。如果異常未被處理且沒有剩餘的激活,則應用程式會終止並顯示適當的錯誤訊息。
由於不同的程式語言在處理異常時有不同的行為,因此異常處理 ABI 提供了一種提供 *特性* 的機制。異常處理特性是透過 *特性函數*(例如 C++ 中的 __gxx_personality_v0
)定義的,該函數會接收異常的上下文、包含異常物件類型和值的 *異常結構*,以及對當前函數的異常表的參考。當前編譯單元的特性函數是在 *通用異常框架* 中指定的。
異常表的組織方式取決於語言。對於 C++,異常表被組織為一系列程式碼範圍,定義了如果在該範圍內發生異常該怎麼做。通常,與範圍相關聯的資訊定義了在該範圍內處理哪些類型的異常物件(使用 C++ *類型資訊*),以及應該採取的相關動作。動作通常會將控制權傳遞給 *著陸區塊*。
著陸區大致對應於在 try
/catch
序列中 catch
部分找到的程式碼。當執行在著陸區恢復時,它會收到一個對應於拋出異常「類型」的「異常結構」和「選擇器值」。然後使用選擇器來確定實際上應該由哪個 catch
來處理異常。
LLVM 程式碼生成¶
從 C++ 開發人員的角度來看,異常是根據 throw
和 try
/catch
語句來定義的。在本節中,我們將以 C++ 範例來描述 LLVM 異常處理的實現。
拋出¶
支援異常處理的語言通常會提供 throw
操作來啟動異常處理程序。在內部,throw
操作分為兩個步驟。
發出請求以分配異常結構的異常空間。此結構需要在當前作用域之外存活。此結構將包含被拋出物件的類型和值。
呼叫執行階段以引發異常,並將異常結構作為參數傳遞。
在 C++ 中,異常結構的分配由 __cxa_allocate_exception
執行階段函式完成。異常引發由 __cxa_throw
處理。異常的類型使用 C++ RTTI 結構表示。
Try/Catch¶
try 語句範圍內的呼叫可能會引發異常。在這種情況下,LLVM C++ 前端會使用 invoke
指令替換呼叫。與呼叫不同,invoke
有兩個潛在的繼續點
呼叫成功時繼續的位置,以及
呼叫因拋出或拋出展開而引發異常時繼續的位置
用於定義 invoke
在異常後繼續執行的位置的術語稱為「著陸區」。LLVM 著陸區在概念上是替代的函式入口點,其中異常結構參考和類型資訊索引作為參數傳入。著陸區會儲存異常結構參考,然後繼續選擇與異常物件的類型資訊相對應的 catch 區塊。
LLVM ‘landingpad’ 指令 用於將有關著陸區的資訊傳遞給後端。對於 C++,landingpad
指令會返回一個指標和整數對,分別對應於指向「異常結構」的指標和「選擇器值」。
landingpad
指令會在父函式的屬性清單中尋找要在此 try
/catch
序列中使用的個性化函式參考。該指令包含一組 _cleanup_、_catch_ 和 _filter_ 子句。異常會依序從第一個到最後一個與子句進行比對。這些子句具有以下含義:
catch <type> @ExcType
此子句表示如果拋出的異常類型為
@ExcType
或@ExcType
的子類型,則應進入 landingpad 區塊。對於 C++ 而言,@ExcType
是指向表示 C++ 異常類型的std::type_info
物件(一個 RTTI 物件)的指標。如果
@ExcType
為null
,則任何異常都符合,因此應始終進入 landingpad。這用於 C++ 的全捕捉區塊(”catch (...)
”)。當此子句符合時,選擇器值將等於 “
@llvm.eh.typeid.for(i8* @ExcType)
” 返回的值。這將始終是一個正值。
filter <type> [<type> @ExcType1, ..., <type> @ExcTypeN]
此子句表示如果拋出的異常與清單中的任何類型_不符_,則應進入 landingpad(對於 C++ 而言,這些類型同樣指定為
std::type_info
指標)。C++ 前端使用此子句來實現 C++ 異常規範,例如 “
void foo() throw (ExcType1, ..., ExcTypeN) { ... }
”。(注意:此功能在 C++11 中已被棄用,並在 C++17 中被移除。)當此子句符合時,選擇器值將為負值。
filter
的陣列參數可以是空的;例如,“[0 x i8**] undef
”。這表示應始終進入 landingpad。(請注意,這樣的filter
不等同於 “catch i8* null
”,因為filter
和catch
分別產生負選擇器值和正選擇器值。)
cleanup
此子句表示應始終進入 landingpad。
C++ 前端使用此子句來呼叫物件的解構函式。
當此子句符合時,選擇器值將為零。
執行階段可能會以不同於 “
catch <type> null
” 的方式處理 “cleanup
”。在 C++ 中,如果發生未處理的例外狀況,語言執行階段會呼叫
std::terminate()
,但執行階段是否會先展開堆疊並呼叫物件解構函式則取決於實作。例如,GNU C++ unwinder 在發生未處理的例外狀況時不會呼叫物件解構函式。這樣做的原因是為了提高可偵錯性:它確保從throw
的上下文呼叫std::terminate()
,以便在展開堆疊時不會遺失此上下文。執行階段通常會透過搜尋相符的非cleanup
子句來實作此功能,如果找不到相符的子句,則在進入任何 landingpad 區塊之前中止。
landing pad 取得型別資訊選擇器後,程式碼會分支到第一個 catch 的程式碼。然後,catch 會根據該 catch 的型別資訊索引檢查型別資訊選擇器的值。由於在後端收集到所有型別資訊之前,型別資訊索引是未知的,因此 catch 程式碼必須呼叫 llvm.eh.typeid.for 內建函式來判斷給定型別資訊的索引。如果 catch 與選擇器不符,則控制權會傳遞給下一個 catch。
最後,catch 程式碼的進入和退出會以對 __cxa_begin_catch
和 __cxa_end_catch
的呼叫括起來。
__cxa_begin_catch
將例外結構參考作為參數,並傳回例外物件的值。__cxa_end_catch
不接受任何參數。此函式會找出最近捕獲的例外並將其處理常式計數減 1,
如果處理常式計數變為零,則從「已捕獲」堆疊中移除例外,以及
如果處理常式計數變為零且例外並非由 throw 重新擲出,則銷毀例外。
注意
catch 內部的重新擲出可能會將此呼叫替換為
__cxa_rethrow
。
清除¶
清除是在展開作用域時需要執行的額外程式碼。C++ 解構函式是一個典型範例,但其他語言和語言擴充功能提供了各種不同的清除方式。一般來說,landing pad 可能需要在實際進入 catch 區塊之前執行任意數量的清除程式碼。若要指示清除的存在,‘landingpad’ 指令 應該包含一個「cleanup」子句。否則,如果沒有任何需要 unwinder 停在 landing pad 的 catch 或 filter,unwinder 將不會停在 landing pad。
注意
不允許新的例外從清除的執行中傳播出去。這可能會損壞 unwinder 的內部狀態。不同的語言針對這些情況描述了不同的高階語義:例如,C++ 要求終止處理程序,而 Ada 則會取消例外並擲出第三個例外。
當所有清除都完成後,如果目前函式沒有處理例外,則透過呼叫 resume 指令 來繼續展開,並傳入原始 landing pad 的 landingpad
指令的結果。
擲出篩選器¶
在 C++17 之前,C++ 允許指定函數可以拋出哪些異常類型。為了表示這一點,可能存在一個頂層著陸塊來過濾掉無效類型。為了在 LLVM 代碼中表達這一點,‘landingpad’ 指令 將包含一個過濾子句。該子句由一個類型信息數組組成。 如果異常與任何類型信息都不匹配,則 landingpad
將返回一個負值。如果沒有找到匹配項,則應調用 __cxa_call_unexpected
,否則調用 _Unwind_Resume
。這些函數中的每一個都需要引用異常結構。請注意,landingpad
指令的最通用形式可以有任何數量的 catch、cleanup 和 filter 子句(儘管擁有多個 cleanup 沒有意義)。由於內聯創建嵌套異常處理作用域,LLVM C++ 前端可以生成此類 landingpad
指令。
限制¶
展開器將是否在調用框架中停止的決定委託給該調用框架的特定於語言的人格函數。並非所有展開器都保證它們會停止執行清理。例如,GNU C++ 展開器不會這樣做,除非異常實際上在堆棧的更上層被捕獲。
為了使內聯行為正確,著陸塊必須準備好處理它們最初沒有宣傳的選擇器結果。假設一個函數捕獲類型為 A
的異常,並且它被內聯到一個捕獲類型為 B
的異常的函數中。內聯器將更新內聯著陸塊的 landingpad
指令,以包含 B
也被捕獲的事實。如果該著陸塊假設它只會被輸入以捕獲 A
,那麼它就會遇到麻煩。因此,著陸塊必須測試它們理解的選擇器結果,然後如果沒有條件匹配,則使用 resume 指令 恢復異常傳播。
異常處理內建函數¶
除了 landingpad
和 resume
指令之外,LLVM 還使用多個內建函數(名稱以 llvm.eh
為前綴)在生成代碼中的各個點提供異常處理信息。
llvm.eh.typeid.for
¶
i32 @llvm.eh.typeid.for(i8* %type_info)
此內建函數返回當前函數的異常表中的類型信息索引。此值可用於與 landingpad
指令的結果進行比較。單個參數是對類型信息的引用。
此內建函數的使用由 C++ 前端生成。
llvm.eh.exceptionpointer
¶
i8 addrspace(N)* @llvm.eh.padparam.pNi8(token %catchpad)
此內建函數檢索指向由給定 catchpad
捕獲的異常的指針。
SJLJ 內建函數¶
LLVM 後端內部會使用 llvm.eh.sjlj
內建函式。它們的使用是由後端的 SjLjEHPrepare
階段產生的。
llvm.eh.sjlj.setjmp
¶
i32 @llvm.eh.sjlj.setjmp(i8* %setjmp_buf)
對於基於 SJLJ 的例外處理,此內建函式會強制儲存目前函式的暫存器,並儲存下一條指令的位址,以便 llvm.eh.sjlj.longjmp 將其用作目的地位址。緩衝區格式和此內建函式的整體功能與 GCC __builtin_setjmp
實作相容,允許使用 clang 和 GCC 建置的程式碼進行互通。
單一參數是指向五個字組緩衝區的指標,呼叫上下文會儲存在其中。前端將框架指標放在第一個字組中,而此內建函式的目標實作應該將 llvm.eh.sjlj.longjmp 的目的地位址放在第二個字組中。後面的三個字組可用於特定目標的方式。
llvm.eh.sjlj.longjmp
¶
void @llvm.eh.sjlj.longjmp(i8* %setjmp_buf)
對於基於 SJLJ 的例外處理,llvm.eh.sjlj.longjmp
內建函式用於實作 __builtin_longjmp()
。單一參數是指向由 llvm.eh.sjlj.setjmp 填充的緩衝區的指標。框架指標和堆疊指標會從緩衝區中恢復,然後控制權會轉移到目的地位址。
llvm.eh.sjlj.lsda
¶
i8* @llvm.eh.sjlj.lsda()
對於基於 SJLJ 的例外處理,llvm.eh.sjlj.lsda
內建函式會傳回目前函式的語言特定資料區域 (LSDA) 位址。SJLJ 前端程式碼會將此位址儲存在例外處理函式上下文中,供執行階段使用。
llvm.eh.sjlj.callsite
¶
void @llvm.eh.sjlj.callsite(i32 %call_site_num)
對於基於 SJLJ 的例外處理,llvm.eh.sjlj.callsite
內建函式會識別與下列 invoke
指令相關聯的呼叫站點值。這用於確保以相符的順序產生 LSDA 中的登陸平台項目。
組合語言表格格式¶
例外處理執行階段會使用兩個表格來判斷擲回例外時應採取的動作。
例外處理框架¶
例外處理框架 eh_frame
與 DWARF 除錯資訊使用的回溯框架非常相似。框架包含清除目前框架和恢復先前框架狀態所需的所有資訊。編譯單元中的每個函式都有一個例外處理框架,還有一個通用的例外處理框架,定義單元中所有函式共用的資訊。
但是,此呼叫框架資訊 (CFI) 的格式通常取決於平台。例如,ARM 定義了自己的格式。Apple 有自己的精簡回溯資訊格式。在 Windows 上,從 32 位元 x86 開始,所有架構都使用另一種格式。LLVM 將發出目標所需的任何資訊。
例外表格¶
例外表包含當函數代碼的特定部分拋出例外時要採取的操作的相關信息。 這通常被稱為語言特定數據區域 (LSDA)。 LSDA 表的格式特定於個性函數,但大多數個性函數都使用 __gxx_personality_v0
所使用的表的變體。 除葉函數和僅調用非拋出函數的函數外,每個函數都有一個例外表。 它們不需要例外表。
使用 Windows 執行階段的例外處理¶
Windows 例外的背景¶
在 Windows 上與例外互動比在 Itanium C++ ABI 平台上複雜得多。 這兩種模型之間的根本區別在於 Itanium EH 是圍繞「連續展開」的概念設計的,而 Windows EH 則不是。
在 Itanium 下,拋出例外通常涉及分配線程本地內存來保存例外,並調用 EH 運行時。 運行時會識別具有適當例外處理操作的框架,並將當前線程的寄存器上下文連續重置為具有要運行的操作的最近活動框架。 在 LLVM 中,執行在 landingpad
指令處恢復,該指令產生運行時提供的寄存器值。 如果函數僅清理已分配的資源,則該函數負責在完成清理後調用 _Unwind_Resume
以轉換到最近活動的框架。 最終,負責處理例外的框架會調用 __cxa_end_catch
來銷毀例外、釋放其內存並恢復正常的控制流程。
Windows EH 模型不使用這些連續的寄存器上下文重置。 相反,活動例外通常由堆疊上的框架描述。 對於 C++ 例外,例外對象在堆疊內存中分配,並且其地址被傳遞給 __CxxThrowException
。 通用結構化例外 (SEH) 更類似於 Linux 信號,並且它們由 Windows 提供的用戶空間 DLL 分派。 堆疊上的每個框架都有一個分配的 EH 個性例程,它決定要採取哪些操作來處理例外。 C 和 C++ 代碼有幾個主要的個性:C++ 個性 (__CxxFrameHandler3
) 和 SEH 個性 (_except_handler3
、_except_handler4
和 __C_specific_handler
)。 它們都通過回調到父函數中包含的「函數」來實現清理。
在此上下文中,函數是父函數的區域,可以像調用具有非常特殊的調用約定的函數指針一樣調用它們。 父框架的框架指針使用標準 EBP 寄存器或作為第一個參數寄存器傳遞到函數中,具體取決於架構。 函數通過框架指針訪問內存中的局部變量並返回一些適當的值來實現 EH 操作,繼續 EH 過程。 進入或離開函數的任何變量都不能在寄存器中分配。
C++ 個性也使用 funclet 來包含 catch 區塊的程式碼(亦即,在 catch (Type obj) { ... }
大括號之間的所有使用者程式碼)。執行階段必須使用 funclet 來處理 catch 主體,因為 C++ 異常物件是在處理異常的函式的子堆疊框架中配置的。如果執行階段將堆疊回溯到 catch 的框架,則持有異常的記憶體將會被後續的函式呼叫迅速覆蓋。 funclet 的使用也允許 __CxxFrameHandler3
在不訴諸 TLS 的情況下實作重新擲回。相反的,執行階段會擲回一個特殊的異常,然後使用 SEH (__try / __except
) 在子框架中使用新的資訊來繼續執行。
換句話說,連續回溯方法與 Visual C++ 異常和通用 Windows 異常處理不相容。因為 C++ 異常物件存在於堆疊記憶體中,LLVM 無法提供使用 landingpad 的自訂個性函式。同樣地,SEH 沒有提供任何重新擲回異常或繼續回溯的機制。因此,LLVM 必須使用本文檔稍後描述的 IR 建構來實作相容的異常處理。
SEH 過濾器運算式¶
SEH 個性函式也使用 funclet 來實作過濾器運算式,允許執行任意使用者程式碼來決定要捕捉哪些異常。過濾器運算式不應與 LLVM landingpad
指令的 filter
子句混淆。通常,過濾器運算式用於判斷異常是來自特定的 DLL 或程式碼區域,或者程式碼在存取特定記憶體位址範圍時是否出錯。LLVM 目前沒有 IR 來表示過濾器運算式,因為很難表示它們的控制依賴關係。過濾器運算式在 EH 的第一階段執行,在清理執行之前,這使得建構忠實的控制流程圖變得非常困難。目前,新的 EH 指令無法表示 SEH 過濾器運算式,前端必須事先將它們概述。可以使用 llvm.localescape
和 llvm.localrecover
內建函式對父函式的區域變數進行跳脫和存取。
新的異常處理指令¶
新的 EH 指令的主要設計目標是在保留 CFG 相關資訊的同時支援 funclet 生成,以便 SSA 形成仍然有效。作為次要目標,它們的設計是在 MSVC 和 Itanium C++ 異常中保持通用。它們對個性所需的數據做了很少的假設,只要它使用熟悉的核心 EH 操作:catch、cleanup 和 terminate。但是,如果不知道 EH 個性的詳細資訊,就很難修改新的指令。雖然它們可以用於表示 Itanium EH,但 landingpad 模型在最佳化目的方面絕對更好。
以下這些新的指令被視為「異常處理 pad」,因為它們必須是基本區塊中第一個非 phi 指令,而這些基本區塊可能是 EH 流程邊緣的 unwind 目的地:catchswitch
、catchpad
和 cleanuppad
。與 landingpad 一樣,當進入 try 範圍時,如果前端遇到可能會丟出異常的呼叫站點,它應該發出一個 unwind 到 catchswitch
區塊的 invoke。同樣地,在具有解構函式的 C++ 物件範圍內,invoke 應該 unwind 到 cleanuppad
。
新的指令也用於標記控制權從 catch/cleanup 處理常式轉移出去的點(這將對應於從產生的 funclet 退出)。透過正常執行到達其結尾的 catch 處理常式會執行 catchret
指令,這是一個終止符,指示控制權在函式中的何處返回。透過正常執行到達其結尾的 cleanup 處理常式會執行 cleanupret
指令,這是一個終止符,指示活動異常接下來將 unwind 到何處。
這些新的 EH pad 指令中的每一個都有一種方法來識別在這個動作之後應該考慮哪個動作。catchswitch
指令是一個終止符,並且具有一個類似於 invoke 的 unwind 目的地運算元的 unwind 目的地運算元。cleanuppad
指令不是終止符,因此 unwind 目的地儲存在 cleanupret
指令上。成功執行 catch 處理常式應該會恢復正常的控制流程,因此 catchpad
和 catchret
指令都不能 unwind。所有這些「unwind 邊緣」都可能指向包含 EH pad 指令的基本區塊,或者它們可能 unwind 到呼叫者。unwind 到呼叫者與 landingpad 模型中的 resume
指令具有大致相同的語義。當透過 invoke 進行內聯時,unwind 到呼叫者的指令會被連接到 unwind 到呼叫站點的 unwind 目的地。
綜上所述,以下是使用所有新 IR 指令的假設性 C++ 降低
struct Cleanup {
Cleanup();
~Cleanup();
int m;
};
void may_throw();
int f() noexcept {
try {
Cleanup obj;
may_throw();
} catch (int e) {
may_throw();
return e;
}
return 0;
}
define i32 @f() nounwind personality ptr @__CxxFrameHandler3 {
entry:
%obj = alloca %struct.Cleanup, align 4
%e = alloca i32, align 4
%call = invoke ptr @"??0Cleanup@@QEAA@XZ"(ptr nonnull %obj)
to label %invoke.cont unwind label %lpad.catch
invoke.cont: ; preds = %entry
invoke void @"?may_throw@@YAXXZ"()
to label %invoke.cont.2 unwind label %lpad.cleanup
invoke.cont.2: ; preds = %invoke.cont
call void @"??_DCleanup@@QEAA@XZ"(ptr nonnull %obj) nounwind
br label %return
return: ; preds = %invoke.cont.3, %invoke.cont.2
%retval.0 = phi i32 [ 0, %invoke.cont.2 ], [ %3, %invoke.cont.3 ]
ret i32 %retval.0
lpad.cleanup: ; preds = %invoke.cont.2
%0 = cleanuppad within none []
call void @"??1Cleanup@@QEAA@XZ"(ptr nonnull %obj) nounwind
cleanupret from %0 unwind label %lpad.catch
lpad.catch: ; preds = %lpad.cleanup, %entry
%1 = catchswitch within none [label %catch.body] unwind label %lpad.terminate
catch.body: ; preds = %lpad.catch
%catch = catchpad within %1 [ptr @"??_R0H@8", i32 0, ptr %e]
invoke void @"?may_throw@@YAXXZ"()
to label %invoke.cont.3 unwind label %lpad.terminate
invoke.cont.3: ; preds = %catch.body
%3 = load i32, ptr %e, align 4
catchret from %catch to label %return
lpad.terminate: ; preds = %catch.body, %lpad.catch
cleanuppad within none []
call void @"?terminate@@YAXXZ"()
unreachable
}
Funclet 父權杖¶
為了產生使用 funclet 的 EH 個性表格,有必要恢復原始碼中存在的巢狀結構。這種 funclet 父關係使用新「pad」指令產生的權杖編碼在 IR 中。「pad」或「ret」指令的權杖運算元指示它所在的 funclet,如果它沒有巢狀在另一個 funclet 中,則為「無」。
catchpad
和 cleanuppad
指令建立新的 funclet,並且它們的權杖由其他「pad」指令使用以建立成員資格。catchswitch
指令不會建立 funclet,但它會產生一個權杖,該權杖始終由其後面的 catchpad
指令使用。這確保了由 catchpad
建模的每個 catch 處理常式都恰好屬於一個 catchswitch
,後者建模了 C++ try 之後的發送點。
以下是一個使用一些假設性 C++ 程式碼的巢狀結構範例
void f() {
try {
throw;
} catch (...) {
try {
throw;
} catch (...) {
}
}
}
define void @f() #0 personality i8* bitcast (i32 (...)* @__CxxFrameHandler3 to i8*) {
entry:
invoke void @_CxxThrowException(i8* null, %eh.ThrowInfo* null) #1
to label %unreachable unwind label %catch.dispatch
catch.dispatch: ; preds = %entry
%0 = catchswitch within none [label %catch] unwind to caller
catch: ; preds = %catch.dispatch
%1 = catchpad within %0 [i8* null, i32 64, i8* null]
invoke void @_CxxThrowException(i8* null, %eh.ThrowInfo* null) #1
to label %unreachable unwind label %catch.dispatch2
catch.dispatch2: ; preds = %catch
%2 = catchswitch within %1 [label %catch3] unwind to caller
catch3: ; preds = %catch.dispatch2
%3 = catchpad within %2 [i8* null, i32 64, i8* null]
catchret from %3 to label %try.cont
try.cont: ; preds = %catch3
catchret from %1 to label %try.cont6
try.cont6: ; preds = %try.cont
ret void
unreachable: ; preds = %catch, %entry
unreachable
}
內部的 catchswitch
會消耗外部 catchswitch 所產生的 %1
。
Funclet 轉移¶
使用 funclet 的 Personality 的 EH 表會隱式地使用 funclet 嵌套關係來編碼 unwind 目的地,因此它們可以表示的 funclet 轉移集合會受到限制。相關的 LLVM IR 指令也會有相應的限制,以確保流程圖中 EH 邊緣的可編碼性。
當 catchswitch
、catchpad
或 cleanuppad
執行時,就稱為「進入」。它可能會透過以下任何一種方式「退出」:
當
catchswitch
中沒有任何組成catchpad
適用於進行中的例外,並且它 unwind 到其 unwind 目的地或呼叫者時,它會立即退出。當執行
catchpad
中的catchret
時,catchpad
及其父catchswitch
都會退出。當執行
cleanuppad
中的cleanupret
時,它會退出。當控制權透過以下方式 unwind 到函數的呼叫者時,所有這些 pad 都會退出:
call
一路 unwind 到函數的呼叫者、標記為「unwinds to caller
」的嵌套catchswitch
,或嵌套cleanuppad
的cleanupret
標記為「unwinds to caller
」。當 unwind 邊緣(來自
invoke
、嵌套catchswitch
或嵌套cleanuppad
的cleanupret
)unwind 到不是給定 pad 後代的目的地 pad 時,所有這些 pad 都會退出。
請注意,ret
指令並*不是*退出 funclet pad 的有效方法;在已進入但尚未退出 pad 時執行 ret
是未定義行為。
單個 unwind 邊緣可以退出任意數量的 pad(但有限制:來自 catchswitch
的邊緣必須至少退出自身,而來自 cleanupret
的邊緣必須至少退出其 cleanuppad
),然後必須精確地進入一個 pad,該 pad 必須與所有已退出的 pad 不同。unwind 邊緣進入的 pad 的父項必須是最近進入且尚未退出的 pad(在退出 unwind 邊緣退出的任何 pad 之後),如果沒有這樣的 pad,則為「無」。這確保了執行時執行的 funclet 堆疊始終對應於父權杖編碼的 funclet pad 樹中的某個路徑。
所有從任何給定 funclet pad 離開的 unwind 邊緣(包括從其 cleanuppad
離開的 cleanupret
邊緣,以及從其 catchswitch
離開的 catchswitch
邊緣)都必須共享相同的 unwind 目的地。同樣地,任何可能透過 unwind 至呼叫端離開的 funclet pad,都不能透過任何 unwind 至呼叫端以外位置的例外邊緣離開。這確保每個 funclet 整體上只有一個 unwind 目的地,這可能是 funclet 個性 EH 表所需的。請注意,任何離開 catchpad
的 unwind 邊緣也會離開其父 catchswitch
,因此這意味著對於任何給定的 catchswitch
,其 unwind 目的地也必須是離開其任何組成 catchpad
的任何 unwind 邊緣的 unwind 目的地。由於 catchswitch
沒有 nounwind
變體,並且因為 IR 生產者*不需要*將不會 unwind 的呼叫標記為 nounwind
,所以可以在 unwind 目的地不是呼叫端的 funclet pad 中嵌套 call
或「unwind to caller
」catchswitch
;如果這樣的 call
或 catchswitch
進行 unwind,則屬於未定義行為。
最後,funclet pad 的 unwind 目的地不能形成循環。這確保 EH 降低可以建構具有樹狀結構的「try 區域」,這可能是基於 funclet 的個性所需的。
目標上的例外處理支援¶
為了在特定目標上支援例外處理,需要實作一些項目。
CFI 指令
首先,您必須為每個目標暫存器分配一個唯一的 DWARF 編號。然後在
TargetFrameLowering
的emitPrologue
中,您必須發出 CFI 指令 來指定如何計算 CFA(Canonical Frame Address,規範框架位址)以及如何使用偏移量從 CFA 指向的位址還原暫存器。組譯器會根據 CFI 指令建構.eh_frame
區段,unwinder 會在例外處理期間使用該區段來解除堆疊。getExceptionPointerRegister
和getExceptionSelectorRegister
TargetLowering
必須實作這兩個函式。*個性函式*會分別透過getExceptionPointerRegister
和getExceptionSelectorRegister
指定的暫存器,將*例外結構*(一個指標)和*選擇器值*(一個整數)傳遞給登陸區塊。在大多數平台上,它們將是 GPR,並且將與呼叫慣例中指定的相同。EH_RETURN
ISD 節點表示未記載的 GCC 擴充功能
__builtin_eh_return (offset, handler)
,它會調整堆疊的偏移量,然後跳轉到處理常式。__builtin_eh_return
用於 GCC 解退器 (libgcc),但不用於 LLVM 解退器 (libunwind)。如果您在libgcc
的頂部,並且對目標有特定要求,則必須在TargetLowering
中處理EH_RETURN
。
如果您沒有利用現有的執行階段環境 (libstdc++
和 libgcc
),則必須查看 libc++ 和 libunwind 以了解需要做些什麼。對於 libunwind
,您必須執行以下操作
__libunwind_config.h
為您的目標定義巨集。
include/libunwind.h
為目標暫存器定義列舉。
src/Registers.hpp
為您的目標定義
Registers
類別,並實作 setter 和 getter 函式。src/UnwindCursor.hpp
為您的
Registers
類別定義dwarfEncoding
和stepWithCompactEncoding
。src/UnwindRegistersRestore.S
編寫一個組語函式,從記憶體中還原所有目標暫存器。
src/UnwindRegistersSave.S
編寫一個組語函式,將所有目標暫存器儲存到記憶體中。