アプリケーションが例外をキャッチしなかったらどうなるか

アプリケーション内で発生した Null Pointer Exception などの例外をアプリケーション自身が捕捉(キャッチ)しなかった場合、デバッガが起動したりダンプファイルが生成されたりしますが、この動作がどうなっているのか追ってみます。

動作確認環境

  • Windows 11 Home 22H2
  • Visual Studio Community 2022 (Visual C++)

例外発生プログラムを作成

調査のため、3 秒後に NULL ポインタ例外(Null Pointer Exception)を発生させる C 言語のプログラム「test.c」を書きます。

#include <windows.h>

int main()
{
    Sleep(3000);     // 3 秒待って、
    *(char *)0 = 3;  // NULL ポインタに書き込んで例外
    return 0;
}

ビルドします。オプションの「/Od」は最適化抑止、「/Zi」はデバッグ情報を付加の意味です。

C:\tmp> cl /Od /Zi test.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.31.31105 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

test.c
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:test.exe
/debug
test.obj

「test.exe」が生成されました。

例外発生で WerFault.exe が Debugger を実行

実行します。

C:\tmp> test.exe

Process Explorer で「text.exe」の起動が確認できます。

3 秒後に例外が発生すると、「もうひとつの text.exe」と「WerFault.exe」が起動します。

「もうひとつの text.exe」が何なのかはわかりません。メモリの使用量が非常に少なく、CPU は「Suspended」です。「もともとの text.exe」の起動直後かつ例外発生前に同ファイルを消しても起動してきます。「もともとの test.exe」プロセスのメモリを、何らかの目的で OS がコピーしたもののように見えます。

「WerFault.exe」は Windows のシステムディレクトリに入っている「Windows 問題レポート」(Windows Error Reporting)です。

「WerFault.exe」の動作を Process Monitor で見ると、レジストリの「AeDebug」を参照していることが確認できます。

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug

「AeDebug」キーは、例外発生時に実行するプログラム(デバッガ)を登録するレジストリです。
「AeDebug\Auto」値が「1」で、かつ、「AeDebug\AutoExclusionList」キー配下に例外発生プログラムの名前がない場合、「WerFalut.exe」は「AeDebug\Debugger」に登録されているデバッガを起動します。

たとえば Visual Studio をデバッガとして登録すると、

レジストリの「AeDebug\Debugger」には「vsjitdebugger.exe」のフルパス名が書き込まれ、

"C:\WINDOWS\system32\vsjitdebugger.exe" -p %ld -e %ld -j 0x%p

例外発生時、「WerFault.exe」は「vsjitdebugger.exe」を起動します。

この「vsijitdebugger.exe」の画面で「Visual Studio」を選択すると、Visual Studio の統合開発環境が開きます。

ここで故障解析ができますが、手に負えなかったら[デバッグ]-[名前を付けてダンプを保存]でダンプファイルを取得し、エスカレーションするといいでしょう。

Visual Studio ではなく WinDbg をデバッガとして登録した場合は、

C:\Program Files (x86)\Windows Kits\10\Debuggers\x64> windbg -I

レジストリの「AeDebug\Debugger」には「WinDbg.exe」のフルパス名が書き込まれ、

"C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe" -p %ld -e %ld -g

例外発生時、「WerFault.exe」は「windbg.exe」を起動します。

ここで故障解析ができますが、手に負えなかったら[Command]ウィンドウから「.dump /mA c:\tmp\tset.dmp」などと入力してダンプファイルを取得し、エスカレーションするといいでしょう。

なお、「AeDebug\Debugger」値の引数「%ld」「%ld」「%p」には、順に「プロセス ID」「イベントハンドル」「JIT_DEBUG_INFO 構造体のアドレス」がセットされます。

また、Windows が 64bit で例外発生プログラムが 32bit の場合、参照先レジストリは「HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug」になります。

自作プログラムをデバッガとして登録してみる

試しに引数の値を表示するプログラムを作り、デバッガとして「AeDebug\Debugger」レジストリに登録してみましょう。

引数の値を表示するプログラムです。OutputDebugString() の内容は DebugView などで閲覧できます。

#include <windows.h>

int WinMain()
{
    for (int i = 0; i < __argc; i++)
    {
        OutputDebugString(__argv[i]);
    }

    return 0;
}

