December 2011

Volume 26 Number 12

Sysinternals ProcDump v4.0 - Sysinternals ProcDump v4.0 用のプラグインを作成する

Andrew Richards | December 2011

ミッション クリティカルなアプリケーションの最新のアップグレードを徹夜でインストールし、すべて順調に完了したとします。そして、他の社員が出社してくるまさにそのとき、アプリケーションがハングしました。このようなとき、損害を食い止め、そのリリースが障害を引き起こしたことを認め、できるだけ迅速に関連する証拠を集め、それから最も重要なロールバックの計画を開始する必要があります。

このような場合、ハングやクラッシュかパフォーマンス上の問題かを問わず、共通の問題解決方法は、アプリケーションのメモリ ダンプをキャプチャすることです。ほとんどのダンプ キャプチャ ツールが採用する手法は、あらゆる情報を詰め込みすぎる (フル ダンプ) か、必要な情報がほとんど含まれていない (ミニ ダンプ) かのいずれかです。ミニ ダンプは、通常非常に小さく、ヒープ領域を含まないため、実りあるデバッグ分析を行うことができません。そのため、フル ダンプが好まれてきましたが、最近ではめったに選択されることがなくなりました。それは、メモリがかつてないほど増加し、フル ダンプに 15 ~ 30 分、場合によっては 60 分かかることもあるためです。さらに、ダンプ ファイルが非常に大きくなり、圧縮したとしても、分析のために移動するのは容易ではありません。

昨年、Sysinternals ProcDump v3.0 では、ネイティブ アプリケーションのサイズの問題に対処するため、MiniPlus スイッチ (-mp) が導入されました。このスイッチは、ミニ ダンプとフル ダンプの中間サイズのダンプを作成します。MiniPlus スイッチは、ダンプにどのメモリを含めるかを、ヒューリスティックな多数のアルゴリズムに基づいて決定します。このアルゴリズムでは、メモリの種類、メモリの保護、アロケーション サイズ、領域サイズ、およびスタックのコンテンツが考慮されます。ダンプ対象のアプリケーションのレイアウトによりますが、フル ダンプより 50 ~ 95% 小さなダンプ ファイルが作成されます。さらに重要な点は、ほとんどの分析作業において、このダンプがフル ダンプと同程度に機能することです。運用中の Microsoft Exchange 2010 インフォメーション ストアが 48 GB のメモリを占有している状態で MiniPlus スイッチを適用した場合、作成されるダンプ ファイルは 1 ~ 2 GB です (95% の削減になります)。

今回は、Mark Russinovich と共同で、新しくリリースされ、ダンプにどのメモリに含めるかを決定できるようになった ProcDump に取り組みます。Sysinternals ProcDump v4.0 は、MiniPlus が内部で使用するのと同じ API を外部 DLL ベースのプラグインとして、-d スイッチを使って公開します。

今回の記事では、一連のサンプル アプリケーションを構築しながら、Sysinternals ProcDump v4.0 のしくみを細かく見ていきます。今回構築する複数のサンプル アプリケーションは、相互に拡張し、ProcDump の機能を数多く実装します。ProcDump の内部動作を掘り下げることで、ProcDump および基盤となる DbgHelp API を操作するプラグインを作成する方法を紹介します。

ダウンロード可能なコードには、サンプル アプリケーションに加えて、さまざまな理由でクラッシュしたアプリケーション一式が含まれています (コードのテストに使用するためです)。MiniDump05 サンプルは、スタンドアロン アプリケーションとして実装し、すべての API を含みます。MiniDump06 サンプルは、Sysinternals ProcDump v4.0 用のプラグインとして MiniDump05 サンプルを実装します。

用語

一連のダンプに付けられた用語には、「Mini (ミニ)」という言葉が多用されているため混同しやすいでしょう。MiniDump というファイル形式、Mini と MiniPlus というダンプ コンテンツ、および MiniDumpWriteDump と MiniDumpCallback という関数があります。

Windows は、DbgHelp.dll を使って MiniDump ファイル形式をサポートします。MiniDump ダンプ ファイル (*.dmp) は、ユーザー モード ターゲットまたはカーネル モード ターゲットにおけるメモリの部分キャプチャまたは完全キャプチャを収容します。MiniDump ファイル形式は、追加のメタデータ (コメント、プロセス統計など) を格納するため "ストリーム" の使用をサポートします。このファイル形式の名前は、最小限 (minimal) のデータのキャプチャをサポートするという要件に由来しています。DbgHelp API の MiniDumpWriteDump 関数と MiniDumpCallback 関数は、この 2 つの関数が作成するファイル形式に対応付けるために、関数名の先頭に MiniDump と付けられています。

Mini、MiniPlus、および Full という用語は、ダンプ ファイルのコンテンツ量の違いをを表しています。Mini が最も少量 (minimal) を意味し、プロセス環境ブロック (PEB)、スレッド環境ブロック (TEB)、スタックの一部、読み込んだモジュール、およびデータ セグメントを含みます。MiniPlus という用語は、Sysinternals ProcDump -mp キャプチャのコンテンツ量を表すために、Mark と共に命名しました。MiniPlus には、Mini ダンプのコンテンツに加えて (plus)、ヒューリスティックに重要と判断されるメモリを含みます。Full ダンプ (procdump.exe -ma) には、メモリが RAM にページインされるかどうかを問わず、プロセスの仮想アドレス空間をすべて含みます。

MiniDumpWriteDump 関数

プロセスを MiniDump ファイル形式でファイルにキャプチャするには、DbgHelp の MiniDumpWriteDump 関数を呼び出します。この関数には、ターゲット プロセスのハンドル (PROCESS_QUERY_INFORMATION および PROCESS_VM_READ アクセス)、ターゲット プロセスの PID、ファイルのハンドル (FILE_GENERIC_WRITE アクセス)、および MINIDUMP_TYPE フラグのビットマスクが必要です。さらに、例外情報の構造体 (例外コンテキスト レコードを含めるのに使用)、ユーザー ストリーム情報の構造体 (通常 CommentStreamA/W MINIDUMP_STREAM_TYPE 型でダンプにコメントを含めるのに使用)、およびコールバック情報の構造体 (呼び出しでキャプチャした内容の変更に使用) の 3 つの省略可能なパラメーターがあります。

