内部のC ++例外処理、またはC ++での例外の動作



翻訳者から



高レベルの言語は世界で勝ち取っており、ruby-python-js開発者の世界では、これをプロやプロで使用する価値がないことをるだけです。 たとえば、例外は低速であり、多くの冗長コードを生成するためです。 それに応じてつぶやきと低音を受け取ったとき、「そしてそれがどのコードを生成するか」を尋ねる価値がありました。 そして真実は-彼らはどのように機能するのですか? さて、-Sフラグを付けてg ++でコンパイルし、何が起こったのか見てみましょう。 表面的に理解することは難しくありませんが、誤解があるという事実は私を眠らせませんでした。 幸いなことに、完成した記事が見つかりました。



Habréには、C ++で例外がどのように機能するかを説明する詳細非常に (同時に良い)の記事がいくつかあります。 しかし、本当に深いものはないので、適切な材料があるため、このギャップを埋めることにしました。 gccの例でC ++の例外がどのように機能するかを誰が気にしますか?



2部

3部



PS翻訳に関するいくつかの言葉:





内部のC ++例外



例外の処理が難しいことは誰もが知っています。 例外の「ライフサイクル」の各レイヤーには、これには多くの理由があります。強力な例外セーフコードを使用してコードを記述することは難しく、予期しない場所から例外がスローされる可能性があります。フードの下に大量のブードゥー教の魔法があるため、これは危険です。エラーを誤ってスローすると、 std::terminate



への呼び出しがstd::terminate



れないためです。 そして、これらすべてにもかかわらず、プログラムで例外を使用するかどうかについての戦いはまだ進行中です。 これはおそらく、それらがどのように機能するかについての浅い理解によるものです。



まず、自分自身に問いかける必要があります。それはどのように機能しますか? これは、 C ++の内部で例外がどのように実装されるかについて書いた長いシリーズの最初の記事です(x86の下のgccプラットフォームで、他のプラットフォームにも適用できるはずです)。 これらの記事では、エラーをスローおよびキャッチするプロセスについて詳しく説明しますが、せっかちな人のために、 gcc / x86での例外のスローに関するすべての記事の簡単な説明を以下に示します



  1. throwステートメントを作成すると、コンパイラーはそれをlibstdc++



    関数の2、3の呼び出しに変換します。これにより、例外がスローされ、 libstdc



    ライブラリー呼び出しでスタックを巻き戻す高速プロセスが開始されます。



  2. 各catchブロックについて、コンパイラはメソッド本体の後に特別な情報を追加し、メソッドがキャッチできる例外のテーブル、およびクリーンアップテーブルを追加します(以下のクリーンアップテーブルを参照)。



  3. スタックをアンワインドするプロセスでは、 libstdc++



    と呼ばれる特別な関数(「パーソナリティルーチン」と呼ばれる)が呼び出され、スタック上の各関数がキャッチできるエラーをチェックします。



  4. このエラーをキャッチする人がいない場合、 std::terminate



    呼び出されます。



  5. それでも誰かが見つかった場合、プロモーションはスタックの一番上から再開されます。



  6. スタックを繰り返し通過すると、「パーソナル機能」が開始され、各メソッドのリソースがクリアされます。



  7. ルーチンは、現在のメソッドのクリーンアップテーブルをチェックします。 クリアするものがある場合、ルーチンはスタックの現在のフレームに「ジャンプ」し、現在のスコープにある各オブジェクトのデストラクタを呼び出すクリアコードを起動します。



  8. プロモーションが例外を処理できるスタックのフラグメントに遭遇すると、例外処理ブロックにジャンプします。



  9. 例外の処理が終了すると、クリーンアップ関数が呼び出され、例外が占有していたメモリが解放されます。


*これは私たちにとって1つの大きな記事であり、断片に分割されます。そのため、「一連の記事」の以降は、混乱を避けるために単に「記事」に置き換えられます。



今でも複雑に見えますが、開始すらしていません。例外を処理するのに必要な難しさを短く不正確に記述しただけです。



libstdlibc++



で発生するすべての詳細を調べるために、次のパートではlibstdlibc++



独自のミニバージョンを実装することから始めlibstdlibc++



