複数のSIGSEGVライクエラーの処理

テーマは追い出され、そのためにいくつかのコピーが壊れていませんでした。 何らかの方法で、C / C ++で記述されたアプリケーションが、たとえばNULLポインターを逆参照した後に落ちないかどうか、人々は疑問を抱き続けています。 短い答えはイエスです。Habréでさえこのテーマに関する記事があります。







この質問に対する最も一般的な回答の1つは、「なぜですか?これは単に起こるべきではありません!」というフレーズです。 人々がこのトピックに興味を持ち続ける本当の理由は異なるかもしれません、それらの1つは怠inessかもしれません。 すべてをチェックするのが怠orまたは高価であり、例外的な状況が非常にまれな場合、コードを複雑化せずに潜在的な落下コードフラグメントをいくつかのtry



/ catch



でラップすることができます。これにより、アプリケーションを美しく最小限に抑えたり、何も起こらなかったように回復して作業を続けることができます。 最も異常なのは、エラーを何度も何度もキャッチしたいことのように思えるかもしれません。これは通常、アプリケーションのクラッシュを引き起こし、それらを処理し、動作を継続します。







それでは、SIGSEGVのようなエラーを処理する問題を解決するために何かを作成してみましょう。 ソリューションは最大限にクロスプラットフォームであり、シングルスレッドおよびマルチスレッド環境で最も一般的なすべてのデスクトップおよびモバイルプラットフォームで動作する必要があります。 ネストされたtry



/ catch



セクションの存在も可能にします。 次のタイプの例外的な状況を処理します:誤ったアドレスでのメモリへのアクセス、無効な命令の実行、およびゼロによる除算。 仮説は、発生したハードウェア例外が通常のC ++例外に変わることです。







ほとんどの場合、同様のタスクを解決するために、Windowsシステムではなく、Windows構造化例外処理(SEH)でPOSIXシグナルを使用することをお勧めします。 このようなことをしますが、SEHの代わりに注意を奪われがちなベクトル化例外処理(VEH)を使用します。 一般的に、マイクロソフトによると、VEHはSEHの拡張です。 より機能的でモダンなもの。 VEHはPOSIXシグナルにやや似ています。イベントのキャッチを開始するには、ハンドラーを登録する必要があります。 ただし、VEHのシグナルとは異なり、いくつかのハンドラーを登録できます。これらのハンドラーは、そのうちの1つがイベントを処理するまで順番に呼び出されます。







シグナルハンドラーに加えて、 setjmp



/ longjmp



ペアを採用します。これにより、緊急後に希望の場所に戻り、この非常に例外的な状況を何らかの形で処理できます。 また、私たちの技術がマルチスレッド環境で動作するには、古き良きスレッドローカルストレージ(TLS)が必要です。これは、関心のあるすべての環境でも利用可能です。







緊急事態に陥らないようにするための最も簡単なことは、ハンドラを作成して登録することです。 ほとんどの場合、ユーザーは必要な量の情報を収集し、アプリケーションを美しく折りたたむだけです。 何らかの方法で、信号プロセッサは既知の方法で登録されます。 POSIX互換システムの場合、これは次のとおりです。







 stack_t ss; ss.ss_sp = exception_handler_stack; ss.ss_flags = 0; ss.ss_size = SIGSTKSZ; sigaltstack(&ss, 0); struct sigaction sa; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_ONSTACK; sa.sa_handler = signalHandler; for (int signum : handled_signals) sigaction(signum, &sa, &prev_handlers[signum - MIN_SIGNUM]);
      
      





上記のコードフラグメントは、次のシグナルのハンドラーを登録します: SIGBUS



SIGFPE



SIGILL



SIGSEGV



。 さらに、 sigaltstack



呼び出しを使用すると、独自の代替スタックでシグナルハンドラーを起動する必要があるsigaltstack



示されます。 これにより、無限の再帰の場合に簡単に発生する可能性のあるスタックオーバーフロー状態でも、アプリケーションは生き残ります。 代替スタックを指定しない場合、この種のエラーは処理できません。アプリケーションは単にクラッシュします。 ハンドラを呼び出して実行するためのスタックは存在せず、ハンドラで何も実行できません。 以前に登録されたハンドラーへのポインターも保存されます。これにより、ハンドラーが何もする必要がないことをハンドラーが理解した場合に、それらを呼び出すことができます。







