FFRIエンジニアブログ

FFRIセキュリティのエンジニアが執筆する技術者向けブログです

WinDbg 拡張機能の作り方 2 ~ UI 拡張編

はじめに

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

前回は regstr コマンド拡張機能を実装しました。 !regstr コマンドを入力するだけで各レジスタ値に対応する文字列を出してくれますが、いちいちコマンドを入力するのは面倒です。 そこで、ステップ実行するたびに現在のレジスタ値と文字列を表示する RegStr UI の実装を通じて UI 拡張機能の作り方を説明します。 また、おまけでレジスタ値の書き換えも実装します。 今回の拡張機能は x64 アプリケーションのデバッグのみ対象とします。

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

RegStr UI

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

今回実装した拡張機能は GitHub にて公開しています。

事前準備

本記事で使用したツールのバージョンは以下の通りです。 なお、WinDbg 1.2603.20001.0, DbgX 20260320.1.0 の組み合わせでも動作することを確認済みです。

  • Windows 11
  • WinDbg 1.2601.12001.0
  • DbgX 20260112.1.0
  • Windows SDK 10.0.26100.7627
  • Visual Studio 2026 18.5.2

UI 拡張

WinDbg の UI は .NET, WPF, MEF を使用して実装されています。 MEF は Managed Extensibility Framework の略で、名前の通り拡張性を持たせるためのフレームワークです。 これを利用することで、外部の DLL から WinDbg の UI を簡単に拡張できます。

ここからは UI 拡張の作り方を説明します。 なお、上記の理由により、UI 拡張の実装には .NET, WPF, MEF の知識が必要です。必要に応じて検索してください。

ただし、本記事執筆時点で UI 拡張は Undocumented です。 非公式であれば、2017 年に kevingosse 氏の windbg-extensions で説明されています。 しかし WinDbg も更新され続けており、一部インターフェースが今は使用できなくなっています。 そのため、本記事もまた WinDbg のバージョンアップで機能しなくなる可能性が大いにあります。 その点に留意しながら実装してください。

ここでは、WinDbg 1.2601.12001.0 で動作する WinDbg UI 拡張機能を実装していきます。 プロジェクト名は RegStr とします。

UI 拡張機能プロジェクトの準備

さて、Visual Studio で作り始める前に、DbgX について調べましょう。 Microsoft.Debugging.Platform.DbgX は WinDbg が使用している DbgEng のラッパーであり、NuGet で公開されています。

そのため後で NuGet から最新版をインストールするのですが、この時に DbgX のサポートバージョンを確認しておきます。DbgX の最新版 20260112.1.0net10.0-windows10.0.17763 のみサポートしています。

これを確認したら Visual Studio を起動して作り始めましょう。

まずは「WPF ユーザーコントロールライブラリ」を作成してください。「WPF クラスライブラリ」や「WPF ユーザーコントロールライブラリ (.NET Framework)」ではありません。 言語はどれでも構いませんが、本記事では C# を使用します。

プロジェクトの選択

「フレームワーク」は DbgX が使用している .NET バージョンを指定します。 WinDbg 1.2601.12001.0 は .NET 10 を使用しているため、フレームワークのバージョンも .NET 10 にします。なお、.NET 10 は Visual Studio 2026 以上でないとサポートされないため、注意してください。

次はプロジェクトのプロパティからターゲット OS バージョンを設定します。 DbgX のサポートバージョンが net10.0-windows10.0.17763 であるため、「ターゲット OS」をそれ (10.0.17763) 以上に設定してください。 今回のプロジェクトでは 10.0.26100.0 を設定しています。

ターゲット OS バージョンの設定

プロジェクトを作成したら、次に依存関係を追加します。

以下のような依存関係になっていれば OK です。

依存関係の追加

タブグループへのボタン追加

では実装に取り掛かっていきます。 まずは以下のように、View リボンタブの Windows タブグループに RegStr ボタンを追加してみましょう。

RegStr ボタンの表示

WPF の UI は MVVM パターンによる実装が主流であり、WinDbg の UI も MVVM を使用しています。 そのため、本記事も MVVM で実装することとします。

「ユーザーコントロール (WPF)」を RegStrTabButtonControl.xaml という名前で作成し、次に「クラス」ファイル RegStrTabButtonViewModel.cs を作成します。 ちなみにデフォルトで作成される UserControl1.xaml および UserControl1.xaml.cs は使用しないため削除して構いません。もしくはこれをリネームして RegStrTabButtonControl.xamlRegStrTabButtonControl.xaml.cs にしても構いません。

まずは、ビューモデル RegStrTabButtonViewModel.cs を実装しましょう。

using System.ComponentModel.Composition;
using System.Windows;
using DbgX.Interfaces;

namespace RegStr;

/// <summary>
/// リボンタブに追加するボタンのビューモデル。
/// 常に右に配置するため、order を小さい値に設定。
/// </summary>
[Export(typeof(IDbgRibbonTabGroupExtension))]
[RibbonTabGroupExtensionMetadata("ViewRibbonTab", "Windows", -1000)]
internal class RegStrTabButtonViewModel : IDbgRibbonTabGroupExtension
{
    // ツールウィンドウを開くために使用
    [Import] private IDbgToolWindowManager? _toolWindowManager = null;

    // リボンタブグループにボタンを追加する
    public IEnumerable<FrameworkElement> Controls =>
        new List<FrameworkElement>
        {
            new RegStrTabButtonControl(this)
        };

    public RegStrTabButtonViewModel()
    {
        OpenRegStrToolWindow = new DelegateCommand(delegate
        {
            // RegStrToolWindow を開く
            _toolWindowManager?.OpenToolWindow("RegStrToolWindow");
        });
    }

    public DelegateCommand OpenRegStrToolWindow { get; }
}

IDbgRibbonTabGroupExtension がタブにコントロール (ユーザーコントロール)を追加するインターフェースです。 これを実装したクラスに Export 属性を付与して、MEF のオブジェクトとしてエクスポートします。 エクスポートすると WinDbg がこのクラスを発見できるようになります。

RibbonTabGroupExtensionMetadata はどこにコントロールを追加するか設定する属性です。定義は以下の通りです。

public RibbonTabGroupExtensionMetadataAttribute(
    // タブ名
    string extendedTabName,
    // タブグループ名
    string extendedGroupName,
    // 優先度
    int order)

他にも UI を拡張するためのインターフェースと属性があります。 これらは後で触れます。

インターフェース 属性 機能
IDbgRibbonTab RibbonTabMetadata リボンタブにタブアイテムを追加
IDbgRibbonTabExtension RibbonTabExtensionMetadata タブアイテムにタブグループを追加
IDbgRibbonTabGroupExtension RibbonTabGroupExtensionMetadata タブグループにコントロールを追加
IDbgToolWindow NamedPartMetadata ツールウィンドウの実装

extendedTabName リボンタブの extendedGroupName タブグループにコントロールを追加します。

View リボンタブの名前は ViewRibbonTab です。既存リボンタブ名は以下の通りです。 既存のリボンタブの表示名と実際の名前はおおむね *RibbonTab という命名規則に従っていますが、Model と Scripting だけ異なります。

タブグループ名は WinDbg の UI に表示されている名前をそのまま使用できます。

表示名 名前 備考
Home HomeRibbonTab
View ViewRibbonTab
Breakpoints BreakpointsRibbonTab
Time Travel TimeTravelRibbonTab
Model DmoToolWindowRibbonTab 命名規則外
Scripting ScriptRibbonTab 命名規則外
Source SourceRibbonTab
Memory MemoryRibbonTab
Extensions ExtensionsRibbonTab
Command CommandRibbonTab Command 表示で出現
Notes NotesRibbonTab Notes 表示で出現

order は優先度で、値が大きいものほど先 (左) に配置され、小さいものは後 (右) に配置されます*1。 ただし、View の Windows にあるものはすべて組み込みのボタンであるため、order を大きくしてもそれらより左には入れられないようです。

order

次に、IDbgRibbonTabGroupExtension をクラスに実装します。 このインターフェースは Controls プロパティを要求します。定義は以下の通りです。

public interface IDbgRibbonTabGroupExtension
{
    // 追加するコントロールを返すプロパティ
    IEnumerable<FrameworkElement> Controls { get; }
}

Controls にはユーザーコントロールを入れます。先ほど作成だけした RegStrTabButtonControl コントロールを追加しましょう。 なお、コントロールは複数配置できます。

後でボタンクリック時に呼び出す OpenRegStrToolWindow コマンドは DelegateCommand 型のプロパティとして実装します。 DelegateCommand, DelegateCommand<T>CommunityToolkit.MvvmRelayCommand を DbgX 側で再実装したようなクラスで、RelayCommand と同じように扱えます。 また、非同期版の AsyncDelegateCommand もあります。

次は IDbgToolWindowManager をインポートします。 これは WinDbg のツールウィンドウ (後述) 全体を管理するマネージャーインターフェースです。 Import 属性を使用して、WinDbg 側でそのインターフェースを実装したインスタンスを得ます。 そして、そのインスタンスの OpenToolWindow 関数で、指定したコントロールをツールウィンドウとして開きます。