。 すべてではなく、エラー処理を備えた部品のみ。 実際には、この部分のすべてでさえ、スロー/キャッチブロックを実装するために必要な最小値ではありません。 また、小さなアセンブラが必要になりますが、ほんの少しだけです。 しかし、残念ながら多くの忍耐が必要です。



好奇心が強い場合は、 ここから開始できます 。 これは、次のパートで実装するものの完全な仕様です。 次回は、独自のABI(アプリケーションバイナリインターフェース)を使用して簡単に開始できるように、この記事をわかりやすくシンプルにしようと思います。



注(免責事項):

例外がスローされたときにどのようなブードゥー教の魔法が起こるかは決してわかりません。 この記事では、秘密を公開し、その仕組みを調べてみます。 いくつかのささいなことや微妙な点は現実とは一致しません。 どこかに問題がある場合はお知らせください。



ご注意 翻訳者:これは翻訳にも当てはまります。



内部のC ++例外:小さなABI



例外がなぜ非常に複雑で、どのように機能するのかを理解しようとすると、大量のマニュアルやドキュメントにdrれるか、自分で例外をキャッチしようとすることができます。 実際、このトピックに関する質の高い情報の不足に驚いた(翻訳者のメモ-ところで、私も):見つけることができるものはすべて、詳細すぎるか、単純すぎるかのいずれかです。 もちろん、仕様(最も文書化されたもの: C ++のABIだけでなく、 CFIDWARFおよびlibstdcもあります)がありますが、内部で何が起こっているのかを本当に理解したい場合、文書を個別に読むだけでは十分ではありません。



明らかなことから始めましょう:車輪の再発明! 純粋なCには例外がないことを知っているので、C ++プログラムを純粋なCリンカーとリンクして、何が起こるか見てみましょう! 私はこのような単純なものから始めました:



 #include "throw.h" extern "C" { void seppuku() { throw Exception(); } }
      
      





extern



忘れないでください。さもないと、G ++が小さな関数を切り取って、純粋なCでプログラムにリンクできなくなります。もちろん、C ++とCの世界を接続できるように、リンク用のヘッダーファイルが必要です。



 struct Exception {}; #ifdef __cplusplus extern "C" { #endif void seppuku(); #ifdef __cplusplus } #endif
      
      





そして非常にシンプルなメイン:



 #include "throw.h" int main() { seppuku(); return 0; }
      
      





このフランクコードをコンパイルしてリンクしようとするとどうなりますか?



 > g++ -c -o throw.o -O0 -ggdb throw.cpp > gcc -c -o main.o -O0 -ggdb main.c
      
      





注:このプロジェクトのすべてのソースコードは、gitリポジトリからダウンロードできます



