【问题标题】:How to properly work with non-primitive ClrInstanceField values using ClrMD?如何使用 ClrMD 正确处理非原始 ClrInstanceField 值?
【发布时间】:2026-01-31 11:15:02
【问题描述】:

我有一些非常大的托管进程的内存转储,我试图从中获取大量统计信息,并且能够呈现堆上相当深的对象图的交互式视图.想想类似于!do <address>prefer_dml 1 在WinDbg 中设置的SOS,您可以在其中不断单击属性并查看它们的值,只有在比较许多对象的更友好的UI 中。

我发现 Microsoft.Diagnostics.Runtime (ClrMD) 特别适合这项任务,但我很难处理数组字段,而且我有点困惑关于对象字段,我做得更好一点。


数组: 如果我将一个地址直接从堆中取出的数组作为目标并使用ClrType.GetArrayLengthClrType.GetArrayElementValue 一切正常,但是一旦我挖掘另一个对象上的字段,我不确定我从中得到什么价值ClrInstanceField.GetValueClrInstanceField.ElementTypeClrElementType.SZArray 时(我还没有遇到Array 在我的对象图中挖掘,但我也想处理它)。

编辑: 我刚刚决定使用ClrType for System.UInt64 来取消引用数组字段(使用parent address + offset of the array field 来计算数组指针所在的地址已存储),然后我可以像从 EnumerateObjects 获得它一样使用它。我现在在一些不支持 ArrayComponentType 属性的数组上遇到了一些困难。我还没有使用结构数组进行测试,所以我也想知道这是否会像int[] 那样是 C 风格的内联结构分配,或者它是否是指向堆上结构的指针数组。 Guid[] 是我在获取 ArrayComponentType 时遇到问题的类型之一。

对象已修复(逻辑错误) 有了TypeClrElementType.ObjectClrInstanceField,我得到了更好的结果,但还需要更多。首先,在调用GetFieldValue 之后,我得到了一个ulong 地址(?),我可以使用ClrInstanceField.Type.Fields 来处理它,所以我可以看到嵌套对象的字段名称和值。也就是说,我必须考虑多态性,所以我尝试在同一个地址上使用ClrHeap.GetObjectType,它要么返回NULL,要么完全不正确。该地址在我的第一个用例中有效,但在第二个用例中无效,这似乎很奇怪。

字符串已修复(找到解决方法) 因为我的真实项目已经使用了带 SOS 的 DbgEng,所以我有一种不同的方法可以通过地址轻松获取字符串的值,但是尝试使用 ClrInstanceField.GetFieldValue 成功返回一个字符串似乎很奇怪,但完全结果不准确(一堆奇怪的字符)。也许我做错了?


编辑:我已经从我的原始代码中提取了一个现在在 LINQPad 中运行的抽象。在这里发帖有点长,但都是here in a gist。所有的复制/粘贴/重构仍然有点混乱,我会进一步清理它,在解决这些问题后,可能会在 CodePlex 或 GitHub 上发布最终源代码。

代码库相当大,并且是针对某个项目的,但如果绝对有必要,我可以提取一个示例集。也就是说,对 ClrMD 对象的所有访问都相当简单。我从诸如!dumpheap -stat 之类的SOS 命令中获取初始地址(这对根对象工作正常),然后我使用ClrHeap.GetTypeByNameClrHeap.GetObjectType。之后,它完全依赖于ClrType.FieldsClrInstanceField 成员TypeElementTypeGetFieldValue

作为额外的奖励,我确实找到了随 NuGet 包提供的 XML 文档的 browser friendly 版本,尽管它与 IntelliSense 提供的文档相同。

【问题讨论】:

标签: c# .net windbg sos clrmd


【解决方案1】:

如果不看你的代码是什么样子,很难非常准确地回答,但基本上,它是这样的:

为了能够调用 GetFieldAddress/GetFieldValue,您需要知道的第一件事是您拥有的对象地址是常规指针还是内部指针。也就是说,如果它直接指向堆上的对象,或者指向实际对象中的内部结构(想想实际对象中的 String 与 Struct 字段)。

如果您从 GetFieldAddress/GetFieldValue 中得到了错误的值,这通常意味着您没有指定您有一个内部指针(或者您认为自己有一个但实际上没有)。

第二部分是理解这些值的含义。

如果 field.IsPrimitive() 为真:GetFieldValue() 将为您获取实际的原始值(即 Int32、Byte 或其他)

如果 field.IsValueClass() 为真,则 GetFieldAddress() 将为您提供指向该结构的内部指针。因此,您在该地址上使用的任何 GetFieldAddress/Value() 调用都需要告诉它它是一个内部指针!

如果 field.ElementType 是 ClrElementType.String,那么我似乎记得你需要调用 GetFieldValue 才能得到实际的字符串内容(需要检查,但应该是这样)。

否则,您有一个对象引用,在这种情况下,GetFieldValue() 将为您获取一个指向新引用对象的常规指针。

这有意义吗?

【讨论】:

  • 这很有意义,尽管我认为我遇到的一些困难是基于我的代码中的抽象,它不知道它是否是内部的;在某些情况下,仅使用 Address 和 Offset 对我来说更容易思考,尽管现在我正在清理我的代码,我可能可以重新使用 GetFieldValue。实际上,我现在已经提取了很多代码,几乎可以在 LINQPad 中独立运行(与由于某种原因不报告其 ArrayComponentType 的 SZArrays 作斗争)。
  • 至于字符串,GetFieldValue 肯定会返回奇怪的值,但我评论了一种解决方法,我发现它获取字符串长度,然后只使用 ReadMemory 填充 Unicode 数据的字节 [],这很容易足以与之合作。一旦我的独立运行正常,我会更新帖子并可能要求更多反馈?非常感谢。
  • 我已经更新了问题,现在还包括代码链接。
  • 我快速浏览了代码,确实似乎您需要在导航对象结构时更好地跟踪内部指针。这确实不难,但确实需要清楚地考虑地址指向的内容。
  • 另外,关于数组,您需要记住的一点是,由于 CLR 调试接口中存在丢失该信息的问题,您需要记住一些实例会丢失 ArrayComponentType。如果你有一个原始/结构数组,这真的很重要,否则,你应该总是使用项目地址从堆中获取类型。