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.setjmpおよびllvm.eh.sjlj.longjmpを使用して、例外処理の制御フローを処理します。

例外処理を行う各関数(try/catchブロックまたはクリーンアップ)は、グローバルフレームリストに自身を登録します。例外がアンワインドされると、ランタイムはこのリストを使用して、どの関数を処理する必要があるかを識別します。

ランディングパッドの選択は、関数コンテキストの呼び出しサイトエントリにエンコードされます。ランタイムはllvm.eh.sjlj.longjmpを介して関数に戻り、そこでswitchテーブルが関数コンテキストに格納されているインデックスに基づいて適切なランディングパッドに制御を転送します。

例外領域とフレーム情報をアウトオブラインテーブルにエンコードするDWARF例外処理とは対照的に、SJLJ例外処理は実行時にアンワインドフレームコンテキストを構築および削除します。これにより、例外処理が高速化されますが、例外がスローされない場合の実行速度は遅くなります。例外は、その性質上、まれなコードパスを対象としているため、一般にSJLJよりもDWARF例外処理が推奨されます。

Windows ランタイム例外処理

LLVMは、Windowsランタイムによって生成された例外の処理をサポートしていますが、非常に異なる中間表現が必要です。他の2つのモデルのように「landingpad」命令に基づいておらず、このドキュメントの後半のWindowsランタイムを使用した例外処理で説明されています。

概要

LLVMコードで例外がスローされると、ランタイムは状況の処理に適したハンドラーを見つけるために最善を尽くします。

ランタイムは最初に、例外がスローされた関数に対応する*例外フレーム*を見つけようとします。プログラミング言語が例外処理をサポートしている場合(例:C++)、例外フレームには、例外の処理方法を記述した例外テーブルへの参照が含まれています。言語が例外処理をサポートしていない場合(例:C)、または例外を前のアクティベーションに転送する必要がある場合、例外フレームには、現在のアクティベーションをアンワインドして前のアクティベーションの状態を復元する方法に関する情報が含まれています。このプロセスは、例外が処理されるまで繰り返されます。例外が処理されず、アクティベーションが残っていない場合、アプリケーションは適切なエラーメッセージで終了します。

例外を処理する際の動作はプログラミング言語によって異なるため、例外処理ABIは*パーソナリティ*を提供するメカニズムを提供します。例外処理パーソナリティは、*パーソナリティ関数*(例:C ++の__gxx_personality_v0)によって定義されます。これは、例外のコンテキスト、例外オブジェクトの型と値を含む*例外構造*、および現在の関数の例外テーブルへの参照を受け取ります。現在のコンパイルユニットのパーソナリティ関数は、*共通例外フレーム*で指定されます。

例外テーブルの構成は言語に依存します。C ++の場合、例外テーブルは、その範囲で例外が発生した場合に何をすべきかを定義する一連のコード範囲として編成されます。通常、範囲に関連付けられた情報は、その範囲で処理される例外オブジェクトの型(C ++ * type info *を使用)と、実行されるべき関連付けられたアクションを定義します。アクションは通常、制御を*ランディングパッド*に渡します。

ランディングパッドは、try/catchシーケンスのcatch部分にあるコードにほぼ対応します。ランディングパッドで実行が再開されると、*例外構造*と、スローされた例外の*型*に対応する*セレクター値*を受け取ります。セレクターは、どの* catch *が実際に例外を処理する必要があるかを決定するために使用されます。

LLVMコード生成

C++開発者の観点からは、例外はthrowおよびtry/catchステートメントで定義されます。このセクションでは、C++の例に関してLLVM例外処理の実装について説明します。

Throw

例外処理をサポートする言語は、通常、例外プロセスを開始するためのthrow操作を提供します。内部的には、throw操作は2つのステップに分解されます。

  1. 例外構造の例外空間を割り当てるリクエストが行われます。この構造は、現在のアクティベーションを超えて存続する必要があります。この構造には、スローされるオブジェクトの型と値が含まれます。

  2. 例外を発生させるためにランタイムへの呼び出しが行われ、例外構造が引数として渡されます。

C++では、例外構造の割り当ては__cxa_allocate_exceptionランタイム関数によって行われます。例外の発生は__cxa_throwによって処理されます。例外の型は、C ++ RTTI構造を使用して表されます。

Try/Catch