BOOL WINAPI MiniDumpWriteDump(
  __in  HANDLE hProcess,
  __in  DWORD ProcessId,
  __in  HANDLE hFile,
  __in  MINIDUMP_TYPE DumpType,
  __in  PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
  __in  PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
  __in  PMINIDUMP_CALLBACK_INFORMATION CallbackParam
);

MiniDump01 サンプル アプリケーションは、省略可能なパラメーターを 1 つも使用しないで、MiniDumpWriteDump を呼び出して Mini ダンプを作成する方法を示しています (図 1 参照)。まず、PID 用のコマンド ライン引数をチェックし、次に OpenProcess を呼び出してターゲットのプロセス ハンドルを取得します。続いて、CreateFile を呼び出し、ファイル ハンドルを取得します (MiniDumpWriteDump はあらゆる I/O ターゲットをサポートします)。ファイルには、名前が重複せず、時系列に並べ替えられるように、ISO の日時に基づくファイル名、「C:\dumps\minidump_YYYY-MM-DD_HH-MM-SS-MS.dmp」を付けます。ディレクトリは "C:\dumps" をハードコーディングし、書き込みアクセスを保証します。これは、事後分析デバッグを行う際に必要になります。現在のフォルダー (System32 など) は、書き込み可能ではない場合があるためです。

図 1 MiniDump01.cpp

