调试器 API

编写 Debugging Tools for Windows 扩展

Andrew Richards

 

下载代码示例

http://download.csdn.net/detail/whatday/7133071

对生产问题进行故障排除可能是任何一位工程师要完成的最令人头疼的工作之一。 但同时它也可能是让人最有成就感的工作之一。 我在 Microsoft 技术支持部工作,每天都要面对这个问题。 应用程序为什么崩溃?它为什么中断? 它为什么出现性能问题?

学习如何调试可能是一项令人望而却步的任务,并且需要长时间的经常性练习才能熟练掌握这项技能。 但它却是使您成为开发全才的一项重要技能。 此外,通过整合一些调试专家的技能,我们可以使各种技能水平的调试器工程师都像执行简单的命令一样来执行极其复杂的调试逻辑。

有许多种故障排除方法可以用来找出崩溃的根本原因,但对于具备调试技能的工程师来说,最有价值也是最具成效的方法是进程转储。 进程转储包含捕获时进程内存的快照。 这可能是整个地址空间,也可能只是一个子集,具体还要取决于转储工具。

当任何应用程序引发未处理的异常时,Windows 便会通过 Windows 错误报告 (WER) 自动创建一个小型转储。 此外,您也可以通过 Userdump.exe 工具手动创建一个转储文件。 Sysinternals 工具 ProcDump (technet.microsoft.com/sysinternals/dd996900) 已逐渐成为 Microsoft 技术支持部的首选进程转储工具,因为它可以根据大量不同的触发器来捕获转储,并且可以生成各种大小的转储。 但是,当您获得转储数据后,您可以使用这些数据来执行哪些操作从而协助调试呢?

各种版本的 Visual Studio 都支持打开转储文件 (.dmp),但最好用的工具是 Debugging Tools for Windows 中的调试器。 这些工具全部基于一个调试引擎,该引擎支持两个调试器扩展 API。 在本文中,我将介绍关于构建自定义调试器扩展的基础知识,以便您可以轻松分析这些转储文件(以及实时系统)。

设置工具

Debugging Tools for Windows (microsoft.com/whdc/devtools/debugging) 是 Windows SDK 和 Windows Driver Kit (WDK) 的一个可安装、可再发行的组件。 在我编写这篇文章时,当前版本是 6.12,Windows SDK 或 WDK 的 7.1 版本中提供了该工具。 我建议使用最新版本,因为调试引擎增加了很多有价值的功能,包括更好的堆栈遍历。

Debugging Tools for Windows 准则中讲到,您应该使用 WDK 构建环境编译调试器扩展。 我使用的是最新版本的 WDK(7.1.0 版,内部版本 7600.16385.1),但任何版本的 WDK 或其前身(驱动程序开发工具包,简称 DDK)就足够了。 使用 WDK 构建扩展时,您应使用 x64 Free Build Environment 和 x86 Free Build Environment。

经过一些努力,您还可以对我的项目进行调整,以便在 Windows SDK 构建环境或 Visual Studio 中进行构建。

有一点需要警告大家:WDK 不支持在路径名中使用空格。 请确保您从未被破坏的路径进行编译。 例如,使用类似于 C:\Projects instead of C:\Users\Andrew Richards\Documents\Projects 这样的路径。

无论您如何构建扩展,您都需要调试工具 SDK(Debugging Tools for Windows 的组件)的头文件和库文件。 本文中的示例使用我的 x86 路径 (C:\debuggers_x86\sdk) 来引用头文件和库文件。 如果您选择将调试器安装到其他位置,请记住在必要时更新路径并添加引号以容纳路径名中的空格。

使用调试工具

Debugging Tools for Windows 调试器与体系结构无关。 任何版本的调试器都可以调试任何目标体系结构。一个常见的例子就是使用 x64 调试器调试 x86 应用程序。 调试器是针对 x86、x64 (amd64) 和 IA64 发布的,但它可以调试 x86、x64、IA64、ARM、EBC 和 PowerPC (Xbox) 应用程序。 您可以并行安装所有版本的调试器。

