以下の内容はhttps://engineers.ffri.jp/entry/2026/03/30/000000より取得しました。


WinDbg 拡張機能の作り方 1 ~ コマンド拡張編

はじめに

基礎技術研究部の末吉です。

WinDbg は Windows アプリケーションやカーネルのデバッグ、ダンプ解析等で有用なツールです。 しかし、標準で用意されているコマンドだけではいくつか不足している機能があります。

そこで、本シリーズでは全 3 編に分けて WinDbg 拡張機能 (WinDbg Extension) の作り方を紹介します。 今回はコマンド拡張機能の実装を数通り紹介します。

結論を言えば、簡易的なコマンド拡張であれば EngExtCpp をベースに実装するのが一番簡単でしょう。

なお、WinDbg や WinDbg Script, および一部を除く WinDbg とは直接関係のない用語等については説明しません。 実際に作りたい方は必要に応じて検索してください。

今回実装した拡張機能は GitHub にて公開しています。 なお、今後の記事で説明する予定の拡張機能のソースコードも既に公開しています。予習されたい方はご覧ください。

拡張機能を実装する理由

WinDbg は WinDbg Script で柔軟に機能を拡張できますが、いくつか不足している機能もあります。 WinDbg Script でなく拡張機能として実装する主な理由は以下が挙げられます。

  • 高速化
  • 大規模な拡張開発
  • 各言語のモジュールの利用
    • 例えば、Rust で実装すれば Rust のクレートを使用可能
  • コールバックの利用
    • メモリが変化したら特定の処理を行うなど
  • UI 拡張

例えば、今回実装するコマンド拡張程度であれば WinDbg Script でも事足りますが、次回紹介する UI 拡張は WinDbg Script では作れません。

事前準備

本記事で使用したツールのバージョンは以下の通りです。

  • Windows 11
  • WinDbg 1.2601.12001.0
  • Windows SDK 10.0.26100.7627
  • Visual Studio 2026 18.3.2

拡張機能の作成

拡張機能は DLL で実現できます。 基本的には必須の関数とコマンド用の関数をエクスポートした DLL を作るだけで動作します。

拡張機能を実装するための公式 API として、WdbgExtsDbgEng があります。 WdbgExts は昔からある C API です。 DbgEng は COM API であり、今でも更新され続けています。 DbgEng は WdbgExts よりも多機能であるため、特別な理由がなければ拡張機能の新規作成には DbgEng をおすすめします。

本記事では、レジスタから値を取得し、値が文字列へのポインタらしければ文字列として表示する regstr コマンド拡張機能を実装します。 コマンド拡張は WdbgExts, DbgEng, EngExtCpp, dbgeng-rs を使用して C, C++, Rust で実装し、それぞれの特徴を説明します。 なお、今回実装する拡張機能は簡単化のため x64 のみを対象とします。

完成形は以下の通りです。

regstr コマンド

WdbgExts による実装

まずは、WdbgExts による拡張機能を C 言語で実装していきます。 Visual Studio を起動し、「ダイナミックリンクライブラリ (DLL)」のプロジェクトを作成してください。 なお、本記事ではプロジェクト名を regstr_c としています。

WdbgExts は C API です。 COM API である DbgEng よりは比較的簡単に扱えますが、機能が貧弱で高度なことはできません。 本記事では後述の DbgEng と比較するために記載していますが、実用はおすすめしません。

必須処理の実装

まず、WdbgExts のヘッダーファイル WDBGEXTS.H をインクルードします。 Windows SDK をインストールしていれば、特別な対応無しにインクルードできるはずです。

今回は 64 ビット版の拡張機能を作成します。 この時 WDBGEXTS.H のインクルード前に必ず KDEXT_64BIT を定義してください。のちに使用する DECLARE_API 等のマクロに関係します。

さらに、 Windows.h のインクルードおよび表示する最大バイト数を示す定数 N, 表示するレジスタの一覧 REG_NAMES も定義しておきます。 ここでは、x64 の汎用レジスタのみ対象とします。

#define KDEXT_64BIT // 64 ビット版であることを明示

#include <Windows.h>
#include <WDBGEXTS.H>

#define N 32 // 最大バイト数

// 表示するレジスタ
const char* const REG_NAMES[] = {
    "rax",
    "rbx",
    "rcx",
    "rdx",
    "rsi",
    "rdi",
    "rbp",
    "rsp",
    "r8",
    "r9",
    "r10",
    "r11",
    "r12",
    "r13",
    "r14",
    "r15",
};

WdbgExts では必須の関数として、ExtensionApiVersion および WinDbgExtensionDllInit を実装・エクスポートする必要があります。 これらを実装していきましょう。

// APIバージョン情報
EXT_API_VERSION ApiVersion = {
    1,                        // MajorVersion (拡張機能のメジャーバージョン)
    0,                        // MinorVersion (拡張機能のマイナーバージョン)
    EXT_API_VERSION_NUMBER64, // Revision     (リビジョン)
    0                         // Reserved     (予約)
};

WINDBG_EXTENSION_APIS ExtensionApis;

// 使用するAPIバージョン情報を返す関数
LPEXT_API_VERSION ExtensionApiVersion(void) {
    return &ApiVersion;
}

まず、EXT_API_VERSION 型のグローバル変数を用意します。

MajorVersionMinorVersion は自作した拡張機能自身のバージョン情報を入れます。 お好みのバージョン番号を入れて構いません。

Revision には拡張機能が使用する WinDbg の API バージョンを入れます。 64 ビットポインタを使用している場合は EXT_API_VERSION_NUMBER64, 32 ビットポインタの場合は EXT_API_VERSION_NUMBER32 を指定します。 ただし、大は小を兼ねるため常に 64 ビットポインタを使用することが推奨されています。 32 ビット版でのみデバッグできる拡張機能を作りたい場合に限り 32 を使用してください。

Reserved は名前の通り予約です。0 にしておきましょう。

次は WinDbgExtensionDllInit を実装します。 この関数の定義時に、WINDBG_EXTENSION_APIS 型のグローバル変数 ExtensionApis も必須なため、併せて用意します。 この変数は WDBGEXTS.H から extern で取り込まれるため、同じ名前で宣言する必要があります。

WINDBG_EXTENSION_APIS ExtensionApis;

// 拡張機能の初期化用関数
VOID WinDbgExtensionDllInit(
    PWINDBG_EXTENSION_APIS lpExtensionApis,
    USHORT MajorVersion,
    USHORT MinorVersion
) {
    UNREFERENCED_PARAMETER(MajorVersion);
    UNREFERENCED_PARAMETER(MinorVersion);
    ExtensionApis = *lpExtensionApis;
}

MajorVersionMinorVersion には Windows のバージョンとビルド番号が入っています。 必要であればグローバル変数にコピーして使用します。*1

lpExtensionApis は、WinDbg の API 関数のポインタが入っています。 WdbgExts は ExtensionApis 経由でこの API を呼び出すため、これを必ず ExtensionApis にコピーしなければなりません。

ここまでがテンプレートです。 この時点で既に拡張機能として動作します。まだ役には立ちませんが。

help コマンド関数の実装

次はコマンド関数を実装します。 コマンド関数は DECLARE_API マクロを使用して実装します。 試しに help コマンドを実装します。

DECLARE_API(help)
{
    UNREFERENCED_PARAMETER(hCurrentProcess);
    UNREFERENCED_PARAMETER(hCurrentThread);
    UNREFERENCED_PARAMETER(dwCurrentPc);
    UNREFERENCED_PARAMETER(dwProcessor);
    UNREFERENCED_PARAMETER(args);
    dprintf("regstr_c help!\n");
}

DECLARE_API マクロは引数の名前を持つ関数を作るマクロです。 KDEXT_64BIT を定義していれば DECLARE_API64 が使用されます。 定義していない場合、64 ビット版ビルドでも 32 ビット版である DECLARE_API32 が使用されます。 DECLARE_API32 の場合、dwCurrentPcULONG として扱われてしまい、64 ビット版のターゲット (デバッギー) に対して本来の値をとれなくなります。

#if   defined(KDEXT_64BIT)
(...省略...)
#define DECLARE_API(s) DECLARE_API64(s)
#endif

(...省略...)

#define DECLARE_API64(s)                           \
    CPPMOD VOID                                    \
    s(                                             \
        HANDLE                 hCurrentProcess,    \
        HANDLE                 hCurrentThread,     \
        ULONG64                dwCurrentPc,        \
        ULONG                  dwProcessor,        \
        PCSTR                  args                \
     )