// MiniDump01.cpp : Capture a hang dump.
//
#include "stdafx.h"
#include <windows.h>
#include <dbghelp.h>
int WriteDump(HANDLE hProcess, DWORD dwProcessId, HANDLE hFile, MINIDUMP_TYPE miniDumpType);
int _tmain(int argc, TCHAR* argv[])
{
  int nResult = -1;
  HANDLE hProcess = INVALID_HANDLE_VALUE;
  DWORD dwProcessId = 0;
  HANDLE hFile = INVALID_HANDLE_VALUE;
  MINIDUMP_TYPE miniDumpType;
  // DbgHelp v5.2
  miniDumpType = (MINIDUMP_TYPE) (MiniDumpNormal | MiniDumpWithProcessThreadData |
    MiniDumpWithDataSegs | MiniDumpWithHandleData);
  // DbgHelp v6.3 - Passing unsupported flags to a lower version of DbgHelp
     does not cause any issues
  miniDumpType = (MINIDUMP_TYPE) (miniDumpType | MiniDumpWithFullMemoryInfo |
    MiniDumpWithThreadInfo);
  if ((argc == 2) && (_stscanf_s(argv[1], _T("%ld"), &dwProcessId) == 1))
  {
    // Generate the filename (ISO format)
    SYSTEMTIME systemTime;
    GetLocalTime(&systemTime);
    TCHAR szFilename[64];
    _stprintf_s(szFilename, 64, _T("c:\\dumps\\minidump_%04d-%02d-
      %02d_%02d-%02d-%02d-%03d.dmp"),
        systemTime.wYear, systemTime.wMonth, systemTime.wDay,
        systemTime.wHour, systemTime.wMinute, systemTime.wSecond,
        systemTime.wMilliseconds);
    // Create the folder and file
    CreateDirectory(_T("c:\\dumps"), NULL);
    if ((hFile = CreateFile(szFilename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
      FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE)
    {
      _tprintf(_T("Unable to open '%s' for write (Error: %08x)\n"), szFilename,
        GetLastError());
      nResult = 2;
    }
    // Open the process
    else if ((hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId)) == NULL)
    {
      _tprintf(_T("Unable to open process %ld (Error: %08x)\n"), dwProcessId,
        GetLastError());
      nResult = 3;
    }
    // Take a hang dump
    else
    {
      nResult = WriteDump(hProcess, dwProcessId, hFile, miniDumpType);
    }
    if (hFile) CloseHandle(hFile);
    if (hProcess) CloseHandle(hProcess);
    if (nResult == 0)
    {
      _tprintf(_T("Dump Created - '%s'\n"), szFilename);
    }
    else
    {
      DeleteFile(szFilename);
    }
  }
  else
  {
    _tprintf(_T("Usage: %s <pid>\n"), argv[0]);
    nResult = 1;
  }
  return 0;
}
int WriteDump(HANDLE hProcess, DWORD dwProcessId, HANDLE hFile, MINIDUMP_TYPE miniDumpType)
{
  if (!MiniDumpWriteDump(hProcess, dwProcessId, hFile, miniDumpType, NULL, NULL, NULL))
  {
    _tprintf(_T("Failed to create hang dump (Error: %08x)\n"), GetLastError());
    return 11;
  }
  return 0;
}

DumpType パラメーターは、特定の種類のメモリを含める (または、除外する) MINIDUMP_TYPE に基づくビットマスクです。MINIDUMP_TYPE フラグは、コールバックを使用した追加コーディングの必要なく、多くのメモリ領域のキャプチャを可能にする、非常に役立つフラグです。MiniDump01 サンプルで使用するオプションは、ProcDump と同じです。これにより、プロセスの要約に使用する (Mini) ダンプを作成します。

DumpType の値が 0x00000000 なので、常に、MiniDumpNormal フラグの値がそのまま使用されます。使用する DumpType には、すべてのスタック (MiniDumpNormal)、すべての PEB と TEB の情報 (MiniDumpWithProcessThreadData)、読み込んだモジュールの情報とすべてのグローバル データ (MiniDumpWithDataSegs)、すべてのハンドル情報 (MiniDumpWithHandleData)、すべてのメモリ領域の情報 (MiniDumpWithFullMemoryInfo)、およびすべてのスレッド時間とアフィニティの情報 (MiniDumpWithThreadInfo) があります。これらのフラグを活用することで、豊富な情報を含みながら、非常に小さな Mini ダンプが作成されます (最も大きなアプリケーションの場合でも 30 MB 未満)。これらの MINIDUMP_TYPE フラグでサポートされるデバッガー コマンドの例を、図 2 の一覧表に示します。

図 2 デバッガー コマンド

MINIDUMP_TYPE デバッガー コマンド
MiniDumpNormal knL99
MiniDumpWithProcessThreadData !peb, !teb
MiniDumpWithDataSegs lm, dt <global>
MiniDumpWithHandleData !handle, !cs
MiniDumpWithFullMemoryInfo !address
MiniDumpWithThreadInfo !runaway

MiniDumpWriteDump を使用する際、ターゲットではなく、キャプチャ プログラムのアーキテクチャに対応するダンプが作成されます。そのため、32 ビットのプロセスをキャプチャする際は 32 ビットのキャプチャ プログラムを使用し、64 ビットのプロセスをキャプチャする際は 64 ビットのキャプチャ プログラムを使用します。"64 ビット版 Windows 上の 32 ビット版 Windows" (WOW64) をデバッグする場合は、32 ビット プロセスの 64 ビット ダンプを作成する必要があります。

(意図的かどうかを問わず) アーキテクチャと対応していない場合、64 ビット ダンプで 32 ビットのスタックにアクセスするために、デバッガーで対象のコンピューター (.effmach x86) を変更する必要があります。多くのデバッガー拡張機能ではこのシナリオを実行できないので注意してください。

例外コンテキスト レコード

マイクロソフトのサポート エンジニアは、「ハング ダンプ」と「クラッシュ ダンプ」という用語を使用します。彼らがクラッシュ ダンプが必要だと言うときは、例外コンテキスト レコードを含むダンプが必要だと言っています。ハング ダンプが必要だと言うときは、(通常) 例外コンテキスト レコードを含まない一連のダンプが必要だと言っています。ただし、例外情報を含むダンプは、クラッシュ時に限らず、いつでも必要になる可能性があります。例外情報は、ダンプに追加データを提供する 1 つの手段にすぎません。ユーザー ストリームの情報は、この点で例外情報と似ています。

例外コンテキスト レコードは、CONTEXT 構造体 (CPU レジスタ群) と EXCEPTION_RECORD 構造体 (例外コード、命令のアドレスなど) を組み合わせた情報です。ダンプに例外コンテキスト レコードを含め、.ecxr を実行すると、現在のデバッガー コンテキスト (スレッドとレジスタの状態) が例外を発生した命令に設定されます (図 3 参照)。

図 3 コンテキストを例外コンテキスト レコードに変更

このダンプ ファイルには、関心のある例外が格納されています。

.ecxr を使用して、格納された例外情報にアクセスできます。

(17cc.1968174c.6f8): Access violation - code c0000005 (first/second chance not available)
eax=00000000 ebx=001df788 ecx=75ba31e7 edx=00000000 esi=00000002 edi=00000000
eip=77c7014d esp=001df738 ebp=001df7d4 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!NtWaitForMultipleObjects+0x15:
77c7014d 83c404          add     esp,4
0:000> .ecxr
eax=00000000 ebx=00000000 ecx=75ba31e7 edx=00000000 esi=00000001 edi=003b3374
eip=003b100d esp=001dfdbc ebp=001dfdfc iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
CrashAV_x86!wmain+0x140xd:
00000001`3f251014 45891b003b100d 8900            mov     dword ptr [r11],r11deax],eax  ds:002b:00000000`00000000=????????=????????

.ecxr をサポートするには、省略可能な MINIDUMP_EXCEPTION_INFORMATION 構造体を MiniDumpWriteDump に渡す必要があります。実行時または事後分析時に例外情報を取得できます。

実行時の例外

デバッガー イベント ループを実装する場合、例外が発生する際に例外情報が渡されます。デバッガー イベント ループは、ブレークポイント、初回の例外、2 回目の例外の情報を含む EXCEPTION_DEBUG_EVENT 構造体を受け取ります。

MiniDump02 サンプル アプリケーションは、デバッガー イベント ループから MiniDumpWriteDump を呼び出し、2 回目の例外のコンテキスト レコードがダンプに含まれるようにする方法を示しています ("procdump.exe -e" に相当)。この機能は、-e スイッチを使用すると実行されます。コードは非常に長いので、図 4 にアプリケーションの疑似コードを示します。完全なソース コードは、今回の記事のコード ダウンロードを参照してください。

図 4 MiniDump02 の疑似コード

Function Main
Begin
  Check Command Line Arguments
  CreateFile(c:\dumps\minidump_YYYY-MM-DD_HH-MM-SS-MS.dmp)
  OpenProcess(PID)
  If "–e" Then
    DebugEventDump
    TerminateProcess(Process)
  Else
    WriteDump(NULL)
  CloseHandle(Process)
  CloseHandle(File)
End
Function WriteDump(Optional Exception Context Record)
Begin
  MiniDumpWriteDump(Optional Exception Context Record)
End
Function DebugEventDump
Begin
  DebugActiveProcess(PID)
  While (Not Done)
  Begin
    WaitForDebugEvent
    Switch (Debug Event Code)
    Begin
    Case EXCEPTION_DEBUG_EVENT
      If EXCEPTION_BREAKPOINT
        ContinueDebugEvent(DBG_CONTINUE)
      Else If "First Chance Exception"
        ContinueDebugEvent(DBG_EXCEPTION_NOT_HANDLED)
      Else "Second Chance Exception"
        OpenThread(Debug Event Thread ID)
        GetThreadContext
        WriteDump(Exception Context Record)
        CloseHandle(Thread)
        Done = True
    Case EXIT_PROCESS_DEBUG_EVENT
      ContinueDebugEvent(DBG_CONTINUE)
      Done = True
    Case CREATE_PROCESS_DEBUG_EVENT
      CloseHandle(CreateProcessInfo.hFile)
      ContinueDebugEvent(DBG_CONTINUE)
    Case LOAD_DLL_DEBUG_EVENT
      CloseHandle(LoadDll.hFile)
      ContinueDebugEvent(DBG_CONTINUE)
    Default
      ContinueDebugEvent(DBG_CONTINUE)
    End Switch
  End While
  DebugActiveProcessStop(PID)
End

アプリケーションでは、まず PID 用のコマンド ライン引数をチェックします。次に、OpenProcess を呼び出してターゲットのプロセス ハンドルを取得し、CreateFile を呼び出してファイル ハンドルを取得します。-e スイッチがない場合は、前と同じようにハング ダンプを作成します。-e スイッチがある場合は、DebugActiveProcess を使用して、アプリケーションをターゲット (デバッガー) にアタッチします。while ループ内で、WaitForDebugEvent が DEBUG_EVENT 構造体を返すのを待機します。Switch ステートメントは、DEBUG_EVENT 構造体の dwDebugEventCode メンバーを使用します。ダンプの作成、またはプロセスの終了後、DebugActiveProcessStop を呼び出しターゲットからデタッチします。

DEBUG_EVENT 構造体の EXCEPTION_DEBUG_EVENT には、例外ごとに 1 つの例外レコードが格納されます。例外レコードがブレークポイントの場合は、DBG_CONTINUE を指定して ContinueDebugEvent を呼び出し、ローカルに処理します。例外が初回の場合は、2 回目の例外に持ち込むために処理しません (ターゲットにハンドラーがない場合)。このため、DBG_EXCEPTION_NOT_HANDLED を指定して ContinueDebugEvent を呼び出します。残るシナリオは、2 回目の例外です。DEBUG_EVENT 構造体の dwThreadId を使用して、例外が発生したスレッドのハンドルを取得するために OpenThread を呼び出します。このスレッド ハンドルは、GetThreadContext で使用し、必要な CONTEXT 構造体を設定します (注: CONTEXT 構造体のサイズは、プロセッサにレジスタが追加され、年を追うごとに大きくなっています。新しい OS で CONTEXT 構造体のサイズが増大した場合、このコードを再コンパイルする必要があります)。取得した CONTEXT 構造体と DEBUG_EVENT の EXCEPTION_RECORD は、EXCEPTION_POINTERS 構造体の設定に使用し、EXCEPTION_POINTERS 構造体は MINIDUMP_EXCEPTION_INFORMATION 構造体の設定に使用します。MINIDUMP_EXCEPTION_INFORMATION 構造体は、MiniDumpWriteDump で使用するため、アプリケーションの WriteDump 関数に渡します。

EXIT_PROCESS_DEBUG_EVENT は、例外が発生する前にターゲットが終了する場合に限り処理します。DBG_CONTINUE を指定して ContinueDebugEvent を呼び出してこのイベントを認識し、while ループから抜け出します。

CREATE_PROCESS_DEBUG_EVENT イベントと LOAD_DLL_DEBUG_EVENT イベントは、ハンドルを閉じる必要がある場合に限り処理します。これらの領域では、DBG_CONTINUE を指定して ContinueDebugEvent を呼び出します。

Default ケースでは、実行を継続し、渡されたハンドルを閉じるために、DBG_CONTINUE を指定して ContinueDebugEvent を呼び出し、他のすべてのイベントを処理します。

事後分析の例外

Windows Vista では、事後分析デバッガー コマンド ラインに 3 つ目のパラメーターを導入し、例外情報の受け渡しをサポートしています。3 つ目のパラメーターを受け取るには、(AeDebug キーに) 3 つの %ld 置換を含む Debugger 値が必要です。3 つの値は、プロセス ID、イベント ID、および JIT アドレスです。JIT アドレスとは、ターゲットのアドレス空間における JIT_DEBUG_INFO 構造体のアドレスです。Windows エラー報告 (WER) は、ハンドルされない例外の結果として WER が呼び出されたときに、このメモリをターゲットのアドレス空間に割り当てます。JIT_DEBUG_INFO 構造体に情報を設定し、(割り当てたアドレスを渡して) 事後分析デバッガーを呼び出して、事後分析デバッガー終了後にメモリを解放します。

例外コンテキスト レコードを判断するのに、事後分析アプリケーションはターゲットのアドレス空間から JIT_DEBUG_INFO 構造体を読み取ります。この構造体には、ターゲットのアドレス空間における CONTEXT 構造体と EXCEPTION_RECORD 構造体のアドレスが含まれます。今回は、ターゲットのアドレス空間から CONTEXT 構造体と EXCEPTION_RECORD 構造体を読み取る代わりに、EXCEPTION_POINTERS 構造体にそれらのアドレスを設定し、次に MINIDUMP_EXCEPTION_INFORMATION 構造体の ClientPointers メンバーに TRUE を設定しているだけです。これにより、デバッガーがすべての厄介な処理を行うようになります。デバッガーは、ターゲットのアドレス空間からデータを読み取ります (32 ビット プロセスの 64 ビット ダンプを作成できるように、アーキテクチャの違いを許容します)。

MiniDump03 サンプル アプリケーションは、JIT_DEBUG_INFO サポートを実装する方法を示しています (図 5 参照)。

図 5 MiniDump03 - JIT_DEBUG_INFO ハンドラー

int JitInfoDump(HANDLE hProcess, DWORD dwProcessId, HANDLE hFile, MINIDUMP_TYPE miniDumpType, ULONG64 ulJitInfoAddr)
{
  int nResult = -1;
  JIT_DEBUG_INFO jitInfoTarget;
  SIZE_T numberOfBytesRead;
  if (ReadProcessMemory(hProcess, (void*)ulJitInfoAddr, &jitInfoTarget, sizeof(jitInfoTarget), &numberOfBytesRead) &&
    (numberOfBytesRead == sizeof(jitInfoTarget)))
  {
    EXCEPTION_POINTERS exceptionPointers = {0};
    exceptionPointers.ContextRecord = (PCONTEXT)jitInfoTarget.lpContextRecord;
    exceptionPointers.ExceptionRecord = (PEXCEPTION_RECORD)jitInfoTarget.lpExceptionRecord;
    MINIDUMP_EXCEPTION_INFORMATION    exceptionInfo = {0};
    exceptionInfo.ThreadId = jitInfoTarget.dwThreadID;
    exceptionInfo.ExceptionPointers = &exceptionPointers;
    exceptionInfo.ClientPointers = TRUE;
    nResult = WriteDump(hProcess, dwProcessId, hFile, miniDumpType, &exceptionInfo);
  }
  else
  {
    nResult = WriteDump(hProcess, dwProcessId, hFile, miniDumpType, NULL);
  }
  return nResult;
}

アプリケーションが事後分析デバッガーとして呼び出されるときに、TerminateProcess の呼び出しによってプロセスを強制的に終了するのはアプリケーションの役割です。次に示すように、MiniDump03 サンプルでは、ダンプの取得後に TerminateProcess を呼び出します。

// Post Mortem (AeDebug) dump - JIT_DEBUG_INFO - Vista+
else if ((argc == 4) && (_stscanf_s(argv[3], _T("%ld"), &ulJitInfoAddr) == 1))
{
  nResult = JitInfoDump(hProcess, dwProcessId, hFile, miniDumpType, ulJitInfoAddr);
  // Terminate the process
  TerminateProcess(hProcess, -1);
}

事後分析デバッガーを自身のアプリケーションに置き換えるには、AeDebug キーの Debugger 値を、該当するアーキテクチャの事後分析アプリケーションに設定します。対応するアプリケーションを使用することで、デバッガーが対象とするコンピューター (.effmach) を調整する必要がなくなります。

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug
Debugger (REG_SZ) = "C:\dumps\minidump03_x64.exe %ld %ld %ld"
HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug
Debugger (REG_SZ) = "C:\dumps\minidump03_x86.exe %ld %ld %ld"

MiniDumpCallback 関数

ここまでは、DumpType パラメーターが示すメモリ (および、省略可能な例外コンテキスト レコード) を含むダンプを作成してきました。MiniDumpCallback 関数のプロトタイプを実装することで、追加のメモリ領域だけでなく、エラーの処理も追加できます。MiniDumpCallback 関数のプロトタイプだけを実装し、Sysinternals ProcDump v4.0 と共に使用する方法は後で説明します。

現在、ダンプの複数の側面を管理するためのコールバックが 16 種類あります。管理する側面には、モジュールとスレッド用に含まれるメモリ、メモリ自体、進行中のダンプを中止する機能、ダンプ ファイル I/O の管理、エラーの処理などがあります。

このテンプレート コードの Switch ステートメントには、コールバックのすべての種類を、最初に呼び出される順番におおまかに従って含めています (図 6 参照)。複数回呼び出されるコールバックは、2 回目以降は異なる順番で呼び出される場合があります。同時に、この順番は決まりごとではないので、将来のリリースで変わる可能性があります。

図 6 MiniDumpCallback 関数のプロトタイプのテンプレート実装

BOOL CALLBACK MiniDumpCallbackRoutine(
  __in     PVOID CallbackParam,
  __in     const PMINIDUMP_CALLBACK_INPUT CallbackInput,
  __inout  PMINIDUMP_CALLBACK_OUTPUT CallbackOutput
)
{    // Callback supported in Windows 2003 SP1 unless indicated
  // Switch statement is in call order
  switch (CallbackInput->CallbackType)
  {
  case IoStartCallback:  //  Available in Vista/Win2008
    break;
  case SecondaryFlagsCallback:  //  Available in Vista/Win2008
    break;
  case CancelCallback:
    break;
  case IncludeThreadCallback:
    break;
  case IncludeModuleCallback:
    break;
  case ModuleCallback:
    break;
  case ThreadCallback:
    break;
  case ThreadExCallback:
    break;
  case MemoryCallback:
    break;
  case RemoveMemoryCallback:
    break;
  case WriteKernelMinidumpCallback:
    break;
  case KernelMinidumpStatusCallback:
    break;
  case IncludeVmRegionCallback:  //  Available in Vista/Win2008
    break;
  case IoWriteAllCallback:  //  Available in Vista/Win2008
    break;
  case IoFinishCallback:  //  Available in Vista/Win2008
    break;
  case ReadMemoryFailureCallback:  // Available in Vista/Win2008
    break;
  }
  return TRUE;
}

MiniDump04 サンプルに含めているコールバックは、スタックのコンテンツをすべて含み、読み取りエラーを無視するという 2 つのことを行います。この例では、スタック全体を含めるために ThreadCallback と MemoryCallback を使用し、読み取りエラーを無視するために ReadMemoryFailureCallback を使用します。

コールバックを呼び出すには、省略可能な MINIDUMP_CALLBACK_INFORMATION 構造体を MiniDumpWriteDump 関数に渡します。この構造体の CallbackRoutine メンバーは、実装した MiniDumpCallback 関数 (このテンプレートとサンプルでは MiniDumpCallbackRoutine) の指定に使用します。CallbackParam メンバーは、コールバックの呼び出し間でコンテキストを維持することを可能にする VOID* ポインターです。図 7 に示すのは、MiniDump04 サンプルの WriteDump 関数です。

図 7 MiniDump04 サンプルの WriteDump 関数

int WriteDump(HANDLE hProcess, DWORD dwProcessId, HANDLE hFile, MINIDUMP_TYPE miniDumpType, PMINIDUMP_EXCEPTION_INFORMATION pExceptionParam)
{
  MemoryInfoNode* pRootMemoryInfoNode = NULL;
  MINIDUMP_CALLBACK_INFORMATION callbackInfo;
  callbackInfo.CallbackParam = &pRootMemoryInfoNode;
  callbackInfo.CallbackRoutine = MiniDumpCallbackRoutine;
  if (!MiniDumpWriteDump(hProcess, dwProcessId, hFile, miniDumpType, pExceptionParam, NULL, &callbackInfo))
  {
    _tprintf(_T("Failed to create hang dump (Error: %08x)\n"), GetLastError());
    while (pRootMemoryInfoNode)
    {    // If there was an error, we'll need to cleanup here
      MemoryInfoNode* pNode = pRootMemoryInfoNode;
      pRootMemoryInfoNode = pNode->pNext;
      delete pNode;
    }
    return 11;
  }
  return 0;
}

次に示すように、呼び出し間でコンテキストを維持するための定義済みの構造体 (MemoryInfoNode) は、アドレスとサイズを含むリンク リスト ノードです。

struct MemoryInfoNode
{
  MemoryInfoNode* pNext;
  ULONG64 ulBase;
  ULONG ulSize;
};

スタック全体

DumpType パラメーターで MiniDumpWithProcessThreadData フラグを使用すると、スタック ベースから現在のスタック ポインターまでの各スタックのコンテンツが含められます。MiniDump04 サンプルで実装した MiniDumpCallbackRoutine 関数は、残りのスタックを含めるようにこれを拡張しています。スタック全体を含めることで、スタックが壊れた原因を特定できる場合があります。

スタックが壊れるのは、スタック ベースのバッファーにバッファー オーバーフローが生じるときです。バッファー オーバーフローは、スタックのリターン アドレスを上書きし、"call" オペコードで命令ポインターを PUSH するのではなく、"ret" オペコードを実行してバッファーの内容を命令ポインターとして POP する原因になります。これは、無効なメモリ アドレス、またはもっと悪い場合は、コードのランダムな場所から実行することになります。

スタックが壊れたときは、現在のスタック ポインターの下にあるメモリには (注: スタックは下方に拡大します)、変更されていないスタック データがまだ格納されています。このデータから、バッファーの内容と、多くの場合、バッファーの内容を生成するために呼び出された関数を把握できます。

作成するダンプに、スタックの上限より上のメモリを追加のスタック メモリとして含める場合と含めない場合を比べると、通常のダンプではメモリは不足していること (? 記号) が表示されますが、コールバックで作成したダンプにはメモリが含まれているのが確認されます (図 8 参照)。

図 8 スタック コンテンツの比較

0:003> !teb
TEB at 000007fffffd8000
  ExceptionList:        0000000000000000
  StackBase:            000000001b4b0000
  StackLimit:           000000001b4af000
  SubSystemTib:         0000000000000000
...
// No Callback
0:003> dp poi($teb+10) L6
00000000`1b4af000  ????????`???????? ????????`????????
00000000`1b4af010  ????????`???????? ????????`????????
00000000`1b4af020  ????????`???????? ????????`????????
// Callback
0:003> dp poi($teb+10) L6
00000000`1b4af000  00000000`00000000 00000000`00000000
00000000`1b4af010  00000000`00000000 00000000`00000000
00000000`1b4af020  00000000`00000000 00000000`00000000

"スタック全体" をダンプするコードの冒頭部分は、ThreadCallback コールバックの処理です (図 9 参照)。この種のコールバックは、プロセスでスレッドごとに 1 回呼び出されます。コールバックには、CallbackInput パラメーターを経由して MINIDUMP_THREAD_CALLBACK 構造体を渡します。この構造体には、スレッドのスタック ベースである StackBase メンバーがあります。StackEnd メンバーは、現在のスタック ポインターです (x86 は esp、x64 は rsp)。この構造体には、スタックの上限 (スレッド環境ブロックの一部) は含まれていません。

図 9 各スレッドのスタック領域を収集するために使用する ThreadCallback

case ThreadCallback:
{    // We aren't passed the StackLimit so we use a 1MB offset from StackBase
  MemoryInfoNode** ppRoot = (MemoryInfoNode**)CallbackParam;
  if (ppRoot)
  {
    MemoryInfoNode* pNode = new MemoryInfoNode;
    pNode->ulBase = CallbackInput->Thread.StackBase - 0x100000;
    pNode->ulSize = 0x100000; // 1MB
    pNode->pNext = *ppRoot;
    *ppRoot = pNode;
  }
}
break;

この例では単純なアプローチを採用し、スタックのサイズは 1 MB (多くのアプリケーションで既定のサイズ) を想定しています。スタック ポインターの下のメモリは、DumpType パラメーターにより含められます。スタックが 1 MB を超える場合、スタックの一部が含められることになります。スタックが 1 MB 未満の場合は、余分にデータが含められるだけです。コールバックが要求するメモリ範囲が空き領域を含む場合、または他のメモリとオーバーラップする場合でも、エラーは発生しません。

StackBase と 1 MB のオフセットは、今回定義している MemoryInfoNode 構造体の新しいインスタンス作成に記録されます。この新しいインスタンスの作成は、CallbackParam 引数を経由してコールバックに渡されるリンク リストの前に追加します。ThreadCallback を複数回呼び出すと、リンク リストにはダンプに含める追加メモリの複数のノードが格納されます。

"スタック全体" をダンプするコードの最後の部分は、MemoryCallback コールバックの処理です (図 10 参照)。MemoryCallback はコールバックが TRUE を返す限り繰り返し呼び出され、MINIDUMP_CALLBACK_OUTPUT 構造体の MemoryBase メンバーと MemorySize メンバーにゼロ以外の値を提供します。

図 10 スタック領域を返す間繰り返し呼び出される MemoryCallback

case MemoryCallback:
{    // Remove the root node and return its members in the callback
  MemoryInfoNode** ppRoot = (MemoryInfoNode**)CallbackParam;
  if (ppRoot && *ppRoot)
  {
    MemoryInfoNode* pNode = *ppRoot;
    *ppRoot = pNode->pNext;
    CallbackOutput->MemoryBase = pNode->ulBase;
    CallbackOutput->MemorySize = pNode->ulSize;
    delete pNode;
  }
}
break;

このコードは、CallbackOutput パラメーターに値を設定し、次にリンク リストからノードを削除します。MemoryCallback を複数回呼び出し、リンク リストに含まれたノードをすべて削除すると、値ゼロを返し MemoryCallback の呼び出しを終了します。MemoryBase メンバーと MemorySize メンバーは渡されたときにゼロを設定するので、TRUE を返すだけです。

.dumpdebug コマンド出力の MemoryListStream 領域を使用して、ダンプのすべてのメモリ領域を確認できます (隣接するブロックは結合される場合があることに注意してください)。図 11 を参照してください。

図 11 .dumpdebug コマンド出力

0:000> .dumpdebug
----- User Mini Dump Analysis
...
Stream 3: type MemoryListStream (5), size 00000194, RVA 00000E86
  25 memory ranges
  range#    RVA      Address      Size
       0 0000101A    7efdb000   00005000
       1 0000601A    001d6000   00009734
       2 0000F74E    00010000   00021000
       3 0003074E    003b0f8d   00000100
       4 0003084E    003b3000   00001000
...
Read Memory Failure

コードの最後の部分は非常に簡単です (図 12 参照)。MINIDUMP_CALLBACK_OUTPUT 構造体の Status メンバーに S_OK を設定し、キャプチャで読み取られなかったメモリ領域は無視してもよい (OK) ことを示します。

図 12 読み取りエラーのときに呼び出される ReadMemoryFailureCallback

case ReadMemoryFailureCallback:  // DbgHelp.dll v6.5; Available in Vista/Win2008
  {    //  There has been a failure to read memory. Set Status to S_OK to ignore it.
    CallbackOutput->Status = S_OK;
  }
  break;

この簡単な実装で、コールバックに MiniDumpIgnoreInaccessibleMemory フラグと同じ機能を実装しています。ReadMemoryFailureCallback コールバックには、CallbackInput パラメーターの MINIDUMP_READ_MEMORY_FAILURE_CALLBACK 構造体経由で、オフセット、バイト数、およびエラー コードを渡します。より複雑なコールバックでは、この情報を使ってメモリがダンプ分析に適しているか、ダンプを中止した方がよいかを判断できます。

メモリの分析

ダンプから除外できる情報と除外できない情報を判断するには、どうすればよいでしょう。Sysinternals VMMap は、アプリケーションのメモリの全体像を把握する優れた方法です。マネージ プロセスで Sysinternals VMMap を使用すると、ガベージ コレクション (GC) の対象となるヒープに関連付けられたアロケーションと、アプリケーションのイメージ ファイルとマップ ファイルに関連付けられたアロケーションがあるのがわかります。マネージ プロセスのダンプで必要なのは、GC ヒープです。これは、Son of Strike (SOS) デバッガー拡張機能は、ダンプを解釈するのに GC ヒープ内のそのままのデータ構造が必要だからです。

GC ヒープの場所を特定するには、DebugCreate と IDebugClient::AttachProcess を使用して、ターゲットに対して Debugging Engine (DbgEng) セッションを開始する正確な方法を採用できます。このデバッグ セッションで、SOS デバッガー拡張機能を読み込み、ヒープの情報を返すコマンドを実行できます (これは領域に依存した情報を使う方法の一例です)。

ヒューリスティックを使用する方法もあります。メモリの種類がプライベート (MEMORY_PRIVATE) か、読み取りまたは書き込み保護 (PAGE_READWRITE または PAGE_EXECUTE_READWRITE) のすべての領域を含めます。この方法では確実に必要となるよりも多くのメモリを収集しますが、アプリケーション自体を除外するので、大きな節約になることには変わりありません。MiniDump05 サンプルでは (図 13 参照)、この方法を採用するため MiniDump04 サンプルのスレッド スタック コードを ThreadCallback コールバックの 1 回だけ反復する VirtualQueryEx ループに置き換えます (この新しいロジックでも、スタック全体を含めることには変わりありません)。次に、ダンプにメモリを含めるために、MiniDump04 サンプルと同じ MemoryCallback コードを使用します。

図 13 メモリ領域を収集するために 1 回使用する MiniDump05 の ThreadCallback

case ThreadCallback:
{    // Collect all of the committed MEM_PRIVATE and R/W memory
  MemoryInfoNode** ppRoot = (MemoryInfoNode**)CallbackParam;
  if (ppRoot && !*ppRoot)    // Only do this once
  {
    MEMORY_BASIC_INFORMATION mbi;
    ULONG64 ulAddress = 0;
    SIZE_T dwSize = VirtualQueryEx(CallbackInput->ProcessHandle, (void*)ulAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION));
    while (dwSize == sizeof(MEMORY_BASIC_INFORMATION))
    {
      if ((mbi.State == MEM_COMMIT) &&
        ((mbi.Type == MEM_PRIVATE) || (mbi.Protect == PAGE_READWRITE) || (mbi.Protect == PAGE_EXECUTE_READWRITE)))
      {
        MemoryInfoNode* pNode = new MemoryInfoNode;
        pNode->ulBase = (ULONG64)mbi.BaseAddress;
        pNode->ulSize = (ULONG)mbi.RegionSize;
        pNode->pNext = *ppRoot;
        *ppRoot = pNode;
      }
      // Loop
      ulAddress = (ULONG64)mbi.BaseAddress + mbi.RegionSize;
      dwSize = VirtualQueryEx(CallbackInput->ProcessHandle, (void*)ulAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION));
    }
  }
}
break;

