次の方法で共有


デバッガー エンジン API

Windows 用デバッグ ツールの拡張機能を作成する (第 2 部): 出力

Andrew Richards

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

デバッガー API についての連載 2 回目となる今回は、独自に作成したデバッガー エンジン (DbgEng) 拡張機能から生成される出力を強化する方法について説明します。このような強化の過程では、さまざまな落とし穴にはまる可能性があります。このコラムをお読みいただき、こうした落とし穴をすべて回避できるようになればさいわいです。

詳細に入る前に、デバッガー拡張機能の概要について (また、今回のコラムの例を作成およびテストする方法について) 理解するため、前回のコラムをお読みになることをお勧めします (msdn.microsoft.com/ja-jp/magazine/gg650659)。

デバッガー マークアップ言語

デバッガー マークアップ言語 (DML: Debugger Markup Language) は、HTML の発想を基にしたマークアップ言語です。太字、斜体、下線によってテキストを強調したり、ハイパーリンクを使ってナビゲーションを行ったりすることが可能です。DML は、デバッガー API のバージョン 6.6 に追加されました。

Windows SDK for Windows Vista で最初にデバッガー API のバージョン 6.6.7.5 が同梱され、x86、x64、および IA64 をサポートしていました。Windows 7、.NET 3.5 SDK、WDK には、次のリリース (バージョン 6.11.1.404) が同梱されました。現在は、Windows 7、.NET 4 SDK、WDK に最新のリリース (バージョン 6.12.2.633) が同梱されています。マイクロソフトの最新版の Windows 用デバッグ ツールを入手できるのは、Windows 7、.NET 4 のリリース媒体のみです。現在、x86、x64、IA64 用のパッケージを直接ダウンロードすることはできません。これらの今後のリリースでは、バージョン 6.6 で定義されている DML 関連の API を拡張しないことに注意してください。ただし、DML のサポートに関連する有益な修正プログラムは配布されています。

"Hello DML World"

ご想像のとおり、DML で強調のために使用するマークアップは、HTML で使用するマークアップと同じです。テキストを太字にするには <b>...</b>、斜体にするには <i>...</i>、下線を引くには <u>...</u> を使用します。この 3 種類のマークアップを使って "Hello DML World!" を出力するコマンドの例を図 1 に示します。

図 1 !hellodml の実装

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

  IDebugControl* pDebugControl;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), 
    (void **)&pDebugControl)))
  {
    pDebugControl->ControlledOutput(
      DEBUG_OUTCTL_AMBIENT_DML, DEBUG_OUTPUT_NORMAL,  
      "<b>Hello</b> <i>DML</i> <u>World!</u>\n");
    pDebugControl->Release();
  }
  return S_OK;
}

拡張機能をテストするため、このコラムのコード ダウンロードの Example04 フォルダーには test_windbg.cmd というスクリプトを含めています。このスクリプトは、拡張機能を C:\Debuggers_x86 フォルダーにコピーします。続いて WinDbg を起動し、拡張機能を読み込み、メモ帳の新しいインスタンスを (デバッグ対象として) 起動します。すべてが計画どおりに進んでいれば、デバッガーのコマンド プロンプトに「!hellodml」と入力すると、次のように太字、斜体、および下線のマークアップが適用された "Hello DML World!" が表示されます。

0:000> !hellodml
Hello DML World!

また、同じ手順を実行して NTSD デバッガーを読み込む、test_ntsd.cmd というスクリプトもあります。このデバッガーのコマンド プロンプトに「!hellodml」と入力すると、"Hello DML World!" が返されますが、マークアップは適用されません。これは、NTSD がテキストのみのデバッグ クライアントのため、DML がテキストに変換されるためです。次のように、DML がテキスト ベース (または、テキストのみ) のクライアントに出力されると、すべてのマークアップが取り除かれます。

0:000> !hellodml
Hello DML World!

マークアップ

HTML と同様、マークアップの開始と終了には注意を払う必要があります。図 2 は、コマンド引数を DML として、DML 出力の前後のマーカーをテキスト出力としてエコーする簡単な拡張コマンド (!echoasdml) を示しています。

図 2 !echoasdml の実装

HRESULT CALLBACK 
echoasdml(PDEBUG_CLIENT pDebugClient, PCSTR args)
{
  IDebugControl* pDebugControl;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), 
    (void **)&pDebugControl)))
  {
    pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "[Start DML]\n");
    pDebugControl->ControlledOutput(
      DEBUG_OUTCTL_AMBIENT_DML, 
      DEBUG_OUTPUT_NORMAL, "%s\n", args);
    pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "[End DML]\n");
    pDebugControl->Release();
  }
  return S_OK;
}