引数にある hCurrentProcess, hCurrentThread, dwCurrentPc, dwProcessor はターゲットのプロセス・スレッドハンドル、現在の命令ポインタの値、現在のプロセッサのインデックスです。 ハンドルの用途ですが、例えばターゲットのプロセス ID を取得したい場合、WdbgExts にはそのような機能はないため WinAPI の GetProcessId を用いて GetProcessId(hCurrentProcess) のようにして取得する必要があります。 args はコマンドに与えられたコマンドライン引数です。!regstr_c.help aaa bbb ccc とコマンドを入力すれば、args には aaa bbb ccc が入ります。引数がない場合は空文字列が入ります。 引数のパースは自分で実装する必要があります。 今回、これらは使用しないため、UNREFERENCED_PARAMETER で消しておきます。

dprintf マクロ*2で WinDbg のコマンドウィンドウに出力できます。 なお、dprintf では DML を使用できません。

動作確認 (help コマンド)

さて、これで一通り実装できました。 ここで一旦動作確認してみましょう。

その前にモジュール定義ファイルを作成し、エクスポートする関数を指定します。 エクスポート対象は必須の ExtensionApiVersion, WinDbgExtensionDllInit およびコマンド関数である help の 3 つです。

LIBRARY regstr_c
EXPORTS
    ExtensionApiVersion
    WinDbgExtensionDllInit
    help

次は DLL をビルドしますが、DLL はアーキテクチャをターゲットと合わせる必要があります。x64 ターゲットをデバッグする拡張機能は x64 DLL としてビルドする必要があります。 同様に x86 用は x86 DLL としてビルドします。 アーキテクチャに x64 を指定したら DLL をビルドし、WinDbg を起動して適当な x64 アプリケーションをデバッグしましょう。

まずは .load コマンドで DLL の絶対パスを指定して読み込みます。 読み込んだ拡張機能は !DLL名.コマンド名 でコマンドを呼び出せます。単に !コマンド名 とすると、そのコマンドを持つ DLL が検索され、最初に見つかった DLL の当該コマンドが呼び出されます。 regstr_c.dll の help コマンドを呼び出したいわけですが、!help だけでは他の拡張機能と競合するため、DLL 名を明示的に指定した !regstr_c.help を実行します。 すると、dprintf に指定した文言が表示されるはずです。

help コマンドの動作確認 (WdbgExts 版)

拡張機能のバージョンも確認しておきましょう。 バージョンは .chain コマンドで表示できます。 API 1.0.6 という箇所がバージョンです。<MajorVersion>.<MinorVersion>.<Revision> の形でバージョンが表示されます。

バージョンの確認

regstr コマンド関数の実装

では、本題の regstr コマンドを実装していきましょう。

void ShowRegStr(PCSTR RegName)
{
    char fmt[8] = "@";
    strcat_s(fmt, _countof(fmt), RegName);
    // "@rax" 等を式評価
    const ULONG64 RegValue = GetExpression(fmt);

    char buf[N] = { 0 };
    ULONG nr = 0;
    // 読み取りに失敗した場合も続行
    ReadMemory(RegValue, buf, N, &nr);

    // bufを文字列に変換する
    const PSTR s = MakeString(buf, N);
    if (!s)
    {
        dprintf("Insufficient memory\n");
        return;
    }
    if (s[0] == '\0')
    {
        dprintf("%-3s = 0x%016I64x\n", RegName, RegValue);
    }
    else
    {
        // 先頭1文字でも正当な文字であれば表示する
        dprintf("%-3s = 0x%016I64x \"%s\"\n", RegName, RegValue, s);
    }
    free(s);
}

DECLARE_API(regstr)
{
    UNREFERENCED_PARAMETER(hCurrentProcess);
    UNREFERENCED_PARAMETER(hCurrentThread);
    UNREFERENCED_PARAMETER(dwCurrentPc);
    UNREFERENCED_PARAMETER(dwProcessor);
    UNREFERENCED_PARAMETER(args);

    for (size_t i = 0; i < _countof(REG_NAMES); i++) ShowRegStr(REG_NAMES[i]);
}

help コマンドの時と同様に DECLARE_API で regstr コマンドを用意します。 取り立てて説明することはありません。

次に、レジスタ名に対応する値と文字列を表示する ShowRegStr 関数を実装します。 WdbgExts にはレジスタ名からレジスタ値を取得する API がありません。 そのため、WinDbg の式評価値を返す GetExpression マクロで値を取得します。 @rax のような値を渡して、シンボル名でないことを明示して評価させます。

なお、式評価でない方法だと GetContext でも取得できます。

// 別の方法
void ShowRegStr(PCSTR RegName, const ULONG dwProcessor)
{
    // コンテキストから取る方法もある (が、レジスタ名で検索する場合は面倒)
    CONTEXT ctx;
    GetContext(0, &ctx, sizeof(ctx));

    ULONG64 RegValue;
    if (!strcmp(RegName, "rax")) RegValue = ctx.Rax;
    else if (!strcmp(RegName, "rbx")) RegValue = ctx.Rbx;
    else if (!strcmp(RegName, "rcx")) RegValue = ctx.Rcx;
    else if (!strcmp(RegName, "rdx")) RegValue = ctx.Rdx;
    else if (!strcmp(RegName, "rdi")) RegValue = ctx.Rdi;
    else if (!strcmp(RegName, "rsi")) RegValue = ctx.Rsi;
    else if (!strcmp(RegName, "r8")) RegValue = ctx.R8;
    else if (!strcmp(RegName, "r9")) RegValue = ctx.R9;
    else if (!strcmp(RegName, "r10")) RegValue = ctx.R10;
    else if (!strcmp(RegName, "r11")) RegValue = ctx.R11;
    else if (!strcmp(RegName, "r12")) RegValue = ctx.R12;
    else if (!strcmp(RegName, "r13")) RegValue = ctx.R13;
    else if (!strcmp(RegName, "r14")) RegValue = ctx.R14;
    else if (!strcmp(RegName, "r15")) RegValue = ctx.R15;
    else return;

    (...省略...)
}

値を取得できたら、それをアドレスとみなして ReadMemory でメモリを読み取り、buf にコピーします。 メモリを読み取れたかどうかは返り値で判定できますが、今回は読み取れなくても (すなわち、正当なアドレスでなくても) 関係ないため、返り値は無視しています。

ここまでで必要な値を取得できました。 次に読み取ったメモリが文字列らしいかどうかを判定し、文字列らしいデータまでを文字列としてパースして返す MakeString 関数を実装します。 文字列らしさの判定は簡易的に行っています。

// 文字列らしいデータまでを文字列として返す。
PSTR MakeString(PCSTR Buffer, const ULONG BufferLength)
{
    const PSTR ret = (PSTR)malloc((size_t)BufferLength * 2 * sizeof(char) + 4);
    if (!ret) return NULL;
    PSTR it = ret;
    for (ULONG i = 0; i < BufferLength; i++)
    {
        const CHAR ch = Buffer[i];
        // 0x80 以上はASCIIでない
        if ((UCHAR)ch >= 0x80) goto Finish;
        if (ch < 0x20)
        {
            // 0x20 未満はエスケープシーケンス
            // 以下のエスケープシーケンス以外は文字とみなさないこととする
            switch (ch)
            {
            case '\t':
                *it++ = '\\';
                *it++ = 't';
                break;
            case '\n':
                *it++ = '\\';
                *it++ = 'n';
                break;
            case '\v':
                *it++ = '\\';
                *it++ = 'v';
                break;
            case '\r':
                *it++ = '\\';
                *it++ = 'r';
                break;
            default:
                goto Finish;
            }
        }
        else
        {
            if (ch == '"' || ch == '\\') *it++ = '\\';
            *it++ = ch;
        }
    }
    // ここまで来た場合、文字列の途中で切れた可能性があるため、... を付けて続きを示唆しておく。
    *it++ = '.';
    *it++ = '.';
    *it++ = '.';
Finish:
    *it = '\0';
    return ret;
}

動作確認 (regstr コマンド)

ここまで実装できたら、モジュール定義ファイルに regstr を追記してエクスポートされるようにしてビルドします。

LIBRARY regstr_c
EXPORTS
    ExtensionApiVersion
    WinDbgExtensionDllInit
    help
    regstr

ビルドしたら先ほどと同様の手順で動作を確認しましょう。