マップされたメモリのイメージ ファイル

イメージ領域 (MEM_IMAGE) のないダンプをどうやってデバッグするのか、不思議に思うでしょう。たとえば、実行しているコードをどのように見るのでしょう。その方法は、従来とは少し異なります。デバッガーが Full ダンプ以外のダンプで、不足しているイメージ領域にアクセスする必要があるときは、イメージ ファイルからデータを取得します。PDB のもともとのイメージ ファイルの場所であるモジュールの読み込みパスか、.sympath/.exepath 検索パスを使用してイメージ ファイルを見つけます。lmvm <module> を実行した場合、ダンプにマップされたファイルを示す "Mapped memory image file" (マップされたメモリ イメージ ファイル) という行が表示されます。たとえば、次のようになります。

0:000> lmvm CrashAV_x86
start    end        module name
003b0000 003b6000   CrashAV_x86   (deferred)            
  Mapped memory image file: C:\dumps\CrashAV_x86.exe
  Image path: C:\dumps\CrashAV_x86.exe
  Image name: CrashAV_x86.exe
...

デバッガーの "Mapped memory image file" の機能を活用することは、ダンプのサイズを小さくとどめる良い方法です。この方法は、特にネイティブ アプリケーションの場合に有効です。コンパイル済みのバイナリが使用されるので、社内のビルド サーバーで利用可能なため (そして、PDB により指定されるため) です。マネージ アプリケーションの場合、顧客のリモート コンピューターでは JIT コンパイルが行われるため、問題が複雑になります。他のコンピューターからマネージ アプリケーション ダンプをデバッグする場合は、(ダンプと共に) バイナリをローカルにコピーする必要があります。複数のダンプが迅速に作成され、次に単一の (大きな) アプリケーション イメージ ファイルのコレクションが停止することなく作成されるため、これでもまだ節約になります。ファイル コレクションを簡略化するには、ModuleCallback を使用して、ダンプで参照したモジュール (ファイル) を収集するスクリプトを出力できます。