OpenToolWindow の定義は以下の通りです。

IDbgToolWindowInstance OpenToolWindow(
    // ツールウィンドウの名前
    string name,
    // 必要に応じてツールウィンドウのクラスに渡すデータ
    object parameter = null);

name にはツールウィンドウの名前を指定します。RegStrToolWindow は後で作成します。 parameter は今回は使用しません。

これでビューモデルはひとまず完成です。次はビュー RegStrTabButtonControl.xaml および RegStrTabButtonControl.xaml.cs を実装します。

まずは RegStrTabButtonControl.xaml を実装しましょう。

<UserControl x:Class="RegStr.RegStrTabButtonControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             d:DataContext="{d:DesignInstance local:RegStrTabButtonViewModel}"
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Button Content="RegStr" HorizontalAlignment="Left" VerticalAlignment="Top" Height="54" Width="54"
                Command="{Binding OpenRegStrToolWindow}" />
    </Grid>
</UserControl>

これ自体は単なる WPF の XAML であり、WinDbg 固有のものではありません。 Grid の中に RegStr と書いた 54x54 のボタンを左上に配置しただけです。 このボタンをクリックすると OpenRegStrToolWindow コマンドが呼ばれるようになっています。

次は RegStrTabButtonControl.xaml.cs を実装します。

using System.Windows;
using System.Windows.Controls;

namespace RegStr;

/// <summary>
/// RegstrTabButtonControl.xaml の相互作用ロジック
/// </summary>
public partial class RegStrTabButtonControl : UserControl
{
    public RegStrTabButtonControl(RegStrTabButtonViewModel viewModel)
    {
        DataContext = viewModel;
        InitializeComponent();
    }
}

対応するビューモデルである RegStrTabButtonViewModel をコンストラクタで DataContext に設定しています。

UI 拡張 DLL の配置と動作確認

ここまで実装できたら一旦動作確認しましょう。 いつも通りに DLL をビルドします。

そして WinDbg を起動していつも通り .load ... してもコントロールは追加されません。 WinDbg の UI は起動時に設定されます。後で読み込んでも意味がありません。

UI 拡張 DLL は %LOCALAPPDATA%\DBG\UIExtensions フォルダに格納します。 ここに格納した DLL は起動時に自動で読み込まれます。 最初は UIExtensions フォルダが存在しないため、作成しておきましょう。

UIExtensions フォルダを作成したら、その中にビルドした RegStr.dll を入れます。 依存関係の DLL もビルドディレクトリにコピーされている場合は、それらは入れず RegStr.dll のみを入れてください。*2

UIExtensions への配置

DLL を格納後 WinDbg を起動し View タブを見てみると、無機質な RegStr ボタンが Command browser の隣に表示されているはずです。

RegStr ボタンの表示

RegStr ボタンを押すと _toolWindowManager?.OpenToolWindow("RegStrToolWindow") が実行されますが、そもそも RegStrToolWindow をまだ用意していないため、何も起こりません。

ツールウィンドウの実装

ツールウィンドウとは、WinDbg で結合したり分離したりできるウィンドウを指します。 今度は、RegStr ボタンをクリックすると以下の RegStr ツールウィンドウが出現するように実装します。

RegStr ツールウィンドウ

RegStrToolWindow の実装に取り掛かりましょう。 ボタンを作成した時と同様に、ユーザーコントロールで RegStrToolWindowControl.xaml, クラスで RegStrToolWindowViewModel.cs を作成します。

まずビューモデル RegStrToolWindowViewModel.cs を実装します。

using System.ComponentModel.Composition;
using System.Windows;
using DbgX.Interfaces;

namespace RegStr;

/// <summary>
/// RegStrToolWindow のビューモデル。
/// </summary>
[Export(typeof(IDbgToolWindow))]
[NamedPartMetadata("RegStrToolWindow")]
public class RegStrToolWindowViewModel : IDbgToolWindow
{
    public FrameworkElement GetToolWindowView(object parameter)
    {
        // ツールウィンドウを新規作成して返す
        return new RegStrToolWindowControl(this);
    }
}

ツールウィンドウのビューモデルには IDbgToolWindow のエクスポートおよび継承をさせます。 IDbgToolWindow はツールウィンドウのインターフェースです。 リボンタブへの追加では IDbgRibbonTabGroupExtension を使用したのと同様に、ツールウィンドウではこのインターフェースを使用します。 NamedPartMetadata 属性を使用してツールウィンドウの名前を指定します。先ほど OpenToolWindow で指定した名前 RegStrToolWindow は、この属性で指定した名前です。

IDbgToolWindow の定義は以下の通りです。

public interface IDbgToolWindow
{
    // ツールウィンドウコントロールを返す
    FrameworkElement GetToolWindowView(
        // 必要に応じてツールウィンドウのクラスに渡すデータ (OpenToolWindow の parameter が入る)
        object parameter
    );
}

このインターフェースは GetToolWindowView 関数のみを持ちます。 ツールウィンドウを開くとこの関数が呼ばれ、返り値としてツールウィンドウのクラスが返ります。 今回は RegStrToolWindowControl インスタンスを作成して返します。

次は RegStrToolWindowControl.xaml です。

<ui:ToolWindowView
    x:Class="RegStr.RegStrToolWindowControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:ui="clr-namespace:DbgX.Interfaces.UI;assembly=DbgX.Interfaces"
    mc:Ignorable="d"
    ui:ToolWindowView.IsWindowPersisted="true"
    d:DesignHeight="450" d:DesignWidth="800">
    <ui:ToolWindowView.TabTitle>
        <!-- ツールウィンドウのタイトルとツールチップを設定 -->
        <ui:ToolWindowTitle
            Title="RegStr"
            ToolTip="Show strings from register values." />
    </ui:ToolWindowView.TabTitle>
    <TextBlock Text="My RegStr Tool Window!" />
</ui:ToolWindowView>

ツールウィンドウは UserControl を直接使用せず、ToolWindowView クラスを使用します。

ToolWindowViewUserControl を継承したクラスで、WinDbg の他のウィンドウと連携するための UI を持っています。

ui:ToolWIndowView.TabTitle タグでウィンドウのタイトルを RegStr に設定します。 ToolTip はウィンドウにマウスカーソルを載せた時に表示される簡単な説明を記したポップアップです。 ToolTip を記述しておけばツールチップを表示できます。

中身はとりあえず、ツールウィンドウの中に文字列を表示する TextBlock を配置しただけです。

次は RegStrToolWindowControl.xaml.cs の実装です。

namespace RegStr
{
    /// <summary>
    /// RegStrToolWindowControl.xaml の相互作用ロジック
    /// </summary>
    public partial class RegStrToolWindowControl
    {
        public RegStrToolWindowControl(RegStrToolWindowViewModel viewModel)
        {
            DataContext = viewModel;
            InitializeComponent();
        }
    }
}

タブボタンと同様に、ビューモデルを DataContext に与えます。

ここまでできたら、再度動作確認をしましょう。UIExtensions にビルドした RegStr.dll のみをコピーし、WinDbg を起動します。 RegStr ボタンをクリックしてツールウィンドウが表示されれば OK です。 他のツールウィンドウと同様に RegStr ツールウィンドウを自由に動かしたり、結合したりできることも確認できるはずです。

RegStr ツールウィンドウの表示

リボンタブへのタブアイテム追加

さて、ここまでで View タブ内にボタンを配置してきました。 今はボタンが 1 つだけですが、色々な機能を増やしたくなった場合、大量の独自ボタンで既存のタブを占領するのは少々気が引けます。 そこで、Register string リボンタブを実装して、RegStr ボタンもそこに配置するようにしてみましょう。 なお、ここは飛ばしてデータバインディングの実装に移っても構いません。

独自タブへボタンを移動

いつも通り、RegStrRibbonTabControl.xamlRegStrRibbonTabViewModel.cs を作成します。

RegStrTabItemViewModel.cs を実装します。

using System.ComponentModel.Composition;
using System.Windows.Controls;
using DbgX.Interfaces;

namespace RegStr;

[Export(typeof(IDbgRibbonTab))]
[RibbonTabMetadata("RegStrRibbonTab", 5)]
internal class RegStrRibbonTabViewModel : IDbgRibbonTab
{
    // RegStrRibbonTabControl を返す
    public Control Tab => new RegStrRibbonTabControl();
}

IDbgRibbonTab がリボンタブにアイテムを追加するインターフェースです。 Tab プロパティで返されるインスタンスがタブに追加されます。

public interface IDbgRibbonTab
{
    // 追加するタブ
    Control Tab { get; }
}

RibbonTabMetadata の定義は以下の通りです。 RibbonTabGroupExtensionMetadata と同様、name にリボンタブ名、order に優先度 (大きいほど先) を指定します。

public RibbonTabMetadataAttribute(
    // リボンタブ名
    string name,
    // 優先度
    int order = 0
)

