LLVM 中的例外處理

簡介

本文檔是關於 LLVM 中例外處理的所有資訊的中央儲存庫。它描述了 LLVM 例外處理資訊採用的格式,這對於那些有興趣建立前端或直接處理資訊的人很有用。此外,本文檔提供了 C 和 C++ 中例外處理資訊用途的具體範例。

Itanium ABI 零成本例外處理

大多數程式語言的例外處理旨在從應用程式一般使用期間很少發生的情況中恢復。為此,例外處理不應透過執行檢查點任務(例如儲存目前的 pc 或暫存器狀態)來干擾應用程式演算法的主要流程。

Itanium ABI 例外處理規範定義了一種方法,用於以例外表格的形式提供外部資料,而無需在應用程式主要演算法的流程中內嵌推測性例外處理程式碼。因此,該規範被認為為應用程式的正常執行增加了「零成本」。

關於 Itanium ABI 例外處理執行階段支援的更完整描述,請參閱 Itanium C++ ABI:例外處理。關於例外框架格式的描述,請參閱 例外框架,關於 DWARF 4 規範的詳細資訊,請參閱 DWARF 4 標準。關於 C++ 例外表格格式的描述,請參閱 例外處理表格

Setjmp/Longjmp 例外處理

基於 Setjmp/Longjmp (SJLJ) 的例外處理使用 LLVM 內建函數 llvm.eh.sjlj.setjmpllvm.eh.sjlj.longjmp 來處理例外處理的控制流程。

對於每個執行例外處理的函數(無論是 try/catch 區塊還是清理),該函數都會在全域框架列表中註冊自己。當例外狀況正在解除堆疊時,執行階段會使用此列表來識別哪些函數需要處理。

著陸點選擇編碼在函數內容的呼叫站點條目中。執行階段透過 llvm.eh.sjlj.longjmp 返回到函數,其中切換表格根據儲存在函數內容中的索引將控制權轉移到適當的著陸點。

與 DWARF 例外處理(在外部表格中編碼例外區域和框架資訊)相比,SJLJ 例外處理在執行階段建立和移除解除堆疊框架內容。這導致更快的例外處理,但以在未拋出例外狀況時執行速度較慢為代價。由於例外狀況就其本質而言,旨在用於不常見的程式碼路徑,因此 DWARF 例外處理通常優於 SJLJ。

Windows 執行階段例外處理

LLVM 支援處理 Windows 執行階段產生的例外狀況,但它需要非常不同的中間表示法。它不是基於「landingpad」指令(如其他兩個模型),而是在本文檔稍後的 使用 Windows 執行階段的例外處理 中描述。

概述

當在 LLVM 程式碼中拋出例外狀況時,執行階段會盡力尋找適合處理該情況的處理常式。

執行階段首先嘗試尋找與拋出例外狀況的函數相對應的例外框架。如果程式語言支援例外處理(例如 C++),則例外框架包含對例外表格的參考,該表格描述如何處理例外狀況。如果語言不支援例外處理(例如 C),或者如果例外狀況需要轉發到先前的啟用,則例外框架包含關於如何解除目前啟用並還原先前啟用狀態的資訊。重複此過程,直到例外狀況被處理。如果例外狀況未被處理且沒有剩餘啟用,則應用程式將終止並顯示適當的錯誤訊息。

由於不同的程式語言在處理例外狀況時有不同的行為,因此例外處理 ABI 提供了一種機制來提供人格化。例外處理人格化是透過人格化函數(例如 C++ 中的 __gxx_personality_v0)來定義的,該函數接收例外狀況的內容、包含例外物件類型和值的例外結構,以及對目前函數的例外表格的參考。目前編譯單元的人格化函數在通用例外框架中指定。

例外表格的組織方式取決於語言。對於 C++,例外表格組織為一系列程式碼範圍,定義如果該範圍內發生例外狀況該怎麼辦。通常,與範圍相關聯的資訊定義了在該範圍內處理的例外物件類型(使用 C++ 類型資訊),以及應發生的相關動作。動作通常將控制權傳遞給著陸點