regstr コマンドの実行 (WdbgExts 版)

WinDbg の組み込みコマンドと照合し、正しい値を取得できていれば完成です。 なお、da コマンドの出力と比べると \\\ になっていますが、これは MakeString でエスケープする仕様にしたためです。

DbgEng による実装

次は DbgEng による拡張機能を C++ で実装します。 プロジェクト名は regstr_cpp としています。

DbgEng はデバッガエンジンを操作するための DLL および COM API です。 dbgeng.h のリファレンスを読むと、DbgEng は IDebugClient, IDebugClient2, IDebugClient3... など、連番でインターフェースが細分化されています*3。 一番大きい数字のインターフェースが一番新しいものであり、下のインターフェースをすべて継承しています。 古いバージョンの DbgEng を考慮しないのであれば、一番大きい数字のインターフェースを使用してください。

本記事執筆時点における DbgEng のインターフェース一覧を以下に示します。 最新版の数値を確認したい場合は、各々でインストールされている dbgeng.h の DEFINE_GUID(...) の行を読んでください。

インターフェース名 最新版 機能
IDebugAdvanced 4 コンテキスト操作等
IDebugBreakpoint 3 ブレークポイント操作
IDebugClient 9 クライアント操作 (リファレンスだと 8 までになっているが、現在の Windows には 9 が存在)
IDebugPlmClient 3 PLM デバッグ
IDebugOutputStream 1 debug output stream への書き込み
IDebugControl 7 デバッガ操作
IDebugDataSpaces 4 メモリ操作
IDebugEventCallbacks 1 デバッガ/ターゲットの状態変化時のコールバック制御
IDebugEventCallbacksWide 1 上記の Unicode 版
IDebugEventContextCallbacks 1 上記にイベントコンテキストを設定できるようにしたコールバック制御
IDebugInputCallbacks 1 デバッガの入力待機が切り替った時のコールバック制御
IDebugOutputCallbacks 2 出力時のコールバック制御
IDebugRegisters 2 レジスタ操作
IDebugSymbolGroup 2 シンボルグループ操作
IDebugSymbols 5 シンボル操作
IDebugSystemObjects 4 システムオブジェクト (プロセスハンドルや PEB 等) 操作

DbgEng は番号だけでなく、それぞれの機能においてもインターフェースが分割されています。 今回のようにレジスタを読み取り、さらにメモリを読み取って結果を出力する場合は IDebugClient, IDebugDataSpaces, IDebugRegisters が必要です。

注意として、インターフェース名で API を探すといくつかの落とし穴にはまることがあります。 例えば、ブレークポイントを貼る API AddBreakpoint は IDebugControl に存在し、IDebugBreakpoint には存在しません。 IDebugBreakpoint はブレークポイント自体の設定を変更するためのインターフェースであり、ブレークポイントを追加・削除するものではないためですが、それを知らないとついついブレークポイントを貼る API を求めてここを探すことになります。

必須処理の実装

まず初めに Windows.hDbgEng.h をインクルードします。 今回は std::string も使用するため、string もインクルードしておきます。 NREG_NAMES も定義しておきましょう。

#include <Windows.h>
#include <DbgEng.h>
#include <string>

#define N 32 // 最大バイト数

const char* const REG_NAMES[] = {(...省略...)}; // WdbgExts と同じものを使用

DbgEng による実装では DebugExtensionInitialize 関数のみ必須です。 WdbgExts では WinDbgExtensionDllInit という名前でしたが、異なるため注意してください。

今回は C++ で実装するため、extern "C" (EXTERN_C) を忘れずに付けておきます。

EXTERN_C HRESULT CALLBACK
DebugExtensionInitialize(_Out_ PULONG Version,
    _Out_ PULONG Flags)
{
    // 拡張機能のバージョンを指定
    *Version = DEBUG_EXTENSION_VERSION(1, 0);
    // help コマンドを定義するため、フラグを立てておく
    *Flags = DEBUG_EXTINIT_HAS_COMMAND_HELP;

    return S_OK;
}

DebugExtensionInitializeVersionFlags へのポインタ引数を受け取るため、適宜値を入れて返します*4

Version は拡張機能自身のバージョンです。WdbgExts の MajorVersionMinorVersion と同じです。 DEBUG_EXTENSION_VERSION(MajorVersion, MinorVersion) マクロを使うと、見やすく実装できます。 なお、WdbgExts では Revision の指定がありましたが、DbgEng の場合、指定する必要はなく、必ず 0 になります。気になる方は .chain コマンドで確認してみましょう。

Flags には、拡張機能のフラグを渡します。現在は DEBUG_EXTINIT_HAS_COMMAND_HELP のみ存在しているようです。 help コマンドを実装する場合はこのフラグを立てておきます。

初期化に成功したら S_OK を返します。

必須の実装はこれだけです。

コマンドの実装

コマンドを実装していきましょう。まずは help コマンドを実装します。

コマンド関数コールバックである PDEBUG_EXTENSION_CALL に沿う形でコールバック関数を実装します。 PDEBUG_CLIENT (IDebugClient*) および PCSTR 型の引数を持ち、HRESULT 型を返します。 第一引数の PDEBUG_CLIENT が DbgEng クライアントの COM インターフェースであり、これを他のインターフェースにキャストすることで、色々な操作を実現していきます。 第二引数は引数の文字列です。例えば !command aaa bbb としてコマンドを呼び出したのなら aaa bbb が入ります。これは WdbgExts の時と変わりません。

初めに、QueryInterface を使用して、Client をサポートされている他の COM インターフェースにキャストします。 使用するインターフェースを得られなかった場合は中断してエラーを返すようにします*5。 といっても、この QueryInterface が失敗することはまず無いはずです。

なお、グローバル変数に保持するなどして前のクライアントのインターフェースを使い回すと予期せぬバグを生む可能性があります。 引数から渡されるクライアントは毎回同じとは限らないため、インターフェースはコマンドが呼ばれるたびに引数の Client から QueryInterface して得る必要があります。

コンソール出力は IDebugControl::Output 関数を使用します。 Client をキャストして IDebugControl を作ります。今回はとりあえず一番新しい IDebugControl7 を得ましょう。

最後に、QueryInterface で取得した COM ポインタは使い終わったら Release で解放しなければなりません。忘れるとリークします。 なお、解放するのは自分で QueryInterface したポインタのみです。 コマンドの引数として渡される Client は自分で作成したものではないため、Release しないでください。

EXTERN_C HRESULT CALLBACK
help(PDEBUG_CLIENT Client, PCSTR Args)
{
    UNREFERENCED_PARAMETER(Args);

    HRESULT hr;

    // 必要なインターフェースを取得する
    IDebugControl7* Control = nullptr;
    hr = Client->QueryInterface(__uuidof(IDebugControl7), reinterpret_cast<PVOID*>(&Control));
    if (FAILED(hr)) goto Finish;

    Control->Output(DEBUG_OUTPUT_NORMAL, "regstr_cpp help!\n");
    hr = S_OK;

Finish:
    if (FAILED(hr))
    {
        // 必要なインターフェース取得に失敗したらエラーを返す
        if (Control) Control->Output(DEBUG_OUTPUT_NORMAL, "Failed to get interfaces!\n");
    }
    // インターフェースの解放
    if (Control) Control->Release();
    return hr;
}

次は regstr コマンドで用いる関数を実装していきます。 C++ 用に std::string を用いた MakeString 関数に書き換えています。

static std::string MakeString(PCSTR Buffer, const ULONG BufferLength)
{
    std::string ret;
    ret.reserve(BufferLength);
    for (ULONG i = 0; i < BufferLength; i++)
    {
        const CHAR ch = Buffer[i];
        // 0x80 以上 はASCII でない
        if (static_cast<UCHAR>(ch) >= 0x80) return ret;
        if (ch < 0x20)
        {
            // 0x20 未満はエスケープシーケンス
            // 以下のエスケープシーケンス以外は文字とみなさないこととする
            switch (ch)
            {
            case '\t':
                ret += "\\t";
                break;
            case '\n':
                ret += "\\n";
                break;
            case '\v':
                ret += "\\v";
                break;
            case '\r':
                ret += "\\r";
                break;
            default:
                return ret;
            }
        }
        else
        {
            if (ch == '"' || ch == '\\') ret += '\\';
            ret += ch;
        }
    }
    // ここまで来た場合、文字列の途中で切れた可能性があるため、... を付けて続きを示唆しておく。
    ret += "...";
    return ret;
}