これまでのところ、とても良い。 g ++とgccはどちらも、小さな世界で満足しています。 それらを一緒にリンクしようとするとすぐにカオスが始まります:



 > gcc main.o throw.o -o app throw.o: In function `foo()': throw.cpp:4: undefined reference to `__cxa_allocate_exception' throw.cpp:4: undefined reference to `__cxa_throw' throw.o:(.rodata._ZTI9Exception[typeinfo for Exception]+0x0): undefined reference to `vtable for __cxxabiv1::__class_type_info' collect2: ld returned 1 exit status
      
      





そしてもちろん、gccはC ++宣言の欠落について不満を言っています。 これらは非常に具体的なC ++宣言です。 エラーの最後の行を見てください: cxxabiv1



vtable



cxxabiv1



ます。 libstdc++



で宣言されたcxxabi



、C ++のABIを参照します。 これで、宣言されたC ++ ABIインターフェイスを持つ標準C ++ライブラリを使用してエラー処理が実行されることがわかりました。



C ++ ABIは、1つのプログラムでオブジェクトをリンクできる標準のバイナリ形式を発表します。 異なるABIを使用する2つの異なるコンパイラで.oファイルをコンパイルする場合、それらを1つのアプリケーションに結合することはできません。 ABIは、他のさまざまな標準、たとえば、スタックをアンワインドしたり、例外をスローしたりするためのインターフェイスも宣言できます。 この場合、ABIは、C ++とスタックプロモーションを提供するアプリケーション内の他のライブラリとの間のインターフェイス(必ずしもバイナリ形式ではなく、単なるインターフェイス)を定義します。 言い換えると、ABIは、アプリケーションがC ++以外のライブラリと通信できることにより、C ++固有のことを定義します。これにより、C ++でキャッチされる他の言語から例外をスローできるようになります。



いずれの場合でも、リンカエラーは開始点であり、 cxxabi



の例外の動作の分析における最初の層cxxabi



。実装する必要があるインターフェイスはcxxabi



です。 次の章では、 C ++ ABIとして正確に定義された独自のミニABIから始めます。



ボンネットの下のC ++例外:ABIをプッシュしてリンカーを喜ばせます



例外を理解する過程で、すべての重量挙げがlibstdc++



で実装されており、その定義はC ++ ABIで提供されていることを発見しました。 リンカのエラーを調べて、エラー処理のために、C ++ ABIに助けを求めるべきだと推測しました。 純粋なCプログラムにリンクされたC ++スピッティングエラープログラムを作成し、コンパイラーが例外を直接スローするいくつかのlibstd ++関数を呼び出すものにスロー命令を何らかの形で変換することがわかりました。



それでも、例外がどのように機能するかを正確に理解したいので、エラーを投げるメカニズムを提供する独自のミニABIを実装してみましょう。 これを行うには、 RTFMのみが必要ですが、 LLVMの完全なインターフェイスはこちらにあります。 欠落している機能を正確に思い出してください。



 > gcc main.o throw.o -o app throw.o: In function `foo()': throw.cpp:4: undefined reference to `__cxa_allocate_exception' throw.cpp:4: undefined reference to `__cxa_throw' throw.o:(.rodata._ZTI9Exception[typeinfo for Exception]+0x0): undefined reference to `vtable for __cxxabiv1::__class_type_info' collect2: ld returned 1 exit status
      
      





__cxa_allocate_exception



名前は自給自足だと思います。 __cxa_allocate_exceptionは、 size_t



を取り、例外をスローする間、例外を保持するのに十分なメモリを割り当てます。 これは見た目よりも複雑です:エラーが処理されると、スタックに魔法があります。スタック上の割り当て(翻訳者のコメント-この言葉は許してください。でも時々使用します)は悪い考えです。 ヒープ(ヒープ)にメモリを割り当てることも、一般に悪い考えです。例外が発生したメモリをどこに割り当てて、メモリが不足したことを知らせるのでしょうか。 メモリ内の静的ストレージも、スレッドセーフにする必要がある限り、悪い考えです(そうしないと、例外をスローする2つの競合するスレッドが災害につながります)。 これらの問題を考えると、ストリームのローカルストレージ(ヒープ)のメモリ割り当てが最も有利に見えますが、必要に応じて、メモリが不足している場合は緊急ストレージ(おそらく静的)にアクセスします。 もちろん、恐ろしい詳細については心配しないので、必要に応じて静的バッファを使用するだけです。



__cxa_throw



この機能は、転送のすべての魔法を実行します! ABIによると、例外が発生したら、__ cxa_throwを呼び出す必要があります。 この関数は、スタックプロモーションを呼び出します。 重要な効果: __cxa_throwは戻りを意味することはありません。 また、適切なcatchブロックに制御を渡して、例外を処理するか、デフォルトでstd::terminate



呼び出しますが、何も返しません。



__cxxabiv1::__class_type_info



vtable



__cxxabiv1::__class_type_info





奇妙な... __class_type_infoは明らかにいくつかのRTTI(ランタイム型情報、ランタイム型識別、動的データ型識別)ですが、どれですか? これまでのところ、これに答えることは簡単ではありません。また、ミニABIにとってはそれほど重要ではありません。 例外をスローするプロセスの分析の後に提示する「アプリケーション」のこの部分を残しましょう。ここで、これが実行時のABI定義のエントリポイントであり、「これら2つのタイプは同じかどうか」という質問に答えます。 これは、特定のcatchブロックがこのエラーを処理できるかどうかを判断するために呼び出される関数です。 ここで主なことに焦点を当てます:リンカーのアドレスとして指定する必要があります(つまり、それを定義するだけでは十分ではなく、開始する必要もあります)。vtableが必要です(はい、はい、仮想メソッドが必要です)。