次は RegStrRibbonTabControl.xaml の実装ですが、リボンタブは Fluent.Ribbon を使用して実装しなければなりません。 TabControl 型になっていますが、ただの Control (例えば WPF 標準のリボンタブである ribbon:RibbonTab) を返しても無視されてしまい、追加されません。

ということで、まずは依存関係を追加します。Fluent.Ribbon の最新版を NuGet からインストールしましょう。

Fluent の追加

RegStrRibbonTabControl.xaml を実装します。

<fluent:RibbonTabItem
    x:Class="RegStr.RegStrRibbonTabControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:fluent="urn:fluent-ribbon"
    mc:Ignorable="d" 
    Header="Register string">
    <fluent:RibbonGroupBox Header="Register"></fluent:RibbonGroupBox>
</fluent:RibbonTabItem>

RibbonTabItemHeader 属性がタブの表示名、RibbonGroupBoxHeader 属性がタブグループの表示名になります。

次に RegStrRibbonTabControl.xaml.cs を実装します。 といってもテンプレートから UserControl の継承を消しただけです。

namespace RegStr;

/// <summary>
/// RegStrTabItem.xaml の相互作用ロジック
/// </summary>
public partial class RegStrRibbonTabControl
{
    public RegStrRibbonTabControl()
    {
        InitializeComponent();
    }
}

では、動作確認に移りましょう。 ビルドして UIExtensions に移して WinDbg を起動します。 Register string タブができていて、Register タブグループが存在していることを確認しましょう。 といっても、今は何も要素がないため R… になってしまっていますが、独自のタブを追加できました。

リボンタブの追加

後は RegStr ボタンをここに移すだけです。 これは簡単で、RegStrTabButtonViewModel.csRibbonTabGroupExtensionMetadata の行を以下に書き換えるだけです。

[RibbonTabGroupExtensionMetadata("RegStrRibbonTab", "Register", 0)]

ちゃんとボタンが移動したか確認しましょう。

独自タブへボタンを移動

タブグループの追加

Extensions リボンタブに RegStr グループを追加します。

タブグループは IDbgRibbonTabExtensionRibbonTabExtensionMetadata で追加できます。 こちらのコントロールは Fluent.Ribbon の RibbonGroupBox でなければなりません。 実装方法はリボンタブとほぼ変わらないため、実装コードのみに留めます。

RegStrExtensionTabGroupViewModel.cs:

using System.ComponentModel.Composition;
using System.Windows.Controls;
using DbgX.Interfaces;

namespace RegStr;

/// <summary>
/// タブグループの追加。
/// Extensions リボンタブにグループを追加する。
/// </summary>
[Export(typeof(IDbgRibbonTabExtension))]
[RibbonTabExtensionMetadata("ExtensionsRibbonTab", 1000)]
internal class RegStrExtensionTabGroupViewModel : IDbgRibbonTabExtension
{
    public Control TabGroup => new RegStrExtensionTabGroupControl();
}

RegStrExtensionTabGroupControl.xaml:

<fluent:RibbonGroupBox
    x:Class="RegStr.RegStrExtensionTabGroupControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:fluent="urn:fluent-ribbon"
    mc:Ignorable="d"
    Header="RegStr"
/>

RegStrExtensionTabGroupControl.xaml.cs:

namespace RegStr;

/// <summary>
/// RegStrExtensionTabGroupControl.xaml の相互作用ロジック
/// </summary>
public partial class RegStrExtensionTabGroupControl
{
    public RegStrExtensionTabGroupControl()
    {
        InitializeComponent();
    }
}

ボタンの見栄えを良くする

無機質なボタンを 1 個だけ表示させるのは不格好です。良い感じにしましょう。

Fluent.Ribbon の Button を使うと、組み込みのボタンと同じ見栄えになって馴染めます。

RegStrTabButtonControl.xaml を修正します。

<fluent:Button
    x:Class="RegStr.RegStrTabButtonControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:fluent="urn:fluent-ribbon"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:RegStr"
    mc:Ignorable="d"
    d:DataContext="{d:DesignInstance local:RegStrTabButtonViewModel}"
    Header="Register string"
    ToolTip="Show strings from register values."
    LargeIcon="pack://application:,,,/RegStr;component/Resources/regstr_32.png"
    Icon="pack://application:,,,/RegStr;component/Resources/regstr_16.png"
    Size="Large"
    Command="{Binding OpenRegStrToolWindow}" />

Header がアイコンの下に現れるテキスト、LargeIconIcon がアイコン画像です。アイコンを指定しない場合は Header のみ表示されます。 LargeIcon は 32x32, Icon は 16x16 の画像を指定しましょう。16x16 の画像は WinDbg 自体のウィンドウが小さいときのアイコンです。 今回は ChatGPT に CPU のレジスタをイメージして生成してもらった画像を regstr_32.pngregstr_16.png として使用します。

regstr_32.png

regstr_16.png

画像は DLL にリソースとして含めています。

リソース配置

動作確認して、良い感じになったことを確かめましょう。

良い感じのボタン

データバインディングの実装

さて、ここまでは UI の追加方法を説明してきました。 ここからはレジスタ値と文字列を取得してそれを UI に反映させる機能を実装していきます。

まずはデータバインディングによってビューに適当なデータを反映させてみましょう。

まず、RegStrItem.cs を実装します。 レジスタ名、値、文字列を保持するクラスです。

namespace RegStr;

/// <summary>
/// ObservableCollection で管理しやすくするためのタプル代わりのクラス。
/// </summary>
public class RegStrItem(string name, ulong value, string str)
{
    public string Name { get; } = name;

    public ulong Value { get; } = value;

    public string Str { get; } = str;
}

RegStrToolWindowViewModel.cs を以下のように修正します。

データが変化した場合にビューへ反映させるため、INotifyPropertyChanged を付与しています。

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows;
using DbgX.Interfaces;
using DbgX.Interfaces.Enums;
using DbgX.Interfaces.Events;

namespace RegStr;

/// <summary>
///     RegStrToolWindow のビューモデル。
/// </summary>
[Export(typeof(IDbgToolWindow))]
[NamedPartMetadata("RegStrToolWindow")]
public class RegStrToolWindowViewModel : IDbgToolWindow, INotifyPropertyChanged
{
    // ビューに反映させるためのプロパティ
    public ObservableCollection<RegStrItem> RegStrItems
    {
        get;
        set
        {
            field = value;
            // 代入時に反映するようにする
            OnPropertyChanged();
        }
    } = [new("rax", 32, "hello"), new("rbx", 64, "world")];

    public FrameworkElement GetToolWindowView(object parameter)
    {
        // ツールウィンドウを新規作成して返す
        return new RegStrToolWindowControl(this);
    }

    // プロパティ変化時のイベントハンドラ
    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

ビューにバインドするためのプロパティ RegStrItems を用意し、これ経由でデータを変更すると反映されるようにします。 今回はデータがちゃんとバインドできているかの確認であるため、適当なデータを初期値として入れておきます。

次に、RegStrToolWindowControl.xaml を以下のように修正します。

<ui:ToolWindowView
    x:Class="RegStr.RegStrToolWindowControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
    xmlns:ui="clr-namespace:DbgX.Interfaces.UI;assembly=DbgX.Interfaces"
    xmlns:local="clr-namespace:RegStr"
    mc:Ignorable="d"
    ui:ToolWindowView.IsWindowPersisted="true"
    d:DataContext="{d:DesignInstance local:RegStrToolWindowViewModel}"
    d:DesignHeight="450" d:DesignWidth="800">

    <ui:ToolWindowView.TabTitle>
        <!-- ツールウィンドウのタイトルとツールチップを設定 -->
        <ui:ToolWindowTitle
            Title="RegStr"
            ToolTip="Show strings from register values." />
    </ui:ToolWindowView.TabTitle>
    <Grid>
        <ListBox ItemsSource="{Binding RegStrItems}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal" TextElement.FontFamily="Consolas">
                        <TextBlock Margin="0,2,0,2" Text="{Binding Name, StringFormat={}{0,-3}}" />
                        <TextBlock Margin="8,2,0,2" Text="{Binding Value, StringFormat=0x{0:x16}}" />
                        <TextBlock Margin="8,2,0,2">
                            <TextBlock.Style>
                                <Style TargetType="TextBlock">
                                    <Setter Property="Text" Value="{Binding Str, StringFormat='{}&quot;{0}&quot;'}" />
                                    <Style.Triggers>
                                        <!-- Str が空文字列の場合はダブルクオーテーションも表示しないようにする -->
                                        <DataTrigger Binding="{Binding Str}" Value="">
                                            <Setter Property="Text" Value="" />
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </TextBlock.Style>
                        </TextBlock>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</ui:ToolWindowView>

ListBoxRegStrItems をバインドし、TextBlock 3 つで名前、値、文字列を表示させています。

ここまでできたら動作確認を行います。 初期値が RegStr ツールウィンドウに表示されているか確認しましょう。

データバインディングの確認

バインディングを確認できたらテストデータは消しておきましょう。