ShowRegStr 関数を実装します。 レジスタ名に対応する値の取得には、WdbgExts と異なりそれ用の API がありますが、それでも少々面倒です。 まず IDebugRegisters::GetIndexByName で名前に対応するレジスタインデックスを取得します。 次に IDebugRegisters::GetValue でレジスタ値を取得します。

GetValue は値を DEBUG_VALUE 型として返します。 これは共用体で、取得したレジスタに応じた型の値が格納されます。 値のタイプは Type 変数に格納されます。 例えば rax なら DEBUG_VALUE_INT64, xmm0 なら DEBUG_VALUE_VECTOR128 です。 また、rax の下位レジスタ eaxal なども INT64 扱いになります (当然、取得できる値は各レジスタのビット幅に制約されますが)。

DEBUG_VALUE から値を取り出す際には、先に Type を見て対応する共用体の変数を選択する必要があります。 例えば Type = DEBUG_VALUE_INT32 であれば I32 変数のみ正常に使用できます。 もしそれ以外の変数を用いたい場合は IDebugControl::CoerceValue で型を強制することで、安全に使用できるようになります。 CoerceValue は IDebugRegisters でなく IDebugControl の関数であるため、注意しましょう。 なお、共用体の性質上、INT64 でも I32 などへのダウンキャストアクセスであればおそらく問題ないと思いますが、この際の挙動は Undocumented です。 特定の条件で失敗する可能性もあるため、万全を期してダウンキャストでも CoerceValue するか (unsigned int)I64I64 をキャストしたほうが良いでしょう。 今回は rax などの 64 ビット汎用レジスタのみ表示するため、CoerceValue せずとも Type には DEBUG_VALUE_INT64 が入っているはずです。

欲しい型のメンバーを指定して取り出します。

メモリの読み取りは IDebugDataSpaces::ReadVirtual で行います。

// RegName に対応するレジスタの値を ULONG64 型で返す。
HRESULT GetRegisterValueUlong64ByName(IDebugControl7* Control, IDebugRegisters2* Registers, PCSTR RegName, PULONG64 Value)
{
    ULONG Register;

    // RegName に対応するレジスタのインデックスを取得する
    HRESULT hr = Registers->GetIndexByName(RegName, &Register);
    if (FAILED(hr)) return hr;

    // 取得したレジスタインデックスから値を取り出す
    DEBUG_VALUE DebugValue, DebugValueOut;
    hr = Registers->GetValue(Register, &DebugValue);
    if (FAILED(hr)) return hr;

    if (DebugValue.Type == DEBUG_VALUE_INT64) {
        // タイプが一致していればそのまま使用する
        // DEBUG_VALUE から対応する型を取り出して返す
        *Value = DebugValue.I64;
    }
    else {
        // タイプが一致していなければ合わせる
        hr = Control->CoerceValue(&DebugValue, DEBUG_VALUE_INT64, &DebugValueOut);
        if (FAILED(hr)) return hr;
        *Value = DebugValueOut.I64;
    }

    return S_OK;
}

void ShowRegStr(IDebugControl7* Control, IDebugDataSpaces4* DataSpaces, IDebugRegisters2* Registers, PCSTR RegName)
{
    ULONG64 RegValue;
    if (FAILED(GetRegisterValueUlong64ByName(Control, Registers, RegName, &RegValue)))
    {
        // 取得失敗
        Control->Output(DEBUG_OUTPUT_NORMAL, "Failed to get register %s value\n", RegName);
        return;
    }

    // RegValue をアドレスとみなしてメモリの読み取りを試みる
    // 読み取りの成否は気にしない
    char buf[N] = { 0 };
    ULONG nr;
    DataSpaces->ReadVirtual(RegValue, buf, N, &nr);
    const std::string s = MakeString(buf, N);
    if (s.empty())
    {
        Control->Output(DEBUG_OUTPUT_NORMAL, "%-3s = 0x%016I64x\n", RegName, RegValue);
    }
    else
    {
        // 先頭 1 文字でも正当な文字であれば表示する
        Control->Output(DEBUG_OUTPUT_NORMAL, "%-3s = 0x%016I64x \"%s\"\n", RegName, RegValue, s.c_str());
    }
}

最後に regstr コマンドを実装します。 help の時と同様に、必要なインターフェースを取得してから、ShowRegStr を呼び出します。

EXTERN_C HRESULT CALLBACK
regstr(PDEBUG_CLIENT Client, PCSTR Args)
{
    UNREFERENCED_PARAMETER(Args);

    HRESULT hr;

    // 必要なインターフェースを取得する
    IDebugControl7* Control = nullptr;
    IDebugDataSpaces4* DataSpaces = nullptr;
    IDebugRegisters2* Registers = nullptr;
    hr = Client->QueryInterface(__uuidof(IDebugControl7), reinterpret_cast<PVOID*>(&Control));
    if (FAILED(hr)) goto Finish;
    hr = Client->QueryInterface(__uuidof(IDebugDataSpaces4), reinterpret_cast<PVOID*>(&DataSpaces));
    if (FAILED(hr)) goto Finish;
    hr = Client->QueryInterface(__uuidof(IDebugRegisters2), reinterpret_cast<PVOID*>(&Registers));
    if (FAILED(hr)) goto Finish;

    for (size_t i = 0; i < _countof(REG_NAMES); i++) ShowRegStr(Control, DataSpaces, Registers, REG_NAMES[i]);
    hr = S_OK;

Finish:
    if (FAILED(hr))
    {
        // 必要なインターフェース取得に失敗したらエラーを返す
        if (Control) Control->Output(DEBUG_OUTPUT_NORMAL, "Failed to get interfaces!\n");
    }
    if (Control) Control->Release();
    if (DataSpaces) DataSpaces->Release();
    if (Registers) Registers->Release();

    return hr;
}

動作確認

最後にモジュール定義ファイルを作成します。

LIBRARY regstr_cpp
EXPORTS
    DebugExtensionInitialize
    help
    regstr

WdbgExts と同様にビルド・動作確認し、正しい結果が得られることを確認して完成です。

regstr コマンドの実行 (DbgEng 版)

EngExtCpp による実装

C++ の場合は DbgEng のラッパーフレームワークである EngExtCpp *6が用意されており、それを使用することで実装しやすくなります。

では、EngExtCpp で実装していきましょう。 プロジェクト名は regstr_cpp2 としています。

engextcpp のビルド

EngExtCpp はソースコードからビルドします。 Windows SDK のパス %PROGRAMFILES(X86)%\Windows Kits\<バージョン>\Debuggers\incengextcpp.hppengextcpp.cpp が入っているため、これらを自分のプロジェクトディレクトリにコピーします。

engextcpp.cpp はデフォルトだとコンパイルできない*7ため、以下のように書き換えます。

  • #include <engextcpp.hpp>#include "engextcpp.hpp" に変更
  • PSTR Value = "";PSTR Value = (PSTR)""; に変更
  • m_OptionChars = "/-";m_OptionChars = (PSTR)"/-"; に変更
  • BufferChars > 0)*BufferChars > 0) に変更 (複数個所あるため注意)

また、直さなくても問題ありませんが、C++ 17 以上を使用していると動的例外指定が警告表示されます。煩わしい場合は throw (...) をすべて削除してください。 書き換えたら拡張機能を実装していきます。

必須処理の実装

まずは engextcpp.hpp をインクルードします。 なお、この中で自動的に Windows.h もインクルードされるため、Windows.h のインクルードは必要ありません。

#include "engextcpp.hpp"
#include <string>

#define N 32 // 最大バイト数

const char* const REG_NAMES[] = {(...省略...)}; // WdbgExts と同じものを使用

EngExtCpp は ExtExtension を継承した EXT_CLASS クラスを実装する形で拡張機能を実装します。 クラス名は EXT_CLASS にしてください。これは内部で Extension という名前に置き換えられます。 もし Extension 以外の名前にしたい場合は #define EXT_CLASS MyExtension のような文を engextcpp.hpp のインクルード前に書いてください。 コマンドは EXT_COMMAND_METHOD マクロを使用してメンバーに含めます。 その後、EXT_DECLARE_GLOBALS マクロで EngExtCpp に必要なグローバルインスタンスを用意します。

class EXT_CLASS : public ExtExtension
{
    void ShowRegStr(PCSTR RegName);
public:
    EXT_COMMAND_METHOD(regstr);
};