* try *ステートメントのスコープ内での呼び出しは、潜在的に例外を発生させる可能性があります。このような状況では、LLVM C ++フロントエンドは呼び出しをinvoke命令に置き換えます。呼び出しとは異なり、invokeには2つの潜在的な継続ポイントがあります。

  1. 通常どおり呼び出しが成功したときに続行する場所、および

  2. 呼び出しがスローまたはスローのアンワインドによって例外を発生させた場合に続行する場所

例外の後にinvokeが続行される場所を定義するために使用される用語は、*ランディングパッド*と呼ばれます。LLVMランディングパッドは概念的に代替の関数エントリポイントであり、例外構造参照と型情報インデックスが引数として渡されます。ランディングパッドは例外構造参照を保存し、例外オブジェクトの型情報に対応するcatchブロックを選択するために進みます.

LLVM 「landingpad」命令は、ランディングパッドに関する情報をバックエンドに伝えるために使用されます。C ++の場合、landingpad命令は、*例外構造*へのポインタと*セレクタ値*に対応するポインタと整数のペアをそれぞれ返します。

landingpad命令は、このtry/catchシーケンスに使用されるパーソナリティ関数への参照を親関数の属性リストで探します。命令には、*クリーンアップ*、*キャッチ*、および*フィルター*句のリストが含まれています。例外は、最初から最後まで順番に句に対してテストされます。句には次の意味があります

  • catch <type> @ExcType

    • この句は、スローされる例外が@ExcType型または@ExcTypeのサブタイプの場合に、ランディングパッドブロックに入力する必要があることを意味します。C ++の場合、@ExcTypeは、C ++例外型を表すstd::type_infoオブジェクト(RTTIオブジェクト)へのポインタです。

    • @ExcTypenullの場合、例外はすべて一致するため、ランディングパッドは常にエントリされます。これは、C ++のキャッチオールブロック(「catch (...)」)に使用されます.

    • この句が一致する場合、セレクタ値は “@llvm.eh.typeid.for(i8* @ExcType)” によって返される値と等しくなります。これは常に正の値になります。

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

    • この句は、スローされている例外がリスト内のいずれの型とも一致*しない*場合に、ランディングパッドに入らなければならないことを意味します(C++ の場合、再び std::type_info ポインタとして指定されます)。

    • C++ フロントエンドはこれを使用して、”void foo() throw (ExcType1, ..., ExcTypeN) { ... }” のような C++ 例外指定を実装します。(注:この機能は C++11 で非推奨となり、C++17 で削除されました。)

    • この句が一致する場合、セレクタ値は負になります。

    • filter への配列引数は空にすることができます。たとえば、”[0 x i8**] undef” です。これは、ランディングパッドに常に 入らなければならないことを意味します。(filtercatch はそれぞれ負と正のセレクタ値を生成するため、このような filter は “catch i8* null” と同等ではないことに注意してください。)

  • 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 や filter が必要としない限り、ランディングパッドで停止しません。

新しい例外がクリーンアップの実行から伝播することを許可しないでください。これは、アンワインダーの内部状態を破損する可能性があります。さまざまな言語が、これらの状況に対してさまざまな高レベルのセマンティクスを記述しています。たとえば、C++ はプロセスの終了を要求しますが、Ada は両方の例外をキャンセルして 3 番目の例外をスローします。

すべてのクリーンアップが完了し、例外が現在の関数によって処理されない場合は、元のランディングパッドの landingpad 命令の結果を渡して、resume 命令 を呼び出すことによって、巻き戻しを再開します。

スローフィルター

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 命令 を使用して例外伝播を再開する必要があります。

例外処理組み込み関数

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.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 でビルドされたコードの相互運用を可能にします。

単一のパラメータは、呼び出しコンテキストが保存される 5 ワードのバッファへのポインタです。フロントエンドはフレームポインタを最初のワードに配置し、この組み込み関数のターゲット実装は、llvm.eh.sjlj.longjmp の宛先アドレスを 2 番目のワードに配置する必要があります。残りの 3 つのワードは、ターゲット固有の方法で使用できます。

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内のランディングパッドエントリが一致する順序で生成されるようにするために使用されます。

アセンブリテーブル形式

例外処理ランタイムが、例外がスローされたときにどのアクションを実行すべきかを決定するために使用する2つのテーブルがあります。

例外処理フレーム

例外処理フレーム `eh_frame` は、DWARFデバッグ情報で使用される巻き戻しフレームと非常によく似ています。フレームには、現在のフレームをティアダウンし、前のフレームの状態を復元するために必要なすべての情報が含まれています。コンパイル単位内の各関数には例外処理フレームがあり、さらに、ユニット内のすべての関数に共通の情報を定義する共通の例外処理フレームがあります。