著陸點大致對應於在 try/catch 序列的 catch 部分中找到的程式碼。當執行在著陸點恢復時,它會接收一個例外結構和一個對應於拋出的例外狀況類型選擇器值。然後使用選擇器來決定哪個 catch 應該實際處理例外狀況。

LLVM 程式碼產生

從 C++ 開發人員的角度來看,例外狀況是根據 throwtry/catch 陳述式來定義的。在本節中,我們將根據 C++ 範例描述 LLVM 例外處理的實作。

Throw(拋出)

支援例外處理的語言通常提供 throw 運算來啟動例外處理程序。在內部,throw 運算分為兩個步驟。

  1. 發出請求以分配例外空間給例外結構。此結構需要存活超過目前的啟用。此結構將包含要拋出的物件的類型和值。

  2. 呼叫執行階段以引發例外狀況,並將例外結構作為引數傳遞。

在 C++ 中,例外結構的分配由 __cxa_allocate_exception 執行階段函數完成。例外狀況引發由 __cxa_throw 處理。例外狀況的類型使用 C++ RTTI 結構表示。

Try/Catch(嘗試/捕捉)

try 陳述式範圍內的呼叫可能會引發例外狀況。在這些情況下,LLVM C++ 前端會將呼叫替換為 invoke 指令。與呼叫不同,invoke 有兩個潛在的繼續點

  1. 當呼叫像往常一樣成功時繼續的位置,以及

  2. 如果呼叫引發例外狀況時繼續的位置,無論是透過拋出還是拋出的解除堆疊

用於定義在例外狀況之後 invoke 繼續的位置的術語稱為著陸點。LLVM 著陸點在概念上是替代函數進入點,其中例外結構參考和類型資訊索引作為引數傳遞進來。著陸點儲存例外結構參考,然後繼續選擇與例外物件的類型資訊相對應的 catch 區塊。

LLVM 「landingpad」指令用於向後端傳達關於著陸點的資訊。對於 C++,landingpad 指令傳回一個指標和整數對,分別對應於指向例外結構選擇器值的指標。

landingpad 指令在父函數的屬性列表中尋找對用於此 try/catch 序列的人格化函數的參考。該指令包含 cleanupcatchfilter 子句的列表。例外狀況會從第一個到最後一個依序針對子句進行測試。子句具有以下含義

  • catch <type> @ExcType

    • 此子句表示,如果拋出的例外狀況是 @ExcType 類型或 @ExcType 的子類型,則應輸入著陸點區塊。對於 C++,@ExcType 是指向 std::type_info 物件(RTTI 物件)的指標,該物件表示 C++ 例外類型。

    • 如果 @ExcTypenull,則任何例外狀況都符合,因此應始終輸入著陸點。這用於 C++ 全域捕捉區塊(「catch (...)」)。

    • 當此子句符合時,選擇器值將等於「@llvm.eh.typeid.for(i8* @ExcType)」傳回的值。這將始終是一個正值。

  • filter <type> [<type> @ExcType1, ..., <type> @ExcTypeN]

    • 此子句表示,如果拋出的例外狀況符合列表中的任何類型(對於 C++ 而言,再次指定為 std::type_info 指標),則應輸入著陸點。

    • C++ 前端使用它來實作 C++ 例外規範,例如「void foo() throw (ExcType1, ..., ExcTypeN) { ... }」。 (注意:此功能在 C++11 中已棄用,並在 C++17 中已移除。)

    • 當此子句符合時,選擇器值將為負值。

    • filter 的陣列引數可能為空;例如,「[0 x i8**] undef」。這表示應始終輸入著陸點。 (請注意,這樣的 filter 不等同於「catch i8* null」,因為 filtercatch 分別產生負值和正值選擇器值。)

  • cleanup

    • 此子句表示應始終輸入著陸點。

    • C++ 前端使用它來呼叫物件的解構子。

    • 當此子句符合時,選擇器值將為零。

    • 執行階段可能會將「cleanup」與「catch <type> null」區別對待。

      在 C++ 中,如果發生未處理的例外狀況,語言執行階段將呼叫 std::terminate(),但執行階段是否先解除堆疊並呼叫物件解構子是實作定義的。例如,GNU C++ 解除堆疊器在發生未處理的例外狀況時不會呼叫物件解構子。這樣做的原因是為了提高可除錯性:它確保 std::terminate() 是從 throw 的內容中呼叫的,以便此內容不會因解除堆疊而遺失。執行階段通常會透過搜尋符合的非 cleanup 子句來實作此功能,如果找不到,則在輸入任何著陸點區塊之前中止。