但是很多人并不了解这种灵活性。 并非所有调试器扩展都能像调试器引擎那样适应目标体系结构。 某些调试器扩展假设目标的指针大小与调试器的指针大小相同。 同样,它们使用错误的硬编码注册表(尤其是代替 rsp 等),而不是伪寄存器,如 $csp。

如果您在使用调试器扩展时遇到问题,您应尝试运行专为与目标环境相同的体系结构设计的调试器。 这样可能会克服扩展编写得不好的情况。

各个应用程序构建类型和相关的处理器体系结构都有其自己的一系列调试难题。 为调试版本生成的汇编程序相对来说是线性的,但为发布版本生成的汇编程序是优化的,而且就像一碗意大利面一样。 在 x86 体系结构中,帧指针省略 (FPO) 会对调用堆栈的重建造成严重破坏(最新调试器可很好地解决此问题)。 在 x64 体系结构中,函数参数和本地变量存储在注册表中。 捕获转储时,它们可能已被推送到了堆栈,或者可能由于重用注册表而不复存在。

经验是关键。 确切地说,一个人的经验是关键。 您只需要将此人在调试器扩展方面的知识整合在一起供其他人使用。 只需要重复几次类似的调试序列,我便可以将该调试序列作为调试器扩展来自动执行。 我使用了我的许多扩展,以至于我都忘记了我是怎么使用基本的调试命令来执行重复操作的。

使用调试器 API

有两种调试器扩展 API:弃用的 WdbgExts API (wdbgexts.h) 和当前的 DbgEng API (dbgeng.h)。

WdbgExts 扩展基于在初始化 (WinDbgExtensionDllInit) 时配置的全局调用:

 

WINDBG_EXTENSION_APIS ExtensionApis; 

      

 

 

全局调用提供了必要的功能,可在没有任何命名空间的情况下运行 dprintf(“\n”) 和 GetExpression(“@$csp”) 等函数。 此类扩展与您在进行 Win32 编程时要编写的代码类似。

DbgEng 扩展基于调试器接口。 调试引擎将 IDebugClient 接口传递给您作为各个调用的参数。 这些接口支持 QueryInterface 访问不断增多的各种调试器接口。 此类扩展与您在进行 COM 编程时要编写的代码类似。

可以将这两种扩展结合在一起。 您将扩展公开为 DbgEng,但通过调用在运行时将 WdbgExts API 的功能添加到 IDebugControl::GetWindbgExtensionApis64。 例如,我用 C 语言将经典的“Hello World”编写为一个 DbgEng 扩展。 如果您喜欢用 C++,请参考调试工具 SDK (. \inc\engextcpp.cpp) 中的 ExtException 类。

将扩展编译为 MyExt.dll(图 1 中显示的源文件中的 TARGETNAME)。 它公开了一个名为 !helloworld 的命令。 此扩展可动态链接到 Microsoft Visual C 运行时 (MSVCRT)。 如果您想使用静态链接,可在源文件中将 USE_MSVCRT=1 语句更改为 USE_LIBCMT=1。

图 1 源

 

TARGETNAME=MyExt
TARGETTYPE=DYNLINK
 
_NT_TARGET_VERSION=$(_NT_TARGET_VERSION_WINXP)
 
DLLENTRY=_DllMainCRTStartup
 
!if "$(DBGSDK_INC_PATH)" != ""
INCLUDES = $(DBGSDK_INC_PATH);$(INCLUDES)
!endif
!if "$(DBGSDK_LIB_PATH)" == ""
DBGSDK_LIB_PATH = $(SDK_LIB_PATH)
!else
DBGSDK_LIB_PATH = $(DBGSDK_LIB_PATH)\$(TARGET_DIRECTORY)
!endif
 
TARGETLIBS=$(SDK_LIB_PATH)\kernel32.lib \
           $(DBGSDK_LIB_PATH)\dbgeng.lib
 
USE_MSVCRT=1
 
UMTYPE=windows
 
MSC_WARNING_LEVEL = /W4 /WX
 
SOURCES= dbgexts.rc      \
         dbgexts.cpp     \
         myext.cpp

 

 

 