EXT_DECLARE_GLOBALS(); // 拡張機能に必要なインスタンスを用意するマクロ

EngExtCpp は DbgEng をバックエンドに持っていますが、DebugExtensionInitialize, Uninitialize *8, Notify *9 は EngExtCpp 内で自動で作成されます。 また、COM ポインタの初期化や解放も内部で勝手に行ってくれるため、我々が意識する必要はありません。

コマンドの実装

まずは help コマンドの実装を...と言いたいところですが、EngExtCpp は自動的に良い感じの help コマンドを用意してくれます。 したがって、自分で実装する必要はありません。

ということで、早速 regstr コマンドを実装しましょう。

コマンドは EXT_COMMAND マクロで実装します。 マクロの第一引数がコマンド名、第二引数が (help コマンドで表示される) コマンドの説明文、第三引数が引数パーサーの設定です。引数が不要な場合は空文字列にします。 なお、今回は使用していませんが引数パーサーが付属しており、簡単に引数をパースして使用できます。

EXT_COMMAND(regstr, "Print register string", "")
{
    for (size_t i = 0; i < _countof(REG_NAMES); i++) ShowRegStr(REG_NAMES[i]);
}

MakeString 関数は DbgEng 版を使い回すこととし、次は ShowRegStr 関数を実装します。 ShowRegStr 関数は EXT_CLASS のメンバーとして実装する必要がある点に注意してください。

EngExtCpp ではレジスタ値の取得が GetRegisterU64 関数を使うだけで、簡単にできるようになっています。 メモリの読み取りは ExtRemoteData を使用して読み取ります。 ExtRemoteData::ReadBuffer でデータを読み出します。 なお、DbgEng の時は仮想メモリしか想定していないため省略していましたが、この ReadBuffer もメモリが物理でも仮想でも読み出せるような実装になっています。

DbgEng ではエラー判定が HRESULT でしたが、EngExtCpp では主に例外です。 メモリ読み取り成否の無視は try-catch で例外を握りつぶす形で行います。

void EXT_CLASS::ShowRegStr(PCSTR RegName)
{
    const ULONG64 RegValue = GetRegisterU64(RegName);
    char buf[N] = { 0 };
    try
    {
        ExtRemoteData data;
        data.Set(RegValue, N);
        // data.GetString(buf, N); // これでも良い
        data.ReadBuffer(buf, N, false);
    }
    catch (...)
    {
        // 何もしない
    }
    // buf を文字列に変換する
    const std::string s = MakeString(buf, N);
    if (s.empty())
    {
        Out("%-3s = 0x%016I64x\n", RegName, RegValue);
    }
    else
    {
        // 先頭 1 文字でも正当な文字であれば表示する
        Out("%-3s = 0x%016I64x \"%s\"\n", RegName, RegValue, s.c_str());
    }
}

動作確認

以下のモジュール定義ファイルを作成します。 DebugExtension* 関数は EngExtCpp が勝手に作っているため影が薄いですが、拡張機能 DLL として必須なため、忘れずにエクスポートしてください。

LIBRARY regstr_cpp2
EXPORTS
    DebugExtensionInitialize
    DebugExtensionUninitialize
    DebugExtensionNotify
    help
    regstr

意図した結果になっていれば完成です。

help と regstr コマンドの動作確認 (EngExtCpp 版)

dbgeng-rs での実装

公式の拡張機能の実装方法は上記に示しましたが、サードパーティーのモジュールを活用すれば比較的容易に他言語でも実装できます。

試しに、0vercl0k 氏の dbgeng-rs を用いて Rust で実装してみましょう。 これは前回のブログで紹介した wtf で使用される、スナップショットを撮るための拡張機能 snapshot 用に作られたクレートのようです。 snapshot を実装するための機能がメインですが、Rust による拡張機能実装の良い参考になります。

dbgeng-rs は名前の通り、DbgEng をバックエンドに持っています。 DbgEng のインターフェースはマイクロソフト公式の WinAPI バインディング windows-rs から使用できるため、それ経由で DbgEng を呼び出しています。

では dbgeng-rs で実装していきましょう。 プロジェクト名は regstr_rs としています。

必須処理の実装

まず、Cargo.toml を以下のように設定します。

[package]
name = "regstr_rs"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
dbgeng = { version = "0.4" }

拡張機能を実装していきましょう。

まずは、必要なものを use します。

use dbgeng::{
    DEBUG_EXTENSION_VERSION,
    client::DebugClient,
    windows::{
        Win32::{
            Foundation::{E_ABORT, S_OK},
            System::Diagnostics::Debug::Extensions::DEBUG_EXTINIT_HAS_COMMAND_HELP,
        },
        core::{HRESULT, IUnknown, Interface, PCSTR},
    },
};

const N: usize = 32; // 取得する文字数の最大サイズ

// 表示するレジスタ
const REG_NAMES: &[&str] = &[
    "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp", "r8", "r9", "r10",
    "r11", "r12", "r13", "r14", "r15",
];

DbgEng バックエンドなため、まずは DebugExtensionInitialize を実装・エクスポートします。 #[unsafe(no_mangle)] でマングリングを止め、extern "C" でエクスポートします。

#[unsafe(no_mangle)]
extern "C" fn DebugExtensionInitialize(Version: *mut u32, Flags: *mut u32) -> HRESULT {
    unsafe {
        // 生ポインタ処理のため unsafe
        *Version = DEBUG_EXTENSION_VERSION(1, 0);
        *Flags = DEBUG_EXTINIT_HAS_COMMAND_HELP;
    }
    S_OK
}

コマンドの実装

次は help コマンドを実装します。 第一引数が生ポインタなため、まず IUnknown に型変換し、そこから DebugClient に変換します。 DebugClient は dbgeng-rs にある DbgEng COM インターフェースをまとめて扱うための型です。 DebugClient::new 内で今回使用するインターフェースが得られます。 コンソール出力は dbg.log を使用します。

/// help コマンド
#[unsafe(no_mangle)]
extern "C" fn help(
    debug_client: *mut std::ffi::c_void,
    _args: PCSTR,
) -> HRESULT {
    // 生ポインタを IUnknown 型にキャスト
    let Some(client) = (unsafe { IUnknown::from_raw_borrowed(&debug_client) })
    else {
        return E_ABORT;
    };

    // DebugClient 内で必要なインターフェースにキャストされる
    let Ok(dbg) = DebugClient::new(client) else {
        return E_ABORT;
    };

    /*
    // 引数を使いたい場合
    let Ok(args) = (unsafe { _args.to_string() }) else {
        return E_ABORT;
    };
    */

    let _ = dbg.log("regstr_rs help!\n");
    S_OK
}

次に regstr コマンドを実装します。

/// regstr コマンド
#[unsafe(no_mangle)]
extern "C" fn regstr(
    debug_client: *mut std::ffi::c_void,
    _args: PCSTR,
) -> HRESULT {
    let Some(client) = (unsafe { IUnknown::from_raw_borrowed(&debug_client) })
    else {
        return E_ABORT;
    };

    let Ok(dbg) = DebugClient::new(client) else {
        return E_ABORT;
    };

    for reg_name in REG_NAMES {
        show_reg_str(&dbg, reg_name);
    }

    S_OK
}

今までの MakeString にあたる make_string 関数を実装します。

fn make_string(buf: &[u8]) -> String {
    let mut ret = String::with_capacity(N);
    for ch in buf {
        let ch = *ch;
        // 0x80 以上は ASCII でない
        if ch >= 0x80 {
            return ret;
        }
        // 0x20 未満はエスケープシーケンス
        // 以下のエスケープシーケンス以外は文字とみなさないこととする
        if ch < 0x20 {
            match ch {
                0x09 => ret += "\\t",
                0x0a => ret += "\\n",
                0x0b => ret += "\\v",
                0x0d => ret += "\\r",
                _ => return ret,
            }
        } else {
            if ch == 0x22 || ch == 0x5c {
                ret.push('\\');
            }
            ret.push(ch as char);
        }
    }
    // ここまで来た場合、文字列の途中で切れた可能性があるため、... を付けて続きを示唆しておく
    ret += "...";
    ret
}

escapeshow_reg_str を実装します。 escape 関数については Output におけるフォーマット文字列の注意点 を参照してください。 DebugClientreg64 で、u64 型のレジスタ値を取得できます。 また、read_virtual でバッファからデータを読み取れます。

/// `%` をエスケープする。
fn escape<S: AsRef<str>>(s: S) -> String {
    s.as_ref().replace('%', "%%")
}