一旦著陸點具有類型資訊選擇器,程式碼就會分支到第一個 catch 的程式碼。然後 catch 會檢查類型資訊選擇器的值,以對照該 catch 的類型資訊索引。由於類型資訊索引在後端收集所有類型資訊之前是未知的,因此 catch 程式碼必須呼叫 llvm.eh.typeid.for 內建函數來判斷給定類型資訊的索引。如果 catch 無法符合選擇器,則控制權會傳遞到下一個 catch。

最後,catch 程式碼的進入和退出都使用對 __cxa_begin_catch__cxa_end_catch 的呼叫括起來。

  • __cxa_begin_catch 採用例外結構參考作為引數,並傳回例外物件的值。

  • __cxa_end_catch 不接受引數。此函數

    1. 找到最近捕捉到的例外狀況並遞減其處理常式計數,

    2. 如果處理常式計數變為零,則從捕捉到的堆疊中移除例外狀況,並且

    3. 如果處理常式計數變為零且例外狀況未被重新拋出 throw,則銷毀例外狀況。

    注意

    從 catch 內部重新拋出可能會將此呼叫替換為 __cxa_rethrow

清理

清理是需要在解除堆疊範圍時執行的額外程式碼。C++ 解構子是一個典型的範例,但其他語言和語言擴充功能提供了各種不同的清理種類。一般而言,著陸點可能需要在實際進入 catch 區塊之前執行任意數量的清理程式碼。為了指示清理的存在,「landingpad」指令應具有 cleanup 子句。否則,如果沒有需要解除堆疊器的 catch 或過濾器,則解除堆疊器將不會在著陸點停止。

注意

不要允許新的例外狀況從清理的執行中傳播出去。這可能會損壞解除堆疊器的內部狀態。不同的語言針對這些情況描述了不同的高階語意:例如,C++ 要求終止進程,而 Ada 則取消兩個例外狀況並拋出第三個。

當所有清理完成後,如果例外狀況未由目前的函數處理,請透過呼叫 resume 指令 恢復解除堆疊,並傳遞原始著陸點的 landingpad 指令的結果。

拋出過濾器

在 C++17 之前,C++ 允許指定可以從函數中拋出的例外類型。為了表示這一點,可能存在頂層著陸點以過濾掉無效類型。為了在 LLVM 程式碼中表達這一點,「landingpad」指令將具有 filter 子句。該子句由類型資訊陣列組成。landingpad 將在例外狀況不符合任何類型資訊時傳回負值。如果找不到符合項,則應呼叫 __cxa_call_unexpected,否則呼叫 _Unwind_Resume。這些函數中的每一個都需要對例外結構的參考。請注意,landingpad 指令的最通用形式可以具有任意數量的 catch、cleanup 和 filter 子句(儘管有多個 cleanup 是毫無意義的)。由於內嵌建立巢狀例外處理範圍,LLVM C++ 前端可以產生這樣的 landingpad 指令。

限制

解除堆疊器將是否在呼叫框架中停止的決定委託給該呼叫框架的特定於語言的人格化函數。並非所有解除堆疊器都保證它們會停止執行清理。例如,GNU C++ 解除堆疊器不會這樣做,除非例外狀況實際上是在堆疊中更上面的某個位置捕捉到的。

為了使內嵌行為正確,著陸點必須準備好處理它們最初未宣告的選擇器結果。假設一個函數捕捉類型為 A 的例外狀況,並且它被內嵌到一個捕捉類型為 B 的例外狀況的函數中。內嵌器將更新內嵌著陸點的 landingpad 指令,以包含也捕捉到 B 的事實。如果該著陸點假設它只會被輸入以捕捉 A,那麼它將會感到非常驚訝。因此,著陸點必須測試它們理解的選擇器結果,然後在沒有任何條件符合時,使用 resume 指令 恢復例外傳播。