これらの関数では多くの作業が発生しますが、単純な例外スローラーを実装してみましょう。例外がスローされたときにプログラムを終了する(exitを呼び出す)ものです。 アプリケーションはほぼ完成していますが、一部のABI関数が欠落しているため、mycppabi.cppを作成しましょう。 ABI仕様読んで__ cxa_allocate_exceptionおよび__cxa_throwの署名を説明できます。



 #include <unistd.h> #include <stdio.h> #include <stdlib.h> namespace __cxxabiv1 { struct __class_type_info { virtual void foo() {} } ti; } #define EXCEPTION_BUFF_SIZE 255 char exception_buff[EXCEPTION_BUFF_SIZE]; extern "C" { void* __cxa_allocate_exception(size_t thrown_size) { printf("alloc ex %i\n", thrown_size); if (thrown_size > EXCEPTION_BUFF_SIZE) printf("Exception too big"); return &exception_buff; } void __cxa_free_exception(void *thrown_exception); #include <unwind.h> void __cxa_throw( void* thrown_exception, struct type_info *tinfo, void (*dest)(void*)) { printf("throw\n"); // __cxa_throw never returns exit(0); } } // extern "C"
      
      





思い出させてください: githubリポジトリでソースを見つけることができます



ここでmycppabi.cppをコンパイルし、他の2つの.oファイルにリンクすると、「alloc ex 1 \ n throw」を出力して終了する作業バイナリが得られます。 非常に簡単ですが、同時に驚くべきことです。libc++を呼び出さずに例外を管理します。C++ ABIの(非常に小さな)部分を記述しました。



独自のミニABIを作成するときに得た知恵のもう1つの重要な部分: throw



キーワードは、libstdc ++からの2つの関数呼び出しにコンパイルされます。 ブードゥー教の魔法はありません、それは単純な変換です。 これをテストするために関数を逆アセンブルすることもできます。 g++ -S throw.cpp



実行します



 seppuku: .LFB3: [...] call __cxa_allocate_exception movl $0, 8(%esp) movl $_ZTI9Exception, 4(%esp) movl %eax, (%esp) call __cxa_throw [...]
      
      





さらに魔法: throw



がこれら2つの呼び出しに変換されると、コンパイラーは例外がどのように処理されるかさえ知りません。 libstdc++



__cxa_throw



とそのフレンドを定義__cxa_throw



と、 libstdc++



実行時に動的にリンクされ、アプリケーションを最初に起動したときに例外処理メソッドを選択できます。



私たちはすでに進歩を見ていますが、それでも私たちは長い学びの道を歩まなければなりません。 現在、ABIは例外のみをスローできます。 エラーをキャッチするために拡張できますか? さて、次の章でこれを行う方法を見てみましょう!



内部のC ++例外:スローするものをキャッチする



この記事では、コンパイラーとリンカーのエラーを観察することにより、例外のスローに関する秘密のベールをわずかに開きましたが、エラーのキャッチについては何も理解していません。 すでにわかったことを要約します。





これまでは非常に単純でしたが、例外をキャッチするのは少し複雑です。特に、少しのリフレクションが必要なためです(プログラムが独自のコードを分析できるようにするため)。 古いメソッドを使用して、コードにcatchブロックを追加し、コンパイルして何が起こるか見てみましょう。



 #include "throw.h" #include <stdio.h> //     struct Fake_Exception {}; void raise() { throw Exception(); } // ,  ,      catch- void try_but_dont_catch() { try { raise(); } catch(Fake_Exception&) { printf("Running try_but_dont_catch::catch(Fake_Exception)\n"); } printf("try_but_dont_catch handled an exception and resumed execution"); } //   ,   void catchit() { try { try_but_dont_catch(); } catch(Exception&) { printf("Running try_but_dont_catch::catch(Exception)\n"); } catch(Fake_Exception&) { printf("Running try_but_dont_catch::catch(Fake_Exception)\n"); } printf("catchit handled an exception and resumed execution"); } extern "C" { void seppuku() { catchit(); } }
      
      