ただし、この呼び出しフレーム情報(CFI)の形式は、プラットフォームに依存することがよくあります。たとえば、ARMは独自の形式を定義しています。Appleは独自のコンパクトな巻き戻し情報形式を持っています。Windowsでは、32ビットx86以降、すべてのアーキテクチャで別の形式が使用されています。LLVMは、ターゲットで必要な情報をすべて出力します。

例外テーブル

例外テーブルには、関数のコードの特定の部分で例外がスローされたときに実行するアクションに関する情報が含まれています。これは通常、言語固有データ領域(LSDA)と呼ばれます。LSDAテーブルの形式はパーソナリティ関数に固有ですが、既存のパーソナリティの大部分は、`__gxx_personality_v0` によって消費されるテーブルのバリエーションを使用します。リーフ関数と、スローしない関数へのみ呼び出しを行う関数を除き、関数ごとに1つの例外テーブルがあります。それらには例外テーブルは必要ありません。

Windowsランタイムを使用した例外処理

Windows例外の背景

Windowsで例外を処理することは、Itanium C++ ABIプラットフォームよりもはるかに複雑です。2つのモデルの基本的な違いは、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++パーソナリティは、catchブロックのコード(つまり、`catch (Type obj) { ... }` の中括弧の間にあるすべてのユーザーコード)を含むためにファンクレットも使用します。C++例外オブジェクトは、例外を処理する関数のチャイルドスタックフレームに割り当てられるため、ランタイムはcatch本体にファンクレットを使用する必要があります。ランタイムがスタックをcatchのフレームまで巻き戻した場合、例外を保持しているメモリは、後続の関数呼び出しによってすぐに上書きされます。ファンクレットを使用すると、`__CxxFrameHandler3` はTLSに頼ることなく再スローを実装することもできます。代わりに、ランタイムは特別な例外をスローし、SEH(`__try` / `__except`)を使用して、チャイルドフレームに新しい情報を入れて実行を再開します。

言い換えれば、連続巻き戻しアプローチは、Visual C++例外および汎用Windows例外処理と互換性がありません。C++例外オブジェクトはスタックメモリに存在するため、LLVMはランディングパッドを使用するカスタムパーソナリティ関数を提供できません。同様に、SEHは、例外を再スローしたり、巻き戻しを続けたりするメカニズムを提供していません。したがって、LLVMは、互換性のある例外処理を実装するために、このドキュメントの後半で説明するIR構造を使用する必要があります。

SEHフィルター式

SEHパーソナリティ関数は、ファンクレットを使用してフィルター式を実装します。これにより、任意のユーザーコードを実行して、キャッチする例外を決定できます。フィルター式は、LLVM `landingpad` 命令の `filter` 句と混同しないでください。通常、フィルター式は、例外が特定のDLLまたはコード領域から発生したかどうか、またはコードが特定のメモリアドレス範囲にアクセスしているときに障害が発生したかどうかを判断するために使用されます。LLVMは現在、フィルター式の制御の依存関係を表すのが難しいため、フィルター式を表すIRを持っていません。フィルター式は、クリーンアップが実行される前のEHの最初のフェーズで実行されるため、忠実な制御フローグラフを構築することは非常に困難です。今のところ、新しいEH命令はSEHフィルター式を表すことができず、フロントエンドは事前にそれらをアウトラインする必要があります。親関数のローカル変数は、`llvm.localescape` および `llvm.localrecover` 組み込み関数を使用してエスケープおよびアクセスできます。

新しい例外処理命令

新しいEH命令の主な設計目標は、SSA形成が引き続き機能するようにCFGに関する情報を保持しながら、ファンクレット生成をサポートすることです。二次的な目標として、MSVCとItanium C++例外の両方で汎用的に使用できるように設計されています。使い慣れたコアEHアクション(catch、cleanup、terminate)を使用する限り、パーソナリティに必要なデータについてほとんど前提を置いていません。ただし、新しい命令は、EHパーソナリティの詳細を知らずに変更することは困難です。Itanium EHを表すために使用できますが、ランディングパッドモデルは最適化の目的にはるかに優れています。