加载扩展时将调用 DebugExtensionInitialize 函数(请参见图 2)。 设置 Version 参数时,只需将 DEBUG_EXTENSION_VERSION 宏与我已添加到头文件中的 EXT_MAJOR_VER 和 EXT_MINOR_VER #defines 一起使用:

 

// dbgexts.h
 
#include <windows.h>
#include <dbgeng.h>
 
#define EXT_MAJOR_VER  1
#define EXT_MINOR_VER  0

      

 

图 2 dbgexts.cpp

 

// dbgexts.cpp
 
#include "dbgexts.h"
 
extern "C" HRESULT CALLBACK
DebugExtensionInitialize(PULONG Version, PULONG Flags) {
  *Version = DEBUG_EXTENSION_VERSION(EXT_MAJOR_VER, EXT_MINOR_VER);
  *Flags = 0;  // Reserved for future use.
          return S_OK;
}
 
extern "C" void CALLBACK
DebugExtensionNotify(ULONG Notify, ULONG64 Argument) {
  UNREFERENCED_PARAMETER(Argument);
  switch (Notify) {
    // A debugging session is active.
          The session may not necessarily be suspended.
          case DEBUG_NOTIFY_SESSION_ACTIVE:
      break;
    // No debugging session is active.
          case DEBUG_NOTIFY_SESSION_INACTIVE:
      break;
    // The debugging session has suspended and is now accessible.
          case DEBUG_NOTIFY_SESSION_ACCESSIBLE:
      break;
    // The debugging session has started running and is now inaccessible.
          case DEBUG_NOTIFY_SESSION_INACCESSIBLE:
      break;
  }
  return;
}
 
extern "C" void CALLBACK
DebugExtensionUninitialize(void) {
  return;
}
        

 

 

 

Version 值报告为调试器 .chain 命令中的 API 版本。 要更改文件版本、文件描述、版权和其他值,您需要编辑 dbgexts.rc 文件:

 

  myext.dll: image 6.1.7600.16385, API 1.0.0, built Wed Oct 13 20:25:10 2010
  [path: C:\Debuggers_x86\myext.dll]

 

Flags 参数为保留参数,应将其设置为零。 此函数需要返回 S_OK。

当会话更改其活动状态或可访问状态时,将调用 DebugExtensionNotify 函数。 Argument 参数使用 UNREFERENCED_PARAMETER 宏进行了包装,以消除未使用的参数编译器警告。

为了保持完整,我为 Notify 参数添加了 switch 语句,但我尚未在此区域添加任何功能代码。 switch 语句可处理四种会话状态更改:

  • 附加到目标时,将发生 DEBUG_NOTIFY_SESSION_ACTIVE。
  • 当目标被分离(通过 .detach 或 qd)时,将发生 DEBUG_NOTIFY_SESSION_INACTIVE。
  • 如果目标挂起(例如,遇到断点),将向函数传递 DEBUG_NOTIFY_SESSION_ACCESSIBLE。
  • 如果目标重新运行,将向函数传递 DEBUG_NOTIFY_SESSION_INACCESSIBLE。

卸载扩展时将调用 DebugExtensionUninitialize 函数。

每个要公开的扩展命令都被声明为 PDEBUG_EXTENSION_CALL 类型的函数。 函数的名称是扩展命令的名称。 因为我编写的是“Hello World”,所以我将函数命名为 helloworld(请参见图 3)。

图 3 MyExt.cpp

 

// MyExt.cpp
 
#include "dbgexts.h"
 
HRESULT CALLBACK 
helloworld(PDEBUG_CLIENT pDebugClient, PCSTR args) {
  UNREFERENCED_PARAMETER(args);
 
  IDebugControl* pDebugControl;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), 
    (void **)&pDebugControl))) {
    pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "Hello World!
          \n");
    pDebugControl->Release();
  }
  return S_OK;
}

 

请注意,约定将使用小写的函数名称。 因为我使用的是 WDK 构建环境,所以 myext.def 文件也需要更改。需要添加扩展命令的名称,以便将扩展命令导出:

 

 
 
;-------------
;   MyExt.def
;-------------
EXPORTS
  helloworld
  DebugExtensionNotify
  DebugExtensionInitialize
  DebugExtensionUninitialize
        

 