例外處理內建函數

除了 landingpadresume 指令之外,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.eh.sjlj 內建函數在 LLVM 的後端內部使用。它們的用途由後端的 SjLjEHPrepare 傳遞產生。

llvm.eh.sjlj.setjmp

i32 @llvm.eh.sjlj.setjmp(i8* %setjmp_buf)

對於基於 SJLJ 的例外處理,此內建函數強制目前的函數儲存暫存器,並儲存以下指令的位址,以供 llvm.eh.sjlj.longjmp 用作目的地地址。此內建函數的緩衝區格式和整體功能與 GCC __builtin_setjmp 實作相容,允許使用 clang 和 GCC 建置的程式碼進行互操作。

單一參數是指向五個字組緩衝區的指標,呼叫內容儲存在其中。緩衝區的格式和內容是特定於目標的。在某些目標(ARM、PowerPC、VE、X86)上,前端將框架指標放在第一個字組中,將堆疊指標放在第三個字組中,而此內建函數的目標實作會填寫剩餘的字組。在其他目標 (SystemZ) 上,將呼叫內容儲存到緩衝區完全留給目標實作。

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 中的著陸點條目以符合的順序產生。

Asm 表格格式

例外處理執行階段使用兩個表格來判斷在拋出例外狀況時應採取哪些動作。

例外處理框架

例外處理框架 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 分派。堆疊上的每個框架都有一個指定的人格化常式,該常式決定要採取哪些動作來處理例外狀況。C 和 C++ 程式碼有一些主要的人格化函數:C++ 人格化函數 (__CxxFrameHandler3) 和 SEH 人格化函數 (_except_handler3_except_handler4__C_specific_handler)。它們都透過回呼到父函數中包含的「funclet」來實作清理。

Funclet 在此上下文中是父函數的區域,可以像使用非常特殊的呼叫慣例的函數指標一樣呼叫。父框架的框架指標透過標準 EBP 暫存器或作為第一個參數暫存器傳遞到 funclet 中,具體取決於架構。Funclet 透過透過框架指標存取記憶體中的區域變數來實作 EH 動作,並傳回一些適當的值,繼續 EH 處理。沒有存活到 funclet 內或 funclet 外的變數可以分配在暫存器中。

C++ 的特性也使用 funclet 來包含 catch 區塊的程式碼(即 catch (Type obj) { ... } 中大括號之間的所有使用者程式碼)。執行時期環境必須對 catch 主體使用 funclet,因為 C++ 例外物件配置在處理例外之函數的子堆疊框架中。如果執行時期環境將堆疊回溯到 catch 的框架,則保存例外狀況的記憶體將很快被後續的函數呼叫覆寫。funclet 的使用也允許 __CxxFrameHandler3 實作重新拋出 (rethrow) 而無需訴諸 TLS。相反地,執行時期環境會拋出一個特殊的例外,然後使用 SEH (__try / __except) 在子框架中以新的資訊恢復執行。

換句話說,連續解開堆疊的方法與 Visual C++ 例外和通用 Windows 例外處理不相容。由於 C++ 例外物件存在於堆疊記憶體中,LLVM 無法提供使用 landingpad 的自訂 personality 函數。同樣地,SEH 沒有提供任何機制來重新拋出例外或繼續解開堆疊。因此,LLVM 必須使用本文檔稍後描述的 IR 結構來實作相容的例外處理。

SEH 過濾器表達式

SEH personality 函數也使用 funclet 來實作過濾器表達式,這允許執行任意使用者程式碼來決定要 catch 哪些例外。過濾器表達式不應與 LLVM landingpad 指令的 filter 子句混淆。通常,過濾器表達式用於判斷例外是否來自特定的 DLL 或程式碼區域,或者程式碼是否在存取特定的記憶體位址範圍時發生錯誤。LLVM 目前沒有 IR 來表示過濾器表達式,因為很難表示它們的控制依賴性。過濾器表達式在 EH 的第一階段執行,在清理 (cleanup) 執行之前,這使得建立忠實的控制流程圖非常困難。目前,新的 EH 指令無法表示 SEH 過濾器表達式,前端必須預先概述它們。父函數的區域變數可以使用 llvm.localescapellvm.localrecover 內建函數來跳脫並存取。