fn show_reg_str(dbg: &DebugClient, reg_name: &str) {
    let val = match dbg.reg64(reg_name) {
        Ok(x) => x,
        Err(e) => {
            // レジスタの取得に失敗
            let _ = dbg.log(escape(format!(
                "Getting {reg_name} value is failed: {e}\n"
            )));
            return;
        }
    };
    let mut buf = [0; N];

    let _ = dbg.read_virtual(val, &mut buf);
    let s = make_string(&buf);
    if s.is_empty() {
        let _ = dbg.log(escape(format!("{reg_name:3} = 0x{val:016x}\n")));
    } else {
        let _ =
            dbg.log(escape(format!("{reg_name:3} = 0x{val:016x} \"{s}\"\n")));
    }
}

動作確認

cargo build --release でビルド後、いつも通り .load して動作を確認しましょう。

regstr コマンドの実行 (dbgeng-rs 版)

拡張機能は完成しましたが、毎回 .load でパスを指定して読み込むのは面倒です。 そこで、Extension Gallery に登録し自動で読み込まれるようにします。 Extension Gallery への登録方法は config.xml と UserExtensions の 2 通りあります。 どちらを使用するかはお好みで構いません。

config.xml を介した登録

まずは Extension Gallery への登録に必要な ManifestVersion.txt, config.xml, Manifest.1.xml を作成しましょう。

フォルダ構成は以下の通りです。 適当なフォルダ (ここでは C:\regstr とします) を作成し、以下のようなフォルダ構成にします。 regstr_*.dll はどれでも良いですが、ここでは regstr_cpp.dll を例に説明します。

C:\regstr
+-- regstr_cpp.dll
+-- Manifest
    +-- config.xml
    +-- Manifest.1.xml
    +-- ManifestVersion.txt

まずは ManifestVersion.txt を作成します。 これは以下を記述するだけです。

1
1.0.0.0
1

次に、マニフェスト全体を管理するための config.xml を作成します。 Namespace は最後の Namespace (下記でいう RegStr Gallery の部分)のみ、自由な名前を付けられます。 Id は適当な GUID を指定します。 LocalCacheRootFolder にはマニフェストファイルのあるフォルダを絶対パス (今回の例でいえば C:\regstr\Manifest) で指定します。 IsEnabledtrue を指定します。読み込ませたくない場合はこれを false にしてください。

<?xml version="1.0" encoding="UTF-8"?>
<Settings Version="1">
  <Namespace Name="Extensions">
    <Setting Name="ExtensionRepository" Type="VT_BSTR" Value="Implicit"></Setting>
    <Namespace Name="ExtensionRepositories">
      <Namespace Name="RegStr Gallery">
        <Setting Name="Id" Type="VT_BSTR" Value="1B97B855-DA44-4C9F-B50F-89B1E6402D77"></Setting>
        <Setting Name="LocalCacheRootFolder" Type="VT_BSTR" Value="C:\regstr\Manifest"></Setting>
        <Setting Name="IsEnabled" Type="VT_BOOL" Value="true"></Setting>
      </Namespace>
    </Namespace>
  </Namespace>
</Settings>

最後に、読み込む拡張機能について記述した Manifest.1.xml を作成します。 ファイル名の 1 は 1 からの連番で複数のマニフェストに分割できます。 config.xml はフォルダ内にある Manifest.<数字>.xml を読み込みます。

ExtensionPackages の中に追加する ExtensionPackage を追加していきます。 Name, Version, Description はお好みの拡張機能名、バージョン、説明文を記述してください。

Components に自動で読み込んで欲しい拡張機能を記述します。 Components の中には WinDbg Script を指定できる ScriptComponent と DLL 拡張を指定できる BinaryComponent を指定できます。 今回は BinaryComponent が 1 つだけです。

BinaryComponentName 属性は DLL と同じ名前 (case-insensitive)、TypeEngine を記述してください。

Files は読み込むファイルの情報を指定します。FileArchitecture はどのアーキテクチャでも機能するのであれば Any を入れます。 今回は amd64 (x64) でのみ動作するため、amd64 を入れます。x64 はエラーになります。x86 のみの場合は x86 で大丈夫です。 次に、パスの指定方法を FilePathKind に記述します。RepositoryRelative を指定すると、config.xml からの相対パスで指定できるようになります。 Module には DLL の相対パスを記述します。

次は EngineCommands を記述します。ここには DLL のコマンドを記述していきます。 EngineCommandName はコマンド名を記述します。 EngineCommandItemSyntax は構文、Description は説明を記述します。これらの要素は必須です。

<?xml version="1.0" encoding="utf-8"?>
<ExtensionPackages Version="1.0.0.0" Compression="none">
    <ExtensionPackage>
        <Name>regstr</Name>
        <Version>1.0.0.1</Version>
        <Description>A sample WinDbg extension.</Description>
        <Components>
            <BinaryComponent Name="regstr_cpp" Type="Engine">
                <Files>
                    <File Architecture="amd64" Module="..\regstr_cpp.dll" FilePathKind="RepositoryRelative"/>
                </Files>
                <EngineCommands>
                    <EngineCommand Name="regstr">
                        <EngineCommandItem>
                            <Syntax><![CDATA[!regstr]]></Syntax>
                            <Description><![CDATA[Print the string for registers.]]></Description>
                        </EngineCommandItem>
                    </EngineCommand>
                </EngineCommands>
            </BinaryComponent>
        </Components>
    </ExtensionPackage>
</ExtensionPackages>

.settings load C:\regstr\Manifest\config.xml でマニフェストを読み込みます。 以下が表示されることを確認しましょう。 次に .settings save で設定を保存してください。

Gallery への登録

その後 .restart すると !regstr が使えるようになっているはずです。 また、WinDbg を再起動してもすぐにコマンドが使えるはずです。 !regstr コマンドを使用できない場合は Extension Gallery マニフェストのデバッグを参照してください。

再起動後の regstr コマンド

UserExtensions を介した登録

上記は config.xml を使用して DLL を自動で読み込む方法でした。 これ以外にも %LOCALAPPDATA%\DBG\UserExtensions に配置する方法もあります。 この方法は config.xml が必要ないため、いちいち絶対パスを書き換えなくて済みます。

最初に、フォルダ構成を示します。

UserExtensions
+- regstr                # <--- ExtensionPackage の Name をフォルダ名にする
   +- Manifest.xml
   +- 1.0.0.0            # <--- ExtensionPackage の Version をフォルダ名にする
      +- regstr_cpp.dll

まず、%LOCALAPPDATA%\DBGUserExtensions フォルダを作成します。

次に、Manifest.xml (ファイル名はなんでも構いません) を作成します。 これは先ほどの Manifest.1.xml とほぼ同じですが、FileModule に書いたパスがカレントフォルダの DLL を指すようになっています。

この xml ファイルの ExtensionPackage 内の Name (つまり、regstr) を UserExtensions 直下に作成するフォルダ名にし、Version の値 (つまり、1.0.0.0) を regstr フォルダの直下に作成するフォルダ名にします。

FileModule に指定したパスは、その Version 1.0.0.0 フォルダがカレントフォルダとして検索されます。

<?xml version="1.0" encoding="utf-8"?>
<ExtensionPackages Version="1.0.0.0" Compression="none">
    <ExtensionPackage>
        <Name>regstr</Name>
        <Version>1.0.0.0</Version>
        <Description>A sample WinDbg extension.</Description>
        <Components>
            <BinaryComponent Name="regstr_cpp" Type="Engine">
                <Files>
                    <File Architecture="amd64" Module="regstr_cpp.dll" FilePathKind="RepositoryRelative"/>
                </Files>
                <EngineCommands>
                    <EngineCommand Name="regstr">
                        <EngineCommandItem>
                            <Syntax><![CDATA[!regstr]]></Syntax>
                            <Description><![CDATA[Print the string for registers.]]></Description>
                        </EngineCommandItem>
                    </EngineCommand>
                </EngineCommands>
            </BinaryComponent>
        </Components>
    </ExtensionPackage>
</ExtensionPackages>

ファイルを配置できたら WinDbg を起動し、特別な対応無しに拡張コマンドが使用できることを確認できれば OK です。

その他

拡張機能のデバッグ