Windowsの場合、コードははるかに短くなります。







 exception_handler_handle = AddVectoredExceptionHandler(1, vectoredExceptionHandler);
      
      





ハンドラーは1つだけで、すべてのイベントを一度にキャッチし(ハードウェア例外を言う必要があるだけでなく)、たとえばLinuxのようなスタックで何かをする方法はありません。 AddVectoredExceptionHandler



関数の最初の引数で指定されたユニットは、他の既存のハンドラーより先にハンドラーを最初に呼び出す必要があることを示しています。 これにより、私たちが最初になり、必要な行動を取る機会が与えられます。







POSIXシステムのハンドラー自体は次のとおりです。







 static void signalHandler(int signum) { if (execution_context) { sigset_t signals; sigemptyset(&signals); sigaddset(&signals, signum); sigprocmask(SIG_UNBLOCK, &signals, NULL); reinterpret_cast<ExecutionContextStruct *>(static_cast<ExecutionContext *>(execution_context))->exception_type = signum; longjmp(execution_context->environment, 0); } else if (prev_handlers[signum - MIN_SIGNUM].sa_handler) { prev_handlers[signum - MIN_SIGNUM].sa_handler(signum); } else { signal(signum, SIG_DFL); raise(signum); } }
      
      





私たちのシグナルプロセッサが再利用可能になる、つまり 新しいエラーが発生した場合は何度でも呼び出すことができますが、入力するたびにトリガー信号をロック解除する必要があります。 これは、後で説明する一部のtry



/ catch



でラップされたコードのセクションで例外が発生したことをハンドラーが認識している場合に必要です。 予期しない事態が発生した場合、以前に登録されたシグナルハンドラに転送され、ない場合はデフォルトハンドラが呼び出され、クラッシュしているアプリケーションが終了します。







Windowsのハンドラーは次のとおりです。







 static LONG WINAPI vectoredExceptionHandler(struct _EXCEPTION_POINTERS *_exception_info) { if (!execution_context || _exception_info->ExceptionRecord->ExceptionCode == DBG_PRINTEXCEPTION_C || _exception_info->ExceptionRecord->ExceptionCode == 0xE06D7363L /* C++ exception */ ) return EXCEPTION_CONTINUE_SEARCH; reinterpret_cast<ExecutionContextStruct *>(static_cast<ExecutionContext *>(execution_context))->dirty = true; reinterpret_cast<ExecutionContextStruct *>(static_cast<ExecutionContext *>(execution_context))->exception_type = _exception_info->ExceptionRecord->ExceptionCode; longjmp(execution_context->environment, 0); }
      
      





前述のように、WindowsのVEHハンドラーは、ハードウェア例外以外にも多くのことをキャッチします。 たとえば、 OutputDebugString



呼び出すと、コードDBG_PRINTEXCEPTION_C



で例外がスローされます。 このようなイベントを処理せず、単にEXCEPTION_CONTINUE_SEARCH



返します。これにより、OSはこのイベントを処理する次のハンドラーを探します。 また、通常の名前のないマジックコード0xE06D7363L



対応するC ++例外を処理する必要はありません。







POSIX互換システムとWindowsの両方で、ハンドラーの最後にlongjmp



が呼び出されます。これにより、スタックをtry



セクションの最初に戻り、 catch



ブランチで一度バイパスして、操作を復元して続行するために必要なすべてを実行できますひどいことは何もなかったかのように動作します。







通常のC ++に特有ではないHW_TO_SW_CONVERTER



状況をキャッチtry



するには、最初に小さなマクロHW_TO_SW_CONVERTER



を配置する必要があります。







 #define HW_TO_SW_CONVERTER_UNIQUE_NAME(NAME, LINE) NAME ## LINE #define HW_TO_SW_CONVERTER_INTERNAL(NAME, LINE) ExecutionContext HW_TO_SW_CONVERTER_UNIQUE_NAME(NAME, LINE); if (setjmp(HW_TO_SW_CONVERTER_UNIQUE_NAME(NAME, LINE).environment)) throw HwException(HW_TO_SW_CONVERTER_UNIQUE_NAME(NAME, LINE)) #define HW_TO_SW_CONVERTER() HW_TO_SW_CONVERTER_INTERNAL(execution_context, __LINE__)
      
      





