调试器引擎 API

编写 Debugging Tools for Windows 扩展,第 3 部分:客户端和回调

Andrew Richards

下载代码示例

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

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

在继续前,确保您已阅读前两个部分的内容,了解什么是调试器扩展(包括我如何构建和测试本文中的示例)以及如何正确生成输出。 这些内容可在 msdn.microsoft.com/magazine/gg650659msdn.microsoft.com/magazine/hh148143 中找到。

输出控制

可以通过两种方法来在扩展中生成输出:IDebugControl::Output* 函数和 IDebugControl::ControlledOutput* 函数。 Output 函数仅仅是 ControlledOutput 函数的简化。 这两种函数可检索当前的输出控制设置,并将其传递给 ControlledOutput 作为第一个参数:

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

在第二篇文章中,我概述了用于 OutputControl 参数的 DEBUG_OUTCTL_XXX 常量。 在本文中,我将重点讨论控制输出范围的低位。 对于低位,有一个基于 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)。

在随附代码下载(与前一篇文章相同)的 Example06 中,!outctlpassed 命令(参见图 1)基于传入的 IDebugClient 接口。

图 1 !outctlpassed

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

  IDebugControl* pDebugControl;
  if (SUCCEEDED(pDebugClient->
    QueryInterface(__uuidof(IDebugControl), (void **)&pDebugControl)))
  {
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_THIS_CLIENT,           
      DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_THIS_CLIENT\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_CLIENTS,       
      DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_ALL_CLIENTS\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_OTHER_CLIENTS,  
      DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_ALL_OTHER_CLIENTS\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_IGNORE,             
      DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_IGNORE\n");
    pDebugControl->ControlledOutput(DEBUG_OUTCTL_LOG_ONLY,          
      DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_LOG_ONLY\n");

    pDebugControl->Release();
  }
  return S_OK;
}

该函数可为(传入的 IDebugClient 的)IDebugControl 接口执行 QueryInterface。 调用 ControlledOutput 时将使用每一个输出控制范围。 将 Mask 设置为 Normal (DEBUG_OUTPUT_NORMAL),这样它就不会被排除在输出之外。 默认情况下,只有值为 Verbose 的 Mask (DEBUG_OUTPUT_VERBOSE) 会被排除在 WinDBG“输出”窗口之外。 为了测试命令,我使用了 test_windbg.cmd 脚本。 在启动的 WinDBG 中,打开日志文件 (.logopen),运行 !outctlpassed 命令,然后关闭日志文件 (.logclose),如下所示:

0:000> .logopen outctlpassed.txt
打开日志文件“outctlpassed.txt”
0:000> !outctlpassed
DEBUG_OUTCTL_THIS_CLIENT
DEBUG_OUTCTL_ALL_CLIENTS
0:000> .logclose
关闭打开的日志文件 outctlpassed.txt

!outctlpassed 命令只包括 DEBUG_OUTCTL_THIS_CLIENT 输出和 DEBUG_OUTCTL_ALL_CLIENTS 输出。 在“输出”窗口中省略了 DEBUG_OUTCTL_ALL_OTHER_CLIENTS、DEBUG_OUTCTL_IGNORE 以及 DEBUG_OUTCTL_LOG_ONLY 的输出控制。 但如果您查看日志文件,会有不同的结果,如下所示:

打开日志文件“outctlpassed.txt”
0:000> !outctlpassed
DEBUG_OUTCTL_THIS_CLIENT
DEBUG_OUTCTL_ALL_CLIENTS
DEBUG_OUTCTL_ALL_OTHER_CLIENTS
DEBUG_OUTCTL_LOG_ONLY
0:000> .logclose
关闭打开的日志文件 outctlpassed.txt

日志文件中唯一缺少的输出控制为 DEBUG_OUTCTL_IGNORE。

调试器体系结构

要了解 WinDBG“输出”窗口和日志文件中缺少的输出所发生的情况,我们需要深入了解调试器体系结构。 调试基于四个层次:调试器助手、调试器引擎、调试器客户端和调试器扩展。

最底层为调试器助手 (dbghelp.dll)。 该库包含所有用于解析符号的功能。

接下来一层为调试器引擎 (dbgeng.dll)。 该库可处理调试会话,尤其是可为远程目标的调试提供支持。 该库中包含各种调试器接口(如 IDebugClient、IDebugControl 等)的处理。

接下来一层为调试器客户端层。 在该层上,输入和输出的处理按照客户认为适当的方式进行。 WinDBG 和 NTSD 调试器均位于这一层。 客户端使用调试器引擎 DebugCreate 和 DebugConnect 函数来创建附加到所需目标的 IDebugClient 对象。 目标可以是本地的,也可以是远程的(通过调试器引擎代理支持)。

