March 2011

Volume 26 Number 03

デバッガー API - Windows 用デバッグ ツールの拡張機能を作成する

Andrew Richards | March 2011

コード サンプルのダウンロード

運用環境で起こる問題点のトラブルシューティングは、エンジニアであれば誰でも経験することで、最も煩わしい作業の 1 つです。しかし、一番実りのある作業の 1 つでもあります。私はマイクロソフト サポート部門に勤務しているため、このようなトラブルシューティングに毎日直面します。アプリケーションがクラッシュした原因、ハングした理由、パフォーマンスの問題が発生する理由などを調査しています。

デバッグ方法を学ぶことは難しく、熟練したデバッグ実行者であり続けるため、定期的な訓練に多くの時間を必要とするスキルの 1 つです。しかし、すべてを兼ね備えた開発者になるためには不可欠なスキルです。ただし、デバッグのエキスパート数人のスキルを詰め込んだデバッガーを用意すれば、どのようなスキル レベルのデバッグ エンジニアであっても、きわめて複雑なデバッグ ロジックを簡単なコマンドのように実行できるようになります。

クラッシュの根本的原因を掴むトラブルシューティング手法は、数え切れないほどたくさんありますが、最も有益、かつデバッグ スキルを持ったエンジニアにとって最も実りが多いのが、プロセス ダンプです。プロセス ダンプには、ダンプをキャプチャしたときのプロセス メモリのスナップショットが含まれています。ダンプ ツールによっては、対象となるプロセス メモリがアドレス空間全体だったり、サブセットにすぎなかったりします。

Windows は、アプリケーションがハンドルされない例外をスローすると、Windows エラー報告 (WER) を通じてミニダンプを自動的に作成します。また、Userdump.exe ツールを使用すれば、ダンプ ファイルを手動で作成することもできます。Sysinternals のツール ProcDump (technet.microsoft.com/ja-jp/sysinternals/dd996900) は、さまざまなトリガーに基づいてダンプをキャプチャしたり、さまざまなサイズのダンプを生成したりできるため、マイクロソフト サポート部門が好んで使用するプロセス ダンプ ツールになりつつあります。とは言え、ダンプ データがあったとしても、どのようにデバッグに役立てるかが問題です。

さまざまなバージョンの Visual Studio でダンプ ファイル (.dmp) を開くことができますが、最適なツールは Windows 用デバッグ ツールのデバッガーです。これらのツールはどれも、2 つのデバッガー拡張 API をサポートする 1 つのデバッグ エンジンに基づいています。この記事では、このようなダンプ ファイル (および運用システム) を簡単に分析できるようになるために、カスタム デバッガー拡張機能をビルドするうえでの基礎について説明します。

ツールのセットアップ

Windows 用デバッグ ツール (msdn.microsoft.com/ja-jp/windows/hardware/gg463009.aspx) は、Windows SDK および Windows Driver Kit (WDK) の、インストール可能かつ再頒布可能なコンポーネントです。この記事の執筆時点の最新バージョンは 6.12 で、Windows SDK または WDK のバージョン 7.01 で利用できます。強化されたスタック ウォークなど、多くの有益な機能がデバッグ エンジンに追加されているため、最新バージョンを使用することをお勧めします。

Windows 用デバッグ ツールのガイドラインには、WDK ビルド環境を使用してデバッガーの拡張機能をコンパイルする必要があると記載されています。ここでは WDK の最新リリース (バージョン 7.1.0 ビルド 7600.16385.1) を使用していますが、古いバージョンの WDK や、WDK の前身であるドライバー開発キット (DDK) でも十分です。WDK を使用して拡張機能をビルドする際は、x64 Free Build Environment および x86 Free Build Environment を使用します。

また、少し労力をかければ、今回のプロジェクトを Windows SDK ビルド環境または Visual Studio でビルドするように適合させることもできます。

1 つ注意することは、WDK ではパス名に空白文字を使用しないようにすることです。つまり、途切れていないパスからコンパイルするようにします。たとえば、C:\Users\Andrew Richards\Documents\Projects ではなく C:\Projects を使用します。

どのような方法で拡張機能をビルドするにしても、デバッグ ツール SDK (Windows 用デバッグ ツールのコンポーネント) のヘッダー ファイルとライブラリ ファイルが必要です。今回の例では、ヘッダー ファイルとライブラリ ファイルを参照する際に x86 パス (C:\debuggers_x86\sdk) を使っています。別の場所にデバッガーをインストールする場合は、このパスを更新してください。パス名にスペースを含めるときは引用符で囲むことを忘れないでください。

デバッグ ツールを使用する