この例のシーケンスは、マークアップを終了しないと何が起きるかを示しています。

0:000> !echoasdml Hello World
[Start DML]
Hello World
[End DML]

0:000> !echoasdml <b>Hello World</b>
[Start DML]
Hello World
[End DML]
 
0:000> !echoasdml <b>Hello
[Start DML]
Hello
[End DML]

0:000> !echoasdml World</b>
[Start DML]
World
[End DML]

"<b>Hello" コマンドは太字を終了していなため、以降のマークアップやプロンプト出力も太字で表示されます。これは、テキストの出力がテキスト モードの場合も DML モードの場合も同じです。ご覧のように、その後の太字マークアップの終了によって、状態は戻ります。

もう 1 つの一般的な問題は、XML タグが含まれている文字列です。出力が (次の 1 つ目の例のように) 切り捨てられたり、XML タグが削除されたりすることがあります。

0:000> !echoasdml <xml
[Start DML]
 
0:000> !echoasdml <xml>Hello World</xml>
[Start DML]
Hello World
[End DML]

この問題には、HTML と同じように、出力前の文字列をエスケープ シーケンスにすることで対処できます。これは、自身で行っても、デバッガーに実行させてもかまいません。エスケープが必要な文字は、&、<、>、および " の 4 つです。それぞれのエスケープ シーケンスは、&amp;、&lt;、&gt;、および &quot; です。

図 3 にエスケープ シーケンスの例を示します。

図 3 !echoasdmlescape の実装

HRESULT CALLBACK 
echoasdmlescape(PDEBUG_CLIENT pDebugClient, PCSTR args)
{
  IDebugControl* pDebugControl;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), 
    (void **)&pDebugControl)))
  {
    pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "[Start DML]\n");
    if ((args != NULL) && (strlen(args) > 0))
    {
      char* szEscape = (char*)malloc(strlen(args) * 6);
      if (szEscape == NULL)
      {
        pDebugControl->Release();
        return E_OUTOFMEMORY;
      }
      size_t n=0; size_t e=0;
      for (; n<strlen(args); n++)
      {
        switch (args[n])
        {
          case '&':
                memcpy(&szEscape[e], "&amp;", 5);
                e+=5;
                break;
          case '<':
                memcpy(&szEscape[e], "&lt;", 4);
                e+=4;
                break;
          case '>':
                memcpy(&szEscape[e], "&gt;", 4);
                e+=4;
                break;
          case '"':
                memcpy(&szEscape[e], "'", 6);
                e+=6;
                break;
          default:
                szEscape[e] = args[n];
                e+=1;
                break;
        }
      }
      szEscape[e++] = '\0';
      pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_DML,  
        DEBUG_OUTPUT_NORMAL, "%s\n", szEscape);
      free(szEscape);
    }
    pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "[End DML]\n");
    pDebugControl->Release();
  }
  return S_OK;
}

echoasdmlescape コマンドは、元の 6 倍のサイズの新しいバッファを確保しています。これは、すべてが " 文字の引数文字列を扱うのに十分な領域を確保するためです。この関数は、(常に ANSI 文字を含む) 引数文字列を繰り返し処理して、バッファに適切なテキストを追加します。その後、IDebugClient::ControlledOutput 関数に %s 書式指定子を指定して、エスケープ シーケンスしたバッファを渡します。!echoasdmlescape コマンドは、文字列を DML マークアップとして解釈しないで、引数をエコー出力します。

0:000> !echoasdmlescape <xml
[Start DML]
<xml
[End DML]
 
0:000> !echoasdmlescape <xml>Hello World</xml>
[Start DML]
<xml>Hello World</xml>
[End DML]

一部の文字列は、入力の指定方法によっては、期待どおりの出力が得られない場合があります。このように一貫性のない状態は、エスケープ シーケンス (または DML) とはまったく関係なく、デバッガーのパーサーが原因です。注意しなくてはならないのが、" (文字列の一部) と ; (コマンドの終了) です。次にこれらの文字が含まれた例を示します。

0:000> !echoasdmlescape "Hello World"
[Start DML]
Hello World
[End DML]
 
0:000> !echoasdmlescape Hello World;
[Start DML]
Hello World
[End DML]
 
0:000> !echoasdmlescape "Hello World;"
[Start DML]
Hello World;
[End DML]

