構造化例外処理(Structured Exception Handling、通称SEH)とは、Windowsのシステム的な例外を捕捉する手段。
無効なメモリアドレスへのアクセスとか0除算とかの不正な処理(例外が発生する)は、ほっとくとアプリケーションが即座に異常終了してしまうが、構造化例外処理でアプリケーションが捕捉して処理することが出来る。
__try { 処理; } __except (EXCEPTION_EXECUTE_HANDLER) { エラー処理; }
キーワード__tryと__exceptを使う。
__exceptの代わりに__finallyを指定すると、例外が起きても起きなくても実行されるブロックになる。(__exceptと__finallyを同時に使うことは出来ない→使いたい場合は二重化する)
__leaveを使うと、ブロックの中から脱出できるらしい。(breakと同じようなもの?)
__exceptは「catch」とは異なり、捕捉する例外の種類を書くのではない。
発生した例外に対してどう処理するのかを指定する。
値 | 概要 | 備考 | |
---|---|---|---|
EXCEPTION_EXECUTE_HANDLER | 1 | 発生した例外を自分で処理する。 | |
EXCEPTION_CONTINUE_SEARCH | 0 | 自分より外側の__tryブロックに処理を任せる。 | 要するに例外の再スロー。 javaで書くならこんな感じ↓ catch(Exception e) { |
EXCEPTION_CONTINUE_EXECUTION | -1 | 例外が発生した箇所から再実行させる。 |
これらはマクロなので、実際は整数が割り当てられている。
しかし__except()は正か0か負かという判断しかしてないらしいので、実はどんな値でもよい…?
とは言え、上記のようなマクロが定義されているんだから、それを指定するべきだろう。
という訳で、__exceptの括弧内に式(MSDNではフィルタ式と称している)を書いて最終的に整数を返すことにより、条件に応じて処理を変更することが出来る。
例えばカンマ演算子を使ったり、独自の別関数(MSDNではフィルタ関数と称している)を呼び出したりすればよい。
_EXCEPTION_POINTERS* info; __try { 処理; } __except (info = GetExceptionInformation(), EXCEPTION_EXECUTE_HANDLER) { //構造化例外の情報をinfoに入れている。 //カンマ演算子では一番右の値が最終的な結果となるので、この例では下記の例外処理が行われる。 例外処理; }
LONG MyFilter(DWORD code) { //0除算のときだけ自分で処理する(という値を返す) if (code == EXCEPTION_FLT_DIVIDE_BY_ZERO) { return EXCEPTION_EXECUTE_HANDLER; } else { return EXCEPTION_CONTINUE_SEARCH; } } 〜 __try { 処理; } __except (MyFilter(GetExceptionCode())) { 例外処理; }
なお、「__except(GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION)
」とか「__except(code
= GetExceptionCode(), TRUE)
」とかの例を見かける事があるが、定義されたマクロを返しているのではないので良いコーディングとは言えないだろう。
現状では「比較演算の結果が真なら1、偽なら0になる」のでたまたまマクロの値と一致しており、意図した動作はするが。
前者はMSDNの例に出てたりするので性質が悪い(苦)
ちなみに、デフォルト動作(ダイアログを表示して中断か続行かユーザーに選択させる)には、以下のようにコーディングすればよい。
というか、デフォルト動作はこういうコーディングになってるんだろうなぁ。
__try { 処理; } __except (UnhandledExceptionFilter(GetExceptionInformation())) { }
事前にSetErrorMode()を呼び出しておくとデフォルト動作を変更できる(ダイアログを出さなくしたりできる)らしいが。
__exceptと__finallyを1つの__tryに対して同時に使うことは出来ないので、使いたい場合は二重の__tryブロックにする必要がある。
__try { __try { int *p = (int*)0x111; *p = 1; }__except(EXCEPTION_EXECUTE_HANDLER){ ::OutputDebugString(TEXT("except\n")); return GetExceptionCode(); } } __finally { ::OutputDebugString(TEXT("finally\n")); }
構造化例外が発生すると、GetExceptionCode()やGetExceptionInformation()といった関数でその情報を取得できる。
ただしこれが使える場所には制限があって、使えない場所に記述するとコンパイルエラーになる。(VC++2005)
内容 | 例 | 概要 | 使用可能箇所 | 使用不能箇所で使った場合のコンパイルエラー |
---|---|---|---|---|
エラーコード | DWORD code = GetExceptionCode(); |
エラーコードを取得する。 コード値はwinbase.hでマクロ定義されている。 例えばアクセス違反( EXCEPTION_ACCESS_VIOLATION )は0xC0000005。 |
__exceptの()内か、直後の{ }内。 | C2707: '_exception_code' : 組み込み関数のコンテキストが間違っています。 |
詳細情報 | _EXCEPTION_POINTERS *info = GetExceptionInformation(); |
詳細なエラー情報を取得する。_EXCEPTION_POINTERS構造体 はPEXCEPTION_RECORD とPCONTEXT という2つの情報を持っている。前者はエラーコードやアドレス、後者はCPUのレジスターの値などを保持している。 |
__exceptの()内。 | C2707: '_exception_info' : 組み込み関数のコンテキストが間違っています。 |
情報取得の例:
LONG dump_exception(_EXCEPTION_POINTERS *ep) { PEXCEPTION_RECORD rec = ep->ExceptionRecord; TCHAR buf[1024]; wsprintf(buf, TEXT("code:%x flag:%x addr:%p params:%d\n"), rec->ExceptionCode, rec->ExceptionFlags, rec->ExceptionAddress, rec->NumberParameters ); ::OutputDebugString(buf); for (DWORD i = 0; i < rec->NumberParameters; i++){ wsprintf(buf, TEXT("param[%d]:%x\n"), i, rec->ExceptionInformation[i] ); ::OutputDebugString(buf); } return EXCEPTION_EXECUTE_HANDLER; }
int _tmain(int argc, _TCHAR* argv[]) __try { int *p = (int*)0x123; *p = 456; //例外発生 } __except (dump_exception(GetExceptionInformation())){ return GetExceptionCode(); } return 0; }
ちなみにアクセス違反の場合、情報は以下のようになる。
識別子 | 名称 | 内容 | 例 |
---|---|---|---|
ExceptionCode | コード | 構造化例外のエラーコード | 0xc0000005 |
ExceptionFlags | フラグ | ?とりあえず、例外フィルタ内で例外が発生すると0x10のビットがセットされるらしいが。 | 0 |
ExceptionAddress | アドレス | 例外発生箇所のプログラムのアドレスだと思う | 0x00414094 |
NumberParameters | 例外情報の個数 | 例外情報の明細の配列の情報数 | 2 |
ExceptionInformation[0] | 例外情報0 | たぶん、メモリの読み込み中だと0 書き込み中だと1 |
1 |
ExceptionInformation[1] | 例外情報1 | アクセスしようとしたアドレス | 0x00000123 |
自分で構造化例外を発生させるには、RaiseException()を使う。
エラーコードやフラグ、例外情報を引数に指定するようなので、それがGetExceptionInformation()で取得できるんだろう。
構造化例外の__try{ }ブロック内でローカル変数としてクラスをインスタンス化していた場合、(VC++2005では)コンパイルエラーとなる。
class SampleClass { public: ~SampleClass() { ::OutputDebugString(TEXT("SampleClass::destructor\n")); } };
__try { SampleClass s; int *p = NULL; *p = 1; } __except (EXCEPTION_EXECUTE_HANDLER) { }
warning C4509: 非標準の拡張機能が使用されています : '関数名' は SEH と デストラクタ を含む 's' 使われています。 error C2712: オブジェクト アンワインディングが必要な関数内で __try を使用できません。
ところが、__tryブロックの中身をまるごと関数にして外出ししてしまうとコンパイルエラーは取れるし構造化例外もちゃんと捕捉できる。
void sample_class() { SampleClass s; int *p = NULL; *p = 1; } 〜 __try { sample_class(); } __except (EXCEPTION_EXECUTE_HANDLER) { }
ただし、関数内のオブジェクトのデストラクターは呼ばれない。
デストラクターを呼ばせたいなら、通常のC++例外処理を使う。
コンパイルオプションに/EHaを指定してコンパイルすると、C++の標準の例外処理(EH)でも構造化例外を捕捉することが出来る。
(VC++2005の場合、プロパティページを開き、左のツリーから「C/C++」⇒「コード生成」を選んで、右ペインの「C++の例外を有効にする」の行を「はい
- SEHの例外あり (/EHa)」にする。通常は「/EHsc」)
(/EHaにすると、実行時の効率が多少落ちるらしいが…)
try { int *p = NULL; *p = 1; //C++コンパイルオプション/EHaを付けると構造化例外もキャッチできる } catch(...) { ::OutputDebugString(TEXT("catch\n")); }
このtry{ }ブロック内でローカル変数としてクラスをインスタンス化していた場合、構造化例外発生時でも ちゃんとデストラクターが呼ばれる。
ただ、デフォルト状態では、全例外キャッチ(catch()にピリオド3つを指定)以外の、構造化例外の特定(例外情報の取得)は出来ないっぽい。
しかし_set_se_translator()という、構造化例外を変換する方法は用意されている。(この関数を使う際にも/EHaのオプションが必要)
変換用の関数を自分で定義しておき、_set_se_translator()を呼び出して登録する。(この呼び出しはスレッド毎に必要らしい)
すると、構造化例外が発生したときにその関数が呼ばれる。
try{ }ブロック内のデストラクターもちゃんと呼ばれる。
#include <eh.h>
//構造化例外が発生すると、この関数が呼ばれる void se_translator_function(unsigned int code, struct _EXCEPTION_POINTERS* ep) { throw ep; //標準C++の例外を発生させる }
int trans_test() { //スレッド毎に変換関数を登録する _set_se_translator(se_translator_function); try { SampleClass s; int *p = (int*)0x345; *p = 1; } catch (_EXCEPTION_POINTERS* ep) { dump_exception(ep); //デバッグダンプ return ep->ExceptionRecord->ExceptionCode; } return 0; }
※この例では渡された_EXCEPTION_POINTERS*をそのままスローして
いる。一応動いているが、ポインターのスコープ的にこれで正しいのかどうか不明。
別途クラスを用意して値をコピーしてそのオブジェクトをスローするか、エラーコード等の数値のみをスローするようにした方が安全かな?
なお、C++標準例外と構造化例外を1つの関数の中で同時に使うことは出来ない(コンパイルエラー)。
__try { //構造化例外 try { //標準例外 throw 123; } catch(int e) { ::OutputDebugString(TEXT("catch\n")); return e; } } __finally { ::OutputDebugString(TEXT("finally\n")); }
error C2713: 関数ごとに許されている例外ハンドルのフォームは 1 つです。
アドレスが有効かどうかは、IsBadReadPtr()やIsBadWritePtr()といった関数で確認することが出来るらしい。
でも(マルチスレッドで)安全を期す為には、結局は構造化例外を使わなければならないようだ。