プラグイン

スタンドアロンのアプリケーションを Sysinternals ProcDump v4.0 を使用するように変更することで、日常の仕事を大幅に簡略化できます。もう MiniDumpWriteDump の呼び出しに関連するコードをすべて実装する必要はありません。さらに重要なことは、ダンプを適切なタイミングで開始するための全コードを実装する必要もありません。必要なのは、MiniDumpCallback 関数を実装し、MiniDumpCallbackRoutine として DLL にエクスポートすることだけです。

MiniDump06 サンプルに含めたコールバックのコードは、MiniDump05 にわずかな修正しか加えていません (図 14 参照)。

図 14 CallbackParam の代わりにグローバル データを使用するよう変更した MiniDumpCallbackRoutine

MemoryInfoNode* g_pRootMemoryInfoNode = NULL;
...
case IncludeThreadCallback:
{
  while (g_pRootMemoryInfoNode)
  {    //Unexpected cleanup required
    MemoryInfoNode* pNode = g_pRootMemoryInfoNode;
    g_pRootMemoryInfoNode = pNode->pNext;
    delete pNode;
  }
}
break;
...
case ThreadCallback:
{    // Collect all of committed MEM_PRIVATE and R/W memory
  if (!g_pRootMemoryInfoNode)    // Only do this once
  {
...
    pNode->pNext = g_pRootMemoryInfoNode;
    g_pRootMemoryInfoNode = pNode;
...
  }
}
break;
...
case MemoryCallback:
{    // Remove the root node and return its members in the callback
  if (g_pRootMemoryInfoNode)
  {
    MemoryInfoNode* pNode = g_pRootMemoryInfoNode;
    g_pRootMemoryInfoNode = pNode->pNext;
    CallbackOutput->MemoryBase = pNode->ulBase;
    CallbackOutput->MemorySize = pNode->ulSize;
    delete pNode;
  }
}
break;