ただし、自身でエスケープ シーケンスに取り組む必要はありません。デバッガーは、このようなケースのために特別な書式指定子をサポートします。エスケープ シーケンスした文字列を生成してから %s 書式指定子を使用するのではなく、元の文字列で、%Y{t} 書式指定子を使用するだけです。

また、メモリの書式指定子を使用しても、エスケープ シーケンスを回避できます。%ma、%mu、%msa、および %msu 書式指定子により、文字列をターゲットのアドレス空間から直接出力することが可能になります。デバッグ エンジンが文字列と表示の読み取りを処理します (図 4 参照)。

図 4 メモリの書式指定子からの文字列と表示の読み取り

0:000> !memorydml test02!g_ptr1
[Start DML]
Error (  %ma): File not found
Error (%Y{t}): File not found
Error (   %s): File not found
[End DML]
 
0:000> !memorydml test02!g_ptr2
[Start DML]
Error (  %ma): Value is < 0
Error (%Y{t}): Value is < 0
Error (   %s): Value is [End DML]
 
0:000> !memorydml test02!g_ptr3
[Start DML]
Error (  %ma): Missing <xml> element
Error (%Y{t}): Missing <xml> element
Error (   %s): Missing  element
[End DML]

図 4 の 2 つ目と 3 つ目の例の %s で出力される文字列は < 文字と > 文字が原因で切り捨てられるか取り除かれますが、%ma および %Y{t} の出力は正常です。Test02 アプリケーションを図 5 に示します。

図 5 Test02 の実装

// Test02.cpp : Defines the entry point for the console application.
//

#include <windows.h>

void* g_ptr1;
void* g_ptr2;
void* g_ptr3;

int main(int argc, char* argv[])
{
  g_ptr1 = "File not found";
  g_ptr2 = "Value is < 0";
  g_ptr3 = "Missing <xml> element";
  Sleep(10000);
  return 0;
}

!memorydml の実装を図 6 に示します。

図 6 !memorydml の実装

HRESULT CALLBACK 
memorydml(PDEBUG_CLIENT pDebugClient, PCSTR args)
{
  IDebugDataSpaces* pDebugDataSpaces;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugDataSpaces), 
    (void **)&pDebugDataSpaces)))
  {
    IDebugSymbols* pDebugSymbols;
    if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugSymbols), 
      (void **)&pDebugSymbols)))
    {
      IDebugControl* pDebugControl;
      if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), 
        (void **)&pDebugControl)))
      {
        // Resolve the symbol
        ULONG64 ulAddress = 0;
        if ((args != NULL) && (strlen(args) > 0) && 
          SUCCEEDED(pDebugSymbols->GetOffsetByName(args, &ulAddress)))
        {   // Read the value of the pointer from the target address space
          ULONG64 ulPtr = 0;
          if (SUCCEEDED(pDebugDataSpaces->
            ReadPointersVirtual(1, ulAddress, &ulPtr)))
          {
            char szBuffer[256];
            ULONG ulBytesRead = 0;
            if (SUCCEEDED(pDebugDataSpaces->ReadVirtual(
              ulPtr, szBuffer, 255, &ulBytesRead)))
            {
              szBuffer[ulBytesRead] = '\0';

              // Output the value via %ma and %s
              pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_TEXT,
                DEBUG_OUTPUT_NORMAL, "[Start DML]\n");
              pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_DML, 
                DEBUG_OUTPUT_ERROR, "<b>Error</b> (  %%ma): %ma\n", ulPtr);
              pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_DML, 
                DEBUG_OUTPUT_ERROR, "<b>Error</b> (%%Y{t}): %Y{t}\n", szBuffer);
              pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_DML, 
                DEBUG_OUTPUT_ERROR, "<b>Error</b> (   %%s): %s\n", szBuffer);
              pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_TEXT, 
                DEBUG_OUTPUT_NORMAL, "[End DML]\n");
            }
          }
        }
        pDebugControl->Release();
      }
      pDebugSymbols->Release();
    }
    pDebugDataSpaces->Release();
  }
  return S_OK;
}

(Example05 フォルダーの) テスト スクリプトは、出力する文字列が見つかるように、メモ帳を起動するのではなく、Test02 アプリケーションのダンプを読み込むように変更しています。

したがって、ターゲットのアドレス空間から文字列の表示を実装する最も簡単な方法は、単に %ma などを使用することです。読み取った文字列を表示前に操作する必要がある場合、または独自の文字列を作成した場合は、%Y{t} によってエスケープ シーケンスを適用します。文字列を書式文字列として渡す必要がある場合は、自身でエスケープ シーケンスを適用します。また、出力を複数の IDebugControl::ControlledOutput 呼び出しに分割し、コンテンツの DML の部分に DML の出力制御 (DEBUG_OUTCTL_AMBIENT_DML) を使用し、残りの部分にエスケープ シーケンスを適用しないで TEXT (DEBUG_OUTCTL_AMBIENT_TEXT) として出力します。