以前と同様に、CとC ++の世界を接続するseppuku関数がありますが、今回はスタックをより面白くするためにいくつかの関数呼び出しを追加し、ブロックのtry / catchブランチも追加したため、libstdc ++の処理方法を分析できますそれら。



また、ABI関数の欠落に関するリンカーエラーが発生します。



 > g++ -c -o throw.o -O0 -ggdb throw.cpp > gcc main.o throw.o mycppabi.o -O0 -ggdb -o app throw.o: In function `try_but_dont_catch()': throw.cpp:12: undefined reference to `__cxa_begin_catch' throw.cpp:12: undefined reference to `__cxa_end_catch' throw.o: In function `catchit()': throw.cpp:20: undefined reference to `__cxa_begin_catch' throw.cpp:20: undefined reference to `__cxa_end_catch' throw.o:(.eh_frame+0x47): undefined reference to `__gxx_personality_v0' collect2: ld returned 1 exit status
      
      





繰り返しますが、興味深いものがたくさんあります。 __cxa_begin_catchおよび__cxa_end_catchの呼び出しを予期していましたが、それらが何であるかはまだわかりませんが、 throw / __ cxa_allocate / throwと同等であると想定できます。 __gxx_personality_v0は新しいものであり、次のパートのメインテーマになります。



個人的な機能は何をしますか? (翻訳者と一緒に-より良い名前を思いつきませんでした。アイデアがあればコメント欄で教えてください)。 導入部で彼女についてはすでに述べましたが、次回は彼女についてさらに詳しく見ていきます。また、2人の新しい友人__cxa_begin_catch__cxa_end_catch について見ていきます。



内部の C ++例外: __cxa_begin_catchおよび__cxa_end_catchの魔法



例外がどのようにスローされるかを調べた後、どのようにキャッチされるかを調べるための道を見つけます。 前の章では、サンプルアプリケーションにtry-catch-blockを追加してコンパイラーの動作を確認しました。また、スローブロックを追加するとどうなるかを前回見たときと同じようにリンカーエラーが発生しました。 リンカが記述する内容は次のとおりです。



 > g++ -c -o throw.o -O0 -ggdb throw.cpp > gcc main.o throw.o mycppabi.o -O0 -ggdb -o app throw.o: In function `try_but_dont_catch()': throw.cpp:12: undefined reference to `__cxa_begin_catch' throw.cpp:12: undefined reference to `__cxa_end_catch' throw.o: In function `catchit()': throw.cpp:20: undefined reference to `__cxa_begin_catch' throw.cpp:20: undefined reference to `__cxa_end_catch' throw.o:(.eh_frame+0x47): undefined reference to `__gxx_personality_v0' collect2: ld returned 1 exit status
      
      





私のgitリポジトリでコードを取得できることを思い出させてください。



理論上(もちろん、理論上)、catchブロックはlibstdc ++から__cxa_begin_catch / end_catchのペアに変換されますが、 個人用関数と呼ばれる新しいものにも変換されますが 、これについてはまだ何もわかりません。



__cxa_begin_catch__cxa_end_catchについての理論をテストしてみましょう。 -Sフラグを指定してthrow.cppをコンパイルし、アセンブラコードを分析します。 興味深いことがたくさんあります。最も必要なものにカットします。



 _Z5raisev: call __cxa_allocate_exception call __cxa_throw
      
      





すべてが順調に進んでいます:raise()に同じ定義があり、例外をスローするだけです:



 _Z18try_but_dont_catchv: .cfi_startproc .cfi_personality 0,__gxx_personality_v0 .cfi_lsda 0,.LLSDA1
      
      





try_but_dont_catch()の定義は、コンパイラーによって切り取られます。 これは新しいものです。__gxx_personality_v0へのリンクとLSDAと呼ばれる他の何か。 これは小さな定義のように思えますが、実際には非常に重要です。





次の章でCFIとLSDAについて説明しますが、それらについては忘れないでください。次に進みましょう。



 [...] call _Z5raisev jmp .L8
      
      





別の要素主義: raise



を呼び出してからL8にジャンプするだけです。 L8は関数から正常に戻ります。 raise



