.NET runtime Data Contract 解析:Object Contract 如何描述托管对象、字符串、数组、委托与异步延续
【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
导读
本文深入解析 .NET runtime 仓库中 Data Contract 体系的核心契约之一 ——Object Contract(见 Object.md)。该契约定义了诊断/调试基础设施如何从目标进程内存中读取"众所周知"的托管对象(字符串、数组、委托、同步块、异步延续等)的结构化信息,是 SOS、dotnet-dump 等诊断工具实现对象内存布局解析的底层依据。读完本文,你将掌握 Object Contract 的完整 API 语义、其底层依赖的数据描述符与全局变量,以及每个 API 的源码级实现原理。
一、Object Contract 在 Data Contract 体系中的定位
.NET runtime 的 Data Contract(数据契约)是一组面向诊断场景的托管契约接口:诊断工具通过target抽象(读取目标进程内存的句柄)调用这些接口,从而在不理解具体内存布局的前提下,安全地读取托管运行时内部数据。Data Contract 的设计文档与契约清单见 datacontracts_design.md,其余契约(如 RuntimeTypeSystem.md、SyncBlock.md)共同构成完整的诊断视图。
Object Contract 的核心职责正如文档开篇所述:
This contract is for getting information about well-known managed objects.
即:针对"众所周知的托管对象"——System.String、数组、委托(Delegate)、异步状态机的延续(continuation)对象、以及所有托管对象共有的对象头(ObjectHeader)与同步块(SyncBlock)——提供结构化的读取接口。它不负责通用的字段遍历(那是 RuntimeTypeSystem 等契约的职责),而是聚焦于运行时明确识别、布局固定的那几类特殊对象。
二、契约 API 总览
Object Contract 对外暴露的 API 完整清单如下(C# 形态,定义于 Object.md):
public enum DelegateType { Unknown, Closed, Open, } public readonly record struct DelegateInfo( TargetPointer TargetObject, TargetCodePointer TargetMethodPtr, DelegateType DelegateType); // DiagnosticIP is TargetPointer.Null when the continuation has no ResumeInfo. public readonly record struct ContinuationInfo( TargetPointer Next, TargetPointer DiagnosticIP, uint State); // Get the method table address for the object TargetPointer GetMethodTableAddress(TargetPointer address); // Get the string corresponding to a managed string object. Error if address does not represent a string. string GetStringValue(TargetPointer address); // Get the pointer to the data and shape information corresponding to a managed array object. // Error if address does not represent an array. TargetPointer GetArrayData(TargetPointer address, out uint count, out TargetPointer boundsStart, out TargetPointer lowerBounds, out uint[] dimensionLengths, out int[] lowerBoundsValues); // Get the length (in chars) and the offset from the object base to the first character // for a managed string object. Error if address does not represent a string. void GetStringData(TargetPointer address, out uint length, out uint offsetToFirstChar); // Get built-in COM data for the object if available. Returns false if address does not represent a COM object using built-in COM. bool GetBuiltInComData(TargetPointer address, out TargetPointer rcw, out TargetPointer ccw, out TargetPointer ccf); // Try to get the runtime-assigned hash code for the object. Returns 0 if the runtime has not // assigned a default hash code. This will never be 0 for objects that have been hashed. int TryGetHashCode(TargetPointer address); // Returns the SyncBlock address for the object, or TargetPointer.Null if no sync block is associated with it. TargetPointer GetSyncBlockAddress(TargetPointer address); DelegateInfo GetDelegateInfo(TargetPointer address); // Get the linked-list / diagnostic-IP / state triple for a runtime-async continuation object. ContinuationInfo GetContinuationInfo(TargetPointer address); // Returns the logical size of the object in bytes (base size plus any variable-size component data). ulong GetSize(TargetPointer address);可以归纳为五组能力:
- 对象身份与大小:
GetMethodTableAddress(对象 → 方法表)、GetSize(对象逻辑大小); - 特殊对象内容:
GetStringValue/GetStringData(字符串)、GetArrayData(数组,含多维/下界信息); - 对象头与锁/哈希:
GetSyncBlockAddress、TryGetHashCode; - 互操作对象:
GetBuiltInComData(内置 COM 的 RCW/CCW/CCF); - 委托与异步延续:
GetDelegateInfo、GetContinuationInfo。
其中TargetPointer与TargetCodePointer是 Data Contract 体系中的地址抽象类型(分别表示数据指针与代码指针),诊断端所有内存访问都通过target.Read<T>(...)系列方法完成。
三、底层依赖:数据描述符、全局变量与其他契约
Object Contract 的"版本 1"(Version 1)实现依赖三层信息来源,这些信息全部在契约文档中以表的形式固定,便于诊断端与运行时二进制同步演进。
3.1 数据描述符(Data Descriptors)
数据描述符(见 data_descriptor.md)由cdac-build-tool(位于 src/coreclr/tools/cdac-build-tool)从运行时二进制中生成,描述了类型中各字段的偏移量与类型。Object Contract 使用的描述符如下:
| Data Descriptor | Field | Type | Meaning |
|---|---|---|---|
Array | (type size) | uint32 | Size of the fixed portion of an array object |
Array | m_NumComponents | uint32 | Number of items in the array |
AsyncResumeInfo | DiagnosticIP | pointer | Native IP into the resumed method used for diagnostics (may be null) |
ContinuationObject | Next | pointer | Pointer to the next continuation in the linked list |
ContinuationObject | ResumeInfo | pointer | Pointer to theResumeInfofor this suspension point (may be null) |
ContinuationObject | State | int32 | State index identifying the suspension point within the resumed method |
Delegate | ExtraData | nint | Invocation count for multicast, UnmanagedMarker for unmanaged, MethodDesc otherwise |
Delegate | HelperObject | pointer | Invocation list for multicast, MethodInfo otherwise |
Delegate | MethodPtr | CodePointer | Primary method pointer |
Delegate | MethodPtrAux | CodePointer | Auxiliary method pointer |
Delegate | Target | pointer | Boundthisreference for closed delegates |
Object | m_pMethTab | pointer | Method table for the object |
ObjectHeader | (type size) | uint32 | Size of the object header |
ObjectHeader | SyncBlockValue | uint32 | Sync block value from the object header |
String | m_FirstChar | pointer | Address of the first UTF-16 character in the string |
String | m_StringLength | uint32 | Length of the string in UTF-16 characters |
SyncBlock | HashCode | uint32 | Hash code stored in the sync block |
几个值得注意的设计细节:
Array与String共享m_NumComponents/m_StringLength字段布局(都是"固定头 + 可变长度组件数据"),这为GetSize统一处理两类对象提供了便利;Delegate的ExtraData是一个三重含义字段:多播委托存调用计数、非托管委托存哨兵值UnmanagedMarker、其余情况存 MethodDesc;ObjectHeader是位于对象基址之前(低地址方向)的头部结构,其中的SyncBlockValue要么直接内联哈希码,要么是一个同步块索引。
3.2 全局变量(Global Variables)
| Global | Type | Meaning |
|---|---|---|
ArrayBoundsZero | pointer | Known value for a single-dimensional, zero-lower-bound array |
ObjectToMethodTableUnmask | uint8 | Bits to clear when converting an object header value to a method table address |
StringMethodTable | pointer | Pointer to the method table forSystem.String |
SyncBlockHashCodeMask | uint32 | Mask for extracting the hash code from the sync block value |
SyncBlockIndexMask | uint32 | Mask for extracting the sync block index |
SyncBlockIsHashCode | uint32 | Bit indicating that the remaining sync block value contains a hash code |
SyncBlockIsHashOrSyncBlockIndex | uint32 | Bit indicating that the sync block value contains a hash code or sync block index |
这些全局变量是诊断端访问运行时"符号级"数据的入口:例如ObjectToMethodTableUnmask用于从对象头中剥离低位的标记位(如代龄/卡标记等),得到真正的方法表地址;StringMethodTable用于校验对象是否为字符串。
3.3 契约常量
| Name | Type | Purpose | Value |
|---|---|---|---|
UnmanagedMarker | nint | Sentinel value for detecting unmanaged pointer delegates. | -1 |
UnmanagedMarker = -1是Delegate::ExtraData的哨兵值:当ExtraData等于-1时,表示该委托是非托管指针委托(unmanaged function pointer delegate),其分类逻辑在GetDelegateInfo中体现。
3.4 依赖的其他契约
RuntimeTypeSystem(见 RuntimeTypeSystem.md):提供GetTypeHandle、IsArray、GetBaseSize、GetComponentSize、GetSignatureCorElementType等类型系统查询能力,用于数组识别、元素类型判断与对象大小计算;SyncBlock(见 SyncBlock.md):提供同步块表访问(GetSyncBlock、GetSyncBlockObject)、锁信息(TryGetLockInfo)与内置 COM 数据读取(GetBuiltInComData)。
四、API 实现深度解析
以下实现均来自 Object.md 中"Version 1"的伪代码(/* ... offset */表示由数据描述符提供的运行时偏移量)。
4.1 方法表地址:GetMethodTableAddress
任何托管对象的第一个字段就是方法表指针(Object::m_pMethTab)。但该字段的低位可能带有运行时标记(如代龄位),因此需要按全局变量ObjectToMethodTableUnmask掩码清除:
TargetPointer GetMethodTableAddress(TargetPointer address) { TargetPointer mt = target.ReadPointer(address + /* Object::m_pMethTab offset */); return mt.Value & ~target.ReadGlobal<byte>("ObjectToMethodTableUnmask"); }从源码结构看,该方法被后续几乎所有 API 复用(字符串、数组、大小、委托判断都需要先解析方法表),是整个契约的"入口密钥"。
4.2 字符串对象:GetStringValue与GetStringData
字符串是最常见的托管对象之一。读取逻辑分为两步:
- 身份校验:先取方法表,若为
TargetPointer.Null则抛出ArgumentException("Address represents a set-free object")(对象已被释放);再与全局StringMethodTable指向的方法表比对,不一致则抛出ArgumentException("Address does not represent a string object"); - 数据读取:长度来自
String::m_StringLength(UTF-16 字符数),字符数据从String::m_FirstChar开始,按length * sizeof(char)字节读出,再通过MemoryMarshal.Cast<byte, char>还原为托管字符串。
string GetStringValue(TargetPointer address) { TargetPointer mt = GetMethodTableAddress(address); if (mt == TargetPointer.Null) throw new ArgumentException("Address represents a set-free object"); TargetPointer stringMethodTable = target.ReadPointer(target.ReadGlobalPointer("StringMethodTable")); if (mt != stringMethodTable) throw new ArgumentException("Address does not represent a string object", nameof(address)); uint length = target.Read<uint>(address + /* String::m_StringLength offset */); Span<byte> span = stackalloc byte[(int)length * sizeof(char)]; target.ReadBuffer(address + /* String::m_FirstChar offset */, span); return new string(MemoryMarshal.Cast<byte, char>(span)); }而GetStringData是轻量版本:它只返回字符长度与首个字符相对对象基址的偏移,不拷贝字符数据,适合诊断端需要自行按偏移读取的场景:
void GetStringData(TargetPointer address, out uint length, out uint offsetToFirstChar) { TargetPointer mt = GetMethodTableAddress(address); if (mt == TargetPointer.Null) throw new ArgumentException("Address represents a set-free object"); TargetPointer stringMethodTable = target.ReadPointer(target.ReadGlobalPointer("StringMethodTable")); if (mt != stringMethodTable) throw new ArgumentException("Address does not represent a string object", nameof(address)); length = target.Read<uint>(address + /* String::m_StringLength offset */); offsetToFirstChar = /* String::m_FirstChar offset */; }4.3 数组对象:GetArrayData
数组的读取是最复杂的部分,因为它需要区分多维数组与一维零基数组两种布局:
TargetPointer GetArrayData(TargetPointer address, out uint count, out TargetPointer boundsStart, out TargetPointer lowerBounds, out uint[] dimensionLengths, out int[] lowerBoundsValues) { TargetPointer mt = GetMethodTableAddress(address); if (mt == TargetPointer.Null) throw new ArgumentException("Address represents a set-free object"); Contracts.IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; TypeHandle typeHandle = rts.GetTypeHandle(mt); uint rank; if (!rts.IsArray(typeHandle, out rank)) throw new ArgumentException("Address does not represent an array object", nameof(address)); count = target.Read<uint>(address + /* Array::m_NumComponents offset */; CorElementType corType = rts.GetSignatureCorElementType(typeHandle); if (corType == CorElementType.Array) { // Multi-dimensional - has bounds as part of the array object // The object is allocated with: // << fields that are part of the array type info >> // int32_t bounds[rank]; // int32_t lowerBounds[rank]; boundsStart = address + /* Array size */; lowerBounds = boundsStart + (rank * sizeof(int)); } else { // Single-dimensional, zero-based - doesn't have bounds boundsStart = address + /* Array::m_NumComponents offset */; lowerBounds = target.ReadGlobalPointer("ArrayBoundsZero"); } dimensionLengths = new uint[rank]; lowerBoundsValues = new int[rank]; if (corType == CorElementType.Array) { for (int i = 0; i < rank; i++) { dimensionLengths[i] = target.Read<uint>(boundsStart + i * sizeof(int)); lowerBoundsValues[i] = target.Read<int>(lowerBounds + i * sizeof(int)); } } else { dimensionLengths[0] = count; } // Sync block is before `this` pointer, so substract the object header size ulong dataOffset = typeSystemContract.GetBaseSize(typeHandle) - target.ReadGlobal<uint>("ObjectHeaderSize"); return address + dataOffset; }关键点:
- 多维数组(
CorElementType.Array):bounds 与 lowerBounds 内嵌在对象本体中(int32_t bounds[rank]后跟int32_t lowerBounds[rank]),因此boundsStart从固定区末尾开始; - 一维零基数组(如
int[]、string[]):不存储 bounds,lowerBounds直接指向全局已知值ArrayBoundsZero,dimensionLengths[0]即count; - 数据区起始地址= 对象基址 +(类型基大小 − 对象头大小)。因为同步块位于
this指针之前,而GetBaseSize已包含对象头,所以需要减去ObjectHeaderSize才能定位到真正的元素数据起始位置。该逻辑同时说明ObjectHeader存在与否会影响所有对象的数据偏移计算。
4.4 对象头、哈希码与同步块:GetSyncBlockAddress与TryGetHashCode
托管对象的对象头中存有SyncBlockValue,它有三种状态:未赋值(无哈希无同步块)、内联哈希码、同步块索引。TryGetHashCode展示了如何用位掩码区分这些状态:
int TryGetHashCode(TargetPointer address) { // Read the sync block value from the ObjectHeader preceding the object uint syncBlockValue = target.Read<uint>(address - /* ObjectHeader size */ + /* ObjectHeader::SyncBlockValue offset */); if ((syncBlockValue & target.ReadGlobal<uint>("SyncBlockIsHashOrSyncBlockIndex")) == 0) return 0; if ((syncBlockValue & target.ReadGlobal<uint>("SyncBlockIsHashCode")) != 0) { // Hash code is stored inline in the sync block value return (int)(syncBlockValue & target.ReadGlobal<uint>("SyncBlockHashCodeMask")); } // Hash code is stored in the sync block TargetPointer syncBlock = GetSyncBlockAddress(address); if (syncBlock == TargetPointer.Null) return 0; return (int)target.Read<uint>(syncBlock + /* SyncBlock::HashCode offset */); }判断流程分三档:
- 若
SyncBlockIsHashOrSyncBlockIndex位为 0 → 运行时从未为该对象赋值哈希码,返回0(文档强调:已被哈希过的对象永远不会返回 0); - 若
SyncBlockIsHashCode位为 1 → 哈希码内联在对象头中,用SyncBlockHashCodeMask掩码提取; - 否则 → 哈希码存放在独立同步块中,需经
GetSyncBlockAddress解析索引后从SyncBlock::HashCode字段读取。
GetSyncBlockAddress本身通过SyncBlockValueToObjectOffset全局量定位对象头,并验证该值确实是同步块索引(而非内联哈希码)后才向 SyncBlock 契约查询:
TargetPointer GetSyncBlockAddress(TargetPointer address) { uint syncBlockValue = target.Read<uint>(address - target.ReadGlobal<ushort>("SyncBlockValueToObjectOffset")); // Check if the sync block value represents a sync block index (not a hash code) if ((syncBlockValue & (target.ReadGlobal<uint>("SyncBlockIsHashCode") | target.ReadGlobal<uint>("SyncBlockIsHashOrSyncBlockIndex"))) != target.ReadGlobal<uint>("SyncBlockIsHashOrSyncBlockIndex")) return TargetPointer.Null; uint index = syncBlockValue & target.ReadGlobal<uint>("SyncBlockIndexMask"); return target.Contracts.SyncBlock.GetSyncBlock(index); }4.5 内置 COM 互操作数据:GetBuiltInComData
对于使用内置 COM(built-in COM)的对象,Object Contract 委派给 SyncBlock 契约读取 RCW(Runtime Callable Wrapper)、CCW(COM Callable Wrapper)与 CCF(COM Class Factory):
bool GetBuiltInComData(TargetPointer address, out TargetPointer rcw, out TargetPointer ccw, out TargetPointer ccf) { rcw = TargetPointer.Null; ccw = TargetPointer.Null; ccf = TargetPointer.Null; TargetPointer syncBlockPtr = GetSyncBlockAddress(address); if (syncBlockPtr == TargetPointer.Null) return false; // Delegate to the SyncBlock contract so that the interop data can also be read directly // from a sync block address without going through the object (e.g. during cleanup). return target.Contracts.SyncBlock.GetBuiltInComData(syncBlockPtr, out rcw, out ccw, out ccf); }委派而非直接读取的原因在注释中说明:同步块可能在对象清理期间仍然有效,直接以同步块地址为入口读取互操作数据,比从对象出发更安全。SyncBlock 契约侧的读取规则(见 SyncBlock.md)包括:RCW 的 bit 0 是内部锁位需掩码、CCW/CCF 的哨兵值0x1表示"曾经有过、现在为空"。此外InteropSyncBlockInfo的字段(RCW/CCW/CCF)同样由数据描述符表固定。
4.6 委托对象:GetDelegateInfo
委托的分类逻辑基于两个线索:HelperObject是否为数组(多播委托的调用列表)以及ExtraData是否等于UnmanagedMarker:
DelegateInfo GetDelegateInfo(TargetPointer address) { Data.Delegate del = new Data.Delegate(target, address); // Check for multicast and unmanaged first. bool isMulticast = false; TargetPointer helperObject = target.ReadPointer(address + /* Delegate::HelperObject offset */); if (helperObject != TargetPointer.Null) { IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; TargetPointer mt = GetMethodTableAddress(helperObject); Debug.Assert(mt != TargetPointer.Null); isMulticast = rts.IsArray(rts.GetTypeHandle(mt), out _); } const nint UnmanagedMarker = -1; DelegateType delegateType = DelegateType.Unknown; if (!isMulticast && target.ReadNInt(address + /* Delegate::ExtraData offset */) != UnmanagedMarker) { delegateType = del.MethodPtrAux == TargetCodePointer.Null ? DelegateType.Closed : DelegateType.Open; } // Pick the bound object and primary entry point based on the classification. // For Closed delegates the target is the bound `this` and MethodPtr is invoked on it. // For Open delegates MethodPtrAux is the unbound entry point; the bound object is not meaningful. // For Unknown do not provide any info. (TargetPointer targetObject, TargetCodePointer targetMethodPtr) = delegateType switch { DelegateType.Closed => (target.ReadPointer(address + /* Delegate::Target offset */), target.ReadPointer(address + /* Delegate::MethodPtr offset */)), DelegateType.Open => (TargetPointer.Null, target.ReadPointer(address + /* Delegate::MethodPtrAux offset */)), _ => (TargetPointer.Null, TargetCodePointer.Null), }; return new DelegateInfo(targetObject, targetMethodPtr, delegateType); }分类规则总结:
- 多播委托(
isMulticast == true):HelperObject是指向调用列表(MulticastDelegate数组)的数组,此时不参与 Closed/Open 分类,返回Unknown; - 非托管指针委托:
ExtraData == UnmanagedMarker (-1),同样归为Unknown; - Closed 委托(绑定实例):
MethodPtrAux为空、MethodPtr非空,Target字段是绑定的this,返回(Target, MethodPtr); - Open 委托(未绑定实例):
MethodPtrAux是未绑定的入口点,Target无意义,返回(Null, MethodPtrAux)。
4.7 异步延续对象:GetContinuationInfo
针对运行时异步(runtime-async,即AsyncStateMachine/AsyncIterator等)的延续对象,契约返回"链表 / 诊断 IP / 状态"三元组:
ContinuationInfo GetContinuationInfo(TargetPointer address) { TargetPointer next = target.ReadPointer(address + /* ContinuationObject::Next offset */); TargetPointer resumeInfo = target.ReadPointer(address + /* ContinuationObject::ResumeInfo offset */); uint state = (uint)target.Read<int>(address + /* ContinuationObject::State offset */); // ResumeInfo may be null TargetPointer diagnosticIP = resumeInfo != TargetPointer.Null ? target.ReadPointer(resumeInfo + /* AsyncResumeInfo::DiagnosticIP offset */) : TargetPointer.Null; return new ContinuationInfo( Next: next, DiagnosticIP: diagnosticIP, State: state); }Next:延续链表中的下一个节点指针,诊断工具可沿此链遍历所有挂起点;DiagnosticIP:用于诊断的本机 IP(指向恢复方法中的位置);当该挂起点没有ResumeInfo时为TargetPointer.Null(这与ContinuationInfo注释一致);State:标识恢复方法内挂起点的状态索引。
4.8 对象大小:GetSize
GetSize返回对象的逻辑大小(基大小 + 可变组件数据大小),是计算对象占用、分析内存布局的基础:
ulong GetSize(TargetPointer address) { TargetPointer mt = GetMethodTableAddress(address); if (mt == TargetPointer.Null) throw new ArgumentException("Address represents a set-free object"); Contracts.IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; TypeHandle typeHandle = rts.GetTypeHandle(mt); ulong size = rts.GetBaseSize(typeHandle); uint componentSize = rts.GetComponentSize(typeHandle); if (componentSize > 0) { // Variable-size object (array or string): add the component data size. // Both Array and String share the m_NumComponents/m_StringLength field layout. uint numComponents = target.Read<uint>(address + /* Array::m_NumComponents offset */); size += (ulong)numComponents * componentSize; } return size; }实现要点:通过 RuntimeTypeSystem 契约获取类型基大小与组件大小;仅当组件大小非零(即数组或字符串这类可变大小对象)时,才把"组件数量 × 组件大小"加到总大小上。此处再次印证了Array::m_NumComponents与String::m_StringLength共享布局这一关键假设。
五、契约的版本化机制
Object Contract 采用显式版本管理:文档中以## Version 1及生成标记<!-- BEGIN GENERATED: usage contract=Object version=c1 -->标定当前版本("c1" 即契约版本 1)。data-descriptor-meanings.json与 contenteditable="false">【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考