最后一层为调试器扩展。 客户端可调用由调试器扩展 DLL 导出的命令。 调试器客户端收到的来自调试器引擎的 IDebugClient 接口与客户端传递给扩展的接口相同。 尽管由客户端调用,但扩展仅与调试器引擎进行交互。 只要扩展不损坏客户端的配置,其就可以与客户端共享 IDebugClient 接口。 此外,两者也可以设置其自身的客户端(通过 IDebugClient::CreateClient),并根据需要进行配置。

多个调试器客户端

可将多个客户端(IDebugClient 对象)附加到同一个调试会话;会话是唯一的,而客户端并不唯一。 每个客户端都有连接到调试器引擎的专用接口,我们将充分利用这一功能。

最初,当您将 WinDBG 或 NTSD 附加到流程时,仅存在一个客户端 - 由 DebugCreate 或 DebugConnect 返回的 IDebugClient 接口。 当扩展针对该返回接口调用 IDebugClient::CreateClient 时,调试器引擎会设置另一个与会话关联的客户端。 新客户端将连接到同一个调试会话,但其处于默认状态。 没有在新客户端上设置回调,且输出掩码为默认值(除 Verbose 外的所有内容)。

图 2 中,!outctlcreate 的输出与 !outctlpassed 的输出相同,但是前者基于新创建的 IDebugClient 对象。

图 2 !outctlcreate

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

  IDebugClient* pDebugCreated;
  if (SUCCEEDED(pDebugClient->CreateClient(&pDebugCreated)))
  {
    IDebugControl* pDebugControl;
    if (SUCCEEDED(pDebugCreated->QueryInterface(__uuidof(IDebugControl), 
      (void **)&pDebugControl)))
    {
      pDebugControl->ControlledOutput(DEBUG_OUTCTL_THIS_CLIENT,        
        DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_THIS_CLIENT\n");
      pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_CLIENTS,       
        DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_ALL_CLIENTS\n");
      pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_OTHER_CLIENTS, 
        DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_ALL_OTHER_CLIENTS\n");
      pDebugControl->ControlledOutput(DEBUG_OUTCTL_IGNORE,            
        DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_IGNORE\n");
      pDebugControl->ControlledOutput(DEBUG_OUTCTL_LOG_ONLY,          
        DEBUG_OUTPUT_NORMAL, "DEBUG_OUTCTL_LOG_ONLY\n");
      pDebugControl->Release();
    }
    pDebugCreated->Release();
  }
  return S_OK;
}

如上所述,为了测试命令,我使用了 test_windbg.cmd 脚本。 在启动的 WinDBG 中,打开日志文件 (.logopen),运行 !outctlcreate 命令,然后关闭日志文件 (.logclose),如下所示:

0:000> .logopen outctlcreate.txt
打开日志文件“outctlcreate.txt”
0:000> !outctlcreate
DEBUG_OUTCTL_ALL_CLIENTS
DEBUG_OUTCTL_ALL_OTHER_CLIENTS
0:000> .logclose
关闭打开的日志文件 outctlcreate.txt

!outctlcreate 命令只包括 DEBUG_OUTCTL_ALL_CLIENTS 输出和 DEBUG_OUTCTL_ALL_OTHER_CLIENTS 输出。 “所有其他”输出替代了“当前”输出,因为此时 WinDBG 客户端为“其他”客户端。 日志文件结果保持不变,如下所示:

打开日志文件“outctlcreate.txt”
0:000> !outctlcreate
DEBUG_OUTCTL_THIS_CLIENT
DEBUG_OUTCTL_ALL_CLIENTS
DEBUG_OUTCTL_ALL_OTHER_CLIENTS
DEBUG_OUTCTL_LOG_ONLY
0:000> .logclose
关闭打开的日志文件 outctlcreate.txt

在调试引擎中,可以有条件地执行 ControlledOutput 调用。 引擎可调用所有符合输出控制标准的客户端上的输出回调。 因此,如果您设置您自身的客户端,并使用 DEBUG_OUTCTL_THIS_CLIENT 输出控制,输出将留在本地(即不会显示在 WinDBG 中)。 请注意,输出仍会转入日志文件。 如果您添加“未记录”高位 (DEBUG_OUTCTL_NOT_LOGGED),也可以阻止输出转入到日志文件。

再次重申,!outctlthis 命令只执行已创建客户端上的“当前”输出和“未记录”输出 (DEBUG_OUTCTL_THIS_CLIENT | DEBUG_OUTCTL_NOT_LOGGED),如图 3 所示。

图 3 !outctlthis

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

  IDebugClient* pDebugCreated;
  if (SUCCEEDED(pDebugClient->CreateClient(&pDebugCreated)))
  {
    IDebugControl* pDebugControl;
    if (SUCCEEDED(pDebugCreated->QueryInterface(__uuidof(IDebugControl), 
      (void **)&pDebugControl)))
    {
      pDebugControl->ControlledOutput(DEBUG_OUTCTL_THIS_CLIENT |  
        DEBUG_OUTCTL_NOT_LOGGED, DEBUG_OUTPUT_NORMAL, 
        "DEBUG_OUTCTL_THIS_CLIENT | DEBUG_OUTCTL_NOT_LOGGED\n");
      pDebugControl->Release();
    }
    pDebugCreated->Release();
  }
  return S_OK;
}