新的例外處理指令

新的 EH 指令的主要設計目標是支援 funclet 生成,同時保留關於 CFG 的資訊,以便 SSA 形成仍然有效。作為次要目標,它們被設計為在 MSVC 和 Itanium C++ 例外之間通用。它們對 personality 所需的資料幾乎沒有假設,只要它使用熟悉的 EH 核心動作:catch、cleanup 和 terminate。然而,如果不了解 EH personality 的細節,新的指令很難修改。雖然它們可以用於表示 Itanium EH,但 landingpad 模型在最佳化方面嚴格來說更好。

以下新的指令被視為「例外處理 pad」,因為它們必須是基本區塊的第一個非 phi 指令,該基本區塊可能是 EH 流程邊緣的 unwind 目的地:catchswitchcatchpadcleanuppad。與 landingpad 一樣,當進入 try 範圍時,如果前端遇到可能拋出例外的呼叫點,它應該發出一個 unwind 到 catchswitch 區塊的 invoke。同樣地,在具有解構子的 C++ 物件的範圍內,invoke 應該 unwind 到 cleanuppad

新的指令也用於標記控制權從 catch/cleanup 處理常式轉移出去的點(這將對應於從生成的 funclet 退出)。透過正常執行到達其結尾的 catch 處理常式執行 catchret 指令,這是一個終結器,指示控制權返回到函數中的哪個位置。透過正常執行到達其結尾的 cleanup 處理常式執行 cleanupret 指令,這是一個終結器,指示作用中的例外將 unwind 到何處。

每個新的 EH pad 指令都有一種方法來識別在此動作之後應考慮哪個動作。catchswitch 指令是一個終結器,並且具有一個 unwind 目的地運算元,類似於 invoke 的 unwind 目的地。cleanuppad 指令不是終結器,因此 unwind 目的地儲存在 cleanupret 指令上。成功執行 catch 處理常式應恢復正常的控制流程,因此 catchpadcatchret 指令都不能 unwind。所有這些「unwind 邊緣」可能指向包含 EH pad 指令的基本區塊,或者它們可能 unwind 到呼叫者。unwind 到呼叫者大致具有與 landingpad 模型中的 resume 指令相同的語意。當透過 invoke 進行內聯時,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 父代 token

為了為使用 funclet 的 EH personality 生成表格,有必要恢復原始碼中存在的巢狀結構。這種 funclet 父代關係使用新的「pad」指令產生的 token 編碼在 IR 中。「pad」或「ret」指令的 token 運算元指示它在哪個 funclet 中,如果它沒有巢狀在另一個 funclet 中,則為「none」。

catchpadcleanuppad 指令建立新的 funclet,它們的 token 被其他「pad」指令消耗以建立成員關係。catchswitch 指令不會建立 funclet,但它產生一個 token,該 token 始終被其直接後繼者 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
}

「inner」 catchswitch 消耗由外部 catchswitch 產生的 %1

Funclet 轉換

使用 funclet 的 personality 的 EH 表格隱含地使用 funclet 巢狀關係來編碼 unwind 目的地,因此它們在可以表示的 funclet 轉換集合中受到限制。相關的 LLVM IR 指令因此具有約束,以確保 EH 邊緣在流程圖中的可編碼性。

catchswitchcatchpadcleanuppad 在執行時被稱為「進入」。它隨後可以透過以下任何一種方式「退出」

  • 當沒有任何構成 catchpad 適用於正在處理的例外時,catchswitch 會立即退出,並且它會 unwind 到其 unwind 目的地或呼叫者。

  • 當執行來自 catchpadcatchret 時,catchpad 及其父代 catchswitch 都會退出。

  • 當執行來自 cleanuppadcleanupret 時,cleanuppad 會退出。

  • 當控制權 unwind 到函數的呼叫者時,這些 pad 中的任何一個都會退出,無論是透過一直 unwind 到函數呼叫者的 call、標記為「unwinds to caller」的巢狀 catchswitch,還是標記為「unwinds to caller" 的巢狀 cleanuppadcleanupret

  • 當 unwind 邊緣(來自 invoke、巢狀 catchswitch 或巢狀 cleanuppadcleanupret)unwind 到不是給定 pad 的後代的目的地 pad 時,這些 pad 中的任何一個都會退出。

