ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

.NET 运行时中 EnC/Hot Reload 可变类型系统的调试契约(RuntimeMutableTypeSystem)深度解析

.NET 运行时中 EnC/Hot Reload 可变类型系统的调试契约(RuntimeMutableTypeSystem)深度解析 .NET 运行时中 EnC/Hot Reload 可变类型系统的调试契约RuntimeMutableTypeSystem深度解析【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime导读本文基于 dotnet/runtime 仓库中 docs/design/datacontracts/RuntimeMutableTypeSystem.md 这一契约文档深入剖析 .NET 运行时如何通过 Data Contract数据契约机制向调试器、SOS 及 cDACDiagnostics Data Contract Reader暴露类型加载之后发生的类型系统变化——即 Edit and ContinueEnC与 Hot Reload 动态添加字段EnCAddedField的底层数据布局与读取协议。读完本文你将掌握RuntimeMutableTypeSystem契约的 5 个 API 语义、EnCFieldDesc/EnCEEClassData/EnCSyncBlockInfo等核心数据结构的布局以及从目标进程内存中枚举 EnC 新增字段、解析静态/实例字段实际存储地址的完整算法并可对照 src/native/managed/cdac 中的 cDAC 参考实现与 src/coreclr/vm/encee.h 的运行时侧结构定义逐行验证。契约定位为什么需要可变类型系统契约从不可变快照到可变类型系统在 .NET 运行时CoreCLR中常规的类型系统契约RuntimeTypeSystem描述的是静态的、类型加载完成之后固定不变的类型信息FieldDesc、MethodTable、EEClass 等在首次加载后基本不再变化。调试器与诊断工具如 SOS、dotnet-dump 分析器可以通过该契约安全地读取类型布局。然而Edit and ContinueEnC与 Hot Reload打破了这一静态假设当开发者在调试会话中编辑并继续时编译器会向正在运行的进程应用元数据更新ApplyEditAndContinue其中可能新增字段通常仅限私有字段参见 src/coreclr/vm/encee.h 中EnCEEClassData::AddField的注释。新增字段无法像普通字段那样被粘贴到既有对象实例上对象的内存布局在分配时已固化于是运行时设计了一套旁挂side-table机制来存放这些字段的值。RuntimeMutableTypeSystem契约正是这套机制的只读视图它专门暴露初始类型加载之后发生的变化让诊断工具在进程外也能准确定位 EnC 新增字段的存储位置从而支持对加了字段的对象进行正确的内存读取、字段值查看与表达式求值。契约在 cDAC 体系中的位置该契约注册在 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs版本标识为c1并被ValidateIRuntimeMutableTypeSystem(registry)强制校验。注释明确指出其消费方是DacDbiImpl.cs——即调试器 DBIDebugger Interface层在处理 edit-and-continue 可变类型系统时使用。契约的依赖关系见文档Contracts used一节为依赖契约用途GC读取 dependent handle 的 extra infoGetHandleExtraInfo获取 EnC helper 对象地址Loader通过ModuleHandle查询模块的ModuleFlags.EditAndContinue标志与模块指针Object从对象头获取 SyncBlock 地址GetSyncBlockAddress、读取数组数据GetArrayDataRuntimeTypeSystem查询字段描述符的类型GetFieldDescType、获取模块指针契约 API 全景文档给出了契约的 5 个公开 API以下逐一给出语义与使用场景IEnumerableTargetPointer EnumerateAddedFieldDescs(ITypeHandle typeHandle, bool staticFields); bool IsFieldDescEnCNew(TargetPointer fieldDescPointer); bool DoesEnCFieldDescNeedFixup(TargetPointer encFieldDescPointer); TargetPointer GetEnCStaticFieldDataAddress(TargetPointer encFieldDescPointer); TargetPointer GetEnCInstanceFieldAddress(TargetPointer objectAddress, TargetPointer encFieldDescPointer);API作用典型使用方EnumerateAddedFieldDescs枚举某个类型MethodTable通过 EnC 新增的全部字段描述符可分别枚举实例字段与静态字段调试器在类型上浏览所有字段时需把 EnC 字段也纳入IsFieldDescEnCNew判断一个普通 FieldDesc 是否实际上是由 EnC 新增的通过 offset 哨兵值识别字段迭代器区分普通字段与 EnC 字段DoesEnCFieldDescNeedFixup判断该 EnCFieldDesc 是否仍处于未完成初始化NeedsFixup状态调试器在字段尚未 Fixup 时避免错误读取GetEnCStaticFieldDataAddress获取 EnC 新增静态字段实际存储值的地址查看静态字段值GetEnCInstanceFieldAddress获取某对象实例上 EnC 新增实例字段实际存储值的地址查看实例字段值、表达式求值核心数据结构与内存布局契约文档的Data descriptors used一节给出了全部涉及的数据描述符。这些描述符与运行时侧的 C 结构一一对应我们结合 src/coreclr/vm/encee.h 逐一对照。EnCFieldDescEnC 新增字段的描述符EnCFieldDesc继承自FieldDesc见 src/coreclr/vm/encee.h额外携带两个字段Data Descriptor 字段类型含义运行时成员encee.hNeedsFixupint32非零表示该描述符尚未完成初始化Fixup()未调用m_bNeedsFixupStaticFieldDatapointer指向承载该静态字段存储的EnCAddedStaticField未分配存储前为 NULLm_pStaticFieldData从 src/coreclr/vm/encee.h 可见NeedsFixup()返回m_bNeedsFixupFixup(token)会调用EEClass::FixupFieldDescForEnC完成真正的 FieldDesc 初始化可能触发类型加载与 GC因此只能在进程恢复执行后进行随后清除标志。m_pStaticFieldData的cdac_data偏移在 src/coreclr/vm/encee.h 中通过offsetof静态导出。EnCAddedFieldElement 与 EnCEEClassData按类组织的链表EnCAddedFieldElementsrc/coreclr/vm/encee.h是链表节点Next指向下一个元素内嵌一个EnCFieldDesc m_fieldDesc契约中FieldDesc字段即该内嵌描述符的地址与 FieldDesc 布局兼容。一个类新增的每个字段对应一个节点。EnCEEClassDatasrc/coreclr/vm/encee.h按类MethodTable组织 EnC 数据字段类型含义运行时成员MethodTablepointer本条目对应的 MethodTable 指针m_pMTAddedInstanceFieldspointer新增实例字段链表头m_pAddedInstanceFieldsAddedStaticFieldspointer新增静态字段链表头m_pAddedStaticFields运行时还维护m_dwNumAddedInstanceFields/m_dwNumAddedStaticFields计数GetAddedInstanceFields/GetAddedStaticFields文档注释强调EnC 只能添加私有字段因此这些字段对其他类型不可见。Module::EnCClassList模块级的查找表所有EnCEEClassData条目存放在模块的EnCClassList中这是一个UnorderedArrayBase无序数组字段类型含义Countuint32数组中有效条目数Tablepointer指向存放条目的后备存储EnCSyncBlockInfo / EnCAddedField实例字段值旁挂于 SyncBlockEnC 新增实例字段的值不是存在对象本体中而是懒加载地挂在对象的 SyncBlock 上见 src/coreclr/vm/encee.hEnCSyncBlockInfoSyncBlock::EnCInfo挂在一个对象的 SyncBlock 上List字段指向 EnC 新增实例字段链表头。SyncBlock 的EnCInfo是可选项未配置 EnC 时不存在。EnCAddedField是链表节点字段类型含义FieldDescpointer指向对应的EnCFieldDesc区分这是哪个字段FieldDataObjectHandle一个dependent handle 对primary OBJECTREF 是依赖锚点被修改的对象实例secondary OBJECTREF 是System.Diagnostics.EditAndContinueHelper实例Nextpointer链表下一个节点关键设计点见 encee.h 注释每个字段只存在一个EnCFieldDesc但该对象的每个实例都会懒创建各自的EnCAddedField节点。FieldData使用 dependent handle 的目的在于当 primary对象实例被 GC 回收时secondaryhelper 对象也随之释放从而避免内存泄漏EnCSyncBlockInfo::Cleanup在对象被回收后负责清理。SyncBlock 与 Object 描述符契约还引用了两个跨契约的数据描述符SyncBlock::EnCInfopointer指向 Edit-and-Continue 新增字段信息未配置 EnC 时该字段可缺省。Object的(type size)uint32固定 Object 头部到 MethodTable 指针为止的字节大小——用于对装箱值类型执行unbox操作定位数据区Object::Data偏移。FieldDesc::DWord2 与哨兵偏移契约依赖FieldDesc的DWord2uint32打包字段偏移。在 src/coreclr/vm/field.h 中FieldDesc的第二双字是一个 union低 27 位m_dwOffset字段偏移高 5 位m_type。EnC 新增字段没有真实对象内偏移它们未被放置在对象中因此使用哨兵值FIELD_OFFSET_NEW_ENC定义于 src/coreclr/vm/field.h取值为FIELD_OFFSET_MAX - 4 (127)-1-4// Offset to indicate an EnC added field. They dont have offsets as arent placed in the object. #define FIELD_OFFSET_NEW_ENC (FIELD_OFFSET_MAX-4)该值通过 cDAC 全局变量FieldOffsetNewEnc暴露给契约读取方src/coreclr/vm/datadescriptor/datadescriptor.incCDAC_GLOBAL(FieldOffsetNewEnc, T_UINT32, FIELD_OFFSET_NEW_ENC)。契约中的FieldDescFlags2.OffsetMask 0x07ffffff即 27 位偏移掩码在 RuntimeMutableTypeSystem_1.cs 中定义IsFieldDescEnCNew正是把DWord2 0x07ffffff与FieldOffsetNewEnc全局值比较来判定。静态字段的存储EnCAddedStaticFieldEnCAddedStaticFieldsrc/coreclr/vm/encee.h承载 EnC 新增静态字段的值字段类型含义FieldDatapointer内联存储基本类型字段直接是值的起始地址class/值类型字段则是指向 GC 跟踪存储对象引用的指针注意FieldData是结构体的最后一个字段因为它是变长的BYTE m_FieldData参见 encee.h 注释the actual size of this type is variable。EnCFieldDesc::StaticFieldData在存储分配前为 NULLGetOrAllocateStaticFieldData负责按需分配可能抛 OOM。五步读取算法从目标进程内存解析 EnC 字段文档给出了契约的完整伪代码C# 风格cDAC 参考实现位于 RuntimeMutableTypeSystem_1.cs。以下按逻辑拆解为五个算法步骤。步骤一枚举类型的新增字段EnumerateAddedFieldDescs流程获取模块指针与模块句柄通过RuntimeTypeSystem.GetModule(typeHandle)取得模块地址通过Loader.GetModuleHandleFromModulePtr取得模块句柄。快速失败检查cDAC 实现新增的健壮性检查文档伪代码中亦体现没有 EnC 数据则 yield break只有 MethodTable 类型句柄可能有 EnC 新增字段TypeDescTypeVar、FnPtr 等直接yield break见 RuntimeMutableTypeSystem_1.cs模块必须带ModuleFlags.EditAndContinue标志L43-L44Module::EnCClassList字段只在启用FEATURE_METADATA_UPDATER的构建中存在缺省则不可能有 EnC 数据L48-L50。线性搜索 EnCEEClassData 条目遍历UnorderedArrayBaseCount个元素、Table为基址读取每个entry的MethodTable字段与typeHandle.Address比对找到匹配项。沿链表产出字段描述符按staticFields选择AddedStaticFields或AddedInstanceFields头节点循环读取EnCAddedFieldElement::FieldDesc产出的是内嵌 EnCFieldDesc 的地址并沿Next前进直到 NULL。注意此处迭代的是每个类型只维护一份的字段描述符链表EnCEEClassData 层面与后面实例层面挂在 SyncBlock 上的EnCAddedField链表是两套结构不要混淆。步骤二判定字段是否为 EnC 新增IsFieldDescEnCNewuint DWord2 target.Readuint(fieldDescPointer FieldDesc::DWord2); uint offset DWord2 FieldDescFlags2.OffsetMask; // 0x07ffffff return offset target.ReadGlobaluint(FieldOffsetNewEnc);读取 FieldDesc 的DWord2用 27 位掩码取出 offset与全局哨兵值FieldOffsetNewEncFIELD_OFFSET_NEW_ENC比较。这一判定在运行时侧有对应实现FieldDesc::IsEnCNew()src/coreclr/vm/field.h返回m_dwOffset FIELD_OFFSET_NEW_ENC。步骤三判断是否需要 FixupDoesEnCFieldDescNeedFixupint needsFixup target.Readint(encFieldDescPointer EnCFieldDesc::NeedsFixup); return needsFixup ! 0;对应运行时EnCFieldDesc::NeedsFixup()返回m_bNeedsFixup。一个 EnC 字段在添加时先用Init做最小初始化m_bNeedsFixup TRUE待进程恢复执行后才由Fixup完成全部初始化并清除标志。步骤四解析静态字段存储地址GetEnCStaticFieldDataAddressTargetPointer staticFieldData target.ReadPointer(encFieldDescPointer EnCFieldDesc::StaticFieldData); if (staticFieldData TargetPointer.Null) return TargetPointer.Null; // 存储尚未分配 TargetPointer fieldDataAddress staticFieldData EnCAddedStaticField::FieldData; CorElementType fieldType target.Contracts.RuntimeTypeSystem.GetFieldDescType(encFieldDescPointer); return fieldType is ValueType or Class ? target.ReadPointer(fieldDataAddress) // 引用类型/值类型再解引用一次 : fieldDataAddress; // 基本类型内联值起始地址要点EnCAddedStaticField::FieldData对于基本类型直接是值的位置返回其地址即可读值对于 class 与值类型则是指向 GC 跟踪存储的指针需要再解引用一层才能得到对象地址。这正对应 encee.h 中对m_FieldData的注释For primitive types, this is the beginning of the actual value. For reference types and user-defined value types, its the beginning of a pointer to the object.步骤五解析实例字段存储地址GetEnCInstanceFieldAddress这是最复杂的算法完整流程如下取 SyncBlockObject.GetSyncBlockAddress(objectAddress)从对象头获取 SyncBlock 地址为 NULL 则对象没有 SyncBlock直接返回 NULL不可能有 EnC 字段。读 EnCInfoSyncBlock::EnCInfo是可选字段为 NULL 说明该对象没有 EnC 新增字段。遍历 EnCAddedField 链表从EnCSyncBlockInfo::List头开始逐个比对EnCAddedField::FieldDesc是否等于目标encFieldDescPointer找不到则返回 NULL。通过 dependent handle 取 helper 对象FieldData是 dependent handle先ReadObjectHandle得到句柄地址再通过GC.GetHandleExtraInfo(handleAddress)取出 secondary OBJECTREF——即System.Diagnostics.EditAndContinueHelper实例的地址。读取 _objectReference 槽位helper 对象上唯一的字段_objectReferencesrc/coreclr/System.Private.CoreLib/src/System/Diagnostics/EditAndContinueHelper.cs 中的private object? _objectReference持有字段的实际负载读取该地址得到fieldObject。按字段类型换算最终地址与运行时EnCSyncBlockInfo::GetEnCFieldAddrFromHelperFieldDesc一一对应见 src/coreclr/vm/encee.cpp值类型ValueType以装箱对象存储(*pOR)-UnBox()返回fieldObject Object::Dataunbox 后数据区起始引用类型ClassOBJECTREF 槽位本身就是字段值位置返回_objectReference地址其他基本类型存储在 1 元素数组中运行时侧是I1ARRAYREF通过Object.GetArrayData返回首元素地址。这套按类型分派的内存布局正是 EnC 字段值存储的完整真相值类型装箱、引用类型直接存引用、基本类型包在单元素数组中。运行时侧实现佐证EditAndContinueModule 的 Resolve 路径在运行时侧字段值解析的入口是EditAndContinueModulesrc/coreclr/vm/encee.h它是Module的 EnC 特化子类IsEditAndContinueCapable()返回 TRUE并记录m_applyChangesCount版本号等于 ApplyChanges 被调用的次数。ResolveField(thisPointer, pFD)src/coreclr/vm/encee.cpp从对象的 SyncBlock 取EnCSyncBlockInfo调用其ResolveFieldL1336——沿链表查找匹配m_pFieldDesc pFD的条目通过 dependent handle 的 secondary 取 helper 对象GetDependentHandleSecondary/IGCHandleManager::GetDependentHandleSecondary再调用GetEnCFieldAddrFromHelperFieldDesc完成按类型分派的地址换算找不到条目返回 NULL。ResolveOrAllocateFieldL1020解析失败字段值尚未创建时按需分配EnCAddedField并挂到链表上仅在 OOM 时失败。EnCFieldDesc::GetAddress(void *o)L1148则是字段访问器JIT 编译出的 EnC 字段访问代码最终调用的入口pModule-ResolveOrAllocateField(ObjectToOBJECTREF((Object *)o), this)。调试器契约侧与运行时侧的分工非常清晰运行时侧负责写创建/分配/挂链表cDAC 契约侧负责只读地还原两侧通过cdac_dataT模板导出的offsetof偏移如 src/coreclr/vm/encee.h与 datadescriptor.inc 中的CDAC_GLOBAL保持布局一致。全局变量与数据描述符速查契约文档给出了完整的全局变量与数据描述符清单整理如下便于查阅全局变量Global variables全局类型含义FieldOffsetNewEncuint32哨兵偏移值存于FieldDesc::DWord2标记尚未分配存储的 EnC 新增字段源码出处field.h → datadescriptor.inc数据描述符汇总Data descriptors描述符字段类型含义EnCAddedFieldFieldDataObjectHandledependent handle 对primary 为依赖锚点secondary 为System.Diagnostics.EditAndContinueHelper实例EnCAddedFieldFieldDescpointer指向该新增实例字段的EnCFieldDescEnCAddedFieldNextpointer指向 SyncBlock 上链表的下一个条目EnCAddedFieldElementFieldDescpointer内嵌 EnCFieldDesc与 FieldDesc 布局兼容的地址EnCAddedFieldElementNextpointer链表下一节点EnCAddedStaticFieldFieldDatapointer基本类型字段的内联存储地址或 class/值类型字段的 GC 跟踪存储指针地址EnCEEClassDataAddedInstanceFieldspointer新增实例字段链表头EnCEEClassDataAddedStaticFieldspointer新增静态字段链表头EnCEEClassDataMethodTablepointer本条目承载 EnC 数据的 MethodTable 指针EnCFieldDescNeedsFixupint32非零表示仍需 fixup尚未完全初始化EnCFieldDescStaticFieldDatapointer支撑该静态字段的EnCAddedStaticField指针分配存储前为 NULLEnCSyncBlockInfoListpointer与对象关联的 EnC 新增实例字段链表头FieldDescDWord2uint32打包的标志与偏移字FieldOffsetNewEnc哨兵标识尚无存储的 EnC 新增字段ModuleEnCClassListpointerEnC 新增类列表指针Object(type size)uint32固定 Object 头部至 MethodTable 指针字节数SyncBlockEnCInfopointer对象的 EnC 新增字段信息未配置 EnC 时可选System.Diagnostics.EditAndContinueHelper_objectReferencepointerEnC 新增实例字段的每字段存储UnorderedArrayBaseCountuint32数组当前有效条目数UnorderedArrayBaseTablepointer数组后备存储指针其中System.Diagnostics.EditAndContinueHelper在 cDAC 侧对应 EditAndContinueHelperObject.cs其ObjectReferenceAddress通过[FieldAddress(_objectReference)]属性直接映射到托管字段。版本演进与扩展阅读契约当前版本为Version 1c1。文档中的契约使用清单由BEGIN/END GENERATED: usage contractRuntimeMutableTypeSystem versionc1标记自动生成确保文档与数据描述符、全局变量注册保持同步。类似的生成式契约文档还有 RuntimeTypeSystem.md静态类型系统、SyncBlock.mdSyncBlock 与锁状态其中SyncBlock::EnCInfo字段的完整上下文在此、Object.md对象头与GetSyncBlockAddress/GetArrayData的契约定义。数据契约体系的整体设计见 datacontracts_design.md 与 contract-descriptor.mdcdac_dataT模板与全局变量注册机制分别在 encee.h 与 datadescriptor.inc 中。若要观察运行时如何应用一次 EnC 编辑含新增字段可追踪EditAndContinueModule::ApplyEditAndContinue→AddField(mdFieldDef token)encee.h的调用链。小结RuntimeMutableTypeSystem契约回答了一个关键问题EnC/Hot Reload 动态添加的字段到底存在哪里、如何从进程外读取。答案可以浓缩为三句话字段描述符层面每个 EnC 新增字段有一个EnCFieldDesc按类型挂在EnCEEClassData的实例/静态两条链表中通过FieldDesc::DWord2中的FieldOffsetNewEnc哨兵偏移与普通字段区分静态字段值层面存储在EnCAddedStaticField的变长FieldData区域按字段类型决定是否需要再解引用一层实例字段值层面每个对象实例在其 SyncBlock 上懒加载EnCSyncBlockInfo链表节点通过 dependent handle 关联到EditAndContinueHelper托管对象最终按值类型装箱 / 引用类型直接存引用 / 基本类型单元素数组三种策略定位真实存储地址。掌握了这套布局与算法再结合 RuntimeMutableTypeSystem_1.cs 的参考实现读者便能在自己的诊断工具或调试器扩展中正确地枚举和读取 EnC 新增字段——这正是调试器在编辑并继续场景下保持字段视图、表达式求值一致性的底层保障。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表