IDebugClient::ControlledOutput および IDebugClient::Output には出力長の制限もあり、一度に約 16,000 文字しか出力できません。DML 出力を行っているとき、ControlledOutput で常にこの制限に達することがわかっています。マークアップとエスケープ シーケンスは、出力ウィンドウでは比較的少なく見えても、簡単に 16,000 文字を超える文字列に膨れ上がります。

多数の文字列を作成していて、自身でエスケープ シーケンスを適用する場合は、適切な位置で文字列を分割する必要があります。ただし、DML マークアップ内やエスケープ シーケンス内では分割しないようにしてください。適切に解釈されません。

ハイパーリンク

<link> マークアップまたは <exec> マークアップを使って、ハイパーリンクを実現できます。どちらのマークアップでも、マークアップで囲んだテキストには下線が引かれ、ハイパーテキストとして色分け (通常は青) されます。マークアップで実行されるコマンドはどちらも cmd メンバーです。このメンバーは、HTML の <a> マークアップの href メンバーに似ています。

<link cmd="dps @$csp @$csp+0x80">Stack</link>
<exec cmd="dps @$csp @$csp+0x80">Stack</exec>

<link> と <exec> の違いはわかりにくく、その真相を知るまでに、かなりの調査を必要としました。違いは、(Alt キーを押しながら 1 キーを押すと表示される) 出力ウィンドウではなく、(Ctrl キーを押しながら N キーを押すと表示される) コマンド ブラウザー ウィンドウでのみわかります。どちらのウィンドウでも、<link> または <exec> リンクの出力は、関連する出力ウィンドウで表示されます。違いは、各ウィンドウのコマンド プロンプトに現れます。

出力ウィンドウでは、コマンド プロンプトの内容は変化しません。なんらかの実行されていないテキストがそこにある場合は、変更されないままです。

コマンド ブラウザー ウィンドウでは、<link> が呼び出されると、そのコマンドがコマンドの一覧に追加され、コマンド プロンプトがそのコマンドに設定されます。一方で、<exec> が呼び出されると、コマンドは一覧には追加されず、コマンド プロンプトは変わりません。コマンド履歴を変更しないことにより、ユーザーを決定プロセスに導く一連のハイパーリンクを作成することが可能になります。この最も一般的な例が、ヘルプの表示です。ヘルプのナビゲーションは、ハイパーリンクに適していますが、ナビゲーションをログ記録しないことが望まれます。

ユーザー設定

では、ユーザーが DML を確認したいと考えていることはどうすればわかるでしょう。いくつかのシナリオでは、ログ ファイル (.logopen) にテキストを保存する操作でも、出力ウィンドウからコピーや貼り付けを行う操作でも、出力がテキストに変換されるときにデータに関する問題が発生します。DML の省略形が原因でデータが失われたり、出力のテキスト バージョンではナビゲーションを行うことができないことによりデータが冗長になったりする場合があります。

同様に、DML 出力を生成するのが煩わしい場合は、その出力の内容が変換されるということがわかっていればその作業を避けるべきです。非常に時間がかかる操作には、通常、メモリのスキャンやシンボルの解決が関与します。

また、ユーザーが、出力に DML を含めたくないと考えることもあるでしょう。

次の <link> ベースの省略形の例では、Object found at 0x0C876C32 という出力のみを受け取り、情報の重要な部分 (アドレスのデータ型) を見逃してしまいます。

Object found at <link cmd="dt login!CSession 0x0C876C32">0x0C876C32</link>

これに対処する適切な方法は、DML が有効になっていないときに省略形を回避する条件を付けることです。問題を解決する方法の例を次に示します。

if (DML)
  Object found at <link cmd="dt login!CSession 0x0C876C32">0x0C876C32</link>
else
  Object found at 0x0C876C32 (login!CSession)

.prefer_dml 設定は、ユーザー設定に最も近いものです (このため、省略された出力や過剰な出力について、この条件付きの判断を実行することがができます)。この設定は、DML が拡張されたバージョンの組み込みのコマンドと操作を、デバッガーが既定で実行するかどうかを指定するのに使用します。これは、DML を (拡張機能内で) グローバルに使用すべきかどうかを指定することを明示的に意図したものではありませんが、適切な代替案になります。