Windows 用デバッグ ツールのデバッガーは、アーキテクチャにとらわれません。どのエディションのデバッガーも、すべてのターゲット アーキテクチャをデバッグできます。たとえば、x86 アプリケーションのデバッグに x64 デバッガーを使用するのは一般的に行われています。デバッガーは、x86、x64 (amd64)、および IA64 用がリリースされていますが、それ以外にも、ARM、EBC、および PowerPC (Xbox) の各アプリケーションもデバッグできます。あらゆるエディションのデバッガーを、並列にインストールできます。

この適応性はすべての状況に当てはまるとは考えないでください。デバッガー エンジンとは異なり、すべてのデバッガー拡張機能がターゲット アーキテクチャに適合するとは限りません。ターゲット アーキテクチャのポインターのサイズと、デバッガーのポインターのサイズが同じであると想定するデバッガー拡張機能もあります。同様に、$csp などの擬似レジスタではなく、不適切なレジスタ (たとえば、rsp ではなく esp) がハードコードされて使用されているものもあります。

デバッガー拡張機能で問題が生じる場合は、ターゲット環境と同じアーキテクチャ用に設計されているデバッガーを実行することをお勧めします。これにより、不適切に作成された拡張機能によって誤った想定が行われるという問題に対処できます。

各アプリケーションのビルドの種類と、関連するプロセッサのアーキテクチャには、デバッグに関する固有の問題があります。デバッグ ビルド用に生成されるアセンブラーは比較的線型ですが、リリース ビルド用に生成されるアセンブラーは最適化されていて、言うなれば 1 皿のスパゲッティのようです。x86 アーキテクチャでは、フレーム ポインターの省略 (FPO) が、コール スタックの再構成によって大惨事をもたらします (最新のデバッガーは、これを適切に処理します)。x64 アーキテクチャでは、関数パラメーターとローカル変数がレジスタに格納されます。ダンプをキャプチャした時点では、こうしたレジスタがスタックにプッシュされていることもあれば、再利用されて既に存在しなくなっていることもあります。

ここで鍵となるのが経験です。正確に言うと、ここでは 1 人の経験が鍵になります。それ以外の人たちのために、デバッガー拡張機能にその 1 人のノウハウを詰め込むだけでよいのです。そのためには、デバッガー拡張機能として自動化する前に、同じデバッグ シーケンスを数回繰り返すだけです。私は、基礎となるデバッグ コマンドを使用して同じことを何回実行したか忘れるくらい、一部の拡張機能を何回も使用しました。

デバッガー API を使用する

デバッガー拡張 API は 2 つあり、1 つは非推奨の WdbgExts API (wdbgexts.h) 、もう 1 つは最新の DbgEng API (dbgeng.h) です。

WdbgExts 拡張機能は、初期化時に構成されるグローバル呼び出し (WinDbgExtensionDllInit) に基づきます。

WINDBG_EXTENSION_APIS ExtensionApis;

グローバル呼び出しは、名前空間を指定しないで dprintf("\n") や GetExpression("@$csp") などの関数を実行するのに必要な機能を提供します。この拡張機能の形式は、Win32 プログラミングで記述するコードと似ています。

DbgEng 拡張機能は、デバッガー インターフェイスに基づきます。IDebugClient インターフェイスは、各呼び出しのパラメーターとしてデバッグ エンジンから渡されます。このインターフェイスは、絶えず範囲が広がるデバッガー インターフェイスにアクセスするため、QueryInterface をサポートします。この拡張機能の形式は、COM プログラミングで記述するコードと似ています。

こうした拡張機能の形式を組み合わせることも可能です。拡張機能は DbgEng として公開しますが、IDebugControl::GetWindbgExtensionApis64 への呼び出しを使用して、実行時に WdbgExts API の機能を追加します。ここでは一例を示すために、古典的な "Hello World" を DbgEng 拡張機能として、 C で記述しました。C++ で記述する場合は、デバッグ ツール SDK (.\inc\engextcpp.cpp) の ExtException クラスを参照します。

拡張機能を MyExt.dll (図 1 のソース ファイルの TARGETNAME) としてコンパイルします。これは !helloworld というコマンドを公開します。拡張機能は、Microsoft Visual C ランタイム (MSVCRT) に動的にリンクします。静的にリンクするのであれば、ソース ファイルの USE_MSVCRT=1 ステートメントを USE_LIBCMT=1 に変えます。

図 1 ソース

TARGETNAME=MyExt
TARGETTYPE=DYNLINK

_NT_TARGET_VERSION=$(_NT_TARGET_VERSION_WINXP)

DLLENTRY=_DllMainCRTStartup