如上所述,为了测试命令,我使用了 test_windbg.cmd 脚本。 在启动的 WinDBG 中,打开日志文件 (.logopen),运行 !outctlthis 命令,然后关闭日志文件 (.logclose),如下所示:

0:000> .logopen outctlthis.txt
打开日志文件“outctlthis.txt”
0:000> !outctlthis
0:000> .logclose
关闭打开的日志文件 outctlthis.txt

日志文件这次没有记录命令的输出。 请注意,日志文件不记录该执行是因为 WinDBG 客户端在扩展调用之前已创建提示输出,如下所示:

打开日志文件“outctlthis.txt”
0:000> !outctlthis
0:000> .logclose
关闭打开的日志文件 outctlthis.txt

实际上,我已经将输出重定向到 NULL,因为创建的客户端并没有任何关联的输出回调 - 不一定有用。 但如果我更改代码以改用 IDebugControl::Execute 函数,那么此时我就可以执行任何命令,而不会在 WinDBG 中显示或记录,如下所示:

pDebugControl->Execute(DEBUG_OUTCTL_THIS_CLIENT | DEBUG_OUTCTL_NOT_LOGGED, 
  args, DEBUG_EXECUTE_NOT_LOGGED | DEBUG_EXECUTE_NO_REPEAT);

以这种方式使用 Execute 时,您需要设置 DEBUG_EXECUTE_NOT_LOGGED 标记和 DEBUG_EXECUTE_NO_REPEAT 标记,从而使日志文件中不会生成提示输出,且不会保存命令以供重复使用(例如,在用户在命令提示符中按下 Enter 键以执行空命令的情况下)。

图 4 所示,最后 !outctlexecute 命令无提示地执行了提供的参数。

图 4 !outctlexecute

HRESULT CALLBACK 
outctlexecute(PDEBUG_CLIENT pDebugClient, PCSTR args)
{
  IDebugClient* pDebugCreated;
  if (SUCCEEDED(pDebugClient->CreateClient(&pDebugCreated)))
  {
    IDebugControl* pDebugControl;
    if (SUCCEEDED(pDebugCreated->QueryInterface(__uuidof(IDebugControl), 
      (void **)&pDebugControl)))
  {
    pDebugControl->Execute(DEBUG_OUTCTL_THIS_CLIENT | DEBUG_OUTCTL_NOT_LOGGED, 
      args, DEBUG_EXECUTE_NOT_LOGGED | DEBUG_EXECUTE_NO_REPEAT);
    pDebugControl->Release();
  }
  pDebugCreated->Release();
  }
  return S_OK;
}

我正在执行符号的强制性重新加载 (.reload /f)。 这一操作通常会生成大量的符号输出,但 !outctlexecute 已隐藏这些输出,如下所示:

0:000> !outctlexecute .reload /f

输出回调

当调试器引擎将输出请求路由到客户端(由输出控制指示)时,应用了两个附加约束:

  • 类型(掩码)的输出是您想要的吗?
  • 是否有关联的输出回调?

调用 IDebugControl::Output 或 IDebugControl::ControlledOutput 时,调试引擎会将提供的掩码与客户端的输出掩码 (IDebugClient::GetOutputMask) 进行比较。 两个掩码需要与待调用的输出回调相匹配。

接下来,调试引擎将调用 IDebugClient::GetOutputCallbacks 以获取输出回调接口(IDebugOutputCallbacks、IDebugOutputCallbacks2 或 IDebugOutputCallbacksWide)。 如果客户端上已设定输出回调(事先通过 IDebugClient::SetOutputCallbacks 完成的),调试引擎将调用该回调 - 例如 IDebugOutputCallbacks::Output。

在随附代码下载的 Example07 中,我已对 IDebugOutputCallbacks 实现进行编码,可支持 1MB 的正常输出和 1MB 的错误输出。 (标题见图 5,代码见图 6。)

图 5 OutputCallbacks.h

#ifndef __OUTPUTCALLBACKS_H__
#define __OUTPUTCALLBACKS_H__

#include "dbgexts.h"

class COutputCallbacks : public IDebugOutputCallbacks
{
  private:
    long m_ref;
    PCHAR m_pBufferNormal;
    size_t m_nBufferNormal;
    PCHAR m_pBufferError;
    size_t m_nBufferError;

  public:
    COutputCallbacks()
     {
      m_ref = 1;
      m_pBufferNormal = NULL;
      m_nBufferNormal = 0;
      m_pBufferError = NULL;
      m_nBufferError = 0;
    }

    ~COutputCallbacks()
    {
      Clear();
    }