ビルドします。

C:\tmp> cl /Od /Zi ShowArgs.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.31.31105 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

ShowArgs.c
ShowArgs.c(4): warning C4026: 関数はパラメーター リストを使って宣言されています。
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:ShowArgs.exe
/debug
ShowArgs.obj

生成された「ShowArgs.exe」のフルパス名を、「AeDebug\Debugger」レジストリに登録します。

C:\tmp\ShowArgs.exe %ld %ld %p

最初に作った、例外を発生させる「test.exe」を実行します。

C:\tmp> test.exe

想定通り、「WerFault.exe」が「ShowArgs.exe」を起動し、各引数の値が DebugView に表示されました。

ただし、なぜか「ShowArgs」が 2 回起動します。

はっきりとはわからないのですが、マイクロソフトのサイト [1] にある「DWORD (%ld) – Event Handle …… If the postmortem debugger terminates without signaling the event, WER continues the collection of information about the target processes.」という記述を参考に第 2 引数で受け取ったイベントハンドルをシグナル化するようにしてみたところ、

#include <windows.h>

int WinMain()
{
    for (int i = 0; i < __argc; i++)
    {
        OutputDebugString(__argv[i]);
    }

    if (2 <= __argc)
    {
        HANDLE h = (HANDLE)_atoi64(__argv[2]);
        SetEvent(h); // 第 2 引数で受け取ったイベントハンドルをシグナル化
    }

    return 0;
}

「ShowArgs.exe」の起動は 1 回に収まりました。

WerFalut.exe がダンプファイルを生成

「AeDebug」によるデバッガの起動が行われなかった場合、「WerFalut.exe」は例外発生プログラムのダンプファイルを生成します。その動作を見てみましょう。

デバッガの起動を抑止するため、レジストリ「AeDebug\Auto」に「0」を設定します。

また、レジストリ「LocalDumps」にダンプファイルの生成条件を設定します。下記の設定は、「C:\tmp」フォルダに、当該アプリケーションの完全ダンプファイルを(DumpType=2)、10 世代分(DumpCount=0x0000000a)生成する、という意味になります。

HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps

準備ができたので、例外を発生させる「test.exe」を実行します。

C:\tmp> test.exe

「WerFault.exe」が「AeDebug」レジストリを参照しますが、デバッガは起動しません。

その後、「WerFault.exe」は「LocalDumps」レジストリを参照し、

指定の場所にプロセス ID 付きのファイル名でダンプファイルを生成します。

C:\tmp> dir /b *.dmp
test.exe.6680.dmp

生成されたダンプファイルを WinDbg で開いてみます。

ダンプファイルのフラグ(MINIDUMP_TYPE 列挙型の値)は以下でした。

0:000> .dumpdebug
----- User Mini Dump Analysis

MINIDUMP_HEADER:
Version         A793 (A05D)
NumberOfStreams 16
Flags           21826
                0002 MiniDumpWithFullMemory
                0004 MiniDumpWithHandleData
                0020 MiniDumpWithUnloadedModules
                0800 MiniDumpWithFullMemoryInfo
                1000 MiniDumpWithThreadInfo
                20000 MiniDumpIgnoreInaccessibleMemory

「!analyze -v」で解析すると、例外の理由(Access violation)と発生個所が表示されました。

0:000> !analyze -v
......
EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 00007ff7a4d670bf (test!main+0x000000000000000f)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 0000000000000001
   Parameter[1]: 0000000000000000
Attempt to write to address 0000000000000000
......
FAULTING_SOURCE_CODE:  
     2: 
     3: int main()
     4: {
     5:     Sleep(3000);
>    6:     *(char *)0 = 3;
     7:     return 0;
     8: }
......

おわりに

アプリケーション内で発生した例外をアプリケーション自身が補足しなかった場合の動作を見てきました。実際はほかにもさまざまな処理が行われていますし、環境によっても動作に違いがあるでしょうが、おおよそ、「例外発生」→「WerFault.exe が起動」→「WerFault.exe が レジストリ AeDebug を参照してデバッガを起動(成功したらここで終了)」→「WerFault.exe がレジストリ LocalDumps を参照してダンプファイルを生成」と動作していることが確認できました。

参考文献

[1] Microsoft, Enabling Postmortem Debugging,
https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/enabling-postmortem-debugging