!if "$(DBGSDK_INC_PATH)" != ""
INCLUDES = $(DBGSDK_INC_PATH);$(INCLUDES)
!endif
!if "$(DBGSDK_LIB_PATH)" == ""
DBGSDK_LIB_PATH = $(SDK_LIB_PATH)
!else
DBGSDK_LIB_PATH = $(DBGSDK_LIB_PATH)\$(TARGET_DIRECTORY)
!endif

TARGETLIBS=$(SDK_LIB_PATH)\kernel32.lib \
           $(DBGSDK_LIB_PATH)\dbgeng.lib

USE_MSVCRT=1

UMTYPE=windows

MSC_WARNING_LEVEL = /W4 /WX

SOURCES= dbgexts.rc      \
         dbgexts.cpp     \
         myext.cpp

DebugExtensionInitialize 関数 (図 2 参照) は、拡張機能が読み込まれるときに呼び出されます。Version パラメーターを設定するには、ヘッダー ファイルに追加してある、#define で定義した EXT_MAJOR_VER および EXT_MINOR_VER を指定して、DEBUG_EXTENSION_VERSION マクロを使用するだけです。

// dbgexts.h

#include <windows.h>
#include <dbgeng.h>

#define EXT_MAJOR_VER  1
#define EXT_MINOR_VER  0

図 2 dbgexts.cpp

// dbgexts.cpp

#include "dbgexts.h"

extern "C" HRESULT CALLBACK
DebugExtensionInitialize(PULONG Version, PULONG Flags) {
  *Version = DEBUG_EXTENSION_VERSION(EXT_MAJOR_VER, EXT_MINOR_VER);
  *Flags = 0;  // Reserved for future use.
  return S_OK;
}

extern "C" void CALLBACK
DebugExtensionNotify(ULONG Notify, ULONG64 Argument) {
  UNREFERENCED_PARAMETER(Argument);
  switch (Notify) {
    // A debugging session is active. The session may not necessarily be suspended.
    case DEBUG_NOTIFY_SESSION_ACTIVE:
      break;
    // No debugging session is active.
    case DEBUG_NOTIFY_SESSION_INACTIVE:
      break;
    // The debugging session has suspended and is now accessible.
    case DEBUG_NOTIFY_SESSION_ACCESSIBLE:
      break;
    // The debugging session has started running and is now inaccessible.
    case DEBUG_NOTIFY_SESSION_INACCESSIBLE:
      break;
  }
  return;
}

extern "C" void CALLBACK
DebugExtensionUninitialize(void) {
  return;
}

Version 値は、デバッガーの .chain コマンドで API バージョンとして報告されます。ファイルのバージョン、ファイルの説明、著作権などの値を変更するには、dbgexts.rc ファイルを編集する必要があります。

myext.dll: image 6.1.7600.16385, API 1.0.0, built Wed Oct 13 20:25:10 2010
  [path: C:\Debuggers_x86\myext.dll]

Flags パラメーターは予約されており、0 に設定します。また、関数は S_OK を返す必要があります。

セッションのアクティブ状態やアクセス可能状態が変化するときは、DebugExtensionNotify 関数を呼び出します。未使用のパラメーターについてコンパイラから警告されないようにするには、Argument パラメーターを UNREFERENCED_PARAMETER マクロでラップします。

完全を期すために Notify パラメーターに switch ステートメントを追加していますが、この領域にはまだ関数コードを何も追加していません。switch ステートメントでは、次の 4 つのセッション状態変化を処理します。

  • DEBUG_NOTIFY_SESSION_ACTIVE: ターゲットにアタッチすると発生します。
  • DEBUG_NOTIFY_SESSION_INACTIVE: (detach または qd によって) ターゲットがデタッチされると発生します。
  • DE-BUG_NOTIFY_SESSION_ACCESSIBLE: (ブレークポイントにヒットするなどして) ターゲットが中断すると、関数に渡されます。
  • DEBUG_NOTIFY_SESSION_INACCESSIBLE: ターゲットが実行を再開したら、関数に渡されます。

DebugExtensionUninitialize 関数は、拡張機能がアンロードされるときに呼び出されます。

公開する拡張機能の各コマンドは、PDEBUG_EXTENSION_CALL 型の関数として宣言します。関数名を拡張コマンド名にします。ここでは "Hello World" を作成しているため、関数に helloworld という名前を付けました (図 3 参照)。

図 3 MyExt.cpp

// MyExt.cpp

#include "dbgexts.h"

HRESULT CALLBACK 
helloworld(PDEBUG_CLIENT pDebugClient, PCSTR args) {
  UNREFERENCED_PARAMETER(args);

  IDebugControl* pDebugControl;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), 
    (void **)&pDebugControl))) {
    pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "Hello World!\n");
    pDebugControl->Release();
  }
  return S_OK;
}