    // IUnknown
    STDMETHOD(QueryInterface)(__in REFIID InterfaceId, __out PVOID* Interface);
    STDMETHOD_(ULONG, AddRef)();
    STDMETHOD_(ULONG, Release)();

    // IDebugOutputCallbacks
    STDMETHOD(Output)(__in ULONG Mask, __in PCSTR Text);

    // Helpers
    ULONG SupportedMask() { return DEBUG_OUTPUT_NORMAL | DEBUG_OUTPUT_ERROR; }
    PCHAR BufferNormal() { return m_pBufferNormal; }
    PCHAR BufferError() { return m_pBufferError; }
    void Clear();   
};

#endif // #ifndef __OUTPUTCALLBACKS_H__

图 6 OutputCallbacks.cpp

#include "dbgexts.h"
#include "outputcallbacks.h"

#define MAX_OUTPUTCALLBACKS_BUFFER 0x1000000  // 1Mb
#define MAX_OUTPUTCALLBACKS_LENGTH 0x0FFFFFF  // 1Mb - 1

STDMETHODIMP COutputCallbacks::QueryInterface(__in REFIID InterfaceId, 
  __out PVOID* Interface)
{
  *Interface = NULL;
  if (IsEqualIID(InterfaceId, __uuidof(IUnknown)) || IsEqualIID(InterfaceId, 
    __uuidof(IDebugOutputCallbacks)))
  {
    *Interface = (IDebugOutputCallbacks *)this;
    InterlockedIncrement(&m_ref);
    return S_OK;
  }
  else
  {
    return E_NOINTERFACE;
  }
}

STDMETHODIMP_(ULONG) COutputCallbacks::AddRef()
{
  return InterlockedIncrement(&m_ref);
}

STDMETHODIMP_(ULONG) COutputCallbacks::Release()
{
  if (InterlockedDecrement(&m_ref) == 0)
  {
    delete this;
    return 0;
  }
  return m_ref;
}

STDMETHODIMP COutputCallbacks::Output(__in ULONG Mask, __in PCSTR Text)
{
  if ((Mask & DEBUG_OUTPUT_NORMAL) == DEBUG_OUTPUT_NORMAL)
  {
    if (m_pBufferNormal == NULL)
    {
      m_nBufferNormal = 0;
      m_pBufferNormal = (PCHAR)malloc(sizeof(CHAR)*(MAX_OUTPUTCALLBACKS_BUFFER));
      if (m_pBufferNormal == NULL) return E_OUTOFMEMORY;
      m_pBufferNormal[0] = '\0';
      m_pBufferNormal[MAX_OUTPUTCALLBACKS_LENGTH] = '\0';
    }
    size_t len = strlen(Text);
    if (len > (MAX_OUTPUTCALLBACKS_LENGTH-m_nBufferNormal))
    {
      len = MAX_OUTPUTCALLBACKS_LENGTH-m_nBufferNormal;
    }
    if (len > 0)
    {
      memcpy(&m_pBufferNormal[m_nBufferNormal], Text, len);
      m_nBufferNormal += len;
      m_pBufferNormal[m_nBufferNormal] = '\0';
    }
  }
  if ((Mask & DEBUG_OUTPUT_ERROR) == DEBUG_OUTPUT_ERROR)
  {
    if (m_pBufferError == NULL)
    {
      m_nBufferError = 0;
      m_pBufferError = (PCHAR)malloc(sizeof(CHAR)*(MAX_OUTPUTCALLBACKS_BUFFER));
      if (m_pBufferError == NULL) return E_OUTOFMEMORY;
      m_pBufferError[0] = '\0';
      m_pBufferError[MAX_OUTPUTCALLBACKS_LENGTH] = '\0';
    }
    size_t len = strlen(Text);
    if (len >= (MAX_OUTPUTCALLBACKS_LENGTH-m_nBufferError))
    {
      len = MAX_OUTPUTCALLBACKS_LENGTH-m_nBufferError;
    }
    if (len > 0)
    {
      memcpy(&m_pBufferError[m_nBufferError], Text, len);
      m_nBufferError += len;
      m_pBufferError[m_nBufferError] = '\0';
    }
  }
  return S_OK;
}

void COutputCallbacks::Clear()
{
  if (m_pBufferNormal)
  {
    free(m_pBufferNormal);
    m_pBufferNormal = NULL;
    m_nBufferNormal = 0;
  }
  if (m_pBufferError)
  {
    free(m_pBufferError);
    m_pBufferError = NULL;
    m_nBufferError = 0;
  }
}

每次调用 Output 后,类会将字符串附加到正常或错误缓冲区。 除 IUnknown 和 IDebugOutputCallbacks 接口所要求的函数之外,还有其他可提供到缓冲区(BufferNormal 和 BufferError)的访问权限从而清除缓冲区的函数,以及用来获取所支持的输出类型 (SupportedMask) 的函数。