args 参数包含命令的一系列参数。 参数将作为以 Null 结尾的 ANSI 字符串 (CP_ACP) 传递。

pDebugClient 参数是允许扩展与调试引擎交互的 IDebugClient 接口指针。 虽然该接口指针看上去像是一个 COM 接口指针,但它不能被封送,而且以后也不能被访问。 此外,也不能从任何其他线程使用该指针。 为了在备用线程上工作,必须使用 IDebugClient::CreateClient 在此线程上创建一个新的调试器客户端(IDebugClient 的新接口指针)。 这是能够在备用线程上运行的唯一函数。

IDebugClient 接口(像所有接口一样)由 IUnknown 派生而来。 您使用 QueryInterface 来访问其他 DbgEng 接口,无论它们是 IDebugClient 接口 (IDebugClient4) 的后续版本,还是其他接口(IDebugControl、IDebugRegisters、IDebugSymbols、IDebugSystemObjects 等等)的后续版本。 要向调试器输出文本,您需要 IDebugControl 接口。

我的文件夹中有两个可为开发工作提供帮助的非 SDK 文件。 make.cmd 脚本将 Debugger SDK inc 和 lib 路径添加到 WDK 构建环境中,然后运行相应的构建命令:

 

@echo off
set DBGSDK_INC_PATH=C:\Debuggers_x86\sdk\inc
set DBGSDK_LIB_PATH=C:\Debuggers_x86\sdk\lib
set DBGLIB_LIB_PATH=C:\Debuggers_x86\sdk\lib
build -cZMg %1 %2

 

请注意,WDK 构建环境本身可确定将构建 x86 二进制文件还是 x64 二进制文件。 如果您想为多个体系结构进行构建,则需要打开多个提示符并运行各个提示符中的 make.cmd。 同时可进行构建操作。

构建后,我使用 (x86) test.cmd 脚本将编译好的 i386 二进制文件复制到 x86 调试器文件夹 (c:\Debuggers_x86) 中,然后启动记事本的一个实例,同时附加了调试器且加载了扩展:

 

@echo off
copy objfre_win7_x86\i386\myext.dll c:\Debuggers_x86
copy objfre_win7_x86\i386\myext.pdb c:\Debuggers_x86
\Debuggers_x86\windbg.exe -a myext.dll -x notepad

 

如果一切都能按计划进行,那么我就可以在调试器命令提示符下键入“!helloworld”,之后便可以看到一个“Hello World!”响应:

 

0:000> !helloworld
Hello World!

     

 

 

符号解析和读取

“Hello World”应用程序也许非常出色,但您可以做得更好。 现在,我要使用这个基础结构来添加一个命令,该命令实际上与目标交互,并且将帮助您执行某些分析。 test01 示例应用程序有一个全局指针,该指针被分配了一个值:

 

// test01.cpp
 
#include <windows.h>
 
void* g_ptr;
int main(int argc, char* argv[]) {
  g_ptr = "This is a global string";
  Sleep(10000);
  return 0;
}

         

 

 

MyExt.cpp 中的新 !gptr 命令(请参见图 4)将解析 test01!g_ptr 全局字符串函数,读取指针,然后以“x test01!g_ptr”格式输出找到的值。

 

 
0:000> x test01!g_ptr
012f3370 Test01!g_ptr = 0x012f20e4
 
0:000> !gptr
012f3370 test01!g_ptr = 0x012f20e4
<string> 

 

 

 

 

图 4 修订后的 MyExt.cpp

 