    // ビューに反映させるためのプロパティ
    public ObservableCollection<RegStrItem> RegStrItems
    {
        get;
        set
        {
            field = value;
            // 代入時に反映するようにする
            OnPropertyChanged();
        }
    } = [];

レジスタ・メモリの取得

後は今までのコマンド拡張で得た知識を総動員し、DbgEng を使用してレジスタ値等を得たいところですが、ここで問題があります。 UI 拡張が動作するスレッドからはターゲット (デバッギー) の DbgEng クライアントを取得できません。 というのも、DbgEng クライアントはスレッド毎に紐付けられているためです。*3 拡張 DLL から WinDbg が使用している DbgEng クライアントを操作する必要がありますが、DbgEng クライアントは UI スレッドとは異なるスレッドが持っています。 しかし、調べた限りでは DbgEng クライアントを UI スレッドへ渡せるような機能は見つかりませんでした。

では WinDbg UI がどのようにしてこれに対処しているかというと、DbgEng クライアントを持つサービス用スレッドとやり取りして操作しています。 UI はインターフェースを通じてサービスに対して非同期的にリクエストを発行し、サービスはそれを処理してデータを UI に返します。

したがって、UI は DbgEng を直接使用せず、サービス経由でターゲットを操作します。 サービスはインターフェースを経由して操作します。

先ほどツールウィンドウを表示させるために IDbgToolWindowManager を使用していましたが、それと同様の方法で各インターフェースのインスタンスをインポートおよび使用します。

インターフェースには以下のようなものが存在します。 本シリーズで説明していないインターフェースにはリンクを付けていません。

名前 機能
IDbgConsole WinDbg コンソール操作
IDbgEngineControl デバッグエンジン操作 (デバッグシンボル・ソース、コードレベル*4設定等)
IDbgEngineQuery デバッガデータモデル操作
IDbgEngineState コードレベル、ターゲットの接続形態*5
IDbgEventBus イベントバス
IDbgMemoryControl メモリ操作
IDbgOutputEvents コンソール出力イベントハンドラ関連
IDbgPlmClient PLM 関連
IDbgScriptControl データモデルスクリプト関連
IDbgSourceLineService デバッグソース関連
IDbgStatusBarText ステータスバーのテキスト操作
IDbgTargetControl ターゲット操作 (式評価、ブレークポイント、スレッド、デバッグシンボル・ソース関連)
IDbgTargetInitialization ターゲット操作 (デバッグ・ダンプ解析開始関連)
IDbgTargetQuery フレーム、スレッド、逆アセンブル関連
IDbgTargetState ターゲット状態関連

さて、上記の一覧を見て「アレ?」と思った方もいらっしゃるでしょう。 メモリ操作はあるのに、今回の拡張機能を作るために一番欲しい機能であるレジスタ操作がありません。 私が見逃している可能性もありますが、調べた限りではレジスタ操作インターフェースは見つかりませんでした。

では既存の Registers ツールウィンドウではどうやってレジスタを取得しているかというと、デバッガデータモデルを使用して取得しています。 デバッガデータモデルは DbgEng が様々なオブジェクトを統一的に扱うためのモデルです。このモデルオブジェクトを経由してレジスタの読み書き等、様々なことができます。 デバッガデータモデルの操作は IDbgEngineQuery で行います。

以降のコードスニペットは RegStrToolWindowViewModel クラス内に実装します。

レジスタ情報を取得・更新する処理を実装しましょう。 RegStrToolWindowViewModel に今回使用するインターフェースをインポートし、NRegNames も定義しておきます。

public class RegStrToolWindowViewModel : IDbgToolWindow, INotifyPropertyChanged
{
    // レジスタを読み取るために使用
    [Import] private IDbgEngineQuery? _engineQuery;

    // メモリを読み取るために使用
    [Import] private IDbgMemoryControl? _memoryControl;

    // ターゲットイベントを捕捉するために使用
    [Import] private IDbgEventBus? _eventBus;

    // 最大バイト数
    private const ulong N = 32;

    // 表示するレジスタ
    internal readonly List<string> RegNames =
    [
        "rax",
        "rbx",
        "rcx",
        "rdx",
        "rdi",
        "rsi",
        "rbp",
        "rsp",
        "r8",
        "r9",
        "r10",
        "r11",
        "r12",
        "r13",
        "r14",
        "r15"
    ];
    (...省略...)
}

C# 版 MakeString 関数を実装します。

    private static string MakeString(byte[]? buf)
    {
        if (buf == null) return "";
        var ret = new StringBuilder(buf.Length);
        foreach (var ch in buf)
        {
            // 0x80 以上は ASCII でない
            if (ch >= 0x80) return ret.ToString();
            if (ch < 0x20)
            {
                // 0x20 未満はエスケープシーケンス
                // 以下のエスケープシーケンス以外は文字とみなさないこととする
                switch (ch)
                {
                    case 0x09:
                        ret.Append("\\t");
                        break;
                    case 0x0a:
                        ret.Append("\\n");
                        break;
                    case 0x0b:
                        ret.Append("\\v");
                        break;
                    case 0x0d:
                        ret.Append("\\r");
                        break;
                    default:
                        return ret.ToString();
                }
            }
            else
            {
                if (ch is 0x22 or 0x5c) ret.Append('\\');
                ret.Append(Convert.ToChar(ch));
            }
        }

        // ここまで来た場合、文字列の途中で切れた可能性があるため、... を付けて続きを示唆しておく
        ret.Append("...");
        return ret.ToString();
    }

GetRegStrItem 関数を実装します。 IDbgEngineQuery でレジスタから値を読み取り、IDbgMemoryControl でメモリを読み取って結果を返します。

    /// <summary>
    /// regName に対応する値と文字列を入れた RegStrItem を返す。
    /// </summary>
    /// <param name="regName">レジスタ名</param>
    /// <param name="cancellationToken">キャンセル用のトークン</param>
    /// <returns>対応する RegStrItem</returns>
    private async Task<RegStrItem?> GetRegStrItem(string regName, CancellationToken cancellationToken)
    {
        // キャンセルを要求されていないか確認
        cancellationToken.ThrowIfCancellationRequested();
        // 使用するコンポーネントが null の場合は何もせずリターン
        if (_engineQuery == null || _memoryControl == null) return null;

        // 指定したレジスタのモデルオブジェクトを取得
        var register = await _engineQuery.QueryDynamicModelAsync($"@$curthread.Registers.User.{regName}", ModelQueryFlags.Default, 1, cancellationToken);
        // IDbgModelObject にキャストし、値の取得の成否を確認
        if (register is not IDbgModelObject regModel || regModel.Failed)
        {
            // モデルオブジェクトの取得に失敗した場合、何もせずリターン (デバッグ実行前など)
            return null;
        }

        // モデルオブジェクトを ulong 型に変換
        var regValue = Convert.ToUInt64(regModel.PrimitiveValue);

        // メモリからデータを読み取る
        var res = await _memoryControl.ReadVirtualMemoryAsync(regValue, N, cancellationToken);
        var buf = res?.Data;
        var s = MakeString(buf);

        return new RegStrItem(regName, regValue, s);
    }

IDbgEngineQuery の定義は以下の通りです。 QueryDynamicModelAsync がモデルオブジェクトを取得する非同期関数です。

public interface IDbgEngineQuery
{
    // モデルを文字列として得る
    Task<string> QueryModelAsync(
        string query,
        ModelQueryFlags flags = ModelQueryFlags.Default,
        int recursiveDepth = 1,
        CancellationToken cancelToken = default (CancellationToken));

    Task<string> QueryModelWithSecondaryEngineAsync(
        string query,
        ModelQueryFlags flags = ModelQueryFlags.Default,
        int recursiveDepth = 1,
        CancellationToken cancelToken = default (CancellationToken),
        bool forceResync = false);

    // モデルを書き込む
    Task<string> WriteModelAsync(string lvalue, string rvalue, CancellationToken cancelToken = default (CancellationToken));

    Task<string> QueryModelForCompletionsAsync(string query, CancellationToken cancelToken = default (CancellationToken));

    // モデルを動的型で得る
    Task<object> QueryDynamicModelAsync(
        // クエリ
        string query,
        // モード
        ModelQueryFlags flags = ModelQueryFlags.Default,
        // 子要素を再帰的に取得する場合の深さ
        int recursiveDepth = 1,
        // キャンセルトークン
        CancellationToken cancelToken = default (CancellationToken));
}

QueryDynamicModelAsync でレジスタのモデルオブジェクトを取得し、レジスタ値を読み取ります。 この関数の返り値は DbgX.Debugger.Model.DynamicModelObjectImpl? 型で、この型は基本的に IDbgModelObject 型にキャストしてから使用します。 キャスト後、Failed 変数でモデル取得の成否を判定できます。 例えば、デバッグがまだ始まっていない状態でこの関数を呼び出すと、Failedtrue になります。

今回クエリした @$curthread.Registers.User.{regName}regName のレジスタの値を直接返します。 この場合は PrimitiveValue で取得できます。 また、モデルは子要素も取得できます。試したい方は recursiveDepth を 2 以上にして Children プロパティを参照してみましょう。

PrimitiveValue も object 型であるため、Convert.ToUInt64 関数で ulong 型に変換します。 これでレジスタ値を取得できました。

レジスタ値を取得したら今度は IDbgMemoryControlReadVirtualMemoryAsync でメモリを読み出します。 IDbgMemoryControl の定義は以下の通りです。 読み取りは一通りそろっていますが、書き込みは仮想メモリだけです。

public interface IDbgMemoryControl
{
    // 仮想メモリ読み取り
    Task<MemoryReadResult> ReadVirtualMemoryAsync(
        // アドレス
        ulong address,
        // バイト数
        ulong count,
        CancellationToken cancelToken = default (CancellationToken));

