ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

.NET runtime Data Contract 解析:Object Contract 如何描述托管对象、字符串、数组、委托与异步延续

.NET runtime Data Contract 解析:Object Contract 如何描述托管对象、字符串、数组、委托与异步延续 .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.mdpublic 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.ReadT(...)系列方法完成。三、底层依赖数据描述符、全局变量与其他契约Object Contract 的版本 1Version 1实现依赖三层信息来源这些信息全部在契约文档中以表的形式固定便于诊断端与运行时二进制同步演进。3.1 数据描述符Data Descriptors数据描述符见 data_descriptor.md由cdac-build-tool位于 src/coreclr/tools/cdac-build-tool从运行时二进制中生成描述了类型中各字段的偏移量与类型。Object Contract 使用的描述符如下Data DescriptorFieldTypeMeaningArray(type size)uint32Size of the fixed portion of an array objectArraym_NumComponentsuint32Number of items in the arrayAsyncResumeInfoDiagnosticIPpointerNative IP into the resumed method used for diagnostics (may be null)ContinuationObjectNextpointerPointer to the next continuation in the linked listContinuationObjectResumeInfopointerPointer to theResumeInfofor this suspension point (may be null)ContinuationObjectStateint32State index identifying the suspension point within the resumed methodDelegateExtraDatanintInvocation count for multicast, UnmanagedMarker for unmanaged, MethodDesc otherwiseDelegateHelperObjectpointerInvocation list for multicast, MethodInfo otherwiseDelegateMethodPtrCodePointerPrimary method pointerDelegateMethodPtrAuxCodePointerAuxiliary method pointerDelegateTargetpointerBoundthisreference for closed delegatesObjectm_pMethTabpointerMethod table for the objectObjectHeader(type size)uint32Size of the object headerObjectHeaderSyncBlockValueuint32Sync block value from the object headerStringm_FirstCharpointerAddress of the first UTF-16 character in the stringStringm_StringLengthuint32Length of the string in UTF-16 charactersSyncBlockHashCodeuint32Hash code stored in the sync block几个值得注意的设计细节Array与String共享m_NumComponents/m_StringLength字段布局都是固定头 可变长度组件数据这为GetSize统一处理两类对象提供了便利Delegate的ExtraData是一个三重含义字段多播委托存调用计数、非托管委托存哨兵值UnmanagedMarker、其余情况存 MethodDescObjectHeader是位于对象基址之前低地址方向的头部结构其中的SyncBlockValue要么直接内联哈希码要么是一个同步块索引。3.2 全局变量Global VariablesGlobalTypeMeaningArrayBoundsZeropointerKnown value for a single-dimensional, zero-lower-bound arrayObjectToMethodTableUnmaskuint8Bits to clear when converting an object header value to a method table addressStringMethodTablepointerPointer to the method table forSystem.StringSyncBlockHashCodeMaskuint32Mask for extracting the hash code from the sync block valueSyncBlockIndexMaskuint32Mask for extracting the sync block indexSyncBlockIsHashCodeuint32Bit indicating that the remaining sync block value contains a hash codeSyncBlockIsHashOrSyncBlockIndexuint32Bit indicating that the sync block value contains a hash code or sync block index这些全局变量是诊断端访问运行时符号级数据的入口例如ObjectToMethodTableUnmask用于从对象头中剥离低位的标记位如代龄/卡标记等得到真正的方法表地址StringMethodTable用于校验对象是否为字符串。3.3 契约常量NameTypePurposeValueUnmanagedMarkernintSentinel value for detecting unmanaged pointer delegates.-1UnmanagedMarker -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.ReadGlobalbyte(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_StringLengthUTF-16 字符数字符数据从String::m_FirstChar开始按length * sizeof(char)字节读出再通过MemoryMarshal.Castbyte, 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.Readuint(address /* String::m_StringLength offset */); Spanbyte span stackalloc byte[(int)length * sizeof(char)]; target.ReadBuffer(address /* String::m_FirstChar offset */, span); return new string(MemoryMarshal.Castbyte, 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.Readuint(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.Readuint(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 - doesnt 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.Readuint(boundsStart i * sizeof(int)); lowerBoundsValues[i] target.Readint(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.ReadGlobaluint(ObjectHeaderSize); return address dataOffset; }关键点多维数组CorElementType.Arraybounds 与 lowerBounds 内嵌在对象本体中int32_t bounds[rank]后跟int32_t lowerBounds[rank]因此boundsStart从固定区末尾开始一维零基数组如int[]、string[]不存储 boundslowerBounds直接指向全局已知值ArrayBoundsZerodimensionLengths[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.Readuint(address - /* ObjectHeader size */ /* ObjectHeader::SyncBlockValue offset */); if ((syncBlockValue target.ReadGlobaluint(SyncBlockIsHashOrSyncBlockIndex)) 0) return 0; if ((syncBlockValue target.ReadGlobaluint(SyncBlockIsHashCode)) ! 0) { // Hash code is stored inline in the sync block value return (int)(syncBlockValue target.ReadGlobaluint(SyncBlockHashCodeMask)); } // Hash code is stored in the sync block TargetPointer syncBlock GetSyncBlockAddress(address); if (syncBlock TargetPointer.Null) return 0; return (int)target.Readuint(syncBlock /* SyncBlock::HashCode offset */); }判断流程分三档若SyncBlockIsHashOrSyncBlockIndex位为 0 → 运行时从未为该对象赋值哈希码返回0文档强调已被哈希过的对象永远不会返回 0若SyncBlockIsHashCode位为 1 → 哈希码内联在对象头中用SyncBlockHashCodeMask掩码提取否则 → 哈希码存放在独立同步块中需经GetSyncBlockAddress解析索引后从SyncBlock::HashCode字段读取。GetSyncBlockAddress本身通过SyncBlockValueToObjectOffset全局量定位对象头并验证该值确实是同步块索引而非内联哈希码后才向 SyncBlock 契约查询TargetPointer GetSyncBlockAddress(TargetPointer address) { uint syncBlockValue target.Readuint(address - target.ReadGlobalushort(SyncBlockValueToObjectOffset)); // Check if the sync block value represents a sync block index (not a hash code) if ((syncBlockValue (target.ReadGlobaluint(SyncBlockIsHashCode) | target.ReadGlobaluint(SyncBlockIsHashOrSyncBlockIndex))) ! target.ReadGlobaluint(SyncBlockIsHashOrSyncBlockIndex)) return TargetPointer.Null; uint index syncBlockValue target.ReadGlobaluint(SyncBlockIndexMask); return target.Contracts.SyncBlock.GetSyncBlock(index); }4.5 内置 COM 互操作数据GetBuiltInComData对于使用内置 COMbuilt-in COM的对象Object Contract 委派给 SyncBlock 契约读取 RCWRuntime Callable Wrapper、CCWCOM Callable Wrapper与 CCFCOM Class Factorybool 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是否等于UnmanagedMarkerDelegateInfo 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 trueHelperObject是指向调用列表MulticastDelegate数组的数组此时不参与 Closed/Open 分类返回Unknown非托管指针委托ExtraData UnmanagedMarker (-1)同样归为UnknownClosed 委托绑定实例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.Readint(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 对象大小GetSizeGetSize返回对象的逻辑大小基大小 可变组件数据大小是计算对象占用、分析内存布局的基础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.Readuint(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 contractObject versionc1 --标定当前版本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),仅供参考
返回列表