每次调用 Output 或 ControlledOutput(同时符合 IDebugClient 标准)时,系统会调用回调的 Output 函数。 传入的掩码与原始输出调用上设置的掩码相同。 Output 函数会对传入的掩码执行位掩码比较,从而确保文本与正确的缓冲区关联。 输出以单独方式还是组合方式存储还是被忽略,具体取决于回调的实现。 单个 IDebugClient::Execute 调用可能产生数百种不同类型的 Output 调用。 因为我想解释一下整个 Execute 操作的正常输出和错误输出,所以我将这些字符串连接起来存入两个关联的缓冲区。

IDebugOutputCallbacks 接口传递 ANSI 格式和 TEXT 格式的字符串。 如果原始文本的格式是 Unicode 或 DML,引擎将在回调之前执行格式转换。 要捕获 Unicode (wide) 内容,需改用 IDebugOutCallbacksWide 接口。 两种接口的唯一区别在于 Text 参数是作为 PCWSTR 而不是作为 PCSTR 传递的。 这两种接口仅支持基于 TEXT 输出的通知。 它们不接收 DML 格式的文本。

若要获取 DML 内容,您需要使用 IDebugOutputCallbacks2 接口。 使用该接口,您可以控制回调所接收文本的格式化方式:TEXT、DML 或其中任何一个。 GetInterestMask 函数用于定义该格式。 在客户端上通过 IDebugClient::SetOutputCallbacks 设置接口时会调用此函数。 返回是接收 TEXT (DEBUG_OUTCBI_TEXT)、DML (DEBUG_OUTCBI_DML) 还是其中任何一种输出类型 (DEBUG_OUTCBI_ANY_FORMAT)。 请注意,这与 Output Mask 不同。

该接口上的 Output 函数没有特定用途,只是 IDebugCallbacks 的(虚设)接口继承的一种产物。 所有输出回调通知都是由 Output2 函数发出的:

STDMETHOD(Output2)(ULONG Which, ULONG Flags, ULONG64 Arg, PCWSTR Text)

Which 参数指定要传入的是 TEXT (DEBUG_OUTCB_TEXT) 还是 DML (DEBUG_OUTCB_DML),或指定所需的清除 (DEBUG_OUTCB_EXPLICIT_FLUSH)。 请注意,这些是 CB* 常量;而不是用于相关掩码的 CBI* 常量。

Flags 参数指定位掩码中的其他信息。 尤其是针对 DML 内容,该参数指定内容是否包含超链接 (DEBUG_OUTCBF_DML_HAS_TAGS) 或是否具有编码的特殊字符("、&、< 和 >)(DEBUG_OUTCBF_DML_HAS_SPECIAL_CHARACTERS)。 该参数也可以表明完成输出处理之后需要清除 (DEBUG_OUTCBF_COMBINED_EXPLICIT_FLUSH)。 当引擎组合 Output 调用和 FlushCallbacks 调用时会出现这种情况。

Arg 参数包含 DML 回调、TEXT 回调和 FLUSH 回调的输出掩码。

最后,Text 参数为要输出的文本。 当针对清除 (DEBUG_OUTCB_EXPLICIT_FLUSH) 调用回调时,该参数为 NULL。 文本通常以 Unicode 格式传递。

执行

将回调添加到 !outctlexecute 命令并不需要很多代码(参见图 4)。 其他步骤用来分配回调,并在调用 Execute 之前将该回调与创建的 IDebugClient 接口相关联。

我根据引用的计数堆对象实现了我的回调操作。 如果您使用的是 Windows Driver Kit 生成环境,将需要禁用“驱动程序的 PREfast”警告 28197,以便您在每次编译项目时不会收到关于“新”分配的 OACR(Microsoft 自动代码审查)警告。 (PREfast 不会检测类是否为引用计数。)

客户端的回调关联分为两部分:输出掩码 (SetOutputMask) 和输出回调 (SetOutputCallbacks)。 我的回调类中有助手函数,可以返回支持的输出掩码 (COutputCallbacks::SupportedMask),因而我不需要对其进行硬编码。 如果未设置掩码,则可能无法获得预期的输出。

!callbackecho 命令(参见图 7)是在随附下载的 Example07 项目中实现的。

图 7 !callbackecho

HRESULT CALLBACK 
callbackecho(PDEBUG_CLIENT pDebugClient, PCSTR args)
{
  IDebugClient* pDebugCreated;
  if (SUCCEEDED(pDebugClient->CreateClient(&pDebugCreated)))
  {
    IDebugControl* pDebugControl;
    if (SUCCEEDED(pDebugCreated->QueryInterface(__uuidof(IDebugControl), 
      (void **)&pDebugControl)))
    {
      // Create our IDebugOutputCallbacks-based object
      #pragma warning( disable : 28197 ) // PreFAST sees the 'new' as a leak due to 
                                     // the use of AddRef/Release
      COutputCallbacks* pOutputCallbacks = new COutputCallbacks;
      if (pOutputCallbacks != NULL)
      {
        // Apply IDebugOutputCallbacks to the IDebugClient
        if (SUCCEEDED(pDebugCreated->
          SetOutputMask(pOutputCallbacks->SupportedMask())) &&   
          SUCCEEDED(pDebugCreated->SetOutputCallbacks(pOutputCallbacks)))
      {
        // Execute and display 'vertarget'
        pDebugControl->Execute(DEBUG_OUTCTL_THIS_CLIENT | DEBUG_OUTCTL_NOT_LOGGED, 
          "vertarget", DEBUG_EXECUTE_NOT_LOGGED | DEBUG_EXECUTE_NO_REPEAT);
        pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_OTHER_CLIENTS, 
          DEBUG_OUTPUT_NORMAL, "[Normal]\n%s\n", pOutputCallbacks->BufferNormal() ?
pOutputCallbacks->BufferNormal() : "");
        pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_OTHER_CLIENTS, 
          DEBUG_OUTPUT_ERROR , "[Error] \n%s\n", pOutputCallbacks->BufferError() ?
pOutputCallbacks->BufferError() : "");

        // Clear the callback buffers
        pOutputCallbacks->Clear();

        // Execute and display the passed command
        pDebugControl->Execute(DEBUG_OUTCTL_THIS_CLIENT | DEBUG_OUTCTL_NOT_LOGGED, 
          args, DEBUG_EXECUTE_NOT_LOGGED | DEBUG_EXECUTE_NO_REPEAT);
        pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_OTHER_CLIENTS, 
          DEBUG_OUTPUT_NORMAL, "[Normal]\n%s\n", pOutputCallbacks->BufferNormal() ?
pOutputCallbacks->BufferNormal() : "");
        pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_OTHER_CLIENTS, 
          DEBUG_OUTPUT_ERROR , "[Error] \n%s\n", pOutputCallbacks->BufferError() ?
pOutputCallbacks->BufferError() : "");

        pDebugCreated->SetOutputCallbacks(NULL); // It's a best practice to 
                                                // release callbacks 
                                                // before the client release
        }
        pOutputCallbacks->Release();
      }
      pDebugControl->Release();
    }
    pDebugCreated->Release();
  }
  return S_OK;
}