    // 物理メモリ読み取り
    Task<MemoryReadResult> ReadPhysicalMemoryAsync(
        ulong address,
        ulong count,
        CancellationToken cancelToken = default (CancellationToken));
    
    // MSR メモリ読み取り
    Task<MemoryReadResult> ReadMSRMemoryAsync(
        ulong address,
        ulong count,
        CancellationToken cancelToken = default (CancellationToken));
    
    // コントロールメモリ読み取り
    Task<MemoryReadResult> ReadControlMemoryAsync(
        uint processor,
        ulong address,
        ulong count,
        CancellationToken cancelToken = default (CancellationToken));

    // バスメモリ読み取り
    Task<MemoryReadResult> ReadBusMemoryAsync(
        uint busNumber,
        uint slotNumber,
        BUS_DATA_TYPE busDataType,
        ulong address,
        ulong count,
        CancellationToken cancelToken = default (CancellationToken));

    // I/O メモリ読み取り
    Task<MemoryReadResult> ReadIOMemoryAsync(
        uint busNumber,
        uint addressSpace,
        INTERFACE_TYPE interfaceType,
        ulong address,
        ulong count,
        CancellationToken cancelToken = default (CancellationToken));

    // 仮想メモリ書き込み
    Task WriteVirtualMemoryAsync(
        // アドレス
        ulong address,
        // バイト列
        byte[] data,
        CancellationToken cancelToken = default (CancellationToken));
}

ReadVirtualMemoryAsync は必ず address から count バイト読めるか試行して返します。 返り値の MemoryReadResult 型の定義は以下の通りです。

public class MemoryReadResult
{
    // 取得したデータ
    public byte[] Data { get; set; }

    // その位置のデータを正常に読み出せたか?
    public bool[] Valid { get; set; }
}

ReadVirtualMemoryAsync[address, address + count) の範囲内に読み込み不可領域が飛び地のように存在していても、そこを飛ばした上で残りの領域を読み取ろうとします。 Data[i] が 0 のとき、読み出した結果が 0 だったのか、読み出せなかったから初期値の 0 のままなのかわかりません。 その時は Valid[i]true であれば、読み出した値が 0 であったとわかります。 今回はデータを読み出せたか気にする必要がないため、Valid を確認していません。

バイト列を読み取ったら、いつも通り MakeString で可読文字だけ読み出します。

次はデータの取得とビューを更新する RefreshAsync 関数を実装します。

    /// <summary>
    /// データを取得してビューを更新する。
    /// </summary>
    private async void RefreshAsync(CancellationToken cancellationToken)
    {
        try
        {
            var regStrItems = new List<RegStrItem>(RegNames.Count);

            // モデルの更新
            foreach (var regName in RegNames)
            {
                var item = await GetRegStrItem(regName, cancellationToken);
                if (item == null) break;
                regStrItems.Add(item);
            }
            // ビューの更新は最後に行う
            RegStrItems = new ObservableCollection<RegStrItem>(regStrItems);
        }
        catch (OperationCanceledException)
        {
            // キャンセルが発生した場合、何もせずにリターンする
        }
        catch (Exception)
        {
            // キャンセル以外の例外発生時も何もしない
        }
    }

ビューの更新はすべてのレジスタ情報を得てから一括で行うようにします。 キャンセルされた場合に中途半端にデータが更新されないようにするためです。 任意例外のパスは、例えば x64 以外をデバッグしている時などに発生します。 QueryDynamicModelAsync はレジスタが見つからなかった場合 KeyNotFoundException を送出しますが、それをハンドルしなければ UI ごと WinDbg がクラッシュしてしまいます。 今回は x64 以外対応しませんが、この拡張機能が原因で WinDbg がクラッシュするのは避けたいため、例外を無視するようにだけしておきます。

これがデータ更新の一連の流れになります。

イベントハンドラの実装

さて、これで現在のレジスタ状態を取得してビューを更新する処理が実装できました。 次は、ターゲットのレジスタやメモリ状態が変化したらビューも更新されるようにします。

ターゲットの状態変化を捕捉するにはイベントバス IDbgEventBus を使用します。 IDbgEventBus の定義は以下の通りです。

public interface IDbgEventBus
{
    // イベントハンドラ (subscriber) の登録
    void Subscribe<Q>(
        // 登録するイベントハンドラ
        EventHandler<Q> handler,
        // 優先度 (大きい値のほうが優先される)
        int priority = 1000) where Q : EventArgs;

    void WeakSubscribe<Q>(WeakEventHandler<Q> handler, int priority = 1000) where Q : EventArgs;

    // イベントハンドラの登録解除
    void Unsubscribe<Q>(EventHandler<Q> handler) where Q : EventArgs;

    // (publisher として) イベントの送出
    void FireEvent<Q>(object sender, Q eventArgs) where Q : EventArgs;
}

イベントバスにイベントハンドラを登録 (subscribe) すると、Q の型に応じたイベント発生時にそのイベントハンドラが呼び出されるようになります。

この時のイベントハンドラに渡される EventArgs を継承した主な Q は以下の通りです。 本シリーズで説明していない EventArgs にはリンクを付けていません。

名前 条件 EventArgs の中身 備考
ActiveTargetConfigurationChangedEventArgs ターゲット設定変更時 無し UI 起動時の初期化処理でも発生
CommandExecutedEventArgs コマンド実行時 コマンド文字列
DmlOutputEventArgs DML のコンソール出力時 出力された文字列
TextOutputEventArgs テキストのコンソール出力時 出力された文字列
TargetInitializedEventArgs ターゲット初期化後 ターゲット設定 UI 起動時の初期化処理でも発生
TargetLaunchingEventArgs ターゲット初期化前 ターゲット設定
TargetRefreshEventArgs ターゲットの状態変化時 変化後の状態
TargetRestartingEventArgs ターゲットの再起動前 無し
TargetStatePropertyChangedEventArgs ターゲットの状態変化時 変化後の状態

TargetRefreshEventArgs は、ターゲットの状態が変化した時の EventArgs です。今回はこれが使えそうです。

GetToolWindowView 関数を修正し、ツールウィンドウを開いたタイミングでイベントバスに自身のイベントハンドラ (次に実装する OnTargetRefresh)を登録します。

Subscribe は 1 回だけ行えば十分です。 GetToolWindowView 内でイベントバスに登録していますが、ツールウィンドウを開くたびに Subscribe すると、開いたウィンドウの数だけイベントが実行されます。 そのため、初期化したかどうかを示す _isInitialized メンバー変数を用意しておき、1 回だけ登録するようにしておきます。

    private bool _isInitialized;

    public FrameworkElement GetToolWindowView(object parameter)
    {
        if (!_isInitialized)
        {
            // イベントバスにハンドラ OnTargetRefresh を登録
            _eventBus?.Subscribe<TargetRefreshEventArgs>(OnTargetRefresh);
            _isInitialized = true;
        }

        // ツールウィンドウを開く
        return new RegStrToolWindowControl(this);
    }

イベントハンドラの OnTargetRefresh 関数を実装しましょう。 ここで、以前のモデル更新タスクをキャンセルするためのトークンソースもメンバー変数として持たせておきます。 データ取得は非同期に行われます。CancellationToken を作成しておき、キャンセル発生時に処理を中止できるようにしておきましょう。 キャンセルは、例えばステップ実行を連打したりすると発生します。処理が完了する前にステップ実行されて再び処理が呼び出されるとき、現在実行中の処理を中止させて最初からデータ取得を試みるようにします。 また、CancellationTokenSource は破棄前に Dispose する必要があるため、RegStrToolWindowViewModelIDisposable も実装します。

public class RegStrToolWindowViewModel : IDbgToolWindow, INotifyPropertyChanged, IDisposable
{
    (...省略...)

    // タスクをキャンセルするためのトークンソース
    private CancellationTokenSource? _cancellationTokenSource;

    private void CancelAndRefreshAsync()
    {
        // ターゲットの初期化後に更新する
        _cancellationTokenSource?.Cancel(); // 既に更新中だった場合、前のタスクはキャンセル
        _cancellationTokenSource?.Dispose();
        _cancellationTokenSource = new CancellationTokenSource();
        RefreshAsync(_cancellationTokenSource.Token);
    }