この設定の唯一の欠点は、既定では設定がオフになっていることで、ほとんどのデバッグ エンジニアは .prefer_dml コマンドが存在することを知りません。

デバッグ エンジンではなく、拡張機能に .prefer_dml 設定か "能力" を検出するためのコードが必要です ("能力" については後ほど説明します)。デバッグ エンジンは、この設定に基づき、DML 出力を削除しません。デバッガーが DML に対応している場合、常に DML で出力します。

現在の .prefer_dml 設定を取得するには、IDebugControl インターフェイスのために、渡された IDebugClient インターフェイスで QueryInterface を実行する必要があります。次に、現在の DEBUG_ENGOPT_XXX ビットマスクを取得するために GetEngineOptions 関数を使用します。DEBUG_ENGOPT_PREFER_DML ビットを設定すると、.prefer_dml が有効になります。図 7 では、ユーザー設定関数のサンプル実装を示します。

図 7 PreferDML の実装

BOOL PreferDML(PDEBUG_CLIENT pDebugClient)
{
  BOOL bPreferDML = FALSE;
  IDebugControl* pDebugControl;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), 
    (void **)& pDebugControl)))
  {
    ULONG ulOptions = 0;
    if (SUCCEEDED(pDebugControl->GetEngineOptions(&ulOptions)))
    {
      bPreferDML = (ulOptions & DEBUG_ENGOPT_PREFER_DML);
    }
  pDebugControl->Release();
  }
  return bPreferDML;
}

設定を判断するために、いちいちすべてのコマンドで GetEngineOptions 関数を呼び出したくはないはずです。変更に気付くことはできないのでしょうか。結局のところ、おそらくはそれほど頻繁に変更は行われません。もっと適切なやり方は確かにありますが、そこには問題があります。

IDebugClient::SetEventCallbacks を通じて IDebugEventCallbacks 実装を登録するというのが、適切な方法です。実装では、DEBUG_EVENT_CHANGE_ENGINE_STATE 通知に対象を登録します。IDebugControl::SetEngineOptions が呼び出されると、デバッガーは、Flags パラメーターの DEBUG_CES_ENGINE_OPTIONS ビットを設定して IDebugEventCallbacks::ChangeEngineState を呼び出します。Argument パラメーターには、GetEngineOptions が返すものと同じような DEBUG_ENGOPT_XXX ビットマスクを含めます。

問題は、IDebugClient オブジェクトに常に登録できるのは、1 つのイベント コールバックのみであることです。2 つ (以上) の拡張機能が、(モジュールのロードとアンロード、スレッドの開始と停止、プロセスの開始と停止、例外などの、さらに重要な通知を含む) イベント コールバックを登録すると、そのいずれかは通知を受け取りません。そして、渡された IDebugClient オブジェクトを変更すると、その拡張機能がデバッガーになるという結果を招きます。

IDebugEventCallbacks コールバックを実装する場合は、IDebugClient::CreateClient を通じて、独自の IDebugClient オブジェクトを作成する必要があります。その後、独自のコールバックを、この (新しい) IDebugClient オブジェクトと関連付けて、IDebugClient の有効期間を自身で管理します。

簡潔さのため、DEBUG_ENGOPT_PREFER_DML 値を判断する必要があるときは常に、GetEngineOptions を呼び出すことをお勧めします。先ほど説明したように、IDebugControl インターフェイスのために、渡された IDebugClient インターフェイスで QueryInterface を呼び出して、現在の (そして適切な) 設定であることを確認するために GetEngineOptions を呼び出す必要があります。

デバッグ クライアントの能力

では、デバッガーが DML もサポートしていることを知るにはどうすればよいでしょう。

デバッガーが DML をサポートしていないと、ユーザー設定と同じように、データが失われたり過剰になったり、作業が煩わしかったりすることになります。先ほど説明したとおり、NTSD はテキストのみのデバッガーであり、DML が NTSD に出力されると、デバッグ エンジンは、DML を出力から取り除くためにコンテンツの変換を行います。

デバッグ クライアントの能力を得るには、IDebugAdvanced2 インターフェイスのために、渡された IDebugClient インターフェイスで QueryInterface を実行する必要があります。その後、DEBUG_REQUEST_CURRENT_OUTPUT_CALLBACKS_ARE_DML_AWARE という要求の種類を指定して Request 関数を使用します。少なくとも 1 つの出力コールバックが DML に対応している場合 HRESULT には S_OK が、それ以外の場合は S_FALSE が返されます。繰り返しになりますが、フラグは、すべてのコールバックが対応しているということを意味するのではなく、少なくとも 1 つのコールバックが対応していることを意味します。