関数名には小文字を使用するという名前付け規則に注意してください。WDK ビルド環境を使用しているため、MyExt.def ファイルも変更する必要があります。拡張機能コマンドの名前は、エクスポートのために追加する必要があります。

;-------------
;   MyExt.def
;-------------
EXPORTS
  helloworld
  DebugExtensionNotify
  DebugExtensionInitialize
  DebugExtensionUninitialize

args パラメーターには、コマンドの引数の文字列が含まれています。パラメーターは、null で終わる ANSI 文字列 (CP_ACP) として渡します。

pDebugClient パラメーターは、拡張機能からデバッグ エンジンを操作できるようにする IDebugClient インターフェイス ポインターです。インターフェイス ポインターは COM インターフェイス ポインターのように見えても、マーシャリングは不可能で、後からアクセスすることもできません。また、他のスレッドから使用することもできません。別のスレッドで機能させるためには、IDebugClient::CreateClient を使用して、新しいデバッガー クライアント (IDebugClient への新しいインターフェイス ポインター) をそのスレッド上で作成する必要があります。これは、別のスレッドで実行できる唯一の関数です。

IDebugClient インターフェイスは、(すべてのインターフェイスと同様に) IUnknown から派生します。新しいバージョンの IDebugClient インターフェイス (IDebugClient4) でも、異なるインターフェイス (IDebugControl、IDebugRegisters、IDebugSymbols、IDebugSystemObjects など) でも、別の DbgEng インターフェイスにアクセスするには、QueryInterface を使用します。テキストをデバッガーに出力するには、IDebugControl インターフェイスが必要です。

フォルダーには、開発をサポートする目的で、SDK 以外のファイルが 2 つあります。make.cmd スクリプトは、WDK ビルド環境にデバッガー SDK の inc パスと lib パスを追加してから、適切なビルド コマンドを実行します。

@echo off
set DBGSDK_INC_PATH=C:\Debuggers_x86\sdk\inc
set DBGSDK_LIB_PATH=C:\Debuggers_x86\sdk\lib
set DBGLIB_LIB_PATH=C:\Debuggers_x86\sdk\lib
build -cZMg %1 %2

x86 または x64 バイナリのどちらをビルドするかは、WDK ビルド環境自体によって決まります。複数のアーキテクチャ用にビルドを行う場合は、複数のプロンプトを開き、それぞれのコマンド プロンプトで make.cmd を実行する必要があります。ビルドは同時に実行できます。

ビルドしたら、コンパイルされた i386 バイナリを x86 デバッガー フォルダー (c:\Debuggers_x86) にコピーするために (x86 の) test.cmd スクリプトを使用して、アタッチするデバッガーと読み込む拡張機能を指定してメモ帳のインスタンスを起動します。

@echo off
copy objfre_win7_x86\i386\myext.dll c:\Debuggers_x86
copy objfre_win7_x86\i386\myext.pdb c:\Debuggers_x86
\Debuggers_x86\windbg.exe -a myext.dll -x notepad

すべてが計画通りに進んだら、デバッガーのコマンド プロンプトに「!helloworld」と入力すると、次のように "Hello World!" という応答が返ってきます。

0:000> !helloworld
Hello World!

シンボルの解決と読み取り

この "Hello World" アプリケーションもたいしたものですが、これ以上のことを実行できます。これからこのインフラストラクチャを使用して、ターゲットを実際に操作し、いくつか分析を行うのに役立つコマンドを追加します。次に示す簡単な test01 アプリケーションには、値を代入しているグローバル ポインターがあります。

// test01.cpp

#include <windows.h>

void* g_ptr;
int main(int argc, char* argv[]) {
  g_ptr = "This is a global string";
  Sleep(10000);
  return 0;
}

MyExt.cpp の新しい !gptr コマンド (図 4 参照) は、次のように、test01!g_ptr グローバルを解決し、ポインターを読み取り、"x test01!g_ptr" と同じ形式で見つかった値を出力します。

0:000> x test01!g_ptr
012f3370 Test01!g_ptr = 0x012f20e4

0:000> !gptr
012f3370 test01!g_ptr = 0x012f20e4
<string>

図 4 改訂後の MyExt.cpp

