65.9K
CodeProject 正在变化。 阅读更多。
Home

WinDbg 中的 .natvis 文件和类型模板

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2016年7月4日

MIT

5分钟阅读

viewsIcon

11799

WinDbg 中的 .natvis 文件和类型模板

当我们处理二进制数据时,我们经常使用 dt 命令将字节分组为有意义的字段,例如:

0:000> dt ntdll!_PEB @$peb
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0x1 ''
   +0x003 BitField         : 0x8 ''
   +0x003 ImageUsesLargePages : 0y0
   +0x003 IsProtectedProcess : 0y0
   +0x003 IsLegacyProcess  : 0y0
   +0x003 IsImageDynamicallyRelocated : 0y1
   +0x003 SkipPatchingUser32Forwarders : 0y0
    ...

当库所有者未在符号文件中提供类型信息时,就会出现问题。我们通常只能在二进制编辑器中手动分解字节(010 Editor 有一个不错的模板系统)。如果在调试器中也提供类似模板的系统,那不是很好吗?我有个好消息要告诉你:随着 WinDbg 的最新发布,我们获得了一个非常强大的功能:**.natvis 文件**。甚至有两个 Defrag Tools 节目专门介绍此功能:Defrag Tools #138Defrag Tools #139。让我们先分析一下 .natvis 文件是如何构建的,以便以后在二进制数据分析中使用它们。

.natvis 文件

.natvis 文件已经使用了一段时间,用于自定义 Visual Studio 在监视窗口中显示变量的方式。您可以在 %VSINSTALLDIR%\Common7\Packages\Debugger\Visualizers 中找到 Visual Studio 使用的 .natvis 文件。它们是 XML 文件,根据 %VSINSTALLDIR%\XML\Schemas\1033\natvis.xsd 文件中定义的架构构建。您可以在项目中定义自己的 .natvis 文件,Visual Studio 会将它们嵌入 .pdb 文件中(更多信息 此处)。一个示例 .natvis 文件可能如下所示:

<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer 
xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
  <Type Name="tagRECT">
    <AlternativeType Name="CRect"></AlternativeType>
    <DisplayString>{{LT({left}, {top}) RB({right}, {bottom})  
          [{right-left} x {bottom-top}]}}</DisplayString>
    <Expand>
        <Item Name="[top]">top,x</Item>
        <Item Name="[right]">right,x</Item>
        <Item Name="[width]">right - left</Item>
        <Item Name="[bottom]">bottom</Item>
        <Item Name="[left]">left</Item>
    </Expand>
  </Type>
</AutoVisualizer>

Type 标签的 Name 属性非常重要——它是类型标识符,指定此模板仅用于类型与此 string 匹配的对象。DisplayString 标签的值用于显示对象的单行视图,而 Item 标签代表字段。每个字段都是一个 C++ 表达式,在当前对象的上下文中进行评估。在 DisplayString 标签中,表达式用大括号括起来,例如 {left}。此外,我们可以借助格式说明符来控制表达式值的显示。可在 MSDN 上找到可用格式说明符的列表。在我们的示例中,我们对 top 和 right 字段使用了十六进制说明符。

要将我们的 .natvis 文件加载到 WinDbg 中,我们可以使用 .nvload <path-to-the-file> 命令。要卸载它,请使用 .nvunload <file-name> 命令或 .nvunloadall 命令卸载所有 .natvis 文件。如果您希望 WinDbg 自动加载 .natvis 文件,请将其放在调试工具安装目录的 Visualizers 文件夹中。dx 命令允许您使用 .natvis 类型定义来转储对象实例的内容。官方 WinDbg .chm 文件中没有此命令的帮助,因此您只能使用 -? 开关。为了结束此段,让我们看一个加载上述 .natvis 文件的示例 WinDbg 会话:

0:000> .nvload c:\temp\windbg-dx\test.natvis
Successfully loaded visualizers in "c:\temp\windbg-dx\test.natvis"
0:000> .nvlist
Loaded NatVis Files:
    c:\temp\windbg-dx\test.natvis
0:000> dx rect
rect             : {LT(1, 2) RB(3, 4)  [2 x 2]} [Type: tagRECT]
    [<Raw View>]
    [top]            : 0x2
    [right]          : 0x3
    [width]          : 2
    [bottom]         : 4
    [left]           : 1

WinDbg 中的类型模板

在上一段中,我们研究了使用 .natvis 文件的常用方法。但是,当我们没有可用的 private 符号时,对于原始二进制数据怎么办?好消息是,仍然可以使用 dx 命令。在下一个示例中,我们将使用以下 C# 类:

public struct TestClass
{
    public Guid Id { get; set; }

    public int Count { get; set; }

    public String Name { get; set; }
}

以及一个非常简单的程序:

public static void Main() {
    var t = new TestClass() {
        Id = Guid.NewGuid(),
        Count = 2,
        Name = "test class"
    };
    Console.ReadLine();
}

让我们在应用程序等待用户输入时中断执行,并使用来自 netext 扩展的 !wdo 命令转储 TestClass 实例:

0:000> !Name2EE Test TestClass
Module:      01263fbc
Assembly:    Test.exe
Token:       02000002
MethodTable: 01264db0
EEClass:     012617b8
Name:        TestClass
0:000> !wdo -mt 01264db0 0x010ff1d0
...
629ae918   System.String +0000    _Name_k__BackingField 032f2754 test class
629b07a0    System.Int32 +0004    _Count_k__BackingField 2 (0n2)
629aba00     System.Guid +0008    _Id_k__BackingField -mt 629ABA00 00000000 {c41e14c4-95fc-402b-8e54-9f2ec1f4865e}

现在,我们将尝试使用 .natvis 文件和 dx 命令来模仿上述输出。我们的 .natvis 文件将如下所示:

<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
  <Type Name="T1">
    <DisplayString>CLR string</DisplayString>
    <Expand>
      <ArrayItems>
        <Size>*((int *)(this + 4))</Size>
        <ValuePointer>(NvWchar *)(this + 8)</ValuePointer>
      </ArrayItems>
    </Expand>
  </Type>
  <Type Name="T0">
    <Expand>
        <Item Name="Id">*((NvGuid *)(this + 8))</Item>
        <Item Name="Count">*((int *)(this + 4))</Item>
        <Item Name="Name">*((T1 *)(*(int *)this))</Item>
    </Expand>
  </Type>
</AutoVisualizer>

字段定义中大量的 * 不要吓到您:)。由于我们没有任何可以依赖的符号,因此我们需要处理指针。我们的基指针始终是 this。为了使评估器正常工作,我们始终需要指定期望的输出类型。例如,Count 字段位于 TestClass 实例的偏移量 4 处。因此,我们首先将 this 地址加上 4 个字节,将地址转换为 int *,然后对其进行解引用,因为我们对其值感兴趣——因此,表达式为 *((int *)(this + 4))。CLR String 稍微复杂一些,但规则相同。最后需要解释的是类型名称。您可能已经注意到模板中使用了 T0、T1 和 T2 这种奇怪的类型名称,以及 NvWcharNvGuiddx 命令只能操作具有符号的类型。因此,如果我们创建了一个在 .natvis 文件中完全虚构的类型并尝试将内存地址转换为它,dx 命令将不起作用。这时 NatvisTypes 库就可以派上用场了,我在这里为您定义了一些模拟类型:T0、T1、T2、…、T9。此外,还有 NvGuidNvWchar 等类型(我计划将来添加其他类型)。源代码已提交到与 lld 扩展相同的存储库:https://github.com/lowleveldesign/lldext,二进制文件可以在发布页面找到。不过有一个问题:我们需要将 NatvisTypes.dll 加载到进程中。这时 !injectdll 命令可以发挥作用,该命令是我与 lld WinDbg 扩展一起发布的。Nv* 类型的可视化器在该项目中定义,并自动添加到 NatvisTypes.pdb 文件中。WinDbg 会智能地将可视化器与 .pdb 文件一起加载。让我们看看我们的示例 TestClass 实例在调试器中的输出:

0:000> .load lld
0:000> !injectdll d:\dev\src\lldext\Win32\Debug\NatvisTypes.dll
0:000> .nvload c:\temp\TestClass.natvis
Successfully loaded visualizers in "c:\temp\TestClass.natvis"
0:000> ld NatvisTypes
*** WARNING: Unable to verify checksum for d:\dev\src\lldext\Win32\Debug\NatvisTypes.dll
Symbols loaded for NatvisTypes
0:000> dx *((T0 *)0x010ff1d0)
*((T0 *)0x010ff1d0) :  [Type: T0]
    [<Raw View>]
    Id               : 0xc41e14c4-0x95fc-0x402b-0x8e0x54-0x9f0x2e0xc10xf40x860x5e [Type: NvGuid]
    Count            : 2
    Name             : CLR string [Type: T1]
0:000> dx -r1 (*((NatvisTypes!T1 *)0x32f2754))
(*((NatvisTypes!T1 *)0x32f2754)) : CLR string [Type: T1]
    [<Raw View>]
    [0]              : 116 't' [Type: NvWchar]
    [1]              : 101 'e' [Type: NvWchar]
    [2]              : 115 's' [Type: NvWchar]
    [3]              : 116 't' [Type: NvWchar]
    [4]              : 32 ' ' [Type: NvWchar]
    [5]              : 99 'c' [Type: NvWchar]
    [6]              : 108 'l' [Type: NvWchar]
    [7]              : 97 'a' [Type: NvWchar]
    [8]              : 115 's' [Type: NvWchar]
    [9]              : 115 's' [Type: NvWchar]

我知道这个例子不是最好的,但请注意,我们将原始二进制数据转换成了有意义的东西。我还没有触及事后调试的主题。无法将 DLL 注入到转储文件中。当您需要分析转储文件时,您将不得不使用您拥有符号的任何类型,但通常不会使用它们,例如:

0:000> dt ntdll!*
          ...
          ntdll!_ALTERNATIVE_ARCHITECTURE_TYPE
          ntdll!_KUSER_SHARED_DATA
          ntdll!_TP_POOL
          ntdll!_TP_CLEANUP_GROUP
          ntdll!_ACTIVATION_CONTEXT
          ntdll!_TP_CALLBACK_INSTANCE
          ...

您可以在 .natvis 文件中覆盖它们,然后转换内存。这相当繁琐,但我还没有找到更好的方法。最后,如果您还没有被 dx 命令打动,请在您的 WinDbg 会话中查看 dx Debugger 调用的输出。

分类:CodeProject, windbg

© . All rights reserved.