以下の新しい命令は、「例外処理パッド」と見なされます。EHフローエッジの巻き戻し先となる可能性のある基本ブロックの最初の非phi命令でなければならないためです。`catchswitch`、`catchpad`、`cleanuppad`。landingpadと同様に、tryスコープに入るときに、フロントエンドが例外をスローする可能性のある呼び出しサイトに遭遇した場合、`catchswitch`ブロックに巻き戻すinvokeを発行する必要があります。同様に、デストラクタを持つC++オブジェクトのスコープ内では、invokeは`cleanuppad`に巻き戻す必要があります。

また、catch/cleanupハンドラから制御が移されるポイント(生成されたfuncletからのexitに対応)をマークするために、新しい命令が使用されます。正常に実行が終了したcatchハンドラは、`catchret`命令を実行します。これは、関数の制御がどこに戻されるかを示すターミネータです。正常に実行が終了したcleanupハンドラは、`cleanupret`命令を実行します。これは、アクティブな例外が次にどこに巻き戻されるかを示すターミネータです。

これらの新しいEHパッド命令にはそれぞれ、このアクションの後にどのアクションを考慮すべきかを識別する方法があります。`catchswitch`命令はターミネータであり、invokeの巻き戻し先オペランドに類似した巻き戻し先オペランドを持ちます。`cleanuppad`命令はターミネータではないため、巻き戻し先は代わりに`cleanupret`命令に格納されます。catchハンドラが正常に実行されると、通常の制御フローが再開されるため、`catchpad`命令も`catchret`命令も巻き戻すことはできません。これらの「巻き戻しエッジ」はすべて、EHパッド命令を含む基本ブロックを参照するか、呼び出し元に巻き戻すことができます。呼び出し元への巻き戻しは、landingpadモデルの`resume`命令とほぼ同じセマンティクスを持ちます。invokeを介してインライン化する場合、呼び出し元に巻き戻す命令は、呼び出しサイトの巻き戻し先に巻き戻すようにフックされます。

これらをまとめると、すべての新しい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の親子関係は、新しい「パッド」命令によって生成されたトークンを使用してIRにエンコードされます。「パッド」または「ret」命令のトークンオペランドは、それがどのfuncletに属しているかを示します。別のfunclet内にネストされていない場合は「none」を示します。

`catchpad`および`cleanuppad`命令は新しいfuncletを確立し、それらのトークンは他の「パッド」命令によって消費されてメンバーシップを確立します。`catchswitch`命令はfuncletを作成しませんが、常に直後の後続の`catchpad`命令によって消費されるトークンを生成します。これにより、`catchpad`によってモデル化されたすべてのcatchハンドラが、C++ tryの後dispatchポイントをモデル化する単一の`catchswitch`に属することが保証されます。

これは、いくつかの仮説的な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を使用するパーソナリティのEHテーブルは、巻き戻し先をエンコードするためにfuncletのネスト関係を暗黙的に使用するため、表現できるfunclet遷移のセットに制約があります。関連するLLVM IR命令は、それに応じて、フローグラフ内のEHエッジのエンコード可能性を保証する制約を持ちます。

`catchswitch`、`catchpad`、または`cleanuppad`は、実行されると「enter」されたと言われます。その後、以下のいずれかの方法で「exit」される場合があります。

  • `catchswitch`は、その構成要素である`catchpad`のいずれも処理中の例外に適切ではなく、その巻き戻し先または呼び出し元に巻き戻されると、すぐにexitされます。

  • `catchpad`とその親`catchswitch`は両方とも、`catchpad`からの`catchret`が実行されるとexitされます。

  • `cleanuppad`は、そこからの`cleanupret`が実行されるとexitされます。

  • これらのパッドはいずれも、関数の呼び出し元に巻き戻す`call`、関数の呼び出し元までずっと巻き戻すネストされた`catchswitch`(「`unwinds to caller`」とマークされている)、またはネストされた`cleanuppad`の`cleanupret`(「`unwinds to caller`」とマークされている)によって、制御が関数の呼び出し元に巻き戻されるとexitされます。

  • これらのパッドはいずれも、巻き戻しエッジ(`invoke`、ネストされた`catchswitch`、またはネストされた`cleanuppad`の`cleanupret`からの)が、指定されたパッドの子孫ではない宛先パッドに巻き戻されるとexitされます。

`ret`命令はfuncletパッドをexitするための有効な方法ではないことに注意してください。パッドがenterされたがexitされていないときに`ret`を実行するのは未定義の動作です。

