.NET cDAC ManagedTypeSource 合约深度解析:按完全限定名解析托管类型布局与静态字段地址
【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
导读
ManagedTypeSource 是 .NET 运行时 cDAC(Data Contract Reader / data contract 读取器)体系中的一个核心数据合约,它允许诊断工具(SOS、dotnet-dump 等)仅凭类型的完全限定名(Fully Qualified Name,FQN)即可解析出 CLR 托管类型在目标进程中的运行时布局(instance 字段偏移),以及静态字段与线程静态字段的地址。本文以 ManagedTypeSource.md 为主体,结合仓库中 cDAC 的真实实现(ManagedTypeSource_1.cs、IManagedTypeSource.cs)与配套合约文档,逐层拆解其 API、类型解析管线、静态字段寻址模型、缓存与 Flush 语义,帮助读者在阅读诊断工具源码或扩展 cDAC 合约时快速上手。
一、合约定位:什么时候需要"按名字读托管类型"
在 cDAC 架构中,绝大多数的原生运行时数据结构(MethodTable、EEClass、FieldDesc、Object等)都通过**数据描述符(Data Descriptor)**来声明布局,读者(contract 实现)直接按偏移读取目标进程内存。但有一类场景无法用原生描述符表达:算法需要读取定义在托管程序集中的托管类型。例如:
System.Threading.Lock的内部状态;System.Runtime.CompilerServices.ConditionalWeakTable<TKey, TValue>+Container这种泛型容器内部类型;- 运行时动态创建的、没有对应原生描述符的辅助类型。
这些类型的布局信息只存在于 ECMA-335 元数据中,且其运行时布局由 JIT/loader 决定。ManagedTypeSource 合约正是为此而生:它以完全限定名为入参,把"元数据中的 TypeDef → 运行时 MethodTable/FieldDesc"的解析过程封装成稳定 API。
从 ContractRegistry.cs 可以看到,它与其他合约(Loader、EcmaMetadata、RuntimeTypeSystem、Object、Thread等)并列注册在同一个ContractRegistry上,消费方通过Target.Contracts.ManagedTypeSource访问(源码中通过target.Contracts.TryGetContract(out IManagedTypeSource mts)惰性获取)。
二、合约 API 全景
合约接口定义在 IManagedTypeSource.cs,包含 8 个方法,严格按"类型信息 / 类型句柄 / 静态字段 / 线程静态字段"四组两两配对(Try*与Get*):
// 返回 true 并填充 info 为类型的 instance 字段布局;类型无法解析时返回 false。 bool TryGetTypeInfo(string fullyQualifiedName, out Target.TypeInfo info); // 类型无法解析时抛出 InvalidOperationException。 Target.TypeInfo GetTypeInfo(string fullyQualifiedName); // 返回 true 并填充 typeHandle 为运行时类型的 ITypeHandle;无法解析时返回 false。 bool TryGetTypeHandle(string fullyQualifiedName, [NotNullWhen(true)] out ITypeHandle? typeHandle); ITypeHandle GetTypeHandle(string fullyQualifiedName); // 返回 true 并填充 address 为命名静态字段的地址;类型/字段无法解析, // 或该类的 statics 存储尚未分配时返回 false。线程静态字段一律返回 false, // 必须使用下面的线程静态 API。 bool TryGetStaticFieldAddress(string fullyQualifiedName, string fieldName, out TargetPointer address); TargetPointer GetStaticFieldAddress(string fullyQualifiedName, string fieldName); // 返回 true 并填充 address 为指定 thread 上命名线程静态字段的 per-thread 地址; // 类型/字段无法解析,或该线程上封闭类的 per-thread 存储尚未分配时返回 false。 // 非线程静态字段一律返回 false。 bool TryGetThreadStaticFieldAddress(string fullyQualifiedName, string fieldName, TargetPointer thread, out TargetPointer address); TargetPointer GetThreadStaticFieldAddress(string fullyQualifiedName, string fieldName, TargetPointer thread);几个值得注意的语义约定:
Try*返回 false 的三种原因(实现见 ManagedTypeSource_1.cs):- 类型/字段本身解析失败(FQN 不在 CoreLib 元数据中,或字段不存在);
- statics 存储尚未分配:类的静态构造函数尚未运行或类型未初始化时,静态基址指针为
TargetPointer.Null,此时返回 false 可避免调用方对"近似为 0 的小偏移量"解引用; - 字段类别不匹配:对线程静态字段调用静态 API(或反之)时返回 false,而不是悄悄返回错误地址。
Get*方法在失败时统一抛出InvalidOperationException,异常消息包含类型名/字段名,便于诊断(见 ManagedTypeSource_1.cs)。ITypeHandle是透明规范句柄:由RuntimeTypeSystem.GetTypeHandle(TargetPointer)根据目标地址(MethodTable*或TypeDesc*)规范化生成,消费方不应自行构造。其规范身份限定在目标缓存 epoch 内——Target.Flush之后重新解析同一地址可能得到不同的对象引用(详见 RuntimeTypeSystem.md)。
三、名称解析规则:只认 System.Private.CoreLib
合约的全部查找都针对运行时系统程序集System.Private.CoreLib进行,规则如下:
- 不跟随程序集转发(Assembly Forwarders):即使元数据中存在
TypeForwardedTo之类的转发,也不会被跟进,所有经由此合约解析的托管类型都预期物理存在于 CoreLib 中; - 嵌套类型用
+分隔:外层与内层类型名之间以+连接,与 ECMA-335 /Type.FullName约定一致,例如System.Runtime.InteropServices.ComWrappers+NativeObjectWrapper; - 泛型类型的嵌套(如
ConditionalWeakTable<TKey, TValue>+Container)同样适用该规则,+之前为外层类型的完整名字。
实现中对应TryFindTypeDefinition方法(ManagedTypeSource_1.cs),其查找策略刻意避开了"对命名空间/类型名中的点号做字符串拆分"的脆弱做法:
- 用
Split('+')把 FQN 切成若干段,第一段是外层类型; - 遍历
mdReader.TypeDefinitions,跳过所有typedef.IsNested的嵌套条目(嵌套类型在元数据中 Namespace 为空,只能经由外层类型的GetNestedTypes()到达),将外层段与Namespace + "." + Name组合后的字符串逐一比对; - 命中外层后,逐段在
GetTypeDefinition(currentHandle).GetNestedTypes()中按Name继续下钻,直到所有嵌套段匹配完毕。
private bool TryFindTypeDefinition( ModuleHandle moduleHandle, string fullyQualifiedName, [NotNullWhen(true)] out MetadataReader? mdReader, out TypeDefinitionHandle typeDefHandle) { mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); if (mdReader is null) return false; string[] parts = fullyQualifiedName.Split('+'); string outerFqn = parts[0]; TypeDefinitionHandle currentHandle = default; foreach (TypeDefinitionHandle handle in mdReader.TypeDefinitions) { TypeDefinition typedef = mdReader.GetTypeDefinition(handle); if (typedef.IsNested) // 嵌套类型只经 GetNestedTypes() 可达 continue; string ns = mdReader.GetString(typedef.Namespace); string name = mdReader.GetString(typedef.Name); string candidate = ns.Length == 0 ? name : ns + "." + name; if (candidate == outerFqn) { currentHandle = handle; break; } } if (currentHandle == default) return false; for (int i = 1; i < parts.Length; i++) { string nestedName = parts[i]; bool found = false; foreach (TypeDefinitionHandle nestedHandle in mdReader.GetTypeDefinition(currentHandle).GetNestedTypes()) { TypeDefinition nestedDef = mdReader.GetTypeDefinition(nestedHandle); if (mdReader.GetString(nestedDef.Name) == nestedName) { currentHandle = nestedHandle; found = true; break; } } if (!found) return false; } typeDefHandle = currentHandle; return true; }四、类型解析管线:FQN → TypeDef → MethodTable → ITypeHandle
所有 API 最终都收敛到TryResolveType(ManagedTypeSource_1.cs),其调用链完整映射了三个底层合约的协作:
private bool TryResolveType(string managedFqName, [NotNullWhen(true)] out ITypeHandle? th, [NotNullWhen(true)] out MetadataReader? mdReader, out TypeDefinition typeDef) { ILoader loader = _target.Contracts.Loader; // 1. 取得系统程序集地址(System.Private.CoreLib 的 assembly 指针) TargetPointer systemAssembly = loader.GetSystemAssembly(); if (systemAssembly == TargetPointer.Null) return false; // 2. 由 assembly 指针得到模块句柄 ModuleHandle moduleHandle = loader.GetModuleHandleFromAssemblyPtr(systemAssembly); // 3. 通过 EcmaMetadata 合约拿到该模块的 System.Reflection.Metadata 读取器 if (!TryFindTypeDefinition(moduleHandle, managedFqName, out mdReader, out TypeDefinitionHandle typeDefHandle)) return false; // 4. 取 TypeDef 的 ECMA token,经 loader 的 TypeDef → MethodTable 查找映射到运行时 MethodTable 地址 int token = MetadataTokens.GetToken((EntityHandle)typeDefHandle); TargetPointer typeHandlePtr = loader.GetModuleLookupMapElement( moduleHandle, ModuleLookupMapKind.TypeDefToMethodTable, (uint)token, out _); if (typeHandlePtr == TargetPointer.Null) return false; // 5. 包装为 ITypeHandle 并返回元数据中的 TypeDefinition th = _target.Contracts.RuntimeTypeSystem.GetTypeHandle(typeHandlePtr); typeDef = mdReader.GetTypeDefinition(typeDefHandle); return true; }管线要点:
Loader.GetSystemAssembly()定位 CoreLib 的 assembly 指针;若运行时尚未初始化(如 SOS 加载通知在运行时全局就绪前触发),返回TargetPointer.Null即失败。Loader.GetModuleLookupMapElement(module, ModuleLookupMapKind.TypeDefToMethodTable, token, out _)是 coreclr 模块中"TypeDef token → MethodTable"查找映射表的统一入口(对应原生 loader 的 lookup map),token为0x02xxxxxx形式的 TypeDef 元数据 token。RuntimeTypeSystem.GetTypeHandle(mt)读取目标地址处的MethodTable/TypeDesc数据,构造规范化的ITypeHandle;从源码看,ITypeHandle的低位比特约定为MethodTable = 0、TypeDesc = 2(掩码TypeHandleBits.ValidMask),读取逻辑需先区分这两种目标形态(见 RuntimeTypeSystem.md)。
TryGetTypeHandle即TryResolveType的薄封装,而TryGetTypeInfo与字段寻址在其基础上继续加工。
五、实例字段布局:TryGetTypeInfo 的内部构造
TryGetTypeInfo返回的Target.TypeInfo只包含instance(非静态)字段,每个字段给出相对"实例数据起点"的偏移量Offset和元素类型名TypeName。实现要点(ManagedTypeSource_1.cs):
- 通过
rts.IsValueType(th)区分值类型与引用类型; - 对引用类型,预先加上
Object类型描述符的固定大小(Data.Object.GetSize(_target),即"对象头到 MethodTable 指针为止"的字节数)。原因:coreclr 的FieldDesc偏移是相对对象头之后(MT 指针之后)计算的,而对诊断工具而言更自然的基准是对象实例起始地址;加上 Object 大小后,Offset直接就是相对实例地址的偏移; - 遍历
typeDef.GetFields(),跳过FieldAttributes.Static的字段,对每个实例字段调用:rts.GetFieldDescByName(th, fieldName)取 FieldDesc 地址(为空则跳过该字段);rts.GetFieldDescOffset(fdAddr, fd)取运行时偏移;rts.GetFieldDescType(fdAddr)取CorElementType;
TypeName由MapCorElementTypeToDescriptorName把CorElementType映射为描述符类型名(如bool/int32/uint16/nint/pointer等),供TargetFieldExtensions的调试断言做字段类型校验;无精确映射的返回 null(断言视 null 为"跳过校验")。
bool TryGetTypeInfo(string fqn, out Target.TypeInfo info) { if (!TryResolveType(fqn, out ITypeHandle? th, out MetadataReader? mdReader, out TypeDefinition typeDef)) return false; IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; bool isValueType = rts.IsValueType(th); ulong objectSize = 0; if (!isValueType) objectSize = Data.Object.GetSize(target); // 引用类型:偏移基准前移一个对象头 Dictionary<string, Target.FieldInfo> fields = new(); foreach (FieldDefinitionHandle fh in typeDef.GetFields()) { FieldDefinition fd = mdReader.GetFieldDefinition(fh); if ((fd.Attributes & FieldAttributes.Static) != 0) continue; string fieldName = mdReader.GetString(fd.Name); TargetPointer fdAddr = rts.GetFieldDescByName(th, fieldName); if (fdAddr == TargetPointer.Null) continue; uint offset = rts.GetFieldDescOffset(fdAddr, fd); CorElementType et = rts.GetFieldDescType(fdAddr); fields[fieldName] = new Target.FieldInfo { Offset = (int)(offset + objectSize), TypeName = MapCorElementTypeToDescriptorName(et), }; } info = new Target.TypeInfo { Fields = fields }; return true; }消费方读取语义(原文档明确约定):引用类型的消费者需自行加上对象头大小——注意在仓库实现中这一调整已由合约在Offset内完成;**值类型消费者(如数组内嵌的结构体条目)**则直接按槽位偏移读取,无需额外调整。底层FieldDesc的偏移取自FieldDesc.DWord2打包的 offset 位段,其中FieldOffsetNewEnc哨兵值标识 EnC 新增但尚无存储的字段(详见 RuntimeTypeSystem.md 的 Data Descriptor 表)。
六、静态字段与线程静态字段寻址
6.1 普通静态字段:TryGetStaticFieldAddress
bool TryGetStaticFieldAddress(string fqn, string fieldName, out TargetPointer address) { address = TargetPointer.Null; if (!TryGetFieldDesc(fqn, fieldName, out TargetPointer fdAddr)) return false; IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; // 线程静态字段返回的是 per-thread 偏移而非绝对地址,这里直接拒绝 if (rts.IsFieldDescThreadStatic(fdAddr)) return false; // 门控:封闭类的 statics 基址必须已分配,避免类未初始化时解引用"近似为 0 的偏移" TargetPointer enclosingMT = rts.GetMTOfEnclosingClass(fdAddr); ITypeHandle ctx = rts.GetTypeHandle(enclosingMT); CorElementType et = rts.GetFieldDescType(fdAddr); bool isGC = et is CorElementType.Class or CorElementType.ValueType; TargetPointer @base = isGC ? rts.GetGCStaticsBasePointer(ctx) : rts.GetNonGCStaticsBasePointer(ctx); if (@base == TargetPointer.Null) return false; address = rts.GetFieldDescStaticAddress(fdAddr); return true; }关键点:
- 按字段元素类型分流 GC statics 与 NonGC statics:引用类型/值类型字段(
Class、ValueType)存放在 GC 静态基址上,其余原始类型存放在非 GC 静态基址上,分别通过RuntimeTypeSystem.GetGCStaticsBasePointer / GetNonGCStaticsBasePointer取得(两者在 RuntimeTypeSystem.md 的 IRuntimeTypeSystem 接口中定义,底层分别对应LoaderAllocator的 GC/NonGC statics 区域); - 门控(Gate)语义:只有基址非空才返回地址。这防止了"类型已加载但未初始化(class cctor 未运行)"时,调用方拿到一个从 0 附近小偏移构造出的无效指针去解引用;
- 线程静态字段在此 API 中一律返回 false,提示调用方改走线程静态 API。
6.2 线程静态字段:TryGetThreadStaticFieldAddress
与静态字段逻辑对称,差异仅在:
- 先校验
rts.IsFieldDescThreadStatic(fdAddr)为真,非线程静态字段一律返回 false; - 基址获取变为 per-thread 形态:
rts.GetGCThreadStaticsBasePointer(ctx, thread)/rts.GetNonGCThreadStaticsBasePointer(ctx, thread); - 门控对象变为"该线程上封闭类的 per-thread 存储是否已分配"——线程第一次触碰某类型的线程静态字段之前,per-thread 基址尚未建立,此时返回 false;
- 最终地址由
rts.GetFieldDescThreadStaticAddress(fdAddr, thread)计算。
bool TryGetThreadStaticFieldAddress(string fqn, string fieldName, TargetPointer thread, out TargetPointer address) { address = TargetPointer.Null; if (!TryGetFieldDesc(fqn, fieldName, out TargetPointer fdAddr)) return false; IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; if (!rts.IsFieldDescThreadStatic(fdAddr)) return false; TargetPointer enclosingMT = rts.GetMTOfEnclosingClass(fdAddr); ITypeHandle ctx = rts.GetTypeHandle(enclosingMT); CorElementType et = rts.GetFieldDescType(fdAddr); bool isGC = et is CorElementType.Class or CorElementType.ValueType; TargetPointer @base = isGC ? rts.GetGCThreadStaticsBasePointer(ctx, thread) : rts.GetNonGCThreadStaticsBasePointer(ctx, thread); if (@base == TargetPointer.Null) return false; address = rts.GetFieldDescThreadStaticAddress(fdAddr, thread); return true; }两个寻址 API 共同的底层依赖是TryGetFieldDesc(ManagedTypeSource_1.cs):先TryResolveType拿到ITypeHandle,再RuntimeTypeSystem.GetFieldDescByName(th, fieldName)按名字取得 FieldDesc 地址;GetFieldDescByName内部经由EEClass.FieldDescList(GetFieldDescList)与父类字段数相减逻辑定位到具体条目(见 RuntimeTypeSystem.md)。
七、Version 1 的契约依赖矩阵
原文档以生成式表格形式声明了 Version 1 的完整依赖(<!-- BEGIN GENERATED -->注释标记该段由代码生成工具维护):
| 维度 | 内容 |
|---|---|
| 数据描述符(Data Descriptor) | Object的(type size)(uint32):固定 Object 部分从对象头到 MethodTable 指针为止的字节数。它是TryGetTypeInfo为引用类型调整偏移基准的唯一原生布局输入 |
| 全局变量(Global variables) | 无(_None.) |
| 使用的合约(Contracts used) | EcmaMetadata(读元数据)、Loader(定位 CoreLib 模块与 TypeDef→MethodTable 映射)、RuntimeTypeSystem(FieldDesc / statics 基址 / ITypeHandle) |
托管类型使用清单:合约本身不枚举固定的一组托管类型——调用方提供 FQN 即可;消费方应在其自身的### Managed types used小节中记录它读取了哪些具体托管类型。这一设计使合约保持"按需解析"的开放形态,避免把诊断工具的读取清单固化进底层合约。
八、消费方视角:LayoutSet 与缓存语义
8.1 作为类型布局来源被 LayoutSet 消费
ManagedTypeSource在 cDAC 的LayoutSet机制中扮演"托管类型元数据布局来源"的角色。生成器 LayoutSetSource.cs 生成的LayoutSet会按优先级组合多个布局来源:先查原生 cDAC 数据描述符(target.TryGetTypeInfo(name, ...)),再查IManagedTypeSource合约(通过target.Contracts.TryGetContract(out IManagedTypeSource mts)),字段查找逐个来源、逐个候选字段名尝试。这样,同一个字段集合可以同时覆盖"有原生描述符"与"只有托管元数据"两种类型,且来源惰性求值。
8.2 三重缓存与 Flush 语义(源码级细节)
实现 ManagedTypeSource_1.cs 维护了三份缓存:
_typeInfoCache(FQN →Target.TypeInfo?):类型布局缓存,负结果也缓存为 null;_typeHandleCache(FQN →ITypeHandle?):句柄缓存;_fieldDescCache((FQN, FieldName) →TargetPointer):字段描述符缓存。
Flush的清理策略值得注意:
_typeHandleCache每次 Flush 都清空——因为RuntimeTypeSystem在每次 flush 后都会作废其规范化的ITypeHandle实例,即使 CoreLib 类型本身依然加载且不可变;_typeInfoCache与_fieldDescCache仅在FlushScope.All时清空——因为ManagedTypeSource_1只解析System.Private.CoreLib中的名字,而 CoreLib 在运行时启动时就加载进不可收集的默认 AssemblyLoadContext,其 ECMA 元数据永不变化,因此布局与 FieldDesc 信息可以安全地跨FlushScope.ForwardExecution保留。
此外,TryGetTypeInfo内置了重入保护(_inSearch标志,ManagedTypeSource_1.cs):当出现LayoutSet → ManagedTypeSource → IData → LayoutSet之类的递归环路时,短路返回 false 以打破循环,且不缓存负结果——外层搜索可能在递归展开后对该名字合法成功。
九、测试与验证
仓库在 src/native/managed/cdac/tests 下提供了多组针对 cDAC 合约的测试:
- RuntimeTypeSystemDumpTests.cs:基于 dump 验证
ITypeHandle、MethodTable 读取等 RuntimeTypeSystem 行为,是静态/线程静态字段寻址所依赖的底层设施; - AsyncContinuationDumpTests.cs:验证
ContinuationMethodTable全局等运行时类型状态,覆盖了RuntimeTypeSystem对"无元数据 continuation 类型"的识别路径; - CollectibleGenericInstDumpTests.cs、IXCLRDataMethodDefinitionDumpTests.cs 等:从方法定义、可收集泛型实例等角度覆盖类型/方法元数据解析。
这些测试以"真实调试目标(debuggee)dump + 条件化版本跳过(如SkipOnVersion("net10.0", ...))"的方式运行,是合约行为的事实依据:ManagedTypeSource 的字段偏移/静态地址解析结果,最终会被这些上层诊断算法消费并断言。
十、使用注意事项与限制总结
- 作用域限定:只能解析
System.Private.CoreLib中的类型,其他程序集的类型即使加载到目标进程中也无法解析;程序集转发不被跟随; - 命名格式:必须使用 ECMA-335 完整格式,嵌套类型用
+(如System.Runtime.CompilerServices.ConditionalWeakTable<TKey, TValue>+Container、System.Runtime.InteropServices.ComWrappers+NativeObjectWrapper); - 返回 false 不等于类型不存在:也可能是 statics 存储未分配(类型未初始化)、per-thread 存储未建立(该线程未触碰过该类型线程静态字段)、或运行时尚未完成启动;
- 静态/线程静态 API 严格分流:用错 API 会得到 false,不会得到近似地址;
- 偏移基准:
Target.TypeInfo.Fields[].Offset对引用类型已含对象头调整(相对实例起始地址),对值类型直接相对槽位起始;TypeName是描述符类型名,用于断言校验,非托管字段名的元数据表示; - 缓存生命周期:
ITypeHandle随 Flush 失效,跨 Flush 持有句柄需重新解析;布局与 FieldDesc 缓存跨 ForwardExecution 有效。
结合 ManagedTypeSource.md、RuntimeTypeSystem.md 与 ContractRegistry.cs,即可在诊断工具中完整复现"由托管类型名到目标进程内存布局/静态字段地址"的解析链路。
【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考