コマンド拡張 DLL を Visual Studio 上でソースコードデバッグしたい場合は以下の手順で行えます。

  1. DLL をビルド
  2. WinDbg を起動
  3. WinDbg で適当なアプリケーションのデバッグを開始
  4. Visual Studio のタブから デバッグ -> プロセスにアタッチ
  5. 「プロセスにアタッチ」ウィンドウで EngHost.exe を検索し、選択してアタッチ
  6. Visual Studio 上でブレークポイントを貼り、WinDbg 上で .load で DLL を読み込んで操作

また、WinDbg でコマンド拡張 DLL をデバッグしたい場合は .dbgdbg コマンドを使用します。 このコマンドを使用すると新しい WinDbg インスタンスが起動して既存の WinDbg をアタッチします。 アタッチされたら lm コマンドで拡張機能 DLL が読み込まれているか確認し、読み込まれていれば後はいつも通り関数にブレークポイントを貼って再開し、拡張コマンドを実行することでブレークできます。

ターゲットになる WinDbg で:

.load C:\build\regstr_c.dll
.dbgdbg

新しく出てきた WinDbg ウィンドウで:

bp regstr_c!help
g

ターゲット側の WinDbg で:

!regstr_c.help

DML

WinDbg のコマンドを実行すると一部の文字が色付きだったり、リンクになっていてクリックするとコマンドが実行されたりします。 この機能を Debugger Markup Language (DML) といいます。 DML は DbgEng で使用できます。WdbgExts の dprintf からは使用できません。

DML を使用するには、Output の代わりに ControlledOutput を使用し、第一引数 (OutputControl) に DEBUG_OUTCTL_DML を渡します。 文字列の修飾は HTML のようにタグで囲みます。使用可能なタグはドキュメントを参照してください。

例えば exec タグを使用して、クリックするとコマンドが実行されるリンクを出力できます。

g_Control->ControlledOutput(DEBUG_OUTCTL_DML, DEBUG_OUTPUT_NORMAL, "<exec cmd=\"? @rax\">Show rax</exec>\n");

// EngExtCpp の場合は Dml 関数して以下のように書いても良い。
// Dml("<exec cmd=\"? @rax\">Show rax</exec>\n");

上記が出力されると Show rax という文字列がリンクになり、クリックすると cmd 属性に書いた ? @rax コマンドが実行されます。

Show rax をクリックするとコマンドが実行される

特殊記号は HTML と同じようにエスケープできます。 例えば、<> をタグとして判定されたくない場合は < の代わりに &lt; を使用してください。 また、DML を使用しない場合は Output を使用したほうが良いでしょう。

また、col タグを使用すれば文字と背景の色を制御できます*10fg は文字色、bg は背景色の指定です。 なお、色は自由に指定できるわけではなく、決められた色しか使用できません。

srcpair は fg にしか適用してはならないといったルールは無いため、bg に指定して背景を橙にすることもできます。 また、ペア色もペアで使う必要はありません。

ちなみに、black#000000 の黒、clfg#FFFFFF の白になります (white は無いようです)。 wfg, wbg 等はテーマ設定で色が変化します。

以下の EngExtCpp 用コードでいくつかの色の組み合わせを列挙してみましょう。

#define ColDml(pad1, _fg, pad2, _bg) \
    Dml("<col fg=\"" _fg "\" bg=\"" _bg "\">" pad1 _fg ", " pad2 _bg "</col>\n")

    Dml(L"文字色\n");
    ColDml("     ", "srcnum", "        ", "wbg"); // 数値定数       Source numeric constant
    ColDml("    ", "srcchar", "        ", "wbg"); // 文字定数       Source character constant
    ColDml("     ", "srcstr", "        ", "wbg"); // 文字列定数     Source string constant
    ColDml("      ", "srcid", "        ", "wbg"); // 識別子         Source identifier
    ColDml("      ", "srckw", "        ", "wbg"); // キーワード     keyword
    ColDml("    ", "srcpair", "        ", "wbg"); // 括弧のペア     Source brace or matching symbol pair
    ColDml("    ", "srccmnt", "        ", "wbg"); // コメント       Source Comment
    ColDml("    ", "srcdrct", "        ", "wbg"); // ディレクティブ Source directive
    ColDml("    ", "srcspid", "        ", "wbg"); // 特殊な識別子   Source special identifier
    ColDml("   ", "srcannot", "        ", "wbg"); // アノテーション Source annotation
    ColDml("    ", "changed", "        ", "wbg"); // 変更データ     Changed data

    Dml(L"\n背景色\n");
    ColDml("       ", "clfg", "     ", "srcnum");
    ColDml("       ", "clfg", "    ", "srcchar");
    ColDml("       ", "clfg", "     ", "srcstr");
    ColDml("       ", "clfg", "      ", "srcid");
    ColDml("       ", "clfg", "      ", "srckw");
    ColDml("      ", "black", "    ", "srcpair");
    ColDml("      ", "black", "    ", "srccmnt");
    ColDml("       ", "clfg", "    ", "srcdrct");
    ColDml("       ", "clfg", "    ", "srcspid");
    ColDml("       ", "clfg", "   ", "srcannot");
    ColDml("      ", "black", "    ", "changed");

    Dml(L"\nfg/bg ペア\n");
    ColDml("        ", "wfg", "        ", "wbg"); // デフォルト Windows
    ColDml("       ", "clfg", "       ", "clbg"); // 現在行     Current line
    ColDml("      ", "empfg", "      ", "empbg"); // 強調       Emphasized
    ColDml("      ", "subfg", "      ", "subbg"); // 控えめ     Subdued
    ColDml("     ", "normfg", "     ", "normbg"); // 通常       Normal
    ColDml("     ", "warnfg", "     ", "warnbg"); // 警告       Warning
    ColDml("      ", "errfg", "      ", "errbg"); // エラー     Error
    ColDml("     ", "verbfg", "     ", "verbbg"); // 詳細       Verbose

実行結果は以下のようになりました。ご覧のとおり、normbg などはテーマによって色が変わるだけの通常色です。

Light テーマの色合い

Dark テーマの色合い

Output におけるフォーマット文字列の注意点

WdbgExts の dprintf にせよ DbgEng の Output にせよ、フォーマット文字列の処理は普通の printf などにおけるフォーマット文字列と異なるクセがあります。

例えば g_Control->Output(DEBUG_OUTPUT_NORMAL, "%s\n", str)str はコードページの設定に関わらず Latin-1 エンコーディングで扱われます。 つまり、%s はマルチバイト文字を処理できません。 マルチバイト文字を表示したい場合は %ls を使用し、Unicode 文字列を使用する必要があります。 ただし、フォーマット文字列に直接書く場合はコードページの設定が使われるようで、対応するエンコーディングで表示できます。 上記の例ではすべて Output%s を使用していますが、今どきを考えるのであれば Unicode 文字列を使用すべきでしょう。

g_Control->Output(DEBUG_OUTPUT_NORMAL,      "%s\n",      "あいうえお"); // 文字化けする
g_Control->Output(DEBUG_OUTPUT_NORMAL,      "かきくけこ\n");            // 表示できる
g_Control->Output(DEBUG_OUTPUT_NORMAL,      "%ls\n",    L"さしすせそ"); // 表示できる
g_Control->OutputWide(DEBUG_OUTPUT_NORMAL, L"%s\n",     L"たちつてと"); // 表示できる
g_Control->OutputWide(DEBUG_OUTPUT_NORMAL, L"なにぬねの\n");            // 表示できる
g_Control->OutputWide(DEBUG_OUTPUT_NORMAL, L"%ls\n",     "はひふへほ"); // 文字化けする

また、64 ビット整数表示の %ll 系は 32 ビットとして扱われます。64 ビットで表示するには %I64 系を使用してください。

g_Control->Output(DEBUG_OUTPUT_NORMAL, "lld  = %lld\n",  0x1234567890abcdef); // lld  = 2427178479
g_Control->Output(DEBUG_OUTPUT_NORMAL, "llu  = %llu\n",  0x1234567890abcdef); // llu  = 2427178479
g_Control->Output(DEBUG_OUTPUT_NORMAL, "llx  = %llx\n",  0x1234567890abcdef); // llx  = 90abcdef
g_Control->Output(DEBUG_OUTPUT_NORMAL, "I64d = %I64d\n", 0x1234567890abcdef); // I64d = 1311768467294899695
g_Control->Output(DEBUG_OUTPUT_NORMAL, "I64u = %I64u\n", 0x1234567890abcdef); // I64u = 1311768467294899695
g_Control->Output(DEBUG_OUTPUT_NORMAL, "I64x = %I64x\n", 0x1234567890abcdef); // I64x = 1234567890abcdef