HRESULT CALLBACK 
gptr(PDEBUG_CLIENT pDebugClient, PCSTR args) {
  UNREFERENCED_PARAMETER(args);

  IDebugSymbols* pDebugSymbols;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugSymbols), 
    (void **)&pDebugSymbols))) {  
    // Resolve the symbol.
    ULONG64 ulAddress = 0;
    if (SUCCEEDED(pDebugSymbols->GetOffsetByName("test01!g_ptr", &ulAddress))) {
      IDebugDataSpaces* pDebugDataSpaces;
      if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugDataSpaces),
        (void **)&pDebugDataSpaces))) {  
        // Read the value of the pointer from the target address space.
        ULONG64 ulPtr = 0;
        if (SUCCEEDED(pDebugDataSpaces->ReadPointersVirtual(1, ulAddress, &ulPtr))) {
          PDEBUG_CONTROL pDebugControl;
          if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), 
            (void **)&pDebugControl))) {  
            // Output the values.
            pDebugControl->Output(DEBUG_OUTPUT_NORMAL, 
              "%p test01!g_ptr = 0x%p\n", ulAddress, ulPtr);
            pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "%ma\n", ulPtr);
            pDebugControl->Release();
          }
        }
        pDebugDataSpaces->Release();
      }
      pDebugSymbols->Release();
    }
  }
  return S_OK;
}

まず、test01!g_ptr ポインターの場所を決定します。ランダム化されたベース アドレス (ASLR) がモジュールの読み込みアドレスを変更するため、アプリケーションが実行されるたびにポインターが指す場所が変わります。場所を取得するには、QueryInterface を使用して IDebugSymbols インターフェイスを取得してから、GetOffsetByName を使用します。GetOffsetByName 関数は、シンボル名を受け取り、64 ビットのポインターとしてそのアドレスを返します。デバッガー関数は、常に 64 ビットのポインター (ULONG64) を返すため、32 ビットのデバッガーで 64 ビットのターゲットをデバッグできます。

これは、ターゲットのアドレス空間におけるポインターのアドレスで、ユーザー独自のアドレスではないことに注意してください。値を判断するためにそのアドレスから読み取ることはできません。ポインターの値を取得するには、もう一度 QueryInterface を使用して IDebugDataSpaces インターフェイスを取得してから、ReadPointersVirtual を使用します。ReadPointersVirtual は、ターゲットのアドレス空間からポインターを読み取ります。自動的にポインターのサイズとエンディアンの差異が調整されるため、返されたポインターを操作する必要はありません。

IDebugControl::Output は printf と同じ書式指定文字列を受け取りますが、ターゲットのアドレス空間を参照できるようにするフォーマッターもあります。ターゲットのアドレス空間内のグローバル ポインターが指す ANSI 文字列を出力するには、%ma 書式を使用します。%p 書式は、ポインターのサイズを認識するため、ポインターの出力に使用します (ULONG64 を渡す必要があります)。

メモ帳を起動するのではなく、test01 の x86 バージョンのダンプ ファイルを読み込むために、次のようにテスト スクリプトを変更します。

@echo off
copy objfre_win7_x86\i386\myext.dll c:\Debuggers_x86
copy objfre_win7_x86\i386\myext.pdb c:\Debuggers_x86
\Debuggers_x86\windbg.exe -a myext.dll -y "..\Test01\x86;SRV*c:\symbols*http://msdl.microsoft.com/download/symbols" -z ..\Test01\x86\Test01.dmp

また、test01 の x86 フォルダーとマイクロソフトのパブリック シンボル サーバーへのシンボルのパスを設定して、すべてが解決できるようにしました。さらに、x86 テスト スクリプトと同じように機能し、x64 バージョンのテスト アプリケーションのダンプ ファイルを使用する x64 テスト スクリプトも作成しています。

@echo off
copy objfre_win7_x86\i386\myext.dll c:\Debuggers_x86
copy objfre_win7_x86\i386\myext.pdb c:\Debuggers_x86
\Debuggers_x64\windbg.exe -a myext.dll -y "..\Test01\x64;SRV*c:\symbols*http://msdl.microsoft.com/download/symbols" -z ..\Test01\x64\Test01.dmp

スクリプトを実行すると、x86 デバッガーが起動し、適切なダンプ ファイルが開き、x86 バージョンの拡張機能が読み込まれ、シンボルが解決可能になります。

もう一度、すべてが計画通りに進んだら、デバッガーのコマンド プロンプトに「x test01!g_ptr」、「!gptr」と入力すると、同じ応答が返ります。

// x86 Target
0:000> x test01!g_ptr
012f3370 Test01!g_ptr = 0x012f20e4

0:000> !gptr
012f3370 test01!g_ptr = 0x012f20e4
This is a global string

// x64 Target
0:000> x test01!g_ptr
00000001`3fda35d0 Test01!g_ptr = 0x00000001`3fda21a0

0:000> !gptr
000000013fda35d0 test01!g_ptr = 0x000000013fda21a0
This is a global string