    private void OnTargetRefresh(object? sender, TargetRefreshEventArgs e)
    {
        if (e.ConnectionStateChanged)
        {
            // ターゲットの接続状態が変化した場合、アイテムの中身を全削除する
            RegStrItems.Clear();
        }

        if (e.Kinds.HasFlag(RefreshKind.Registers) || e.Kinds.HasFlag(RefreshKind.Memory) || e.Kinds.HasFlag(RefreshKind.Scope))
        {
            // ターゲットのレジスタ、メモリ、スコープのいずれかが変化した場合、更新する
            CancelAndRefreshAsync();
        }
    }

    public void Dispose()
    {
        _cancellationTokenSource?.Dispose();
        GC.SuppressFinalize(this);
    }

    (...省略...)
}

TargetRefreshEventArgs の定義は以下の通りです。

public class TargetRefreshEventArgs : EventArgs
{
    public TargetRefreshEventArgs(RefreshKind refreshKind) => this.Kinds = refreshKind;

    // 変化した状態の種類を示すフラグ
    public RefreshKind Kinds { get; }

    // 接続状態が変化したか?
    public bool ConnectionStateChanged => this.Kinds.HasFlag((Enum) RefreshKind.ConnectionState);

    public override string ToString() => $"TargetRefreshEventArgs({this.Kinds})";
}

TargetRefreshEventArgsConnectionStateChanged は接続状態 *5 が変化した場合に true になります。 接続状態が変化したらレジスタの状態をリセットするようにします。

ターゲット状態の何が変化したかは Kinds を見るとわかります。

RefreshKind の定義は以下の通りです。

[Flags]
public enum RefreshKind
{
    None            = 0,    // 何も変化していない
    Memory          = 1,    // メモリ
    Registers       = 2,    // レジスタ
    DebuggerState   = 4,    // デバッガの状態
    Scope           = 8,    // スコープ (スレッドが切り替わった等)
    SrcSymPaths     = 16,   // ソースシンボルパス
    ConnectionState = 32,   // 接続状態
    Breakpoints     = 64,   // ブレークポイント
    EventFilters    = 128,  // イベントフィルター (スレッド生成、プロセス生成等のイベント)
    Views           = 256,  // ビュー
    Modules         = 512,  // モジュール
    StepFilters     = 1024, // [ステップフィルター](https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-step-filter--set-step-filter-)
    All = StepFilters | Modules | Views | EventFilters | Breakpoints | ConnectionState | SrcSymPaths | Scope | DebuggerState | Registers | Memory, // 全部
}

今回はレジスタ、メモリ、そしてスレッドが切り替わったときにも今見えているレジスタは変化するため、スコープのフラグも監視して、いずれかが変化したらモデルを更新するようにします。

ここまでできたら動作確認しましょう。 しっかり更新され、ステップ実行のたびにレジスタ値と文字列が変化することを確認できれば OK です。

regstr UI の動作確認

ですが、今のコードだと初期状態のレジスタを取れません。ステップ実行して初めてウィンドウに表示されるようになります。

初期状態のレジスタを取るには TargetInitializedEventArgs で、ターゲットの初期化後に更新するようにします。

GetToolWindowView を修正し、OnTargetInitialized を実装します。

    public FrameworkElement GetToolWindowView(object parameter)
    {
        if (!_isInitialized)
        {
            // イベントバスにハンドラを登録
            _eventBus?.Subscribe<TargetInitializedEventArgs>(OnTargetInitialized);
            _eventBus?.Subscribe<TargetRefreshEventArgs>(OnTargetRefresh);
            _isInitialized = true;
        }

        // ツールウィンドウを開く
        return new RegStrToolWindowControl(this);
    }

    private void OnTargetInitialized(object? sender, TargetInitializedEventArgs e)
    {
        // ターゲットの初期化後にも更新する
        CancelAndRefreshAsync();
    }

これで動作確認すると、ターゲットの起動直後のレジスタもちゃんと取れることを確認できます。

しかしまだ問題が残っています。 WinDbg 起動時に RegStr ウィンドウが無い状態でデバッグを開始し、デバッグ開始後にデバッグウィンドウを開くと何も表示されません。 ステップ実行すると表示されます。 ウィンドウを開くタイミングでも更新するように修正しておきましょう。

    public FrameworkElement GetToolWindowView(object parameter)
    {
        (...省略...)

        // ウィンドウを開くときも更新する
        CancelAndRefreshAsync();
        // ツールウィンドウを開く
        return new RegStrToolWindowControl(this);
    }

デバッグ開始後に RegStr ウィンドウを開いてもちゃんと表示されるはずです。

レジスタ書き換えの実装

昔のレジスタウィンドウには、レジスタ値をクリックすれば書き換えられる機能がありました*6。 しかし、1.2402.24001.0 でレジスタウィンドウが刷新された際に、なぜかその機能は消えてしまいました。 これを実装してみましょう。

RegStrItem.cs を修正します。

public class RegStrItem(string name, ulong value, string str) : INotifyPropertyChanged
{
    public string Name { get; } = name;

    public ulong Value
    {
        get;
        set
        {
            if (field == value) return;
            field = value;

            OnPropertyChanged();
        }
    } = value;

    public string Str { get; } = str;

    public event PropertyChangedEventHandler? PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Value を変更する必要があるため、setter を与えます。 また、ビューへ変更を通知するために INotifyPropertyChanged を実装し、OnPropertyChanged を変更後に呼び出すようにします。

RegStrToolWindowControl.xaml を修正します。

<ui:ToolWindowView
    x:Class="RegStr.RegStrToolWindowControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
    xmlns:ui="clr-namespace:DbgX.Interfaces.UI;assembly=DbgX.Interfaces"
    xmlns:local="clr-namespace:RegStr"
    mc:Ignorable="d"
    ui:ToolWindowView.IsWindowPersisted="true"
    d:DataContext="{d:DesignInstance local:RegStrToolWindowViewModel}"
    d:DesignHeight="450" d:DesignWidth="800">

    <ui:ToolWindowView.TabTitle>
        <!-- ツールウィンドウのタイトルとツールチップを設定 -->
        <ui:ToolWindowTitle
            Title="RegStr"
            ToolTip="Show strings from register values." />
    </ui:ToolWindowView.TabTitle>
    <ui:ToolWindowView.Resources>
        <local:RegValueConverter x:Key="RegValueConverter" />
    </ui:ToolWindowView.Resources>
    <Grid>
        <ListBox ItemsSource="{Binding RegStrItems}">
            <ListBox.ItemContainerStyle>
                <!-- TextBox にフォーカスしたら ListBox のアイテムも選択状態にする -->
                <Style TargetType="ListBoxItem">
                    <Style.Triggers>
                        <Trigger Property="IsKeyboardFocusWithin" Value="True">
                            <Setter Property="IsSelected" Value="True" />
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </ListBox.ItemContainerStyle>
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal" TextElement.FontFamily="Consolas">
                        <TextBlock Margin="0,2,0,2" Text="{Binding Name, StringFormat={}{0,-3}}" />
                        <TextBox Margin="10,2,0,2" Width="130" BorderThickness="0" Background="Transparent">
                            <!-- フォーカスを失ったタイミングで Converter を実行 -->
                            <TextBox.Text>
                                <Binding Path="Value"
                                         Converter="{StaticResource RegValueConverter}"
                                         UpdateSourceTrigger="LostFocus"
                                         NotifyOnSourceUpdated="True" />
                            </TextBox.Text>
                            <i:Interaction.Triggers>
                                <!-- データが更新されたら反映 -->
                                <i:EventTrigger EventName="SourceUpdated">
                                    <i:InvokeCommandAction
                                        Command="{Binding DataContext.UpdateValueCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
                                        CommandParameter="{Binding}" />
                                </i:EventTrigger>
                            </i:Interaction.Triggers>
                        </TextBox>
                        <TextBlock Margin="10,2,0,2">
                            <TextBlock.Style>
                                <Style TargetType="TextBlock">
                                    <Setter Property="Text" Value="{Binding Str, StringFormat='{}&quot;{0}&quot;'}" />
                                    <Style.Triggers>
                                        <!-- Str が空文字列の場合はダブルクオーテーションも表示しないようにする -->
                                        <DataTrigger Binding="{Binding Str}" Value="">
                                            <Setter Property="Text" Value="" />
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </TextBlock.Style>
                        </TextBlock>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</ui:ToolWindowView>

レジスタ値 Value のタグを TextBlock から TextBox に変更し、入力後フォーカスを失ったタイミングで値を更新するようにしています。 なお、上記のコードでは Microsoft.Xaml.Behaviors.Wpf を使用しています。 これは Fluent.Ribbon を入れている場合は依存パッケージとして一緒に入ってきます。入れていない場合は NuGet からインストールしてください。

Fluent.Ribbon と Behaviors.Wpf の依存関係

RegValueConverter.cs を実装します。 これは TextBox の文字列から ulong 型の Value プロパティへの相互変換を行う ValueConverter です。 基数の接頭辞/接尾辞は、WinDbg の標準である MASM の表記と、MASM で対応していない 0d, 0o, 0b 表記を使えるようにしています。

using System.Globalization;
using System.Windows.Data;

namespace RegStr;

/// <summary>
/// TextBox に入っているレジスタ値と RegStrItem.Value の相互変換を行うコンバーター。
/// 接頭辞が無い場合は 10 進数として扱う。
/// </summary>
public class RegValueConverter : IValueConverter
{
    // ulong -> string (0x 表記)
    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        return value == null ? null : $"0x{(ulong)value:x16}";
    }

