调试器引擎 API

编写 Debugging Tools for Windows 扩展,第 2 部分:输出

Andrew Richards

下载代码示例

在有关调试器 API 的系列文章的第二篇中,我将向您展示如何增强由调试器引擎 (DbgEng) 扩展所生成的输出。在此过程中,您可能会遭遇各种各样的陷阱。我希望能为您指出所有这些陷阱。

在继续阅读之前,您可能需要先阅读上一篇文章,了解调试器扩展是什么(以及我如何构建和测试本文中的示例)。该文章位于 msdn.microsoft.com/magazine/gg650659

调试器标记语言

调试器标记语言 (DML) 是一种受到 HTML 启发的标记语言。它支持通过粗体/斜体/加下划线表示强调以及通过超链接进行导航。调试器 API 中自 6.6 版开始添加了 DML。

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 7/.NET 4 版本是从 Microsoft 获得 Debugging Tools for Windows 最新版本的唯一途径。不提供可直接下载的 x86、x64 和 IA64 程序包。请注意,这些后续版本并未扩展 6.6 版中定义的 DML 相关 API,而是对 DML 相关支持进行了有价值的修正。

“Hello DML World”

您可能会猜出来,DML 用来表示强调的标记与 HTML 中用于同样目的的标记一样。若要将文字标为粗体,请使用“<b>…</b>”;若要标为斜体,请使用“<i>…</i>”;若要添加下划线,则使用“<u>…</u>”。图 1 显示的命令示例分别以这三种标记输出“Hello DML World!”。

图 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!

我还提供了一个名为 test_ntsd.cmd 的脚本,执行相同的步骤,但加载的是 NTSD 调试器。 如果我在此调试器的命令提示符下输入“!hellodml”,我将看到不带标记的“Hello DML World!”响应。 DML 被转换为文本,是因为 NTSD 是一种仅支持文本的调试客户端。 当 DML 被输出到仅基于文本的客户端时,会去除所有标记:

0:000> !hellodml
Hello DML World!

标记

与 HTML 类似,您在开始和结束任何标记时都需要格外谨慎。 图 2 显示了一个简单的扩展命令 (!echoasdml),该命令在以文本形式输出 DML 的前后,还以带标记的 DML 格式回显命令参数。

图 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 模式输出。 不难想象,后面的结束 bold 标记将还原文字状态。

另一种常见问题出现在字符串中包含 XML 标记时。 其结果可能是截断输出(如本文第一个示例中所示),也可能是去除 XML 标记:

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

此问题的处理方法与 HTML 的处理方法相同:在输出之前对字符串进行转义。 您可以自行执行此操作,也可以让调试器为您完成。 四个需要转义的字符是 &、<、> 和 "。 等价的转义字符版本为:“&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 命令分配新的缓冲区,其大小是原始缓冲区的六倍。 这个缓冲区空间足够大,可以处理以 " 字符开头的参数字符串。 该函数遍历该参数字符串(始终是 ANSI),并将适当的文本添加到缓冲区中。 然后,它将经过转义序列处理的缓冲区与 %s 格式化程序一起传递给 IDebugClient::ControlledOutput 函数。 !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 所示的第二和第三个示例中,以 %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),无需使用转义序列,就可以将其余部分作为文本 (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> 之间的差别很难看出来。 实际上,我花了不少时间才研究明白。 唯一可以观察到的差别出现在“命令浏览器”窗口 (Ctrl+N) 中,而不是“输出”窗口 (Alt+1) 中。 在这两种窗口中,<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”首选项或“ability”(我将简单介绍一下“ability”)。 调试引擎不会根据此项设置来清除 DML 输出;如果调试器支持 DML,则它总是会以 DML 格式输出。

为了获取当前的“.prefer_dml”首选项,您需要对传入的 IDebugClient 接口(用于 IDebugControl 接口)执行 QueryInterface。 然后,使用 GetEngineOptions 函数来获取当前的 DEBUG_ENGOPT_XXX 位掩码。 如果设置了 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 被调用时,调试器会调用 IDebugEventCallbacks::ChangeEngineState,并在 Flags 参数中设置 DEBUG_CES_ENGINE_OPTIONS 位。 Argument 参数包含一个 DEBUG_ENGOPT_XXX 位掩码,就像 GetEngineOptions 返回的一样。

问题是对于一个 IDebugClient 对象而言,任何时候都只能注册一个事件回调。 如果有两个(或更多)扩展希望注册事件回调(包括更重要的通知,如模块加载/卸载、线程启动/停止、进程启动/停止和异常),某些对象就会失去机会。 如果您修改传入的 IDebugClient 对象,这里的“某些对象”就是指调试程序!

如果您想要实现 IDebugEventCallbacks 回调,则需要通过 IDebugClient::CreateClient 生成自己的 IDebugClient 对象。 然后,将您的回调与这个(新)IDebugClient 对象相关联,并让其负责 IDebugClient 的生存期。

为了简单起见,在每次需要确定 DEBUG_ENGOPT_PREFER_DML 值时调用 GetEngineOptions,效果会更好。 正如前文所述,您应该对传入的 IDebugClient 接口(用于 IDebugControl 接口)调用 QueryInterface,然后调用 GetEngineOptions 以确保您具有当前(且正确)的首选项。

调试客户端的能力

那您如何判断调试器是否支持 DML 呢?

如果调试器不支持 DML,数据可能会丢失、冗长,或者所需的工作会非常繁重,就像用户首选项面对的情况一样。 正如前文所述,NTSD 是一个仅支持文本的调试器;如果向其输出 DML,则调试引擎将执行内容转换,以便从输出中删除 DML。

为了获知调试客户端的能力,您需要对传入的 IDebugClient 接口(用于 IDebugAdvanced2 接口)执行 QueryInterface。 然后通过 DEBUG_REQUEST_CURRENT_OUTPUT_CALLBACKS_ARE_DML_AWARE 请求类型来执行 Request 函数。 当至少有一个“输出回调”支持 DML 时,HRESULT 包含 S_OK;否则它返回 S_FALSE。 重申一下,该标记并不意味着支持所有回调,而意味着支持至少一种 回调。

在看起来仅支持文本的环境(如 NTSD)中,您仍然会遇到条件输出问题。 如果某个扩展在 NTSD 中注册了一个支持 DML 的输出回调(通过从 IDebugOutputCallbacks2::GetInterestMask 返回 DEBUG_OUTCBI_DML 或 DEBUG_OUTCBI_ANY_FORMAT),它将导致 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;
}