HRESULT CALLBACK 
gptr(PDEBUG_CLIENT pDebugClient, PCSTR args) {
  UNREFERENCED_PARAMETER(args);
 
  IDebugSymbols* pDebugSymbols;
  if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugSymbols), 
    (void **)&pDebugSymbols))) {  
    // Resolve the symbol.
          ULONG64 ulAddress = 0;
    if (SUCCEEDED(pDebugSymbols->GetOffsetByName("test01!g_ptr", &ulAddress))) {
      IDebugDataSpaces* pDebugDataSpaces;
      if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugDataSpaces),
        (void **)&pDebugDataSpaces))) {  
        // Read the value of the pointer from the target address space.
          ULONG64 ulPtr = 0;
        if (SUCCEEDED(pDebugDataSpaces->ReadPointersVirtual(1, ulAddress, &ulPtr))) {
          PDEBUG_CONTROL pDebugControl;
          if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), 
            (void **)&pDebugControl))) {  
            // Output the values.
          pDebugControl->Output(DEBUG_OUTPUT_NORMAL, 
              "%p test01!g_ptr = 0x%p\n", ulAddress, ulPtr);
            pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "%ma\n", ulPtr);
            pDebugControl->Release();
          }
        }
        pDebugDataSpaces->Release();
      }
      pDebugSymbols->Release();
    }
  }
  return S_OK;
}

      

 

 

第一步是确定 test01!g_ptr 指针的位置。 应用程序每一次运行时,此指针都位于不同的位置,因为地址空间布局随机化 (ASLR) 将更改模块加载地址。 为了确定位置,我使用 QueryInterface 来获得 IDebugSymbols 接口,然后使用 GetOffsetByName。 GetOffsetByName 函数带有一个符号名称,然后将地址作为一个 64 位指针返回。 调试器函数始终返回 64 位指针 (ULONG64),以便可以用 32 位调试器来调试 64 位目标。

请记住,此地址是目标地址空间中的指针地址,而不是您自己的地址。 您不能仅通过从它读取来确定它的值。 为了获得指针的值,我再次使用 QueryInterface 来获得 IDebugDataSpaces 接口,然后使用 ReadPointersVirtual。 这将从目标地址空间读取指针。 ReadPointersVirtual 将针对指针大小和 endian 方面的不同来自动进行调整。 您不必对返回的指针进行操作。

IDebugControl::Output 使用与 printf 的格式相同的字符串,但也拥有允许您引用目标地址空间的格式化程序。 我使用 %ma 格式来输出 ANSI 字符串,全局指针在目标地址空间中指向该字符串。 %p 格式对指针大小敏感,应当用于指针输出(您必须传递 ULONG64)。

我已将测试脚本修改为加载 x86 版 test01 的转储文件,而不是启动记事本:

 

@echo off
copy objfre_win7_x86\i386\myext.dll c:\Debuggers_x86
copy objfre_win7_x86\i386\myext.pdb c:\Debuggers_x86
\Debuggers_x86\windbg.exe -a myext.dll -y "..
          \Test01\x86;SRV*c:\symbols*http://msdl.microsoft.com/download/symbols" -z ..
          \Test01\x86\Test01.dmp  

 

 

 

 

我还设置了指向 test01 x86 文件夹和 Microsoft 公共符号服务器的符号路径,这样一来,一切都可以解析了。 此外,我还制作了一个 x64 测试脚本,它与 x86 测试脚本的作用相同,但有一个 x64 版测试应用程序的转储文件:

 

@echo off
copy objfre_win7_x86\i386\myext.dll c:\Debuggers_x86
copy objfre_win7_x86\i386\myext.pdb c:\Debuggers_x86
\Debuggers_x64\windbg.exe -a myext.dll -y "..
\Test01\x64;SRV*c:\symbols*http://msdl.microsoft.com/download/symbols" -z ..
\Test01\x64\Test01.dmp

 

当我运行脚本时,即会启动 x86 调试器,打开相应的转储文件,加载 x86 版扩展,且可解析符号。

再次重申,如果一切都按计划进行,那么我可以在调试器命令提示符下键入“x test01!g_ptr”和 !gptr,并看到类似的响应:

 

 
// x86 Target
0:000> x test01!g_ptr
012f3370 Test01!g_ptr = 0x012f20e4
 
0:000> !gptr
012f3370 test01!g_ptr = 0x012f20e4
This is a global string
 
// x64 Target
0:000> x test01!g_ptr
00000001`3fda35d0 Test01!g_ptr = 0x00000001`3fda21a0
 
0:000> !gptr
000000013fda35d0 test01!g_ptr = 0x000000013fda21a0
This is a global string

 

如果您使用 x64 调试器、amd64 兼容版调试器扩展和 x86 或 x64 转储文件来重复进行测试,那么您将得到相同的结果。 也就是说,扩展与体系结构无关。