請注意,ret 指令不是退出 funclet pad 的有效方法;當 pad 已進入但尚未退出時執行 ret 是未定義的行為。

單個 unwind 邊緣可以退出任意數量的 pad(限制是來自 catchswitch 的邊緣必須至少退出自身,而來自 cleanupret 的邊緣必須至少退出其 cleanuppad),然後必須恰好進入一個 pad,該 pad 必須與所有退出的 pad 不同。unwind 邊緣進入的 pad 的父代必須是最近進入的尚未退出的 pad(在從 unwind 邊緣退出的任何 pad 退出後),如果沒有這樣的 pad,則為「none」。這確保了執行時期 funclet 的堆疊始終對應於父代 token 編碼的 funclet pad 樹中的某個路徑。

退出任何給定 funclet pad 的所有 unwind 邊緣(包括退出其 cleanuppadcleanupret 邊緣和退出其 catchswitchcatchswitch 邊緣)必須共享相同的 unwind 目的地。同樣地,任何可能透過 unwind 到呼叫者而退出的 funclet pad,不得被任何 unwind 到呼叫者以外任何地方的例外邊緣退出。這確保了每個 funclet 整體只有一個 unwind 目的地,funclet personality 的 EH 表格可能需要這樣。請注意,任何退出 catchpad 的 unwind 邊緣也會退出其父代 catchswitch,因此這意味著對於任何給定的 catchswitch,其 unwind 目的地也必須是任何退出其構成 catchpad 的 unwind 邊緣的 unwind 目的地。由於 catchswitch 沒有 nounwind 變體,並且由於 IR 生產者 需要將不會 unwind 的呼叫註解為 nounwind,因此在具有非呼叫者 unwind 目的地的 funclet pad 內巢狀 call 或「unwind to callercatchswitch 是合法的;對於這樣的 callcatchswitch 進行 unwind 是未定義的行為。

最後,funclet pad 的 unwind 目的地不能形成循環。這確保了 EH 降階可以建構具有樹狀結構的「try 區域」,funclet 基於 personality 可能需要這樣。

目標平台上的例外處理支援

為了在特定目標平台上支援例外處理,需要實作一些項目。

  • CFI 指令

    首先,您必須為每個目標平台暫存器分配一個唯一的 DWARF 編號。然後在 TargetFrameLoweringemitPrologue 中,您必須發出 CFI 指令,以指定如何計算 CFA(標準框架位址),以及如何從 CFA 指向的位址以偏移量還原暫存器。組譯器會收到 CFI 指令的指示,以建構 .eh_frame 區段,unwinder 使用該區段在例外處理期間解開堆疊。

  • getExceptionPointerRegistergetExceptionSelectorRegister

    TargetLowering 必須實作這兩個函數。personality 函數透過 getExceptionPointerRegistergetExceptionSelectorRegister 分別指定的暫存器,將例外結構(指標)和選擇器值(整數)傳遞到 landing pad。在大多數平台上,它們將是 GPR,並且將與呼叫慣例中指定的暫存器相同。

  • EH_RETURN

    ISD 節點表示未公開的 GCC 擴充功能 __builtin_eh_return (offset, handler),它會按偏移量調整堆疊,然後跳到處理常式。__builtin_eh_return 用於 GCC unwinder (libgcc),但不適用於 LLVM unwinder (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 類別定義 dwarfEncodingstepWithCompactEncoding

  • src/UnwindRegistersRestore.S

    編寫組譯函數以從記憶體還原所有目標平台暫存器。

  • src/UnwindRegistersSave.S

    編寫組譯函數以將所有目標平台暫存器儲存到記憶體中。