また、Rust 版の windows-rs はエクスポートされている Output 系関数が可変長引数に対応していないため、% を含む可能性のある文字列は自分でエスケープする必要があります。

/// `%` をエスケープする
fn escape<S: AsRef<str>>(s: S) -> String {
    s.as_ref().replace('%', "%%")
}

fn func() {
    dlogln!(escape("bar%foo\n")); // エスケープしないと %f がフォーマットされる
}

COM ポインタについて

今までのコードは COM 周りの話を簡単化するために生ポインタで実装してきました。 WinDbg 自体には直接関係の無い COM 周りの話ですが、既存の拡張機能やフレームワークを触る以上、簡単には知っておいたほうが良い COM ポインタの扱い方について触れておきます。

DbgEng 等の COM インターフェース取得処理では、以下のように生で COM ポインタを管理していました。 この方法だとうっかり Release し忘れてリークするであろうことは想像に難くありません。

// 生ポインタ。古くて危険
#include <DbgEng.h>

HRESULT Func(IDebugClient* Client)
{
    IDebugControl* Control = nullptr;
    IDebugRegisters* Registers = nullptr;
    HRESULT hr;
    hr = Client->QueryInterface(__uuidof(IDebugControl), (PVOID*)Control);
    if(FAILED(hr)) return hr;
    hr = Client->QueryInterface(__uuidof(IDebugRegisters), (PVOID*)Registers);
    if(FAILED(hr)) return hr; // BUG: ここで FAILED すると Control が解放されない

    // 何らかの処理

    // 使い終わったら解放
    Control->Release();
    Registers->Release();

    return S_OK;
}

WRL の COM 用スマートポインタである ComPtr でラップすると、簡潔に記述できます。 また、WRL のデストラクタで自動的に Release されるため、リークを心配せずに済みます。 QueryInterface でいちいち UUID を指定せずとも ComPtr::As で簡単にキャストできるのも良い点です。

// ComPtr を使用した方法。ただし、これも古い
#include <wrl.h>
#include <wrl/client.h>
#include <wrl/implements.h>
#include <wrl/module.h>
HRESULT Func(Microsoft::WRL::ComPtr<IDebugClient> Client)
{
    Microsoft::WRL::ComPtr<IDebugControl> Control = nullptr;
    Microsoft::WRL::ComPtr<IDebugRegisters> Registers = nullptr;
    HRESULT hr;
    hr = Client.As(&Control);
    if(FAILED(hr)) return hr;
    hr = Client.As(&Registers);
    if(FAILED(hr)) return hr; // ここで FAILED になっても Control は自動で解放される

    // 何らかの処理

    // デストラクタで Control と Registers は自動解放
    return S_OK;
}

しかし、今となっては WRL も古く、WRL から C++/WinRT に移植する方法が公開されています。 今は WinRT を使用するのがおすすめです。 WinRT はクロスプラットフォームで COM を扱うための便利な機能が入っているため、他言語で実装するときも知識を再利用できます。 COM ポインタは com_ptr::as で管理できます。 WRL の ComPtr::As と異なり、com_ptr::as は HRESULT を返すのではなく、失敗時にエラーを送出するようになっています。

// 今どきの書き方
#include <winrt/base.h>

HRESULT Func(winrt::com_ptr<IDebugClient> Client)
{
    winrt::com_ptr<IDebugControl> Control = Client.as<IDebugControl>(); // 失敗すると例外を送出する
    winrt::com_ptr<IDebugRegisters> Registers = Client.as<IDebugRegisters>();

    // 何らかの処理

    // デストラクタで Control と Registers は自動解放
    return S_OK;
}

.settings load で config.xml を読み込んだ時に出てくる loaded successfully というメッセージは、あくまでも config.xml を XML としてパースできた程度の情報しか持ちません。 マニフェスト内のタグ名や属性名を間違えていたりしても上記のように出力され、単に何も読み込まれない状態になります。

詳細なログは %LOCALAPPDATA%\DBG\Logs フォルダ内のログに出力されます。 不正なマニフェストの場合、Read XML manifest error: ... というログが出てきます。 ただし、このログが出てくるのは再起動時のみです。 .settings load.settings save はあくまでも config.xml を読み込んで WinDbg の設定ファイルである DbgX.xml に書き込むだけであり、保存した設定の内容が反映されるのは再起動時です。 ログを出す場合は .restart してください。

例えば、以下のようなエラーログが出てきます。

# Type="Engine" を "Engin" にしていた場合
Error : DbgX.Services.dll : DbgEng : Read XML manifest error: Manifest C:\regstr\Manifest\Manifest.1.xml, failed to parse component target attribute: 'Engin'.
# Architecture="amd64" を "x64" にしていた場合
Error : DbgX.Services.dll : DbgEng : Read XML manifest error: Manifest C:\regstr\Manifest\Manifest.1.xml, failed to parse architecture attribute: 'x64'.
# コンポーネント名と DLL 名が異なる場合
Error : DbgX.Services.dll : DbgEng : Read XML manifest error: Manifest C:\regstr\Manifest\Manifest.1.xml, file name 'ext\regstr_cpp.dll' does not match the component name 'regstr'
# タグ名を誤っていた場合
Error : DbgX.Services.dll : DbgEng : Read XML manifest error: Manifest C:\regstr\Manifest\Manifest.1.xml, unexpected parse error at line:17, position:86.

ただし、いくつかログにも出てこないケースがあります。 読み込みには成功しているのに Read XML manifest error が出てこない場合はまず以下を疑いましょう。

  • config.xmlLocalCacheRootFolderValue に誤ったパスを指定していた場合
  • コンポーネント名と DLL 名を誤っており、かつ誤ったコンポーネント名と DLL 名が一致していた場合
    • 例えば <BinaryComponent Name="regstr_cp" ...><File Module="regstr_cp.dll" ...> にしていた場合

おわりに

WinDbg コマンド拡張機能の作り方を紹介しました。

結論としては、EngExtCpp をベースに実装するのが一番楽でしょう。 WdbgExts は古くて機能が少なく、DbgEng は単体だと少々使いづらいです。

ただし、機能が足りない場合はフレームワーク自体を改造することになります。 EngExtCpp も実態は簡易的なラッパーであり、外れたことをする場合は結局 DbgEng を直接操作することになります。 拡張機能を今後もたくさん作りたいのであれば、DbgEng を直接使用してフレームワークごと自作したほうが賢明でしょう。

今回作成した拡張機能は説明の都合上、簡素な作りにしています。 そのため、機能不足だったり、使いづらかったり、保守性の低い箇所が多々あります。

今回の拡張機能も文字列の最大バイト数 N を固定でなく引数で指定できるようにしたり、レジスタを x86, Arm に対応できればより使いやすくなるでしょう。

次回は UI 拡張機能の作り方を紹介します。

エンジニア募集

FFRIセキュリティではサイバーセキュリティに関する興味関心を持つエンジニアを募集しています。 採用に関してはこちらをご覧ください。

*1: ドキュメントにはバージョン情報もグローバル変数にコピーしなければならないとありますが、ExtensionApis と異なり extern されるわけではないため、今のところは使わないのなら無視しても問題ありません。

*2: WdbgExts には ExtensionApis の関数ポインタを呼び出すためのマクロが用意されており、これもその一部です。

*3: COM API の宿命です。インターフェース拡張時に下位互換性を維持するためこうなっています。

*4: 実は値を代入しなくても動作するようです。とはいえ、その際の挙動は Undocumented であるため、代入しておいたほうが良いでしょう。

*5: コマンド関数の返り値は S_OKDEBUG_EXTENSION_CONTINUE_SEARCH 以外無視されるようなので、エラーコードでなく S_OK を返しても結局同じです。

*6: この URL をよく見ると extengcpp になっています。私もよく EngExtCpp か ExtEngCpp か DbgEngCpp かわからなくなるのですが、中の人も同じようです。

*7: 昔の C++ で実装されており、今の Visual Studio (MSVC) だとエラーや警告の出る書き方が残っています。

*8: .unload で拡張機能をアンロードする前に呼ばれる関数

*9: デバッグセッションの状態変化時に呼ばれる関数

*10: col タグと言えば HTML で table の列を制御する column を連想しますが、DML の col は color の col です。




以上の内容はhttps://engineers.ffri.jp/entry/2026/03/30/000000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14