新しい MiniDump06 プロジェクトは、コールバックのコードを DLL としてコンパイルします。このプロジェクトは、次に示すように、DEF ファイルを使用して MiniDumpCallbackRoutine (大文字と小文字を区別する) をエクスポートします。

LIBRARY    "MiniDump06"
EXPORTS
  MiniDumpCallbackRoutine   @1

ProcDump は CallbackParam に NULL 値を渡すので、MiniDumpCallbackRoutine は MemoryInfoNode 構造体で進行状況を追跡する代わりに、グローバル変数を使用する必要があります。最初の IncludeThreadCallback に、前のキャプチャで設定された場合にグローバル変数をリセット (削除) する新しいコードがあります。WriteDump 関数で MiniDumpWriteDump 呼び出しが失敗した後に実装しているコードをこのコードに置き換えます。

ProcDump で DLL を使うには、キャプチャのアーキテクチャと対応する DLL 名の前に -d スイッチを指定します。-d スイッチは、Mini ダンプ (スイッチなし) と Full ダンプ (-ma) を作成する際に使用可能です。MiniPlus ダンプ (-mp) の作成時には使用できません。

procdump.exe -d MiniDump06_x64.dll notepad.exe
procdump.exe –ma -d MiniDump06_x64.dll notepad.exe