处理器类型和堆栈

现在,我要再次扩展此基础结构。 让我们添加一个命令,用来查找当前线程堆栈上 Sleep 调用的持续时间。!sleepy 命令(请参见图 5)将解析调用堆栈符号、查找 Sleep 函数并读取表示要延迟的毫秒数的 DWORD,然后输出延迟值(如果有)。

图 5 Sleepy

 

 
HRESULT CALLBACK 
sleepy(PDEBUG_CLIENT4 Client, PCSTR args) {
  UNREFERENCED_PARAMETER(args);
  BOOL bFound = FALSE;
 
  IDebugControl* pDebugControl;
 
  if (SUCCEEDED(Client->QueryInterface(__uuidof(IDebugControl), 
    (void **)&pDebugControl))) {
    IDebugSymbols* pDebugSymbols;
 
    if (SUCCEEDED(Client->QueryInterface(__uuidof(IDebugSymbols), 
      (void **)&pDebugSymbols))) {
      DEBUG_STACK_FRAME* pDebugStackFrame = 
        (DEBUG_STACK_FRAME*)malloc(
        sizeof(DEBUG_STACK_FRAME) * MAX_STACK_FRAMES);
 
      if (pDebugStackFrame != NULL) {  
        // Get the Stack Frames.
          memset(pDebugStackFrame, 0, (sizeof(DEBUG_STACK_FRAME) * 
          MAX_STACK_FRAMES));
        ULONG Frames = 0;
 
        if (SUCCEEDED(pDebugControl->GetStackTrace(0, 0, 0, 
          pDebugStackFrame, MAX_STACK_FRAMES, &Frames)) && 
          (Frames > 0)) {
          ULONG ProcessorType = 0;
          ULONG SymSize = 0;
          char SymName[4096];
          memset(SymName, 0, 4096);
          ULONG64 Displacement = 0;
 
          if (SUCCEEDED(pDebugControl->GetEffectiveProcessorType(
            &ProcessorType))) {
            for (ULONG n=0; n<Frames; n++) {  
 
              // Use the Effective Processor Type and the contents 
              // of the frame to determine existence
              if (SUCCEEDED(pDebugSymbols->GetNameByOffset(
                pDebugStackFrame[n].InstructionOffset, SymName, 4096, 
                &SymSize, &Displacement)) && (SymSize > 0)) {
 
                if ((ProcessorType == IMAGE_FILE_MACHINE_I386) && 
                  (_stricmp(SymName, "KERNELBASE!Sleep") == 0) && 
                  (Displacement == 0xF)) {  
                  // Win7 x86; KERNELBASE!Sleep+0xF is usually in frame 3.
          IDebugDataSpaces* pDebugDataSpaces;
 
                  if (SUCCEEDED(Client->QueryInterface(
                    __uuidof(IDebugDataSpaces), 
                    (void **)&pDebugDataSpaces))) {  
                    // The value is pushed immediately prior to 
                    // KERNELBASE!Sleep+0xF
                    DWORD dwMilliseconds = 0;
 
                    if (SUCCEEDED(pDebugDataSpaces->ReadVirtual(
                      pDebugStackFrame[n].StackOffset, &dwMilliseconds, 
                      sizeof(dwMilliseconds), NULL))) {
                      pDebugControl->Output(DEBUG_OUTPUT_NORMAL, 
                        "Sleeping for %ld msec\n", dwMilliseconds);
                      bFound = TRUE;
                    }
                    pDebugDataSpaces->Release();
                  }
                  if (bFound) break;
                }
 
                else if ((ProcessorType == IMAGE_FILE_MACHINE_AMD64) && 
                  (_stricmp(SymName, "KERNELBASE!SleepEx") == 0) && 
                  (Displacement == 0xAB)) {  
                  // Win7 x64; KERNELBASE!SleepEx+0xAB is usually in frame 1.
          IDebugRegisters* pDebugRegisters;
 
                  if (SUCCEEDED(Client->QueryInterface(
                    __uuidof(IDebugRegisters), 
                    (void **)&pDebugRegisters))) {  
                    // The value is in the 'rsi' register.
          ULONG rsiIndex = 0;
                    if (SUCCEEDED(pDebugRegisters->GetIndexByName(
                      "rsi", &rsiIndex)))
                    {
                      DEBUG_VALUE debugValue;
                      if (SUCCEEDED(pDebugRegisters->GetValue(
                        rsiIndex, &debugValue)) && 
                        (debugValue.Type == DEBUG_VALUE_INT64)) {  
                        // Truncate to 32bits for display.
          pDebugControl->Output(DEBUG_OUTPUT_NORMAL, 
                          "Sleeping for %ld msec\n", debugValue.I32);
                        bFound = TRUE;
                      }
                    }
                    pDebugRegisters->Release();
                  }
 
                  if (bFound) break;
                }
              }
            }
          }
        }
        free(pDebugStackFrame);
      }
      pDebugSymbols->Release();
    }
    if (!bFound)
      pDebugControl->Output(DEBUG_OUTPUT_NORMAL, 
        "Unable to determine if Sleep is present\n");
    pDebugControl->Release();
  }
  return S_OK;
}
 

 