一見したところテキストのみの環境 (NTSD など) でも、条件付きの出力に関する問題に直面することがあります。拡張機能が、(IDebugOutputCallbacks2::GetInterestMask から DEBUG_OUTCBI_DML か DEBUG_OUTCBI_ANY_FORMAT を返すことで) NTSD 内で DML に対応している出力コールバックを登録する場合、Request 関数が S_OK を返すようになります。さいわい、このような拡張機能は非常にまれです。このような拡張機能が存在する場合は、DEBUG_REQUEST_CURRENT_OUTPUT_CALLBACKS_ARE_DML_AWARE の状態を確認して、(DML 能力の宣言前に) それに応じて能力を設定します。DML 対応のコールバックの詳細については、次回のコラムで扱います。

図 8 では、能力判定関数のサンプル実装を示します。

図 8 AbilityDML の実装

BOOL AbilityDML(PDEBUG_CLIENT pDebugClient)
{
  BOOL bAbilityDML = FALSE;
  IDebugAdvanced2* pDebugAdvanced2;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugAdvanced2), 
    (void **)& pDebugAdvanced2)))
  {
    HRESULT hr = 0;
    if (SUCCEEDED(hr = pDebugAdvanced2->Request(
      DEBUG_REQUEST_CURRENT_OUTPUT_CALLBACKS_ARE_DML_AWARE, 
      NULL, 0, NULL, 0, NULL)))
    {
      if (hr == S_OK) bAbilityDML = TRUE;
    }
    pDebugAdvanced2->Release();
  }
  return bAbilityDML;
}

DEBUG_REQUEST_CURRENT_OUTPUT_CALLBACKS_ARE_DML_AWARE という要求の種類と、IDebugOutputCallbacks2 インターフェイスは、まだ MSDN ライブラリにドキュメントが公開されていません。

潜在的な不足を踏まえると、ユーザー設定とクライアントの能力に対処する最適な方法は次のとおりです。

if (PreferDML(IDebugClient) && AbilityDML(IDebugClient))
  Object found at <link cmd="dt login!CSession 0x0C876C32">0x0C876C32</link>
else
  Object found at 0x0C876C32 (login!CSession)

(図 9 の) !ifdml 実装では、条件付きの DML 出力が生成されるように PreferDML 関数および AbilityDML 関数が実行されていることを示しています。ほとんどの場合、このような条件付きステートメントは必要なく、デバッガー エンジンのコンテンツの変換を安全に使用できます。

図 9 !ifdml の実装

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

  PDEBUG_CONTROL pDebugControl;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), 
    (void **)&pDebugControl)))
  {
    // A condition is usually not required;
    // Rely on content conversion when there isn't 
    // any abbreviation or superfluous content
    if (PreferDML(pDebugClient) && AbilityDML(pDebugClient))
    {
      pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_DML, 
        DEBUG_OUTPUT_NORMAL, "<b>Hello</b> <i>DML</i> <u>World!</u>\n");
    }
    else
    {
      pDebugControl->ControlledOutput(
        DEBUG_OUTCTL_AMBIENT_TEXT, DEBUG_OUTPUT_NORMAL, 
        "Hello TEXT World!\n");
    }
    pDebugControl->Release();
  }
  return S_OK;
}

test_windbg.cmd テスト スクリプトを使用して WinDbg を読み込むと、!ifdml の出力は次のようになります。

0:000> .prefer_dml 0
DML versions of commands off by default
0:000> !ifdml
Hello TEXT World!

0:000> .prefer_dml 1
DML versions of commands on by default
0:000> !ifdml
Hello DML World!

test_ntsd.cmd テスト スクリプトを使用して NTSD を読み込むと、!ifdml の出力は次のようになります。

0:000> .prefer_dml 0
DML versions of commands off by default
0:000> !ifdml
Hello TEXT World!
 
0:000> .prefer_dml 1
DML versions of commands on by default
0:000> !ifdml
Hello TEXT World!

管理された出力

DML を出力するには、IDebugControl::ControlledOutput 関数を使用する必要があります。

HRESULT ControlledOutput(
  [in]  ULONG OutputControl,
  [in]  ULONG Mask,
  [in]  PCSTR Format,
         ...
);

ControlledOutput と Output の違いは、OutputControl パラメーターです。このパラメーターは、DEBUG_OUTCTL_XXX 定数に基づいています。このパラメーターには 2 つの部分があり、下位ビットは出力の範囲を、上位ビットはオプションを表します。DML を有効にするのは上位ビットです。