単一の巻き戻しエッジは、任意の数のパッドをexitし(`catchswitch`からのエッジは少なくとも自身をexitしなければならないという制限、および`cleanupret`からのエッジは少なくともその`cleanuppad`をexitしなければならないという制限付き)、次に正確に1つのパッドをenterする必要があります。これは、exitされたすべてのパッドとは異なる必要があります。巻き戻しエッジがenterするパッドの親は、最近enterされたまだexitされていないパッド(巻き戻しエッジがexitするパッドからexitした後)、またはそのようなパッドがない場合は「none」でなければなりません。これにより、実行時の実行中のfuncletのスタックが、親トークンがエンコードするfuncletパッドツリーのパスに対応することが常に保証されます。

任意のfuncletパッドをexitするすべての巻き戻しエッジ(`cleanupret`エッジがその`cleanuppad`をexitする場合と`catchswitch`エッジがその`catchswitch`をexitする場合を含む)は、同じ巻き戻し先を共有する必要があります。同様に、呼び出し元への巻き戻しによってexitされる可能性のあるfuncletパッドは、呼び出し元以外の場所に巻き戻す例外エッジによってexitされてはなりません。これにより、各funclet全体に巻き戻し先が1つだけ存在することが保証されます。これは、funcletパーソナリティのEHテーブルで必要になる場合があります。`catchpad`をexitする巻き戻しエッジはその親`catchswitch`もexitするため、これは、任意の`catchswitch`について、その巻き戻し先がその構成要素である`catchpad`のいずれかをexitする巻き戻しエッジの巻き戻し先でもある必要があることを意味します。`catchswitch`には`nounwind`バリアントがなく、IRプロデューサーは巻き戻さない呼び出しを`nounwind`として注釈付けする必要がないため、呼び出し元以外の巻き戻し先を持つfuncletパッド内に`call`または「`unwind to caller`」`catchswitch`をネストすることは合法です。このような`call`または`catchswitch`が巻き戻されるのは未定義の動作です。

最後に、funcletパッドの巻き戻し先はサイクルを形成できません。これにより、EHの低減により、funcletベースのパーソナリティが必要とする可能性のあるツリーのような構造を持つ「try領域」を構築できます。

ターゲットの例外処理サポート

特定のターゲットで例外処理をサポートするには、実装する必要がある項目がいくつかあります。

  • CFIディレクティブ

    まず、各ターゲットレジスタに一意のDWARF番号を割り当てる必要があります。次に、TargetFrameLoweringemitPrologueで、CFIディレクティブを発行して、CFA(Canonical Frame Address:正規フレームアドレス)の計算方法と、CFAが指すアドレスからオフセットを使用してレジスタを復元する方法を指定する必要があります。アセンブラは、CFIディレクティブによって、例外処理中にスタックを巻き戻すためにアンワインダが使用する.eh_frameセクションを構築するように指示されます。

  • getExceptionPointerRegistergetExceptionSelectorRegister

    TargetLowering は両方の関数を実装する必要があります。 *パーソナリティ関数* は、*例外構造体*(ポインタ)と *セレクタ値*(整数)を、それぞれ getExceptionPointerRegistergetExceptionSelectorRegister で指定されたレジスタを介してランディングパッドに渡します。ほとんどのプラットフォームでは、これらはGPRであり、呼び出し規約で指定されているものと同じになります。

  • EH_RETURN

    ISDノードは、文書化されていないGCC拡張機能である __builtin_eh_return (offset, handler) を表します。これは、スタックをオフセットで調整してから、ハンドラにジャンプします。 __builtin_eh_return は、GCCアンワインダ(libgcc)では使用されますが、LLVMアンワインダ(libunwind)では使用されません。 libgcc を使用していて、ターゲットに特定の要件がある場合は、TargetLoweringEH_RETURN を処理する必要があります。

既存のランタイム(libstdc++libgcc)を利用しない場合は、libc++libunwind を調べて、そこで何をする必要があるかを確認する必要があります。 libunwind の場合は、以下を行う必要があります。

  • __libunwind_config.h

    ターゲット用のマクロを定義します。

  • include/libunwind.h

    ターゲットレジスタの列挙型を定義します。

  • src/Registers.hpp

    ターゲットの Registers クラスを定義し、セッター関数とゲッター関数を実装します。

  • src/UnwindCursor.hpp

    Registers クラスの dwarfEncodingstepWithCompactEncoding を定義します。

  • src/UnwindRegistersRestore.S

    メモリからすべてのターゲットレジスタを復元するアセンブリ関数を記述します。

  • src/UnwindRegistersSave.S

    メモリにすべてのターゲットレジスタを保存するアセンブリ関数を記述します。