其执行两种命令(“vertarget”和传递的参数),并分别显示它们的正常输出和错误输出。 使用 ControlledOutput 函数时,您将注意到我使用了 DEBUG_OUTCTL_ALL_OTHER_CLIENTS 输出控制。 这一控制将输出发送给其他所有客户端,并且避免输出被我的其他关联输出回调捕获。 若要遍历所捕获的输出并生成相同掩码的输出,请确保您使用的是 DEBUG_OUTCTL_ALL_OTHER_CLIENTS 输出控制,否则您可能很容易陷入无限循环之中。

如上所述,我使用了 test_windbg.cmd 脚本来测试命令。 在启动的 WinDBG 中,运行 !callbackecho 命令,并查看“vertarget”的正常输出和“r @rip”的错误输出。(register 命令失败,因为这是 x86 目标,而“rip”是 x64 寄存器。)请注意,在 Execute 调用之间,我调用了我的 Clear 助手函数以重置缓冲区。 这就是导致“vertarget”输出从“r @rip”命令输出中缺失的原因,如下所示:

0:000> !callbackecho r @rip
[Normal]
Windows 7 Version 7600 MP (2 procs) Free x86 compatible
产品:WinNt,套件:SingleUserTS
kernel32.dll 版本:6.1.7600.16385 (win7_rtm.090713-1255)
计算机名称:
调试会话时间:2011 年 1 月 29 日,星期六,22:01:59.080 (UTC - 8:00)
系统正常运行时间:0 天 6:22:48.483
流程正常运行时间:0 天 0:01:05.792

内核时间:0 天 0:00:00.015
用户时间:0 天 0:00:00.000
 
[Error]

[Normal]
 
[Error]**
**           ^“r @rip”中严重的寄存器错误**

实际上您可以在不设置自己的客户端的情况下实现相同的功能。 如果小心将您自己的值与传入客户端的输出掩码和输出回调进行交换,可以达到相同的结果。 但是,您执行这一操作时必须相当精确。 传入的 IDebugClient 是 (WinDBG) 调试器针对“输出”窗口的输出所使用的客户端。 如果您没有将其返回到起始状态,调试器将不再正常工作。

图 8(来自代码下载中的 Example07)显示的是使用 !commandtoggle 命令的切换方法。

图 8 !callbacktoggle