x64 デバッガー、amd64 でコンパイルされたバージョンのデバッガー拡張機能、および x86 または x64 のダンプ ファイルを使用してテストを繰り返しても、同じ結果が得られます。つまり、拡張機能はアーキテクチャにとらわれません。

プロセッサの種類とスタック

このインフラストラクチャを、これからもう一度拡張します。現在のスレッド スタック上の Sleep 呼び出しの時間を調べるコマンドを追加してみましょう。!sleepy コマンド (図 5 参照) は、コール スタックのシンボルを解決し、Sleep 関数を探して、遅延するミリ秒を表す DWORD を読み取ったら、遅延値を出力します (見つかった場合)。

図 5 Sleepy

HRESULT CALLBACK 
sleepy(PDEBUG_CLIENT4 Client, PCSTR args) {
  UNREFERENCED_PARAMETER(args);
  BOOL bFound = FALSE;

  IDebugControl* pDebugControl;

  if (SUCCEEDED(Client->QueryInterface(__uuidof(IDebugControl), 
    (void **)&pDebugControl))) {
    IDebugSymbols* pDebugSymbols;

    if (SUCCEEDED(Client->QueryInterface(__uuidof(IDebugSymbols), 
      (void **)&pDebugSymbols))) {
      DEBUG_STACK_FRAME* pDebugStackFrame = 
        (DEBUG_STACK_FRAME*)malloc(
        sizeof(DEBUG_STACK_FRAME) * MAX_STACK_FRAMES);

      if (pDebugStackFrame != NULL) {  
        // Get the Stack Frames.
        memset(pDebugStackFrame, 0, (sizeof(DEBUG_STACK_FRAME) * 
          MAX_STACK_FRAMES));
        ULONG Frames = 0;

        if (SUCCEEDED(pDebugControl->GetStackTrace(0, 0, 0, 
          pDebugStackFrame, MAX_STACK_FRAMES, &Frames)) && 
          (Frames > 0)) {
          ULONG ProcessorType = 0;
          ULONG SymSize = 0;
          char SymName[4096];
          memset(SymName, 0, 4096);
          ULONG64 Displacement = 0;

          if (SUCCEEDED(pDebugControl->GetEffectiveProcessorType(
            &ProcessorType))) {
            for (ULONG n=0; n<Frames; n++) {  

              // Use the Effective Processor Type and the contents 
              // of the frame to determine existence
              if (SUCCEEDED(pDebugSymbols->GetNameByOffset(
                pDebugStackFrame[n].InstructionOffset, SymName, 4096, 
                &SymSize, &Displacement)) && (SymSize > 0)) {

                if ((ProcessorType == IMAGE_FILE_MACHINE_I386) && 
                  (_stricmp(SymName, "KERNELBASE!Sleep") == 0) && 
                  (Displacement == 0xF)) {  
                  // Win7 x86; KERNELBASE!Sleep+0xF is usually in frame 3.
                  IDebugDataSpaces* pDebugDataSpaces;

                  if (SUCCEEDED(Client->QueryInterface(
                    __uuidof(IDebugDataSpaces), 
                    (void **)&pDebugDataSpaces))) {  
                    // The value is pushed immediately prior to 
                    // KERNELBASE!Sleep+0xF
                    DWORD dwMilliseconds = 0;

                    if (SUCCEEDED(pDebugDataSpaces->ReadVirtual(
                      pDebugStackFrame[n].StackOffset, &dwMilliseconds, 
                      sizeof(dwMilliseconds), NULL))) {
                      pDebugControl->Output(DEBUG_OUTPUT_NORMAL, 
                        "Sleeping for %ld msec\n", dwMilliseconds);
                      bFound = TRUE;
                    }
                    pDebugDataSpaces->Release();
                  }
                  if (bFound) break;
                }

                else if ((ProcessorType == IMAGE_FILE_MACHINE_AMD64) && 
                  (_stricmp(SymName, "KERNELBASE!SleepEx") == 0) && 
                  (Displacement == 0xAB)) {  
                  // Win7 x64; KERNELBASE!SleepEx+0xAB is usually in frame 1.
                  IDebugRegisters* pDebugRegisters;

                  if (SUCCEEDED(Client->QueryInterface(
                    __uuidof(IDebugRegisters), 
                    (void **)&pDebugRegisters))) {  
                    // The value is in the 'rsi' register.
                    ULONG rsiIndex = 0;
                    if (SUCCEEDED(pDebugRegisters->GetIndexByName(
                      "rsi", &rsiIndex)))
                    {
                      DEBUG_VALUE debugValue;
                      if (SUCCEEDED(pDebugRegisters->GetValue(
                        rsiIndex, &debugValue)) && 
                        (debugValue.Type == DEBUG_VALUE_INT64)) {  
                        // Truncate to 32bits for display.
                        pDebugControl->Output(DEBUG_OUTPUT_NORMAL, 
                          "Sleeping for %ld msec\n", debugValue.I32);
                        bFound = TRUE;
                      }
                    }
                    pDebugRegisters->Release();
                  }

                  if (bFound) break;
                }
              }
            }
          }
        }
        free(pDebugStackFrame);
      }
      pDebugSymbols->Release();
    }
    if (!bFound)
      pDebugControl->Output(DEBUG_OUTPUT_NORMAL, 
        "Unable to determine if Sleep is present\n");
    pDebugControl->Release();
  }
  return S_OK;
}