    // string (符号なし) -> ulong
    private static ulong? StrToUlong(string s)
    {
        try
        {
            if (s.StartsWith("0x")) return System.Convert.ToUInt64(s[2..], 16);
            if (s.EndsWith('h')) return System.Convert.ToUInt64(s[..^1], 16);
            if (s.StartsWith("0n")) return System.Convert.ToUInt64(s[2..], 10);
            if (s.StartsWith("0t")) return System.Convert.ToUInt64(s[2..], 8);
            if (s.StartsWith("0y")) return System.Convert.ToUInt64(s[2..], 2);
            // MASM が対応していない一般的な基数表現にも対応させる
            if (s.StartsWith("0d")) return System.Convert.ToUInt64(s[2..], 10);
            if (s.StartsWith("0o")) return System.Convert.ToUInt64(s[2..], 8);
            if (s.StartsWith("0b")) return System.Convert.ToUInt64(s[2..], 2);
            // 0 接頭辞は MASM 対応
            if (s.StartsWith('0') && s.Length > 1) return System.Convert.ToUInt64(s[1..], 8);
            // 接頭辞が無い場合は 10 進数として扱う
            return System.Convert.ToUInt64(s, 10);
        }
        catch (Exception)
        {
            // 不正な文字列だった場合
            return null;
        }
    }

    // string -> ulong
    public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        if (value == null) return Binding.DoNothing;
        var s = ((string)value).Trim().ToLower(culture);

        var isMinus = false;

        // 符号処理
        int i;
        for (i = 0; i < s.Length && (s[i] == '-' || s[i] == '+'); i++)
        {
            if (s[i] == '-') isMinus = !isMinus;
        }
        if (i >= s.Length) return Binding.DoNothing;

        var n = StrToUlong(s[i..]);
        if (n == null) return Binding.DoNothing;

        return isMinus ? ~n + 1 : n;
    }
}

RegStrToolWindowViewModel.cs を修正します。 以下をクラスに追記します。

    public RegStrToolWindowViewModel()
    {
        // コンストラクタでコマンドを登録
        UpdateValueCommand = new AsyncDelegateCommand<RegStrItem>(UpdateRegValue);
    }

    public AsyncDelegateCommand<RegStrItem> UpdateValueCommand { get; }

    // レジスタ値の更新
    private async Task UpdateRegValue(RegStrItem reg)
    {
        try
        {
            if (_cancellationTokenSource != null)
            {
                await _cancellationTokenSource.CancelAsync(); // 既に更新中だった場合、前のタスクはキャンセル
                _cancellationTokenSource.Dispose();
            }
            _cancellationTokenSource = new CancellationTokenSource();
            if (_engineQuery == null) return;
            await _engineQuery.WriteModelAsync($"@$curthread.Registers.User.{reg.Name}", reg.Value.ToString(),
                _cancellationTokenSource.Token);
        }
        catch (OperationCanceledException)
        {
            // 何もしない
        }
        catch (Exception)
        {
            // 何もしない
        }
    }

非同期処理を使うため AsyncDelegateCommand<T> で非同期コマンドを作成します。 コマンドの登録はコンストラクタで行います。

レジスタの書き込みは読み込みと同様にデータモデルを使用します。 WriteModelAsync で書き込めます。第 2 引数 (新しい値) は文字列型でなければならないため、ToString() で変換してから渡します。

これで値を書き換えられるようになりました。

rax レジスタの値を RegStr UI から書き換える

これで無事に RegStr UI が完成しました。

その他便利な UI インターフェース

RegStr の実装では用いませんでしたが、いくつかの便利なインターフェースを紹介します。

インターフェース

IDbgStartupListener

WinDbg の UI 表示前に実行されるリスナーです。 初期化処理はこのインターフェースの OnStartup 関数内で行います。

public interface IDbgStartupListener
{
    // UI 表示前に呼び出される
    void OnStartup();
}

実装例:

[Export(typeof(IDbgStartupListener))]
internal class StartupClass : IDbgStartupListener
{
    [Import] private IDbgEventBus? _eventBus;

    public void OnStartup()
    {
        _eventBus?.Subscribe<CommandExecutedEventArgs>(OnCommandExecuted);
    }

    public void OnCommandExecuted(object? obj, CommandExecutedEventArgs e)
    {
        Console.WriteLine(e.Command);
    }
}

IDbgShellStartupListener

WinDbg UI 表示後に実行されるリスナーです。 つまり、IDbgStartupListener よりも後に実行されます。 UI 表示後に行うことがある場合、こちらのリスナーを使用します。

public interface IDbgShellStartupListener
{
    void OnShellStartup();
}

実装例はありません。

IDbgShutdownListener

WinDbg 終了時に実行されるリスナーです。 終了時の後始末で使用します。

public interface IDbgShutdownListener
{
    // シャットダウンの要求が来た時に呼び出される
    // reason はシャットダウンの理由
    // 返り値はシャットダウンするかどうか?false にするとシャットダウンを拒否する
    // IDbgShutdownListener のどれか 1 つがこの関数で false を返した場合、シャットダウンしない
    bool OnShutdownRequested(ShutdownReason reason);

    // シャットダウン時に呼び出される
    Task OnShutdownAsync(ShutdownReason reason);
}

public enum ShutdownReason
{
    UserExit,
    Error,
}

実装例:

[Export(typeof(IDbgShutdownListener))]
internal class ShutdownClass : IDbgShutdownListener
{
    // シャットダウン要求時に呼び出される
    public bool OnShutdownRequested(ShutdownReason reason)
    {
        // シャットダウン理由を出力し、許可する
        Console.WriteLine($"OnShutdownRequested: {reason}");
        return true;
    }

    // シャットダウン時に呼び出される
    public Task OnShutdownAsync(ShutdownReason reason)
    {
        // シャットダウン理由を出力するだけ
        Console.WriteLine($"OnShutdownAsync: {reason}");
        return Task.CompletedTask;
    }
}

IDbgEngineStatusListener, TargetStatePropertyChangedEventArgs

IDbgEngineStatusListener はエンジン状態が変化したときに実行されるリスナーです。 コマンドを入力したとき、入力欄の左側に *BUSY* と表示されて実行が終わるまで入力できなくなりますが、その busy か busy でないかの変化を知らせるリスナーであると考えれば問題ありません。 具体的には、エンジンの状態が EngineBusy になると実行されます。

TargetStatePropertyChangedEventArgs はもっと汎用的で、EngineBusy 以外の状態変化に対しても実行されるイベントです。 例えば最初にブレークする InitialBreak といったエンジン状態を取得できます。

public interface IDbgEngineStatusListener
{
    // ビジーなら busy が true になる
    void OnEngineStatusChanged(bool busy);
}

実装例:

[Export(typeof(IDbgEngineStatusListener))]
internal class EngineStatusListener : IDbgEngineStatusListener
{
    public void OnEngineStatusChanged(bool busy)
    {
        Console.WriteLine($"busy = {busy}");
    }
}
[Export(typeof(IDbgStartupListener))]
internal class TargetStateChecker : IDbgStartupListener
{
    [Import] private IDbgEventBus? _eventBus;

    public void OnStartup()
    {
        _eventBus?.Subscribe<TargetStatePropertyChangedEventArgs>(OnTargetStatePropertyChanged);
    }

    public void OnTargetStatePropertyChanged(object? obj, TargetStatePropertyChangedEventArgs e)
    {
        Console.WriteLine($"OnTargetStatePropertyChanged: {e.PropertyName}");
    }
}

TargetStatePropertyChangedEventArgs はエンジンの状態遷移を捕捉できますが、EngineBusy が発生したときにビジーになったのかビジーでなくなったのかは判定できません。 ビジー状態を得たい場合は IDbgEngineStatusListener を使用します。 なお、IDbgEngineStatusListener と TargetStatePropertyChangedEventArgs を併用した場合、EngineBusy 状態遷移が発生すると先に OnEngineStatusChanged が呼ばれ、次に TargetStatePropertyChanged イベントが発生します。

IDbgKeyBindings

ショートカットキーを設定するインターフェースです。

public interface IDbgKeyBindings
{
    IEnumerable<KeyBinding> GetKeyBindings();
}

Shift+F7 を押すとコンソールに Hello と表示させる実装例:

[Export(typeof(IDbgKeyBindings))]
internal class HelloKeyBindings : IDbgKeyBindings
{
    [Import] private IDbgConsole? _console;

    public IEnumerable<KeyBinding> GetKeyBindings()
    {
        return new List<KeyBinding>
        {
            new(new DelegateCommand(delegate
                {
                    // キーが押されたらここが実行される
                    PrintHello();
                }
            ), new KeyGesture(Key.F7, ModifierKeys.Shift))
        };
    }