正しく実行されない場合、実行は(どういうわけか、まだわかりません!)次の命令から続行するのではなく、例外ハンドラー(ABIの用語ではlanding pads



と呼ばれます)に進む必要があります。



  cmpl $1, %edx je .L5 .LEHB1: call _Unwind_Resume .LEHE1: .L5: call __cxa_begin_catch call __cxa_end_catch
      
      





一見、この作品は少し複雑ですが、実際にはすべてがシンプルです。ここで最大の魔法が発生します:最初にこの例外を処理できるかどうかを確認し、そうでない場合はを呼び出し_Unwind_Resume



、可能であれば呼び出し__cxa_begin_catch



__cxa_end_catch



、その後、関数が正常に続行する必要があるため、L8が実行されます(キャッチブロックのすぐ下にL8 ):



 .L8: leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc
      
      





ただの通常の関数が戻ります... CFIのゴミが入っています。



これはすべてエラー処理のためですが、__ cxa_begin / end_catchがどのように機能するかはまだわかりませんこのペアがランディングパッドが呼び出すものを形成する方法についてのアイデアを持っています-例外ハンドラーが配置されている関数内の場所です。まだ不明なのは、ランディングパッドの検索方法です。Unwindは、何らかの方法でスタック上のすべての呼び出しを通過する必要があります。チェック:呼び出し(正確さのためにスタックフレーム)には、この例外を処理できるランディングパッド付きの有効なブロックがあり、実行を続行します。



これは重要な成果であり、その仕組みについては次の章で説明します。



内部のC ++例外:gcc_except_tableおよび個人用関数



前に、throw__cxa_allocate_exception / throwに変換され、catchブロックは__cxa_begin / end_catchに変換されることと、ランディングパッド(エラーハンドラーのエントリポイント)を検索するためのCFI(呼び出しフレーム情報)と呼ばれるものが見つかりました



これまでのところわからないのは、_Unwindがこれらのランディングパッドの場所をどのように見つけるです。スタック内の一連の関数を通じて例外がスローされると、すべてのCFIにより、スタック拡張プログラムが現在実行中の関数を見つけることができます。また、関数のどのランディングパッドでこの例外を処理できるかを調べる必要があります(そして、複数のtry / catchブロックを持つ関数!)。



このランディングパッドの場所を確認するには、gcc_except_tableと呼ばれるものを使用します。このテーブルは、関数の終了後に(CFIガベージで)見つけることができます。



 .LFE1: .globl __gxx_personality_v0 .section .gcc_except_table,"a",@progbits [...] .LLSDACSE1: .long _ZTI14Fake_Exception
      
      





このセクション.gcc_except_table-ランディングパッドを検出するためのすべての情報が格納されます。これについては、後で個人機能を分析するときに説明します。今のところ、LSDAの意味-個人機能が機能のランディングパッドをチェックする言語固有のデータを持つゾーン(スタック拡張の過程でデストラクタを起動するためにも使用されます)だけを言います。



要約すると、少なくとも1つのcatchブロックがある各関数について、コンパイラーはそれをcxa_begin_catch / cxa_end_catchの呼び出しに変換し__ cxa_throwによって呼び出された個人関数がgcc_except_tableを読み取りますスタック内の各メソッドに対して、LSDAと呼ばれるものを検索します。次に、パーソナル関数は、LSDAにこの例外を処理するブロックがあるかどうか、および何らかの種類のクリアコード(必要に応じてデストラクターを実行する)があるかどうかを確認します。



また、興味深い結論を出すことができます:nothrow(または空のthrowステートメント)を使用する場合、コンパイラはgcc_except_tableを省略できますメソッドの。パフォーマンスに大きな影響を与えないgccでのこの例外の実装方法は、実際にはコードのサイズに大きく影響します。キャッチブロックについて nothrow指定子が宣言されたときに例外がスローされた場合、LSDAは生成されず、パーソナル関数は何をすべきかを知りません。パーソナル関数が何をすべきか分からない場合、デフォルトのエラーハンドラを呼び出します。これは、ほとんどの場合、nothrowメソッドからエラーをスローすると、std :: terminateで終了することを意味します。



個人的な機能が何をするかについてのアイデアができたので、それを実装できますか?さて、見てみましょう!



継続




All Articles