構造化例外処理と UnhandledExceptionFilter

この資料では Windows の例外処理の基本である構造化例外処理と、例外を処理しなかったときの動作を簡単に説明します。 尚、例外処理の書き方はコンパイラによって異なりますが、ここでは Microsoft Visual C++ を用いて説明します。

Windows の例外処理の基本 ~ 構造化例外処理

Windows の例外処理は構造化例外処理 (Structured Exception Handling) と呼ばれる仕組みで成り立っています。 これはどういうものかというと例外 (Exception) が発生した場合に、その例外発生スレッドの処理に割り込み、 そのスレッドの実行コンテキスト情報を保存した上で、例外処理ハンドラを実行するなり、終了ハンドラを実行するなりするものです。

終了ハンドラ

例外が発生あるいは処理が (goto などで) 中断しても、確実に実行したいコードブロックを定義することが出来ます。 これを終了ハンドラ (Terminate Handler) と呼びます。

書き方は次の通りです。

__try {
	// Guarded Body	
}
__finally {
	// Termination Handler
}

__try { } で囲まれている部分がガードボディ (Guarded Body) と呼ばれます。この中を実行中に例外が発生した場合あるいは return や goto 等で処理が中断するときでも、 __finally { } で囲まれている部分 (=終了ハンドラ) 実行されます。

例えば次のコードのように、__try の実行中にいきなり return で関数を抜けようと思っても、__finally の中身が実行されます。

int main(int argc, char* argv[]) {

     __try {
          printf("Hello!\n");
          return 0;
     }
     __finally {
          printf("__finally!\n");
     }

     return 0;
}

実行結果

Hello!
__finally!

ただし、後述の例外ハンドラ等にて Exit/TerminateProcess, Exit/TerminateThread を呼ぶと 終了ハンドラも実行されません。これらの使用をなるべく避けたほうがよい理由のひとつです。

例外ハンドラ

一方例外ハンドラ (Exception Handler) は __except で指定します。

__try  {
	// ガードボディ
}
__except( <例外フィルター>  ) {
	// 例外ハンドラ
}

__except に続く () には例外フィルター (Exception Filter) を記述します。例外発生時に例外フィルターが OS によって評価され、 その結果の評価値によって、例外処理の動作が決められます。評価値は次の3種類あります。

マクロ 意味
EXCEPTION_EXECUTE_HANDLER 1 例外ハンドラを実行する
EXCEPTION_CONTINUE_SEARCH 0 例外ハンドラを探し続ける
EXCEPTION_CONTINUE_EXECUTION -1 元のコードを実行する

上記、「例外ハンドラを探し続ける」 というのはどういうことでしょうか。これを簡単に説明します。

try-except や try-finally はネストすることが可能です。すなわち、次のように書くことが出来ます。

__try {
	__try {
		__try {
			
		}
		__except ( フィルター ) {
		
		}
	}
	__except( フィルター ) {
	
	}
}
__except( フィルター ) {

}

たとえこのように直接 try-except が記述されていなくても、次のように 関数の中で try-except が使用され、結果的に try-except がネストすることもあります。 (次のコードでは Foo の try にて Bar が呼び出され、その中でさらに try-except が存在している)

void Bar() {

	__try {
		
	}
	__except ( フィルター ) {
	
	}
	
}

void Foo() {

	__try {
		Bar();
	}
	__except( フィルター ) {
	
	}
	
}

このようなときに例外フィルタによって、呼び出しもとの例外ハンドラを実行すべきか、呼び出された側の 例外ハンドラを実行するか決めることができます。呼び出された側の __except の例外フィルタで EXCEPTION_EXECUTE_HANDLER とすれば、その例外ハンドラが実行されます。また EXCEPTION_CONTINUE_SEARCH とすると、呼び出し側で例外ハンドラを探します。 呼び出し側も EXCEPTION_CONTINUE_SEARCH とすると、どんどん呼び出し元へと例外ハンドラ探しを続け、 最終的にユーザーのプログラムでそれを処理しない場合、 OS がセットした既定の未処理例外フィルタ (UnhandledExceptionFilter) が実行されます。

UnhandledExceptionFilter

OS の既定の未処理例外フィルタ ~ UnhandledExceptionFilter

試しにここで、既定の未処理例外フィルタで何をしているか確認してみましょう。

次のサンプルコードでは、__try にて strcat (NULL, NULL) を実行して、アクセス違反例外を発生させています。 __except では EXCEPTION_CONTINUE_SEARCH として、この例外を処理しません。

int main(int argc, char* argv[]) {

     __try {

          strcat( NULL, NULL );

     }
     __except( EXCEPTION_CONTINUE_SEARCH ) {

     }

     return 0;
}

これを seh1.exe として実行します。すると、次のようなダイアログボックスが表示されました。

このダイアログが表示されているときの seh1.exe のメインスレッドを確認すると次のようになりました。

0:000> kbn 100
 # ChildEBP RetAddr  Args to Child
