.NET runtime Data Contract 解析:Object Contract 如何描述托管对象、字符串、数组、委托与异步延续
2026/9/19 0:26:45 网站建设 项目流程

.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);

可以归纳为五组能力:

  1. 对象身份与大小GetMethodTableAddress(对象 → 方法表)、GetSize(对象逻辑大小);
  2. 特殊对象内容GetStringValue/GetStringData(字符串)、GetArrayData(数组,含多维/下界信息);
  3. 对象头与锁/哈希GetSyncBlockAddressTryGetHashCode
  4. 互操作对象GetBuiltInComData(内置 COM 的 RCW/CCW/CCF);
  5. 委托与异步延续GetDelegateInfoGetContinuationInfo

其中TargetPointerTargetCodePointer是 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 DescriptorFieldTypeMeaning
Array(type size)uint32Size of the fixed portion of an array object
Arraym_NumComponentsuint32Number of items in the array
AsyncResumeInfoDiagnosticIPpointerNative IP into the resumed method used for diagnostics (may be null)
ContinuationObjectNextpointerPointer to the next continuation in the linked list
ContinuationObjectResumeInfopointerPointer to theResumeInfofor this suspension point (may be null)
ContinuationObjectStateint32State index identifying the suspension point within the resumed method
DelegateExtraDatanintInvocation count for multicast, UnmanagedMarker for unmanaged, MethodDesc otherwise
DelegateHelperObjectpointerInvocation list for multicast, MethodInfo otherwise
DelegateMethodPtrCodePointerPrimary method pointer
DelegateMethodPtrAuxCodePointerAuxiliary method pointer
DelegateTargetpointerBoundthisreference for closed delegates
Objectm_pMethTabpointerMethod table for the object
ObjectHeader(type size)uint32Size of the object header
ObjectHeaderSyncBlockValueuint32Sync block value from the object header
Stringm_FirstCharpointerAddress of the first UTF-16 character in the string
Stringm_StringLengthuint32Length of the string in UTF-16 characters
SyncBlockHashCodeuint32Hash code stored in the sync block

几个值得注意的设计细节:

  • ArrayString共享m_NumComponents/m_StringLength字段布局(都是"固定头 + 可变长度组件数据"),这为GetSize统一处理两类对象提供了便利;
  • DelegateExtraData是一个三重含义字段:多播委托存调用计数、非托管委托存哨兵值UnmanagedMarker、其余情况存 MethodDesc;
  • ObjectHeader是位于对象基址之前(低地址方向)的头部结构,其中的SyncBlockValue要么直接内联哈希码,要么是一个同步块索引。

3.2 全局变量(Global Variables)

GlobalTypeMeaning
ArrayBoundsZeropointerKnown value for a single-dimensional, zero-lower-bound array
ObjectToMethodTableUnmaskuint8Bits to clear when converting an object header value to a method table address
StringMethodTablepointerPointer to the method table forSystem.String
SyncBlockHashCodeMaskuint32Mask for extracting the hash code from the sync block value
SyncBlockIndexMaskuint32Mask for extracting the sync block index
SyncBlockIsHashCodeuint32Bit indicating that the remaining sync block value contains a hash code
SyncBlockIsHashOrSyncBlockIndexuint32Bit indicating that the sync block value contains a hash code or sync block index

这些全局变量是诊断端访问运行时"符号级"数据的入口:例如ObjectToMethodTableUnmask用于从对象头中剥离低位的标记位(如代龄/卡标记等),得到真正的方法表地址;StringMethodTable用于校验对象是否为字符串。

3.3 契约常量

NameTypePurposeValue
UnmanagedMarkernintSentinel value for detecting unmanaged pointer delegates.-1

UnmanagedMarker = -1Delegate::ExtraData的哨兵值:当ExtraData等于-1时,表示该委托是非托管指针委托(unmanaged function pointer delegate),其分类逻辑在GetDelegateInfo中体现。

3.4 依赖的其他契约

  • RuntimeTypeSystem(见 RuntimeTypeSystem.md):提供GetTypeHandleIsArrayGetBaseSizeGetComponentSizeGetSignatureCorElementType等类型系统查询能力,用于数组识别、元素类型判断与对象大小计算;
  • SyncBlock(见 SyncBlock.md):提供同步块表访问(GetSyncBlockGetSyncBlockObject)、锁信息(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 字符串对象:GetStringValueGetStringData

字符串是最常见的托管对象之一。读取逻辑分为两步:

  1. 身份校验:先取方法表,若为TargetPointer.Null则抛出ArgumentException("Address represents a set-free object")(对象已被释放);再与全局StringMethodTable指向的方法表比对,不一致则抛出ArgumentException("Address does not represent a string object")
  2. 数据读取:长度来自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直接指向全局已知值ArrayBoundsZerodimensionLengths[0]count
  • 数据区起始地址= 对象基址 +(类型基大小 − 对象头大小)。因为同步块位于this指针之前,而GetBaseSize已包含对象头,所以需要减去ObjectHeaderSize才能定位到真正的元素数据起始位置。该逻辑同时说明ObjectHeader存在与否会影响所有对象的数据偏移计算。

4.4 对象头、哈希码与同步块:GetSyncBlockAddressTryGetHashCode

托管对象的对象头中存有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 */); }

判断流程分三档:

  1. SyncBlockIsHashOrSyncBlockIndex位为 0 → 运行时从未为该对象赋值哈希码,返回0(文档强调:已被哈希过的对象永远不会返回 0);
  2. SyncBlockIsHashCode位为 1 → 哈希码内联在对象头中,用SyncBlockHashCodeMask掩码提取;
  3. 否则 → 哈希码存放在独立同步块中,需经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_NumComponentsString::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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询