かなり巻き毛のように見えますが、実際には非常に簡単なことがここで行われます:







  1. setjmp



    が呼び出され、開始した場所と、事故の場合に戻る必要がある場所を思い出すことができます。
  2. 実行パスに沿ってハードウェア例外が発生した場合、 longjmp



    がパスに沿ってどこかで呼び出された後、 setjmp



    はゼロ以外の値を返します。 これにより、HwException型のC ++例外がスローされ、発生したエラーの種類に関する情報が含まれます。 スローされた例外は、標準のcatch



    によって簡単にキャッチされます。


上記のマクロを簡素化するために、次の擬似コードに展開されます。







 if (setjmp(environment)) throw HwException();
      
      





setjmp



/ longjmp



アプローチには1つの大きな欠点があります。 通常のC ++例外の場合、パスに沿って作成されたすべてのオブジェクトのデストラクタが呼び出されるスタックが解放されます。 longjmp



の場合、すぐに開始位置にジャンプし、スタックの巻き戻しは行われません。 これにより、このようなtry



セクション内のコードに適切な制限が課せられます。リソースを永久に失うリスクがあり、リークにつながるため、そこにリソースを割り当てることはできません。







もう1つの制限は、 inline



として宣言された関数/メソッドでsetjmp



を使用できないことです。 これはsetjmp



自体の制限です。 最良の場合、コンパイラは単にそのようなコードの収集を拒否し、最悪の場合には収集しますが、結果のバイナリは単にクラッシュします。







Windowsでハードウェア例外を処理した後に実行する必要がある最も異常なアクションは、 RemoveVectoredExceptionHandler



を呼び出すことRemoveVectoredExceptionHandler



。 これが行われない場合、VEHハンドラーへの各エントリとlongjmp



の実行後に、ハンドラーがもう一度登録されたかのように状況がそこで発生します。 これは、その後の各緊急事態で、ハンドラーが連続して何度も呼び出され、悲惨な結果につながるという事実につながります。 このソリューションは、数多くの魔法の実験によってのみ発見され、どこにも文書化されていません。







ソリューションがマルチスレッド環境で機能するためには、各スレッドがsetjmp



を使用して実行コンテキストを保存できる独自の場所を持っている必要があります。 これらの目的のために、TLSが使用されますが、その使用にはトリッキーなものはありません。







実行コンテキスト自体は、次のコンストラクタとデストラクタを持つ単純なクラスとして設計されています。







 ExecutionContext::ExecutionContext() : prev_context(execution_context) { #if defined(PLATFORM_OS_WINDOWS) dirty = false; #endif execution_context = this; } ExecutionContext::~ExecutionContext() { #if defined(PLATFORM_OS_WINDOWS) if (execution_context->dirty) RemoveVectoredExceptionHandler(exception_handler_handle); #endif execution_context = execution_context->prev_context; }
      
      





このクラスにはprev_context



フィールドがあり、ネストされたtry



/ catch



セクションからチェーンを作成できます。







上記の製品の完全なリストは、GitHubで入手できます。

https://github.com/kutelev/hwtrycatch







すべてが説明どおりに機能することを証明するために、Windows、Linux、Mac OS X、およびAndroidプラットフォーム用の自動アセンブリとテストがあります。







https://ci.appveyor.com/project/kutelev/hwtrycatch

https://travis-ci.org/kutelev/hwtrycatch







iOSでもこれは機能しますが、テスト用のデバイスがないため、自動テストはありません。







結論として、通常のCでも同様のアプローチを使用できると言えます。C++のtry



/ catch



動作をシミュレートするマクロをいくつか作成するだけです。







また、ほとんどの場合、説明した方法を使用することは非常に悪い考えであると言う価値があります。特に、信号レベルでSIGSEGV



またはSIGBUS



出現につながったものを見つけることができないと考える場合はSIGBUS



です。 これは、間違ったアドレスでの読み取りや書き込みと同様の可能性があります。 任意のアドレスでの読み取りが破壊的な操作ではない場合、書き込みはスタック、ヒープ、またはコード自体の破壊などの悲惨な結果につながる可能性があります。








All Articles