範囲に基づいた定数の DEBUG_OUTCTL_XXX は、下位ビットに 1 ビット (のみ) 指定しなければなりません。値は、出力の転送先を指定します。転送先は、すべてのデバッガー クライアント (DEBUG_OUTCTL_ALL_CLIENTS)、IDebugControl インターフェイスに関連付けられている単なる IDebugClient (DEBUG_OUTCTL_THIS_CLIENT)、他のすべてのクライアント (DEBUG_OUTCTL_ALL_OTHER_CLIENTS)、どこにも転送しない (DEBUG_OUTCTL_IGNORE)、または単なるログ ファイル (DEBUG_OUTCTL_LOG_ONLY) を指定できます。

上位ビットはビットマスクで、これも DEBUG_OUTCTL_XXX 定数で定義されています。定数は、テキスト ベースと DML ベースの出力のどちらか (DEBUG_OUTCTL_DML)、出力をログ記録しないかどうか (DEBUG_OUTCTL_NOT_LOGGED)、クライアントの出力マスクに従うかどうか (DEBUG_OUTCTL_OVERRIDE_MASK) などを指定します。

出力制御

ここでは、すべての例において、ControlledOutput パラメーターを DEBUG_OUTCTL_AMBIENT_DML に設定しています。MSDN のドキュメントを読むと、DEBUG_OUTCTL_ALL_CLIENTS | DEBUG_OUTCTL_DML も使えるように思われるかもしれませんが、これは IDebugControl 出力制御の設定に従いません。

拡張機能のコマンドが IDebugControl::Executeから呼び出される場合、Execute 呼び出しの OutputControl パラメーターは、関連するすべての出力に使用される必要があります。IDebugControl::Output はこれを本質的に実行しますが、IDebugControl::ControlledOutput を使用するとき、OutputControl 値を認識する役割は呼び出し元にあります。問題は、実際に IDebugControl インターフェイス (またはその他のインターフェイス) から現在の出力制御値を取得する方法がないことです。ただし、希望がないわけではありません。DEBUG_OUTCTL_DML ビットの切り替えを処理する特別な "アンビエント" 定数、DEBUG_OUTCTL_XXX があります。アンビエント定数を使用すると、現在の出力制御に従い、それに応じて DEBUG_OUTCTL_DML ビットが設定されます。

DEBUG_OUTCTL_DML 上位定数と下位定数を 1 つ渡すのではなく、DML 出力を有効にするために単に DEBUG_OUTCTL_AMBIENT_DML を渡すか、DML 出力を無効にするために DEBUG_OUTCTL_AMBIENT_TEXT を渡します。

pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_DML, ...);

Mask パラメーター

今回の例で設定したもう 1 つのパラメーターは Mask パラメーターです。Mask は、出力されるテキストに基づいて、適切な DEBUG_OUTPUT_XXX 定数に設定します。Mask パラメーターは DEBUG_OUTPUT_XXX 定数に基づいています。DEBUG_OUTCTL_XXX 定数と混同しないようにしてください。

最も一般的な値として、通常出力には DEBUG_OUTPUT_NORMAL、警告出力には DEBUG_OUTPUT_WARNING、エラー出力には DEBUG_OUTPUT_ERROR を使用します。拡張機能に問題があるときは、DEBUG_OUTPUT_EXTENSION_WARNING を使用します。

DEBUG_OUTPUT_XXX 出力フラグは、コンソール出力に使用する stdout や stderr に似ています。各出力フラグは、個別の出力チャネルです。どのチャネルをリッスンするか、(仮に行うとすれば) どのように結合するか、そしてどのように表示するかを決定するのは受信 (コールバック) 側です。たとえば、WinDbg は、出力ウィンドウでは、DEBUG_OUTPUT_VERBOSE 出力フラグを除き、既定ですべての出力フラグを表示します。この動作は、[View] (表示) メニューの [Verbose Output] (詳細出力) で切り替えることができます (Ctrl キーと Alt キーを押しながら V キーを押します)。

!maskdml 実装 (図 10 参照) は、関連する出力フラグを使ってスタイル設定された説明を出力します。

