はじめに
基礎技術研究部の末吉です。
WinDbg は Windows アプリケーションやカーネルのデバッグ、ダンプ解析等で有用なツールです。 しかし、標準で用意されているコマンドだけではいくつか不足している機能があります。
そこで、本シリーズでは全 3 編に分けて WinDbg 拡張機能 (WinDbg Extension) の作り方を紹介します。 今回はコマンド拡張機能の実装を数通り紹介します。
結論を言えば、簡易的なコマンド拡張であれば EngExtCpp をベースに実装するのが一番簡単でしょう。
なお、WinDbg や WinDbg Script, および一部を除く WinDbg とは直接関係のない用語等については説明しません。 実際に作りたい方は必要に応じて検索してください。
今回実装した拡張機能は GitHub にて公開しています。 なお、今後の記事で説明する予定の拡張機能のソースコードも既に公開しています。予習されたい方はご覧ください。
- はじめに
- 拡張機能を実装する理由
- 事前準備
- 拡張機能の作成
- WdbgExts による実装
- DbgEng による実装
- EngExtCpp による実装
- dbgeng-rs での実装
- Extension Gallery への登録
- その他
- おわりに
- エンジニア募集
拡張機能を実装する理由
WinDbg は WinDbg Script で柔軟に機能を拡張できますが、いくつか不足している機能もあります。 WinDbg Script でなく拡張機能として実装する主な理由は以下が挙げられます。
- 高速化
- 大規模な拡張開発
- 各言語のモジュールの利用
- 例えば、Rust で実装すれば Rust のクレートを使用可能
- コールバックの利用
- メモリが変化したら特定の処理を行うなど
- UI 拡張
例えば、今回実装するコマンド拡張程度であれば WinDbg Script でも事足りますが、次回紹介する UI 拡張は WinDbg Script では作れません。
事前準備
- WinDbg のインストール
- Windows SDK のインストール
本記事で使用したツールのバージョンは以下の通りです。
- Windows 11
- WinDbg 1.2601.12001.0
- Windows SDK 10.0.26100.7627
- Visual Studio 2026 18.3.2
拡張機能の作成
拡張機能は DLL で実現できます。 基本的には必須の関数とコマンド用の関数をエクスポートした DLL を作るだけで動作します。
拡張機能を実装するための公式 API として、WdbgExts と DbgEng があります。 WdbgExts は昔からある C API です。 DbgEng は COM API であり、今でも更新され続けています。 DbgEng は WdbgExts よりも多機能であるため、特別な理由がなければ拡張機能の新規作成には DbgEng をおすすめします。
本記事では、レジスタから値を取得し、値が文字列へのポインタらしければ文字列として表示する regstr コマンド拡張機能を実装します。 コマンド拡張は WdbgExts, DbgEng, EngExtCpp, dbgeng-rs を使用して C, C++, Rust で実装し、それぞれの特徴を説明します。 なお、今回実装する拡張機能は簡単化のため x64 のみを対象とします。
完成形は以下の通りです。
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 型のグローバル変数を用意します。
MajorVersion と MinorVersion は自作した拡張機能自身のバージョン情報を入れます。
お好みのバージョン番号を入れて構いません。
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; }
MajorVersion と MinorVersion には 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 の場合、dwCurrentPc が ULONG として扱われてしまい、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 に指定した文言が表示されるはずです。
拡張機能のバージョンも確認しておきましょう。
バージョンは .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
ビルドしたら先ほどと同様の手順で動作を確認しましょう。
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.h と DbgEng.h をインクルードします。
今回は std::string も使用するため、string もインクルードしておきます。
N と REG_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; }
DebugExtensionInitialize は Version と Flags へのポインタ引数を受け取るため、適宜値を入れて返します*4。
Version は拡張機能自身のバージョンです。WdbgExts の MajorVersion や MinorVersion と同じです。
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 の下位レジスタ eax や al なども INT64 扱いになります (当然、取得できる値は各レジスタのビット幅に制約されますが)。
DEBUG_VALUE から値を取り出す際には、先に Type を見て対応する共用体の変数を選択する必要があります。
例えば Type = DEBUG_VALUE_INT32 であれば I32 変数のみ正常に使用できます。
もしそれ以外の変数を用いたい場合は IDebugControl::CoerceValue で型を強制することで、安全に使用できるようになります。
CoerceValue は IDebugRegisters でなく IDebugControl の関数であるため、注意しましょう。
なお、共用体の性質上、INT64 でも I32 などへのダウンキャストアクセスであればおそらく問題ないと思いますが、この際の挙動は Undocumented です。
特定の条件で失敗する可能性もあるため、万全を期してダウンキャストでも CoerceValue するか (unsigned int)I64 で I64 をキャストしたほうが良いでしょう。
今回は 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 と同様にビルド・動作確認し、正しい結果が得られることを確認して完成です。
EngExtCpp による実装
C++ の場合は DbgEng のラッパーフレームワークである EngExtCpp *6が用意されており、それを使用することで実装しやすくなります。
では、EngExtCpp で実装していきましょう。
プロジェクト名は regstr_cpp2 としています。
engextcpp のビルド
EngExtCpp はソースコードからビルドします。
Windows SDK のパス %PROGRAMFILES(X86)%\Windows Kits\<バージョン>\Debuggers\inc に engextcpp.hpp と engextcpp.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
意図した結果になっていれば完成です。
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 }
escape と show_reg_str を実装します。
escape 関数については Output におけるフォーマット文字列の注意点 を参照してください。
DebugClient の reg64 で、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 して動作を確認しましょう。
Extension Gallery への登録
拡張機能は完成しましたが、毎回 .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) で指定します。
IsEnabled は true を指定します。読み込ませたくない場合はこれを 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 つだけです。
BinaryComponent の Name 属性は DLL と同じ名前 (case-insensitive)、Type は Engine を記述してください。
Files は読み込むファイルの情報を指定します。File の Architecture はどのアーキテクチャでも機能するのであれば Any を入れます。
今回は amd64 (x64) でのみ動作するため、amd64 を入れます。x64 はエラーになります。x86 のみの場合は x86 で大丈夫です。
次に、パスの指定方法を FilePathKind に記述します。RepositoryRelative を指定すると、config.xml からの相対パスで指定できるようになります。
Module には DLL の相対パスを記述します。
次は EngineCommands を記述します。ここには DLL のコマンドを記述していきます。
EngineCommand の Name はコマンド名を記述します。
EngineCommandItem の Syntax は構文、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 で設定を保存してください。
その後 .restart すると !regstr が使えるようになっているはずです。
また、WinDbg を再起動してもすぐにコマンドが使えるはずです。
!regstr コマンドを使用できない場合は Extension Gallery マニフェストのデバッグを参照してください。
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%\DBG に UserExtensions フォルダを作成します。
次に、Manifest.xml (ファイル名はなんでも構いません) を作成します。
これは先ほどの Manifest.1.xml とほぼ同じですが、File の Module に書いたパスがカレントフォルダの DLL を指すようになっています。
この xml ファイルの ExtensionPackage 内の Name (つまり、regstr) を UserExtensions 直下に作成するフォルダ名にし、Version の値 (つまり、1.0.0.0) を regstr フォルダの直下に作成するフォルダ名にします。
File の Module に指定したパスは、その 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 上でソースコードデバッグしたい場合は以下の手順で行えます。
- DLL をビルド
- WinDbg を起動
- WinDbg で適当なアプリケーションのデバッグを開始
- Visual Studio のタブから
デバッグ -> プロセスにアタッチ - 「プロセスにアタッチ」ウィンドウで
EngHost.exeを検索し、選択してアタッチ - 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 コマンドが実行されます。
特殊記号は HTML と同じようにエスケープできます。
例えば、<> をタグとして判定されたくない場合は < の代わりに < を使用してください。
また、DML を使用しない場合は Output を使用したほうが良いでしょう。
また、col タグを使用すれば文字と背景の色を制御できます*10。
fg は文字色、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 などはテーマによって色が変わるだけの通常色です。
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; }
Extension Gallery マニフェストのデバッグ
.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.xmlのLocalCacheRootFolderのValueに誤ったパスを指定していた場合- コンポーネント名と 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_OK と DEBUG_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 です。 ↩