00 0012f928 77659244 75f1c3e4 00000002 0012f97c ntdll!KiFastSystemCallRet
01 0012f92c 75f1c3e4 00000002 0012f97c 00000001 ntdll!ZwWaitForMultipleObjects+0xc
02 0012f9c8 75f1c64e 0012f97c 0012fa18 00000000 kernel32!WaitForMultipleObjectsEx+0x11d
03 0012f9e4 75f8db5d 00000002 0012fa18 00000000 kernel32!WaitForMultipleObjects+0x18
04 0012fa50 75f8dd89 0012fb20 00000001 00000001 kernel32!WerpReportFaultInternal+0x16d
05 0012fa64 75f6f54d 0012fb20 00000001 b6f74a04 kernel32!WerpReportFault+0x70
06 0012faf0 776785b7 00000000 77609a14 00000000 kernel32!UnhandledExceptionFilter+0x1b5
07 0012faf8 77609a14 00000000 0012ffd4 77663ff8 ntdll!__RtlUserThreadStart+0x6f
08 0012fb0c 776040f4 00000000 00000000 00000000 ntdll!_EH4_CallFilterFunc+0x12
09 0012fb34 77659b99 fffffffe 0012ffc4 0012fc3c ntdll!_except_handler4+0x8e
0a 0012fb58 77659b6b 0012fc20 0012ffc4 0012fc3c ntdll!ExecuteHandler2+0x26
0b 0012fc08 776599f7 0012fc20 0012fc3c 0012fc20 ntdll!ExecuteHandler+0x24
0c 0012fc08 004013c0 0012fc20 0012fc3c 0012fc20 ntdll!KiUserExceptionDispatcher+0xf
0d 0012ff08 00416dd3 00000000 00000000 b6f73048 seh1!strcat+0x20
0e 0012ff40 004017b7 00000001 003c2300 003c2320 seh1!main+0x43
0f 0012ff88 75f14911 7ffdf000 0012ffd4 7763e4b6 seh1!__tmainCRTStartup+0xfb
10 0012ff94 7763e4b6 7ffdf000 2353616b 00000000 kernel32!BaseThreadInitThunk+0xe
11 0012ffd4 7763e489 0040180e 7ffdf000 00000000 ntdll!__RtlUserThreadStart+0x23
12 0012ffec 00000000 0040180e 7ffdf000 00000000 ntdll!_RtlUserThreadStart+0x1b

確かに strcat の後に例外処理ルーチンが走り、UnhandledExceptionFilter を呼び出していることがわかります。

このスレッドでは WaitForMultipleObjects で何かを待っています。何を待っているのか確認しましょう。 WaitForMultipleObjects にはハンドルの配列と要素数を渡すことを念頭に置くと、ここで待っているハンドルは次のようにわかります。

0:000> dc 0012fa18 l2
0012fa18  00000030 00000010                    0.......
0:000> !handle 00000030 8
Handle 30
  Object Specific Information
    Process Id  4736
    Parent Process  5596
    Base Priority 8

このことは UnhandledExceptionFilter から PID 4736 のプロセスを待っていることを示しています。 次の tlist の結果から PID 4736 は次の内容から WerFault.exe であることがわかります。 これは Windows Error Reporting(WER) プロセスです。ちなみに、Parent Process 5596 は seh1.exe 本体です。

4736 WerFault.exe      Microsoft Windows

尚、Windows Error Reporting の設定しだいによっては次のようなダイアログにもなります。

ここでは Windows の Error Reporting については深入りしませんが、今回は UnhandledExceptionFilter が、 WER プロセスを待っていたことが確認できました。

要は例外ハンドラが見つからないときは最終的に OS が用意している UnhandledExceptionFilter が最終的に走るということです。 その中でどのような動きをするか、ということは Windows の設定に依存します。

ユーザー定義の未処理例外フィルタの設定

SetUnhandledExceptionFilter 関数を使うと自前の未処理例外フィルタを設定できます。SetUnhandledExceptionFilter に関数のアドレスを渡します。

LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(
  __in  LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);

試しに以下のコードで自前の未処理例外フィルタをセットし、例外を発生させ、それが実行されるかどうか確認してみましょう。

#include <windows.h>
#include <stdio.h>

LONG WINAPI MyUnhandledExceptionFilter( PEXCEPTION_POINTERS pep ) {

     printf ("Hello, MyUnhandledExceptionFilter!\n");
     
     return EXCEPTION_EXECUTE_HANDLER;
}

int main(int argc, char* argv[]) {

     SetUnhandledExceptionFilter ( MyUnhandledExceptionFilter );

     __try {

          strcat( NULL, NULL );

     }
     __except( EXCEPTION_CONTINUE_SEARCH ) {

     }

     return 0;
}

この実行結果は次のようになります。

> seh1.exe
Hello, MyUnhandledExceptionFilter!

確かに __except では EXCEPTION_CONTINUE_SEARCH としているにもかかわらず、 プログラムはクラッシュせずに自前の未処理例外フィルタが実行されたことがわかります。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説