コールバックは、Full ダンプ (-ma) を作成する場合は、このサンプルで紹介した以外のさまざまな種類のコールバックを呼び出すことに注意してください (MSDN ライブラリのドキュメントを参照してください)。DumpType パラメーターに MiniDumpWithFullMemory を設定している場合、MiniDumpWriteDump 関数はダンプを Full ダンプとして処理します。

Sysinternals ProcDump (procdump.exe) は、必要に応じて 64 ビット版 (procdump64.exe) を抽出する 32 ビットのアプリケーションです。procdump.exe が procdump64.exe を抽出して起動し、次に procdump64.exe が (64 ビット版) DLL を読み込みます。このように、起動したアプリケーションが目的のターゲットではないため、64 ビット版 DLL のデバッグは一筋縄ではいきません。64 ビット版 DLL のデバックをサポートする最も簡単な方法は、一時的な procdump64.exe を他のフォルダーにコピーし、そのコピーを使用してデバッグすることです。こうすれば、抽出は起こらず、デバッガー (Visual Studio など) で起動したアプリケーションで DLL が読み込まれます。

ブレーク

Mini ダンプしか作成する余裕がない場合、クラッシュやハングの出どころを特定するのは容易ではありません。作成するダンプに追加の重要な情報を含めることで、Full ダンプを作成する余裕がなくても、この問題を解決できます。

