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





5.00/5 (2投票s)
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 #138 和 Defrag 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 这种奇怪的类型名称,以及 NvWchar
和 NvGuid
。dx
命令只能操作具有符号的类型。因此,如果我们创建了一个在 .natvis 文件中完全虚构的类型并尝试将内存地址转换为它,dx
命令将不起作用。这时 NatvisTypes
库就可以派上用场了,我在这里为您定义了一些模拟类型:T0、T1、T2、…、T9。此外,还有 NvGuid
和 NvWchar
等类型(我计划将来添加其他类型)。源代码已提交到与 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