図 10 !maskdml の実装

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

  PDEBUG_CONTROL pDebugControl;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), 
    (void **)&pDebugControl)))
  {
    pDebugControl->ControlledOutput(
      DEBUG_OUTCTL_AMBIENT_DML, DEBUG_OUTPUT_NORMAL, 
      "<b>DEBUG_OUTPUT_NORMAL</b> - Normal output.\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_DML, 
      DEBUG_OUTPUT_ERROR, "<b>DEBUG_OUTPUT_ERROR</b> - Error output.\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_DML, 
      DEBUG_OUTPUT_WARNING, "<b>DEBUG_OUTPUT_WARNING</b> - Warnings.\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_DML, 
      DEBUG_OUTPUT_VERBOSE, "<b>DEBUG_OUTPUT_VERBOSE</b> 
      - Additional output.\n");
    pDebugControl->ControlledOutput(
      DEBUG_OUTCTL_AMBIENT_DML, DEBUG_OUTPUT_PROMPT, 
      "<b>DEBUG_OUTPUT_PROMPT</b> - Prompt output.\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_DML, 
      DEBUG_OUTPUT_PROMPT_REGISTERS, "<b>DEBUG_OUTPUT_PROMPT_REGISTERS</b> 
      - Register dump before prompt.\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_DML, 
      DEBUG_OUTPUT_EXTENSION_WARNING, 
      "<b>DEBUG_OUTPUT_EXTENSION_WARNING</b> 
      - Warnings specific to extension operation.\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_DML, 
      DEBUG_OUTPUT_DEBUGGEE, "<b>DEBUG_OUTPUT_DEBUGGEE</b> 
      - Debug output from the target (for example, OutputDebugString or  
      DbgPrint).\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_DML,  
      DEBUG_OUTPUT_DEBUGGEE_PROMPT, "<b>DEBUG_OUTPUT_DEBUGGEE_PROMPT</b> 
      - Debug input expected by the target (for example, DbgPrompt).\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_AMBIENT_DML, 
      DEBUG_OUTPUT_SYMBOLS, "<b>DEBUG_OUTPUT_SYMBOLS</b> 
      - Symbol messages (for example, !sym noisy).\n");
    pDebugControl->Release();
  }
  return S_OK;
}

コマンドが実行された後に [Verbose Output] (詳細出力) に切り替えても、省略された DEBUG_OUTPUT_VERBOSE 出力は表示されません。この出力は失われます。

WinDbg では、各出力フラグに色分けすることができます。[View] (表示) メニューの [Options] (オプション) をクリックすると表示される [Options] (オプション) ダイアログ ボックスでは、各出力フラグの前景色と背景色を指定することができます。色の設定は、ワークスペースに保存されます。グローバルに設定するには、WinDbg を起動して、すべてのワークスペースを削除し、色 (およびその他の設定) を指定してワークスペースを保存します。いつもは、前景色を、エラー (ERROR) は赤、警告 (WARNING) は緑、詳細 (VERBOSE) は青、拡張機能の警告 (EXTENSION WARNING) は紫、シンボル (SYMBOLS) は灰に設定しています。既定のワークスペースは、今後のデバッグ セッションのテンプレートになります。

図 11 は、詳細が有効になっていない !maskdml の出力 (上) と、有効になっている !maskdml の出力 (下) です。

!maskdml with Color Scheme

図 11 配色が行われている !maskdml

ブレーク

DML を使って拡張機能を強化するのは簡単です。そして、少しのインフラストラクチャ コードがあれば、ユーザー設定に従うのも簡単です。出力を適切に生成するのに少し時間をかける価値は十分にあります。特に、出力が省略されていたり冗長になったりしているときは、常にテキストベースと DML ベースの出力を用意するようにして出力を適切な方向に導きます。

デバッガー エンジン API についての次回のコラムでは、デバッガー拡張機能がデバッガーと持つことができる関係について詳しく説明します。デバッガー クライアントとデバッガー コールバックのアーキテクチャの概要を示して、DEBUG_OUTPUT_XXX および DEBUG_OUTCTL_XXX 定数の核心に迫ります。

この基盤を使用して、Son of Strike (SOS) デバッガー拡張機能のカプセル化を実装します。また、DML で SOS 出力を拡張して、拡張機能が必要とする情報を取得するために組み込みのデバッガー コマンドと別の拡張機能を利用する方法について紹介します。

デバッグに興味があり、詳しく知りたい方は、Ntdebugging ブログ (blogs.msdn.com/b/ntdebugging、英語) で、「Advanced Windows Debugging and Troubleshooting」(高度な Windows デバッグとトラブルシューティング) をお読みください。ブログでは、さまざまなトレーニングや事例が紹介されています。

マイクロソフトでは、才能あるデバッグ エンジニアを常に求めています。チームに参加することに興味がある方は、Microsoft Careers (careers.microsoft.com/careers/en/us/home、英語) にアクセスし "Escalation Engineer" で検索をかけてください。

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

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