    private void PrintHello()
    {
        // コンソールに Hello を表示
        _console?.PrintTextToConsole("Hello\n");
    }
}

IDbgTargetInitialization

ターゲットの起動と再起動を行うインターフェースです。 EngineOptions で起動時のオプション設定を付けられます。

ターゲットを停止したい場合は IDbgTargetControl を使用してください。

public interface IDbgTargetInitialization
{
    IDbgServerToken ManualServerToken { get; }

    IDbgServerToken EngHost { get; }

    bool ConnectedToProcessServer { get; }

    string ProcessServerConnectionString { get; }

    string ConnectionString { get; }

    bool IsRestarting { get; }

    Task<bool> IsServerTokenValidAsync(IDbgServerToken serverToken);

    Task<IDbgServerToken> ConnectToProcessServerAsync(string connectArgs, bool isManualConnection, IDbgServerToken serverToken = null);

    Task<IDbgServerToken> ConnectToProcessServerAsync(string connectArgs, bool isManualConnection, EngineArchitecture architecture, IDbgServerToken serverToken = null);

    Task DisconnectFromProcessServerAsync(IDbgServerToken serverToken = null, bool killServer = false);

    Task ConnectToRemoteAsync(string connectArgs, EngineOptions engOpt);

    Task<IDbgServerToken> ConnectToProtocolAsync(string connectArgs, bool isManualConnection, PreferredConnectionKind preferredKind = PreferredConnectionKind.None, EngineArchitecture engineArchitecture = EngineArchitecture.AutoDetect);

    Task AttachToProcessAsync(uint pid, EngineOptions engOpt, IDbgServerToken serverToken = null);

    Task AttachToProcessAsync(string processName, EngineOptions engOpt, IDbgServerToken serverToken = null);

    Task AttachToServiceAsync(string serviceName, EngineOptions engOpt, IDbgServerToken serverToken = null);

    Task OpenDumpFileAsync(string dumpFile, EngineOptions engOpt);

    Task OpenPrivateDumpFileAsync(long dumpHandle, EngineOptions engOpt);

    Task EstablishKernelConnectionAsync(KernelConnectionProperties props, EngineOptions engOpt);

    Task LaunchProcessAsync(string exePath, string args, EngineOptions engOpt, IDbgServerToken serverToken = null);

    Task RestartSessionAsync();

    Task LaunchPlmAppUnderDebuggerAsync(EngineArchitecture architecture, string packageName, string appId, string arguments, EngineOptions engOpt, IDbgServerToken serverToken = null);

    Task LaunchPlmBgTaskUnderDebuggerAsync(bool is64BitPackage, bool isArchitectureNeutral, string packageName, string bgTaskId, EngineOptions engOpt, IDbgServerToken serverToken = null);

    Task<IEnumerable<IDbgRunningProcess>> GetRunningProcessesAsync(bool excludeCurrent = true, IDbgServerToken serverToken = null);
}

実装例はありません。

IDbgTargetControl

ターゲット操作のインターフェースです。 式評価やブレークポイントを設置したりできます。

ターゲットのデバッグ中止はこれを使用します。 ターゲットのデバッグ開始は IDbgTargetInitialization を使用してください。

public interface IDbgTargetControl
{
    // 式評価
    Task<object> EvaluateAsync(string expression, Type requestedType);

    Task<bool> CycleInitialBreakState();

    Task ToggleDebugInfo();

    Task ForceKDReconnect();

    Task<bool> ToggleVerboseOutput();

    // 指定したソースコード行に命令ポインタを設定
    Task SetInstructionPointerAsync(string module, string sourceFile, int line);

    Task SetInstructionPointerAsync(string address);

    Task<ulong> GetCodeAddressForSourceLineAsync(string module, string sourceFile, int line);

    // コマンドを `ExecuteWide` に直接渡すだけ
    // ブレークポイントを貼る目的のようだが、実際のところただの ExecuteCommandAsync として機能する
    Task SetBreakpointAsync(string command);

    // 指定したアドレスにブレークポイントを貼る
    Task SetBreakpointAsync(ulong address);

    // ソースコードにブレークポイントを貼る
    Task SetBreakpointAsync(string module, string sourceFile, int line);

    // 関数にブレークポイントを貼る
    Task SetFunctionBreakpointAsync(string functionName);

    // スレッドを再開
    Task ResumeThread(uint threadId);

    // 指定したアドレスに到着するまで実行
    Task RunToLocationAsync(string address);

    Task RunToLocationAsync(string module, string sourceFile, int line);

    // デバッグの中止
    Task<bool> StopDebuggingAsync(
        // タイムアウト (ms)
        // -1 にすると無期限
        int timeout = 3000
    );

    Task DetachAsync();

    // 今の場所でブレーク
    Task BreakAsync();
}

実装例はありません。

IDbgReporter

ログ出力インターフェースです。 Logs ウィンドウで見られるログファイルにメッセージを書き込みます。

public interface IDbgReporter
{
    void Info(string message);

    void Warning(string message);

    void Error(
        // 致命的なエラーか?
        bool isFatal,
        string message
        );

    void Error(
        bool isFatal,
        // 発生した例外
        Exception exception,
        string message = null
    );
}

実装例はありません。

その他

拡張機能のデバッグ

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

  1. DLL をビルド
  2. UIExtensions に DLL と PDB をコピー
  3. Visual Studio のタブから デバッグ -> プロセスにアタッチ
  4. 「プロセスにアタッチ」ウィンドウで DbgX.Shell.exe を検索し、選択してアタッチ
  5. Visual Studio 上でブレークポイントを貼り、UI を操作

コマンド拡張は DbgX.Shell.exe の子プロセス EngHost.exe が読み込むため、EngHost.exe にアタッチする必要がありましたが、UI 拡張は DbgX.Shell.exe が読み込みます。 また、コマンド拡張の場合は .dbgdbg でデバッグできましたが、UI 拡張はその方法ではデバッグできません。 .dbgdbg でアタッチされるのは EngHost.exe であって DbgX.Shell.exe でないためです。

UI 拡張読み込み時のエラー

UIExtensions に不正な DLL を入れ、WinDbg を起動すると、以下のようなエラーダイアログが表示されます。

UI 拡張機能読み込み時のエラー

この場合は %LOCALAPPDATA%\DBG\Logs フォルダ内のログを読みましょう。

以下のようなエラーが出ている場合、Export したコンポーネントのインターフェース実装を忘れている可能性が高いです。

Error : DbgX.Shell.dll : Failed to compose WinDbgX UI user extensions.Microsoft.VisualStudio.Composition.CompositionFailedException: Errors exist in the composition.
   at Microsoft.VisualStudio.Composition.CompositionConfiguration.ThrowOnErrors()
   at DbgX.Util.Composition.ComponentCompositionContainer.ComposeAsync()
   at DbgX.Shell.ShellApplication.<>c__DisplayClass28_0.<<CreateCompositionContainer>b__0>d.MoveNext()
    [Export(typeof(IDbgRibbonTabGroupExtension))]
    [RibbonTabGroupExtensionMetadata("RegStrRibbonTab", "Register", 0)]
    internal class RegStrTabButtonViewModel // `: IDbgRibbonTabGroupExtension` が無い!

おわりに

WinDbg における UI 拡張機能の作り方を紹介しました。

WinDbg の元の UI は使いづらいところも色々あるため、自由に拡張できると幅が広がります。 Undocumented なため、今後のバージョンアップでこの記事の内容が使えなくなる可能性はありますが、そこまで頻繁に大幅な変更がなされることはあまり無いことを信じましょう。 また、DbgEng を活用すれば WinDbg と同じように動作する独自のデバッガ UI 全体を実装できます。 興味のある方は zodiacon 氏によるサードパーティーのデバッガ UI WinDbgX を見てみましょう。

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

次回はもう少し汎用的な UI 拡張機能を作ります。

エンジニア募集

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

*1: 値が他と同一である場合、DLL をビルド・配置するたびに位置がランダムに前後するようです。HomeRibbonTab の Help グループにある Feedback ボタンは上記の方法で拡張されたボタンであり、order = 0 です。ここに新しく order = 0 のボタンを入れて何度もビルド・配置・実行してみると、時々順番が変わるはずです。

*2: 依存 DLL 群も入れていいですが、DbgX や Fluent.Ribbon 等は既に WinDbg に組み込まれているため不要です。WinDbg に組み込まれていない依存 DLL が必要な場合はそれも UIExtensions フォルダに入れてください。

*3: CreateClient を使用すれば他スレッドの DbgEng クライアントを自スレッドに移せますが、この関数を使用するには元のクライアントが必要です。

*4: コードレベル (DbgX.Interfaces.Service.CodeLevel) はステップ実行するときの移動量。現在はソース行単位かアセンブリ命令単位かの 2 つだけです。

*5: 未接続、ローカルデバッグ中、リモートデバッグ中などの状態 (DbgX.Interfaces.Enums.EngineConnectionState)。

*6: 実は DbgX.xml の UseOldRegistersWindow プロパティを true にすれば古いレジスタウィンドウに戻せます。