请注意,MSDN 库中并未提供 DEBUG_REQUEST_CURRENT_OUTPUT_CALLBACKS_ARE_DML_AWARE 请求类型和 IDebugOutputCallbacks2 接口的详细说明。

考虑到这些潜在的缺点,因此处理用户首选项和客户端能力的最佳方法是:

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

!ifdml 实现(请参见图 9)展示了 PreferDML 和 AbilityDML 函数生成条件 DML 输出的实际效果。 请注意,在绝大多数情况下,不需要类似于此的条件语句;您可以安全地依赖于调试器引擎内容转换。

图 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 常量。 此参数有两个部分:低位表示输出的范围,高位表示选项。 由高位来启用 DML。

对于低位,有且仅有一个基于 DEBUG_OUTCTL_XXX 范围的常量是必须使用的。 该值指示将输出到何处。 该值可以指示:输出到所有调试器客户端 (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_XXX“环境”常量用于处理切换 DEBUG_OUTCTL_DML 位的工作。 当您使用某个环境变量时,会遵循当前的输出控制,并且仅仅相应地设置 DEBUG_OUTCTL_DML 位。

您无需同时传递高位 DEBUG_OUTCTL_DML 常量与某个低位常量,而只需传递 DEBUG_OUTCTL_AMBIENT_DML 以启用 DML 输出,或者传递 DEBUG_OUTCTL_AMBIENT_TEXT 以禁用 DML 输出:

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

Mask 参数

我在示例中设置的另一个参数是 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 输出标记以外的所有输出标记。 您可以通过“视图”|“详细输出”(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;
}

如果您在命令运行之后再切换“详细输出”,则不会显示省略的 DEBUG_OUTPUT_VERBOSE 输出;该输出将丢失。

WinDbg 支持对每种输出标记使用不同的颜色设置。在“视图”|“选项”对话框中,您可以指定每种输出标记的前景色和背景色。这些颜色设置保存在工作区中。若要进行全局设置,请启动 WinDbg,删除所有工作区,设置颜色(以及您需要的其他任何设置),然后保存工作区。我喜欢将“错误”的前景色设置为红色,将“警告”的前景色设置为绿色,将“详细”的前景色设置为蓝色,将“扩展警告”的前景色设置为紫色,将“符号”的前景色设置为灰色。默认工作区将成为未来所有调试会话的模板。

图 11 显示了未启用(顶部)和启用(底部)详细选项的 !maskdml 输出。

图 11 采用配色方案的 !maskdml

结语

利用 DML 来增强任何扩展都很简单。通过极少量的基础代码,也很容易遵循用户首选项。为了正确生成输出,绝对值得投入额外的时间。特别是,在输出被缩写或冗长时,尽力同时提供基于文本和基于 DML 的输出,并且适当地引导这种输出。

在关于调试器引擎 API 的下一篇文章中,我将深入探讨调试器扩展与调试器之间可能存在的关系。我将概要介绍调试器客户端和调试器回调。在此过程中,我将探讨 DEBUG_OUTPUT_XXX 和 DEBUG_OUTCTL_XXX 常量的本质问题。

我将在此基础上实现 Son of Strike(简称 SOS)调试器扩展的封装。我将利用 DML 来增强 SOS 输出,并演示如何利用内置调试器命令和其他扩展来检索扩展所需的信息。

如果您对调试感兴趣并且希望深入了解,您应该看看“高级 Windows 调试和疑难解答”(NTDebugging) 博客 (blogs.msdn.com/b/ntdebugging),该博客提供了大量培训和案例研究文章。

Microsoft 始终在寻找天才的调试工程师。如果您有兴趣加入该团队,请在 Microsoft Careers (careers.microsoft.com) 上搜索职位“Escalation Engineer”(专家级工程师)。

Andrew Richards是 Exchange Server 方面的 Microsoft 高级专家级工程师。他热衷于支持工具,并且在不断创造能够简化支持工程师工作的调试器扩展和应用程序。

衷心感谢以下技术专家对本文的审阅:Drew Bliss