HRESULT CALLBACK 
callbacktoggle(PDEBUG_CLIENT pDebugClient, PCSTR args)
{
  IDebugControl* pDebugControl;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), 
    (void **)&pDebugControl)))
  {
    // Remember the original mask and callback
    ULONG ulOriginalMask;
    IDebugOutputCallbacks* pOriginalCallback;
    if (SUCCEEDED(pDebugClient->GetOutputMask(&ulOriginalMask)) &&
      SUCCEEDED(pDebugClient->GetOutputCallbacks(&pOriginalCallback)))
    {
      // Create our IDebugOutputCallbacks based object
      #pragma warning( disable : 28197 ) // PreFAST sees the 'new' as a leak due to 
                                     // the use of AddRef/Release
      COutputCallbacks* pOutputCallbacks = new COutputCallbacks;
      if (pOutputCallbacks != NULL)
      {
        // Apply IDebugOutputCallbacks to the IDebugClient
        if (SUCCEEDED(pDebugClient->SetOutputMask(pOutputCallbacks->
          SupportedMask())) && (pDebugClient->
          SetOutputCallbacks(pOutputCallbacks)))
        {
          // Execute 'vertarget'
          pDebugControl->Execute(DEBUG_OUTCTL_THIS_CLIENT | 
            DEBUG_OUTCTL_NOT_LOGGED, "vertarget", 
            DEBUG_EXECUTE_NOT_LOGGED | DEBUG_EXECUTE_NO_REPEAT);

          // Revert the mask and callback so we can do some output
          pDebugClient->SetOutputMask(ulOriginalMask);
          pDebugClient->SetOutputCallbacks(pOriginalCallback);

          // Display 'vertarget'
          pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_CLIENTS,  
            DEBUG_OUTPUT_NORMAL, "[Normal]\n%s\n", pOutputCallbacks->
            BufferNormal() ?
pOutputCallbacks->BufferNormal() : "");
          pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_CLIENTS, 
            DEBUG_OUTPUT_ERROR , "[Error] \n%s\n", pOutputCallbacks->
            BufferError() ?
pOutputCallbacks->BufferError() : "");

          // Go back to our mask and callback so we can do the private callback
          pDebugClient->SetOutputMask(pOutputCallbacks->SupportedMask());
          pDebugClient->SetOutputCallbacks(pOutputCallbacks);

          // Clear the callback buffers
          pOutputCallbacks->Clear();

          // Execute the passed command
          pDebugControl->Execute(DEBUG_OUTCTL_THIS_CLIENT | 
            DEBUG_OUTCTL_NOT_LOGGED, args, DEBUG_EXECUTE_NOT_LOGGED |  
            DEBUG_EXECUTE_NO_REPEAT);

          // Revert the mask and callback so we can do some output
          pDebugClient->SetOutputMask(ulOriginalMask);
          pDebugClient->SetOutputCallbacks(pOriginalCallback);

          // Display the passed command
          pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_CLIENTS, 
            DEBUG_OUTPUT_NORMAL, "[Normal]\n%s\n", pOutputCallbacks->
            BufferNormal() ?
pOutputCallbacks->BufferNormal() : "");
          pDebugControl->ControlledOutput(DEBUG_OUTCTL_ALL_CLIENTS, 
            DEBUG_OUTPUT_ERROR , "[Error] \n%s\n", pOutputCallbacks->
            BufferError() ?
pOutputCallbacks->BufferError() : "");
        }

        // Revert the mask (again) for the case 
        // where the mask was set but the callback wasn't
        pDebugClient->SetOutputMask(ulOriginalMask);
        pOutputCallbacks->Release();
      }
    }
    pDebugControl->Release();
  }
  return S_OK;
}

该命令实现了与 !commandecho 完全相同的功能。 为了能够调用 Execute 和 ControlledOutput,我常常需要在传入值和我的值之间切换输出掩码值和输出回调值,从而获得所需的结果。 请注意,在这种情况下,ControlledOutput 传递 DEBUG_OUTCTL_ALL_CLIENTS 输出控制,因为关联客户端此时是所需的输出目标。 此外,您会注意到我在初始调用时(为了简便起见)仅实现了错误处理。

在大多数情况下,客户端的重新使用可能相当繁琐;应谨慎使用此方法。

要执行什么内容?

我使用该专用输出技术的主要原因是为了实现基于 TEXT 的命令的 DML 版本,对于这类版本,我并不了解其内部信息。 我还特别对一些带超链接的 SOS 调试器命令进行了“增值”操作。 有关如何通过解析输出来确定标记内容的信息不在本文讨论范围之内。 但是我会谈谈如何确定要执行的正确命令,以生成解析所需的输出。

加载的调试器扩展及其关联功能的枚举不属于调试器 API 的一部分。 更糟糕的是,相同的调试器扩展可以从磁盘上的同一位置加载多次。 如果使用 .load myext 和 .chain 命令运行 test_windbg.cmd 脚本,您将能看到该情况的一个示例。 MyExt 扩展加载了两次,一次是作为 myext,第二次是作为 myext.dll:

0:000> .load myext
0:000> .chain
扩展 DLL 搜索路径:

C:\Debuggers_x86\WINXP;C:\Debuggers_x86\winext;等等
扩展 DLL 链:

