内郚のC ++䟋倖凊理。 パヌト3

C ++での䟋倖凊理に関する䞀連の蚘事の翻蚳を続ける



1郚

2郚



ボンネットの䞋のC ++䟋倖適切なランディングパッドを芋぀ける



これは私たちの長い歎史の䞭で15番目の章です。 䟋倖がどのように機胜するかに぀いおはすでに倚くのこずを孊びたした。たた、キャッチブロックの堎所を決定する少量のリフレクション䟋倖に関するランディングパッドを備えた独自の機胜を蚘述したした。 前の章では、䟋倖を凊理できる個人甚関数を䜜成したしたが、垞に最初のランディングパッド぀たり、最初のcatchブロックのみを眮き換えたす。 いく぀かのキャッチブロックがある関数で適切なランディングパッドを遞択する機胜を远加しお、個人の機胜を改善したしょう。



TDDテスト駆動開発modに続いお、最初にABIのテストを䜜成できたす。 プログラムthrow.cppを改善し、いく぀かのtry / catchブロックを䜜成したす。



#include <stdio.h> #include "throw.h" struct Fake_Exception {}; void raise() { throw Exception(); } void try_but_dont_catch() { try { printf("Running a try which will never throw.\n"); } catch(Fake_Exception&) { printf("Exception caught... with the wrong catch!\n"); } try { raise(); } catch(Fake_Exception&) { printf("Caught a Fake_Exception!\n"); } printf("try_but_dont_catch handled the exception\n"); } void catchit() { try { try_but_dont_catch(); } catch(Fake_Exception&) { printf("Caught a Fake_Exception!\n"); } catch(Exception&) { printf("Caught an Exception!\n"); } printf("catchit handled the exception\n"); } extern "C" { void seppuku() { catchit(); } }
      
      





テストする前に、このテストの立ち䞊げ䞭に䜕が起こるか考えおみおください。 try_but_dont_catch関数に泚目しおください。最初のtry / catchブロックは䟋倖をスロヌせず、2番目のブロックはキャッチせずにスロヌしたす。 ABIが少し鈍い限り、最初のcatchブロックは2番目のブロックの䟋倖を凊理したす。 しかし、最初のキャッチが凊理された埌はどうなりたすか 実行は、最初のcatch / tryが終了した堎所から続行され、再び䟋倖をスロヌする2番目のtry / catchブロックの盎前になりたす。これにより、最初のハンドラヌは再び凊理したす。 無限ルヌプ さお、再び本圓の非垞に耇雑になりたした



LSDAテヌブルの開始/長さフィヌルドの知識を䜿甚しお、着陞パッドを正しく遞択したす。 これを行うには、䟋倖がスロヌされたずきにIPが䜕であったかを知る必芁があり、すでに知っおいるUnwind関数_Unwind_GetIPでそれを把握できたす。 _Unwind_GetIPが返す内容を理解するために、䟋を芋おみたしょう。



 void f1() {} void f2() { throw 1; } void f3() {} void foo() { L1: try{ f1(); } catch(...) {} L2: try{ f2(); } catch(...) {} L3: try{ f3(); } catch(...) {} }
      
      





この堎合、f2のcatchブロックでパヌ゜ナル関数が呌び出され、スタックは次のようになりたす。



 +------------------------------+ | IP: f2 stack frame: f2 | +------------------------------+ | IP: L3 stack frame: foo | +------------------------------+
      
      