コマンドをいくぶん複雑にするため、コマンドが test01 アプリケーションの x86 バージョンと x64 バージョンをサポートするようにします。呼び出し規約は x86 アプリケーションと x64 アプリケーションで異なるため、コマンドは、進行するたびにターゲット アーキテクチャを認識する必要があります。

まず、スタック フレームを取得します。フレームを取得するには、QueryInterface を使用して IDebugControl インターフェイスを取得し、GetStackTrace を使用して各スタック フレームについての情報を取得します。
GetStackTrace は、DEBUG_STACK_FRAME 構造体の配列を受け取ります。ここではスタック オーバーフローが起こらないように、常にヒープ上に DEBUG_STACK_FRAME 構造体の配列を割り当てます。ターゲットのスレッドでスタック オーバーフローが発生しても、配列が独自のスタックに割り当てられていれば、独自のスタック制限に達していることが予想されます。

GetStackTrace が成功したら、配列には取得したフレームごとに情報が設定されます。関数が成功しても、必ずしもフレームの情報が正しいことを意味するわけではありません。デバッガーは、スタック フレームを調べるのに最善を尽くしますが、シンボルが正しくない場合 (見つからないときや、強制されているとき) 間違いが起こります。".reload /f /i" を使用してシンボルの読み込みを強制した場合、誤ったシンボルの配置がおそらく発生します。

各 DEBUG_STACK_FRAME 構造体のコンテンツを効率的に使用するには、ターゲットで有効なプロセッサの種類を知る必要があります。前述のように、ターゲット アーキテクチャは、デバッガー拡張機能のアーキテクチャとはまったく異なることがあります。有効なプロセッサの種類 (.effmach) は、ターゲットが現在使用しているアーキテクチャです。

また、プロセッサの種類が、ターゲットのホストによって使用されているものとは異なる場合もあります。x64 エディションの Windows で、Windows 32-bit on Windows 64-bit (WOW64) によって実行されている x86 アプリケーションがターゲットの場合がこの最も一般的な例です。この場合、有効なプロセッサの種類は IMAGE_FILE_MACHINE_I386 ですが、実際の種類は IMAGE_FILE_MACHINE_AMD64 です。

つまり、x86 エディションの Windows で実行されていても、x64 エディションの Windows で実行されていても、x86 アプリケーションは x86 アプリケーションであると考えなくてはなりません (この規則の唯一の例外は、x86 プロセスを隠蔽する WOW64 呼び出しをデバッグするときです)。

有効なプロセッサの種類を取得するには、既にある IDebugControl インターフェイスを使用してから、GetEffectiveProcessor-Type を使用します。

有効なプロセッサの種類が i386 であれば、KERNELBASE!Sleep+0xf 関数を探す必要があります。すべてのシンボルが正しく解決されれば、関数はフレーム 3 に存在します。

0:000> knL4
 # ChildEBP RetAddr  
00 001bf9dc 76fd48b4 ntdll!KiFastSystemCallRet
01 001bf9e0 752c1876 ntdll!NtDelayExecution+0xc
02 001bfa48 752c1818 KERNELBASE!SleepEx+0x65
03 001bfa58 012f1015 KERNELBASE!Sleep+0xf

有効なプロセッサの種類が AMD64 であれば、KERNELBASE!SleepEx+0xab 関数を探します。すべてのシンボルが正しく解決されれば、関数はフレーム 1 に存在します。

0:000> knL2
 # Child-SP          RetAddr           Call Site
00 00000000'001cfc08 000007fe'fd9b1203 ntdll!NtDelayExecution+0xa
01 00000000'001cfc10 00000001'3fda101d KERNELBASE!SleepEx+0xab