独自のダンプ アプリケーションや DLL の実装に関心があるなら、まず Sysinternals ProcDump、WER、および AdPlus ユーティリティの効果を検討することをお勧めします。無駄なことに労力を費やさないでください。

コールバックを作成するとき、アプリケーションにおけるメモリのレイアウトを正確に把握することに時間をかけてください。Sysinternals VMMap スナップショットを作成し、アプリケーションのダンプを使用して詳細を分析します。ダンプをより小さくするには、繰り返し取り組む必要があります。判断が容易な領域をダンプに含めるまたは除外することから始め、アルゴリズムを改良してください。目的を達成するためには、ヒューリスティックと領域に依存した情報の両方に基づくアプローチが必要な場合があります。ターゲット アプリケーションを修正することで、意思決定が容易になります。たとえば、それぞれの種類のメモリ使用に、よく知られた (そして固有の) アロケーション サイズを使用できる場合があります。重要なのは、ターゲット アプリケーションとダンプ アプリケーションの両方において、どのメモリが必要かを判断する方法を決める際に、工夫を凝らすことです。

カーネルの開発者でコールバックに関心があるなら、類似したカーネル ベースのメカニズムがあります。msdn.com から、BugCheckCallback ルーチンに関するドキュメントを参照してください。

Andrew Richards は、マイクロソフトの Windows OEM シニア エスカレーション エンジニアです。ツールをサポートすることに情熱を燃やしていて、エンジニアをサポートする作業を簡略化するデバッガー拡張機能やコールバック、およびアプリケーションを作成し続けています。彼の連絡先は andrew.richards@microsoft.com (英語のみ) です。

この記事のレビューに協力してくれた技術スタッフの Drew BlissMark Russinovich に心より感謝いたします。