IPはL3に蚭定されたすが、L2で䟋倖がスロヌされるこずに泚意しおください。 これは、IPが実行されるべき次の呜什を瀺すためです。 これは、䟋倖がスロヌされたIPを取埗する堎合は1を枛算する必芁があるこずを意味したす。そうしないず、_Unwind_GetIPの結果がランディングパッドの倖偎になる可胜性がありたす。 パヌ゜ナル機胜に戻りたす



 _Unwind_Reason_Code __gxx_personality_v0 ( int version, _Unwind_Action actions, uint64_t exceptionClass, _Unwind_Exception* unwind_exception, _Unwind_Context* context) { if (actions & _UA_SEARCH_PHASE) { printf("Personality function, lookup phase\n"); return _URC_HANDLER_FOUND; } else if (actions & _UA_CLEANUP_PHASE) { printf("Personality function, cleanup\n"); //  --    IP //   ,     uintptr_t throw_ip = _Unwind_GetIP(context) - 1; //    LSDA LSDA_ptr lsda = (uint8_t*)_Unwind_GetLanguageSpecificData(context); //   LSDA LSDA_Header header(&lsda); //  LSDA CS LSDA_CS_Header cs_header(&lsda); //    LSDA CS const LSDA_ptr lsda_cs_table_end = lsda + cs_header.length; //      CS while (lsda < lsda_cs_table_end) { LSDA_CS cs(&lsda); //    LP,      ,   if (not cs.lp) continue; uintptr_t func_start = _Unwind_GetRegionStart(context); //    IP   lp //  LP    ,  // IP         uintptr_t try_start = func_start + cs.start; uintptr_t try_end = func_start + cs.start + cs.len; // :    LP   try  if (throw_ip < try_start) continue; if (throw_ip > try_end) continue; //    landing pad   ;   int r0 = __builtin_eh_return_data_regno(0); int r1 = __builtin_eh_return_data_regno(1); _Unwind_SetGR(context, r0, (uintptr_t)(unwind_exception)); // ,        ; //     _Unwind_SetGR(context, r1, (uintptr_t)(1)); _Unwind_SetIP(context, func_start + cs.lp); break; } return _URC_INSTALL_CONTEXT; } else { printf("Personality function, error\n"); return _URC_FATAL_PHASE1_ERROR; } } }
      
      





い぀ものように、珟圚のサンプルコヌドはこちらです。



もう䞀床実行しお、出来䞊がり 無限ルヌプはもうありたせん 簡単な倉曎により、適切なランディングパッドを遞択できたした。 次に、最初の代わりに正しいスタックフレヌムを遞択するように個人機胜を教えたす。



ボンネットの䞋のC ++䟋倖ランディングパッドで適切なキャッチブロックを芋぀ける



耇数のランディングパッドを備えた機胜を凊理できる個人甚機胜を既に䜜成したした。 ここで、どの特定のブロックが特定の䟋倖を凊理できるか、蚀い換えるず、どのブロックがキャッチしお私たちを呌び出すかを認識しようずしたす。



もちろん、どのブロックが䟋倖を凊理できるかを刀断するのは簡単なこずではありたせん。 しかし、あなたは本圓に䜕か他のものを期埅しおいたしたか 珟圚の䞻な問題は次のずおりです。





  struct Base {}; struct Child : public Base {}; void foo() { throw Child; } void bar() { try { foo(); } catch(const Base&){ ... } }
      
      





珟圚のランディングパッドが珟圚の䟋倖を受け入れるこずができるかどうかだけでなく、そのすべおの芪もチェックする必芁がありたす。



タスクをもう少し簡単にしおみたしょう。キャッチブロックが1぀しかないランディングパッドを䜿甚したす。たた、継承はありたせん。 しかし、どのようにしお受け入れられたパッドタむプを芋぀けるのでしょうか



䞀般に、これは.gcc_except_table郚分にありたすが、ただ分析しおいたせんアクションテヌブル。 throw.cppで逆アセンブルし、「try but dont catch」関数の呌び出しサむトテヌブルのすぐ埌にあるものを確認したす。



 LLSDACSE1: .byte 0x1 .byte 0 .align 4 .long _ZTI14Fake_Exception .LLSDATT1:
      
      





倚くの情報があるようには芋えたせんが、䟋倖の名前を持぀䜕かぞの有望なポむンタヌがありたす。 _ZTI14Fake_Exceptionの定矩を芋おみたしょう。



 _ZTI14Fake_Exception: .long _ZTVN10__cxxabiv117__class_type_infoE+8 .long _ZTS14Fake_Exception .weak _ZTS9Exception .section .rodata._ZTS9Exception,"aG",@progbits,_ZTS9Exception,comdat .type _ZTS9Exception, @object .size _ZTS9Exception, 11
      
      





非垞に興味深いものが芋぀かりたした 認識できたすか これはstd :: Fake_Exception構造䜓のtype_infoです



これで、䟋倖に察する䞀皮のリフレクションぞのポむンタヌを取埗する方法があるこずがわかりたした。 プログラムでこれを芋぀けるこずができたすか さらに芋おみたしょう。



内郚のC ++䟋倖䟋倖タむプのリフレクションず読み取り.gcc_except_table



これで、ロヌカルデヌタストア.gcc_except_tableを読み取るこずで、䟋倖に関する倚くの情報を取埗できる堎所がわかりたした。 正しいランディングパッドを決定するために個人機胜に実装する必芁があるもの。



ABIの実装を攟棄し、.gcc_except_tableのアセンブラヌ調査に突入しお、凊理可胜な䟋倖の皮類を芋぀ける方法を理解したした。 テヌブルの䞀郚に、必芁な情報を含むタむプのリストが含たれおいるこずがわかりたした。 クリヌンアップフェヌズでこの情報を読み取りたすが、最初に、LSDAヘッダヌの定矩を思い出しおみたしょう。



 struct LSDA_Header { uint8_t start_encoding; uint8_t type_encoding; //        uint8_t type_table_offset; };
      
      





最埌のフィヌルドは、私たちにずっお新しいものです。これは、タむプテヌブルのオフセットを瀺したす。 各呌び出しの定矩も思い出しおください。



 struct LSDA_CS { //         uint8_t start; //  ,    uint8_t len; // Landing pad uint8_t lp; //   action table + 1 (0  " ") uint8_t action; };
      
      





最埌のフィヌルド「アクション」を芋おください。 これは、アクションテヌブルのオフセットです。 これは、特定のCS呌び出しサむトのアクションを芋぀けるこずができるこずを意味したす。 トリックは、キャッチブロックがあるランディングパッドの堎合、アクションにタむプテヌブルぞのオフセットが含たれおいるこずです。オフセットを䜿甚しお、ヘッダヌから取埗できるタむプテヌブルを取埗できるようになりたした。 話をやめお、コヌドをよく芋おください



 //     LSDA LSDA_ptr lsda = (uint8_t*)_Unwind_GetLanguageSpecificData(context); //   LSDA LSDA_Header header(&lsda); const LSDA_ptr types_table_start = lsda + header.type_table_offset; //  LSDA CS LSDA_CS_Header cs_header(&lsda); //    LSDA CS const LSDA_ptr lsda_cs_table_end = lsda + cs_header.length; //   action tables const LSDA_ptr action_tbl_start = lsda_cs_table_end; //  call site LSDA_CS cs(&lsda); // cs.action --  offset + 1;   cs.action == 0 //        const size_t action_offset = cs.action - 1; const LSDA_ptr action = action_tbl_start + action_offset; //  landing pad   catch the action table //   index   int type_index = action[0]; // types_table_start    ,   //    type_index.    ptr  // std::type_info,    catch- const void* catch_type_info = types_table_start[ -1 * type_index ]; const std::type_info *catch_ti = (const std::type_info *) catch_type_info; //    ,   -  Fake_Exception printf("%s\n", catch_ti->name());
      
      





このコヌドは、type_info構造を取埗する前の間接的なアドレス指定が連続しおいるため耇雑に芋えたすが、実際には耇雑な凊理は行わず、逆アセンブル時に芋぀かった.gcc_except_tableを読み取りたす。



䟋倖タむプの導出は、正しい方向ぞの倧きな䞀歩です。 たた、私たちの個人的な機胜も少し重なっおいたす。 ほずんどのLSDAの読み取りの問題はカヌペットの䞋に隠れおいる可胜性がありたす。これはあたり高䟡ではないはずです぀たり、別の機胜に配眮する。



さらに、凊理される䟋倖のタむプずスロヌされるタむプを比范する方法を孊習したす。



内郚のC ++䟋倖適切なスタックフレヌムの取埗



パヌ゜ナル関数の最新バヌゞョンは、この䟋倖を凊理できるかどうかに関する情報の栌玍堎所を知っおいたすただし、1぀のtry / catchブロックの1぀のcatchブロックでのみ機胜し、継承もありたせん。最初に、䟋倖が凊理可胜なタむプのものであるかどうかを確認したす。



もちろん、最初に䟋倖のタむプを芋぀ける必芁がありたす。 これを行うには、 __ cxa_throwが呌び出されたずきに蚘録する必芁がありたす。



 void __cxa_throw(void* thrown_exception, std::type_info *tinfo, void (*dest)(void*)) { __cxa_exception *header = ((__cxa_exception *) thrown_exception - 1); //       ,   _Unwind_ //          header->exceptionType = tinfo; _Unwind_RaiseException(&header->unwindHeader); }
      
      





そしお、パヌ゜ナル関数で䟋倖の型を読み取り、型の䞀臎を単玔に比范できたす䟋倖の名前はC ++行なので、単玔な「==」で十分です。



 //       const void* catch_type_info = lsda.types_table_start[ -1 * type_index ]; const std::type_info *catch_ti = (const std::type_info *) catch_type_info; //     __cxa_exception* exception_header = (__cxa_exception*)(unwind_exception+1) - 1; std::type_info *org_ex_type = exception_header-&amp;gt;exceptionType; printf("%s thrown, catch handles %s\n", org_ex_type->name(), catch_ti->name()); // :      //   if (org_ex_type->name() != catch_ti->name()) continue;
      
      





gitの最新の倉曎を芋おください。



うヌん、もちろん問題はありたすが、自分で芋぀けられたすか 䟋倖が2぀の段階でスロヌされ、最初の段階で䟋倖を凊理したい堎合、2床目には再床凊理したくないずは蚀えたせん。 _Unwindがこのケヌスを凊理したすが、ドキュメントがないため、未定矩の動䜜が発生する可胜性が高いため、すべおを連続しお凊理するだけでは十分ではありたせん。



どのランディングパッドが䟋倖を凊理できるかを芋぀けるために個人的な機胜を教えおいる限り、どの䟋倖を凊理できるかに぀いおUnwindに嘘を぀きたした。代わりに、ABI 9ですべおを凊理するず蚀いたす。真実はわかりたせん。凊理するかどうか。 これは単なる修正です。次のようなこずができたす。



 _Unwind_Reason_Code __gxx_personality_v0 (...) { printf("Personality function, searching for handler\n"); // ... foreach (call site entry in lsda) { if (call site entry.not_good()) continue; //   landing pad   ;   //     ,  _Unwind_,    if (actions & _UA_SEARCH_PHASE) return _URC_HANDLER_FOUND; //      ,    _UA_CLEANUP_PHASE /*    */ return _URC_INSTALL_CONTEXT; } return _URC_CONTINUE_UNWIND; }
      
      





パヌ゜ナル機胜を起動するず、䜕が埗られたすか 秋 誰が疑うでしょう。 萜䞋機胜を芚えおいたすか 䟋倖がキャッチするものは次のずおりです。



 void catchit() { try { try_but_dont_catch(); } catch(Fake_Exception&) { printf("Caught a Fake_Exception!\n"); } catch(Exception&) { printf("Caught an Exception!\n"); } printf("catchit handled the exception\n"); }
      
      





残念ながら、私たちの個人的な機胜は、ランディングパッドが凊理できる最初のタむプの゚ラヌのみをチェックしたす。 Fake_Exception catchブロックを削陀しお再詊行するず、すべおが正しく機胜したす。 私たちの個人的な機胜は、単䞀のcatchブロックを持぀try-catchブロックによっお提䟛される正しいフレヌム内の正しいcatchブロックを遞択できたす。



次の章では、再び改善したす



ボンネットの䞋のC ++䟋倖ランディングパッドから適切なキャッチを遞択する



C ++の䟋倖に関する第19章LSDAを読み取り、適切なランディングパッド、適切なスタックフレヌムを遞択しお䟋倖を凊理できる個人甚関数を䜜成したしたが、正しいcatchブランチを芋぀けるこずは䟝然ずしお困難です。 䜜業甚個人機胜の最終バヌゞョンでは、アクションテヌブル.gcc_except_table党䜓で䟋倖のタむプを確認する必芁がありたす。



アクションテヌブルを芚えおいたすか もう䞀床芋おみたしょう。ただし、いく぀かのcatchブロックがありたす。



 # Call site table .LLSDACSB2: # Call site 1 .uleb128 ip_range_start .uleb128 ip_range_len .uleb128 landing_pad_ip .uleb128 (action_offset+1) => 0x3 # Rest of call site table # Action table start .LLSDACSE2: # Action 1 .byte 0x2 .byte 0 # Action 2 .byte 0x1 .byte 0x7d .align 4 .long _ZTI9Exception .long _ZTI14Fake_Exception .LLSDATT2: # Types table start
      
      





この䟋のランディングパッドでサポヌトされおいるすべおの䟋倖catchit関数のこのLSDAを読み取る堎合は、次のようにする必芁がありたす。





再び倚くの間接的なアドレス指定がある限り、耇雑に芋えたすが、リポゞトリで最終的なコヌドを芋るこずができたす 。 型テヌブルを読み取っお必芁なcatchブロックを決定できる個人関数の圢でボヌナスがありたす型がnullの堎合、ブロックはすべおの䟋倖を連続しお凊理できたす。 面癜い副䜜甚がありたす。C++プログラムからスロヌされた゚ラヌのみを凊理できたす。



最埌に、䟋倖がスロヌされる方法、スタックが解かれる方法、個人機胜が正しいスタックフレヌムを遞択する方法、およびランディングパッド内のブロックを遞択する方法はわかっおいたすが、デストラクタを開始するずいう問題がただありたす。 それでは、RAIIサポヌトを提䟛するこずにより、個人の機胜を倉曎したす。



内郚のC ++䟋倖プロモヌションでのデストラクタヌの実行



ミニABIバヌゞョン11は 、䟋倖凊理の基本機胜のほずんどすべおに察応しおいたすが、デストラクタを実行するこずはできたせん。 安党なコヌドを曞きたい堎合、これは非垞に重芁な郚分です。 必芁なデストラクタが.gcc_except_tableに栌玍されおいるこずがわかっおいるため、アセンブラコヌドをもう少し芋る必芁がありたす。



 # Call site table .LLSDACSB2: # Call site 1 .uleb128 ip_range_start .uleb128 ip_range_len .uleb128 landing_pad_ip .uleb128 (action_offset+1) => 0x3 # Rest of call site table # Action table start .LLSDACSE2: # Action 1 .byte 0 .byte 0 # Action 2 .byte 0x1 .byte 0x7d .align 4 .long _ZTI14Fake_Exception .LLSDATT2: # Types table start
      
      





通垞のランディングパッドでは、アクションのむンデックスのタむプが0より倧きい堎合、タむプテヌブルにむンデックスを取埗し、それを䜿甚しお必芁なcatchブロックを芋぀けるこずができたす。 それ以倖の堎合、むンデックスが0のずき、クリヌンアップコヌドを実行する必芁がありたす。 ランディングパッドが䟋倖を凊理できない堎合でも、プロモヌション䞭にクリヌンアップを実行できたす。 もちろん、クリヌンアップが完了した埌、ランディングパッドは_Unwind_Resumeを呌び出しおプロモヌションプロセスを続行する必芁がありたす。



新しい最新バヌゞョンのコヌドをgithubリポゞトリにアップロヌドしたしたが、悪いニュヌスがありたすuleb128 == charず蚀ったずきの䞍正行為を芚えおいたすか デストラクタ甚のコヌドの远加を開始するず、.gcc_except_tableのオフセットが倧きくなり「倧きい」ずは127を超えるこずを意味したす、トリックが機胜しなくなりたした。



次のバヌゞョンでは、LSDAリヌダヌを曞き換えお、uleb128コヌドを正しく凊理する必芁がありたす。



それにもかかわらず、私たちは目暙を達成したした 圌らは、libcxxabiラむブラリの助けなしに䟋倖を正しく凊理できるミニABIを䜜成したした



もちろん、この蚀語のネむティブではない䟋倖を凊理したり、コンパむラずリンカヌ間の互換性をサポヌトしたりするなど、ただやるこずがありたす。 たぶん埌で...



内郚のC ++䟋倖結果



䜎レベルの䟋倖凊理に関する20の章を読んだ埌、圚庫を確認したしょう 䟋倖がどのようにスロヌされ、どのようにキャッチされるかに぀いお、私たちは䜕を孊びたしたか



おそらくこの蚘事の最倧の郚分である.gcc_except_tableの読み取りに関する恐ろしい詳现は別ずしお、結論を出すこずができたす。





䟋倖の凊理方法を詳现に怜蚎した結果、䟋倖に察しお安党なコヌドを蚘述するこずが非垞に難しい理由を説明できるようになりたした。



䞀芋するず、䟋倖は玠晎らしく簡単に芋えるかもしれたせんが、少し深く掘り䞋げるず、倚くの困難に遭遇するず、プログラムは文字通りそれ自䜓を掘り䞋げ始めたすリフレクション。これはC ++アプリケヌションでは䞀般的ではありたせん。



䟋倖がスロヌされるずきに高レベル蚀語に぀いお話しおいる堎合でも、通垞のコヌド実行の理解に頌るこずはできたせん。通垞、ifおよびswitchステヌトメントの圢匏で小さな分岐を䜿甚しお盎線的に実行されたす。䟋倖を陀いお、すべおが異なりたす。コヌドは理解できない順序で実行を開始し、スタックを拡匵し、関数の実行を䞭断し、通垞の芏則に埓うこずを停止したす。指瀺ポむンタヌは各ランディングパッドで倉化し、スタックは私たちの制埡なしでスピンしたす。䞀般に、倚くの魔法がボンネットの䞋で発生したす。



結局、䟋倖はプログラムの自然な実行に察する理解を損なうため耇雑です。これは、それらを䜿甚するこずを厳密に犁止されおいるこずを意味するのではなく、それらを䜿甚するずきは垞に泚意する必芁があるずだけ蚀っおいたす




All Articles