ただし、使用できるシンボル解決のレベルによっては、探している関数のシンボルが予想されるフレームにあったりなかったりすることがあります。シンボル パスを指定しないで test01 の x86 のダンプ ファイルを開けば、この例を確認できます。KERNEL-BASE!Sleep 呼び出しは、フレーム 1 ではなくフレーム 3 に存在します。

0:000> knL4
 # ChildEBP RetAddr  
WARNING: Stack unwind information not available. Following frames may be wrong.
00 001bfa48 752c1818 ntdll!KiFastSystemCallRet
01 001bfa58 012f1015 KERNELBASE!Sleep+0xf
02 001bfaa4 75baf4e8 Test01+0x1015
03 001bfab0 76feaf77 kernel32!BaseThreadInitThunk+0x12

デバッガーは、この起こり得る失敗について警告します。拡張機能をこうした種類の問題に適合させるには、予想されるフレームを単に見るのではなく、フレームを反復処理します。

Sleep 関数の存在を判断するには、フレームごとにシンボルを探します。有効なプロセッサの種類とシンボルが有効なペアになっていれば、Sleep 関数が見つかったことになります。このロジックは危ういロジックで、例を簡略化するために使用していることに注意してください。シンボルは、ビルドやプラットフォームごとに変わる場合があります。たとえば、Windows Server 2008 は kernel32!Sleep+0xf ですが、Windows 7 は KERNELBASE!Sleep+0xf です。

シンボルを取得するには、QueryInterface を使用して IDebugSymbol インターフェイスを取得します。その後、GetNameByOffset を使用して、命令のオフセット アドレスのシンボルを取得します。

シンボルには、シンボル名 (KERNELBASE!Sleep) と変位 (0xf) の 2 つの部分があります。シンボル名は、モジュール名と関数名が一緒になったものです (<モジュール>!<関数>)。変位は、呼び出しから戻った後にプログラム フローを戻す位置を、関数の先頭からのバイト オフセットで示した値です。

シンボルがない場合、関数は、大きな変位 (Test01+0x1015) が設定され、モジュール名だけが報告されます。

フレームを見つけたら、次の手順は遅延を抽出することです。ターゲットが x86 ベースの場合、関数呼び出しの直前にスタックにプッシュされる DWORD に遅延の値があります (これはもろいロジックであることを留意してください)。

// @$csp is the pseudo-register of @esp
0:000> dps @$csp
<snip>
001bfa4c  752c1818 KERNELBASE!Sleep+0xf
001bfa50  00002710
<snip>

DEBUG_STACK_FRAME 構造体の StackOffset メンバーは、実際にはこのアドレスを既に指しているため、ポインター演算は必要ありません。値を取得するには、QueryInterface を使用して IDebugDataSpaces インターフェイスを取得してから、ReadVirtual を使用してターゲットのアドレス空間から DWORD を読み取ります。

ターゲットが x64 ベースの場合、遅延の値はスタックではなく rsi レジスタに含まれています (フレームとコンテキストに依存関係があるため、これも危ういロジックです)。

0:000> r @rsi
rsi=0000000000002710

値を取得するには、QueryInterface を使用して IDebugRegisters インターフェイスを取得します。まず、rsi レジスタのインデックスを取得するには、GetIndexByName を使用する必要があります。その後、GetValue を使用して、対象のレジスタからレジスタ値を読み取ります。rsi は 64 ビットのレジスタのため、値は INT64 として返されます。DEBUG_VALUE 構造体は共用体なので、I64 メンバーではなく I32 メンバーを参照するだけで、Sleep に渡された DWORD を表す、切り捨てられたバージョンを取得できます。

もう一度、両方の場合において、結果を出力するために IDebugControl::Output 関数を使用します。

ブレーク

ここでは、何が実現できるかにほんの少しだけ触れました。拡張機能内で調査および変更できるものは、スタック、シンボル、レジスタ、メモリ I/O、および環境の情報以外にたくさんあります。

今後の記事では、デバッガー拡張機能とデバッガーの関係について深く掘り下げる予定です。皆さんに基盤となる .NET 構造の知識がなくても、マネージ アプリケーションをデバッグできる拡張機能を作成できるように、デバッガー クライアントとデバッガーのコールバックについて説明し、それらを使用して SOS デバッガー拡張機能をカプセル化するつもりです。

Andrew Richards は、マイクロソフトの Exchange Server シニア エスカレーション エンジニアです。ツールをサポートすることに情熱を燃やしていて、エンジニアをサポートする作業を簡略化する、デバッガー拡張機能やアプリケーションを作成し続けています。

この記事のレビューに協力してくれた技術スタッフの Drew BlissJen-Lung ChiuMike HendricksonKen JohnsonBrunda Nagalingaiah、および Matt Weber に心より感謝いたします。