此命令将支持 x86 和 x64 版的 test01 应用程序,这增加了命令的复杂性。 x86 和 x64 应用程序的调用约定是不同的,因此,此命令在执行过程中必须知道目标的体系结构。

第一步是获得堆栈帧。 为了获得这些帧,我先用 QueryInterface 来获得 IDebugControl 接口,然后再用 GetStackTrace 来检索各个堆栈帧的信息。 GetStackTrace 带有 DEBUG_STACK_FRAME 结构的一个数组。 我始终将 DEBUG_STACK_FRAME 结构的数组分配给堆,这样我就不会造成堆栈溢出了。 如果将该数组分配给您的堆栈,那么当您 检索堆栈溢出目标线程时,您可能会达到自己的堆栈限制。

如果 GetStackTrace 成功,将用遍历的各个帧的信息来填充该数组。 这里所说的成功不一定是指帧信息正确。 调试器会尽全力遍历堆栈帧,但是如果符号不正确(即符号丢失或被强制加载),则可能会出错。 如果您已经使用“.reload /f /i”来强制符号加载,则将出现符号无法完全对齐。

为了有效地使用各个 DEBUG_STACK_FRAME 结构的内容,我需要知道目标的有效处理器类型。 前面曾经提到,目标体系结构可能与调试器扩展体系结构完全不同。 有效的处理器类型 (.effmach) 是目标当前使用的体系结构。

处理器类型可能与目标主机使用的处理器类型也不相同。 最常见的例子是:目标是 x86 应用程序,它通过 Windows 32-bit on Windows 64-bit (WOW64) 在 x64 版本的 Windows 上运行。 有效的处理器类型是 IMAGE_FILE_MACHINE_I386。 实际类型是 IMAGE_FILE_MACHINE_AMD64。

这意味着,您应该将 x86 应用程序就看作是 x86 应用程序,无论其是在 x86 版本的 Windows 上还是在 x64 版本的 Windows 上运行。 (唯一的例外情况是调试围绕 x86 进程的 WOW64 调用时。)

为了获得有效的处理器类型,我使用已经拥有的 IDebugControl 接口,然后使用 GetEffectiveProcessorType。

如果有效的处理器类型是 i386,那么我需要查找 KERNELBASE!Sleep+0xf 函数。 如果能够正确解析所有符号,此函数应该在帧 3 中:

 

0:000> knL4
 # ChildEBP RetAddr  
00 001bf9dc 76fd48b4 ntdll!KiFastSystemCallRet
01 001bf9e0 752c1876 ntdll!NtDelayExecution+0xc
02 001bfa48 752c1818 KERNELBASE!SleepEx+0x65
03 001bfa58 012f1015 KERNELBASE!Sleep+0xf

       

 

如果有效的处理器类型是 AMD64,那么我要查找 KERNELBASE!SleepEx+0xab 函数。 如果能够正确解析所有符号,此函数应该在帧 1 中:

 

 
0:000> knL2
 # Child-SP          RetAddr           Call Site