myext:图像 6.1.7600.16385,API 1.0.0,构建于 2011 年 1 月 29 日,星期六,22:00:48
   [路径: C:\Debuggers_x86\myext.dll]

myext.dll:图像 6.1.7600.16385,API 1.0.0,构建于 2011 年 1 月 29 日,星期六,22:00:48
   [路径:C:\Debuggers_x86\myext.dll]

dbghelp:图像 6.12.0002.633,API 6.1.6,构建于 2010 年 2 月 1 日,星期一,12:08:26
   [路径:C:\Debuggers_x86\dbghelp.dll]
   ...

调用扩展命令以进行解析时,您需要确保调用的是目标命令,而不是其他扩展(或其他实例)中具有相同名称的命令。

如果您完全限定命令,引擎将仅需要执行指定的命令即可。 但是如果您没有完全限定命令,引擎将需要进行搜索。 引擎按加载顺序查看每个扩展的导出表,并执行第一个具有匹配命令名称的扩展。 .extmatch 命令显示有关给定命令的当前搜索结果。 .extmatch 命令通过“/e”开关支持扩展名和命令通配符,如下所示:

0:000> .extmatch callbackecho
!myext.callbackecho
!myext.dll.callbackecho
0:000> .extmatch /e *myext.dll* *back*
!myext.dll.callbackecho
!myext.dll.callbacktoggle

当您通过调用 .loadby sos.dll mscorwks 加载 SOS 扩展时,完全限定的路径将为之寄存,也就是说,该扩展不会命名为“!sos.dll.<command>”。在以下示例中,我在 Windows 7 x64 中运行了 Test03 示例应用程序,并将调试器附加到该程序 (F6)。 SOS 扩展是从 mscorwks 文件夹 (C:\Windows\Microsoft.NET\Framework64\v2.0.50727) 加载的:

0:004> .loadby sos.dll mscorwks
0:004> .chain
扩展 DLL 搜索路径:
  C:\debuggers\WINXP;C:\debuggers\winext;
扩展 DLL 链:
  C:\Windows\Microsoft.NET\Framework64\v2.0.50727\sos.dll:
    图像 2.0.50727.4952,API 1.0.0,构建于 2010 年 5 月 13 日,星期四,05:15:18
    [路径:C:\Windows\Microsoft.NET\Framework64\v2.0.50727\sos.dll]
    dbghelp:图像 6.12.0002.633,API 6.1.6,构建于 2010 年 2 月 1 日,星期一,12:15:44
    [路径:C:\debuggers\dbghelp.dll]
    ...

安全调用 SOS 命令的唯一方法是采用完全限定。 所以,当利用 SOS 扩展(或任何其他扩展)时,需要确定两件事情:

  • 是否已加载 SOS 扩展?
  • 加载到什么位置?

两者均可通过扩展匹配来确定 (.extmatch /e *sos.dll* <command>)。 在 LoadSOS 函数(Example08 的一部分;代码未在此列出)中,我首先查找位于版本控制位置的命令 (\v2.0.50727\sos.dll.<command>)。 如果找不到版本控制命令,我将返回查找不带版本的命令 (\sos.dll.<command>)。 如果此时我能找到命令,则会返回成功信息,但返回的是 S_FALSE(与 S_OK 相对)。 是否可能出现错误解析取决于 LoadSOS 的调用方。

如果找不到命令的 SOS 版本,那么我假定它还没有加载(或者已加载,但加载方式不符合我的要求),接着执行一次专用“.loadby”。如果这没有生成任何错误,我将重复搜索。 如果无法加载 SOS 或找不到命令,我将返回失败信息 (E_FAIL)。

找到命令后,我会立即执行和重写该命令(例如:ParseDSO)并输出 DML 版本(参考代码下载中的 Example08)。

在 SOS 命令的 DML 包装程序中,我在执行命令前进行该项检查是为了确保 SOS 没有在调用之间被卸载并且所需的命令已存在。 图 9图 10 显示了我正在执行的 !dsox 命令,具有 x86 和 x64 目标。

Test03 x86 !dsox Wrapping !dumpstackobjects

图 9 Test03 x86 !dsox Wrapping !dumpstackobjects

Test03 x64 !dsox Wrapping !dumpstackobjects

图 10 Test03 x64 !dsox Wrapping !dumpstackobjects

命令将 !dumpobj 超链接添加到 !dumpstackobjects 中列出的每个地址;!dsox 的代码位于 Example08 中。

结语

借助极少量的基础结构代码,可以利用所有内置或扩展命令的逻辑。 我的 SOS 扩展解析与添加 DML 有关,但是它可能与收集托管类信息有关。 不必了解任何关于 CLR 内部结构的信息即可实现目标。 我只需要知道如何 导航到我想要显示的数据。

通过数据检索的自动化和显示的丰富化,调试器扩展简化了调试分析。 我希望本系列能为您带来灵感,让您能够实现属于自己的扩展,简化您执行的调试工作。

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

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

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