00 00000000'001cfc08 000007fe'fd9b1203 ntdll!NtDelayExecution+0xa
01 00000000'001cfc10 00000001'3fda101d KERNELBASE!SleepEx+0xab
   

     

 

但是,根据可用的符号解析级别,我要查找的函数符号可能在或可能不在所预料的帧中。 如果您打开 test01 x86 转储文件,且不指定符号路径,那么您可以看到一个相关的示例。 KERNELBASE!Sleep 调用将在帧 1 中,而不在帧 3 中:

 

 
0:000> knL4
 # ChildEBP RetAddr  
WARNING: Stack unwind information not available.
          Following frames may be wrong.
          00 001bfa48 752c1818 ntdll!KiFastSystemCallRet
01 001bfa58 012f1015 KERNELBASE!Sleep+0xf
02 001bfaa4 75baf4e8 Test01+0x1015
03 001bfab0 76feaf77 kernel32!BaseThreadInitThunk+0x12
  

      

 

调试器警告您可能会出现这个错误。 如果您想让您的扩展克服这些类型的问题,您应该像我一样循环访问帧,而不是只查看所预料的帧。

为了确定 Sleep 函数是否存在,我需要查找各个帧的符号。 如果有效的处理器类型与符号配成了有效对,那么就找到了此函数。 请注意,此逻辑非常脆弱,且当前用于简化示例。 该符号可能会随着内部版本和平台的不同而发生变化。 例如,Windows Server 2008 是 kernel32!Sleep+0xf,但 Windows 7 是 KERNELBASE!Sleep+0xf。

为了获得符号,我使用 QueryInterface 来获得 IDebugSymbol 接口。 然后,我使用 GetNameByOffset 来获得指令偏移地址的符号。

此符号有两部分组成:符号名称 (KERNELBASE!Sleep) 和位移 (0xf)。 符号名称是由模块名称和函数名称组成的 (<module>!<function>)。 位移是从函数的开头到调用返回后程序流返回到的位置之间的字节偏移量。

如果没有符号,那么此函数将被认为只是具有较大位移的模块名称 (Test01+0x1015)。

当我找到帧后,下一步就是要提取延迟。 当目标基于 x86 时,在已被推送到与函数调用紧邻且在函数调用之前的堆栈上的 DWORD 中,将出现延迟(请注意,这是一个脆弱的逻辑):

 

// @$csp is the pseudo-register of @esp
0:000> dps @$csp
<snip>
001bfa4c  752c1818 KERNELBASE!Sleep+0xf
001bfa50  00002710
<snip>

     

 

 

DEBUG_STACK_FRAME 结构的 StackOffset 成员实际上已经指向了此地址,因此没必要使用指针算法了。 为了获得值,我使用 QueryInterface 来获得 IDebugDataSpaces 接口,然后使用 ReadVirtual 来从目标地址空间读取 DWORD。

如果目标基于 x64,则延迟不会出现在堆栈中,而会出现在 rsi 注册表中(由于其帧与上下文的依赖关系,这也是脆弱的逻辑):

 

0:000> r @rsi
rsi=0000000000002710

 

 

 

为了获得值,我使用 QueryInterface 来获得 IDebugRegisters 接口。 首先,我需要使用 GetIndexByName 来获得 rsi 注册表的索引。 然后,我使用 GetValue 来从目标注册表读取注册表值。 因为 rsi 是 64 位注册表,所以值作为 INT64 被返回。 由于 DEBUG_VALUE 结构是一个组合,因此,您可以直接引用 I32 成员而不是 I64 成员,以获得代表传递给 Sleep 的 DWORD 的截断版本。

再次重申,在这两种情况下,我都使用 IDebugControl::Output 函数来输出结果。

结语

在本文中,我只是简单介绍了可能获得的结果。 您可以在扩展中询问和更改许多项目,堆栈、符号、注册表、内存、I/O 和环境信息只是其中的一小部分。

在以后的文章中,我将深入探讨调试器扩展与调试器之间可能存在的关系。 我将介绍调试器客户端和调试器回调,我还将使用这些来封装 SOS 调试器扩展,这样我就可以编写一个可以调试托管应用程序的扩展,而不必了解有关基础 .NET 结构的任何知识。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