
.NET 运行时 CallingConvention 数据契约从方法签名到 GCRefMap 参数编码的源码级解析【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime导读本文围绕 dotnet/runtime 仓库中诊断数据契约Data Contract体系下的CallingConvention契约展开深入讲解它如何复用运行时自身的调用约定规则来遍历一个方法MethodDesc的实参签名从而在调用者的 Transition Frame 上定位每一个参数的槽位并判定哪些槽位承载 GC 引用。读者读完本文后将掌握该契约的单一 APITryComputeArgGCRefMapBlob的语义与返回约定、签名解码与类型信息模型的完整链路、ArgIterator所需类型抽象的逐项适配方式、与运行时GCRefMapBuilder字节级兼容的编码规则以及cdacstress如何用ARGITER子检查做逐字节正确性验证。文中所有结论均可在当前仓库对应源码中复现。数据契约背景为什么需要调用约定信息诊断数据契约 的定位是向诊断工具调试器、分析器、profilers描述 .NET 运行时进程内存中一部分内部数据结构的物理形态与语义使工具不必依赖与目标运行时版本严格匹配的 DAC/DBI 库即可直接读取并解释进程内存来获知运行时状态。契约是运行时对诊断工具的一种承诺只要按契约解释内存就能计算出有用的运行时状态信息。对于调试器这类工具而言一个高频需求是给定一个正在执行的调用callsite知道调用者帧callers transition frame上每个实参位于何处以及其中哪些是 GC 引用对象引用、内部指针等。这正是CallingConvention契约的职责。它要回答的问题形如这个方法的第 3 个参数是一个SpanT它的 byref 字段被压到了哪个槽位槽位内容是 GC 引用还是内部指针。没有这类信息调试器无法在断点处正确枚举栈上实参、无法报告 GC 根也就无法可靠地进行现场分析。契约职责边界ABI 定义与契约的分工CallingConvention契约并不重新描述 ABI 本身。真正定义哪些寄存器承载哪些参数、应用何种对齐与填充规则、结构体如何提升到寄存器或压栈、varargs 如何传递等细节的是 CLR ABI 规范即仓库中的 Common CLR ABI conventions。该文档按 x64AMD64、ARM、ARM64、x86 等架构描述了 CLR 对原生 ABI 的约定与例外例如this指针被当作一种特殊参数始终作为第一个参数传递AMD64 的RCXARM/ARM64 的R0托管 varargs 在可选返回缓冲区和this指针之后、其他用户参数之前插入vararg cookiecallee 必须把 cookie 及后续参数溢出到其 home 位置因为实参可能以 cookie 为基址做指针运算访问非 x86 平台上所有方法都必须有 unwind 信息以便 GC 能够展开栈。CallingConvention契约的边界在于它不关心 ABI 规则本身为什么这样设计而是把 ABI 规则应用后的结果每个参数的寄存器/栈位置、GC 分类以 cDAC 可消费的形式暴露出来并且要求与运行时自身产生的结果逐字节兼容。一句话概括职责分工ABI 规范定义规则契约暴露规则的结果。契约 APITryComputeArgGCRefMapBlob契约只暴露一个 API// Encode the argument GCRefMap blob for methodDesc byte-for-byte // compatible with the runtimes ComputeCallRefMap (frames.cpp). // Returns false when this contract declines to encode the method // (e.g. an unported ABI path); callers should map false to E_NOTIMPL. // When false, the value of blob is unspecified. bool TryComputeArgGCRefMapBlob(MethodDescHandle methodDesc, out byte[] blob);语义要点输入是MethodDescHandle见 RuntimeTypeSystem 契约MethodDesc是托管方法在运行时的表示无论其来源是 IL、Reflection.Emit 还是运行时动态生成输出是 GCRefMap blob一段将帧上哪些槽位是 GC 引用压缩编码的字节流要求与运行时ComputeCallRefMapframes.cpp产生的输出逐字节一致返回false表示该契约拒绝编码此方法例如某个尚未移植的 ABI 路径或签名/泛型上下文未被编码器覆盖此时blob值未定义调用方应将false映射为 COM 风格的E_NOTIMPL。在 cDAC 托管实现中CallingConvention_1.cs 的TryComputeArgGCRefMapBlob正是这样实现的核心逻辑ComputeArgGCRefMapBlobCore返回null时清空blob并返回false任何NotImplementedException包括来自GetArgumentLayout的未移植 ABI 路径 NIE同样被捕获并干净地降级为false从而保证未支持路径绝不产生错误数据。版本 1 概览与依赖契约文档给出了版本 1c1的依赖清单其中Data descriptors used无契约不直接声明任何数据结构布局Global variables used无Contracts usedContract NameEcmaMetadataLoaderRuntimeInfoRuntimeTypeSystem这四份依赖的职责分工与实现路径一一对应详见后文RuntimeTypeSystem提供MethodDescHandle的类型信息——GetMethodTable、TryGetMethodSignature、GetGenericContextLoc、IsAsyncMethod、GetBaseSize、TryGetHFAElementSize、TryGetSystemVAmd64EightByteClassification、IsByRefLike、ContainsGCPointers、GetGCDescSeries等EcmaMetadata为模块提供MetadataReader用于读取 IL 签名与字段签名LoaderGetModuleLookupMapElement配合TypeDefToMethodTable/TypeRefToMethodTable两种 lookup-map 种类将类型 token 解析为目标类型句柄RuntimeInfo提供目标架构与操作系统用于构造TransitionBlock。这一零数据描述符 纯算法的形态符合数据契约设计的推荐风格契约算法优先用 C# 风格的伪代码在Target抽象之上工作见 contract_csharp_api_design.cs尽量把复杂数据结构访问收敛到其他契约。签名解码链路契约处理的第一个阶段是读懂方法的签名。从源码实现CallingConvention_1.DecodeMethodSignatureCallingConvention_1.cs可以看到完整调用链通过RuntimeTypeSystem.GetMethodTable(methodDesc)拿到MethodDesc所属的方法表owning method table再经GetTypeHandle得到类型句柄用RuntimeTypeSystem.GetModule得到所属模块并通过Loader.GetModuleHandleFromModulePtr转成ModuleHandle通过EcmaMetadata.GetMetadata(moduleHandle)取得模块的MetadataReader通过RuntimeTypeSystem.TryGetMethodSignature(methodDesc, out ReadOnlySpanbyte signature)取得原始签名字节用System.Reflection.Metadata体系的解码器对签名进行解码。契约文档指出概念上这与SignatureDecoderSignatureTypeInfo, SignatureTypeContext的模型一致实际实现RuntimeSignatureDecoderSignatureTypeInfo, SignatureTypeContext额外理解运行时内部的签名元素类型如Internal、CModInternal等运行时专用 CorElementType定义于 RuntimeTypeSystem 契约 的CorElementType枚举其余遵循 SRM 的解码模型。类型提供者type provider的初始化包含三方面上下文所属模块用于把TypeDef/TypeReftoken 解析成目标类型——通过Loader.GetModuleLookupMapElement和TypeDefToMethodTable、TypeRefToMethodTable两种 lookup-map 种类对应 Loader 契约 中的Module描述符字段TypeDefToMethodTableMap/TypeRefToMethodTableMap方法的MethodDescHandle用于解析方法泛型参数!!T所属类型的结构类型信息用于解析类型泛型参数!T。在 SignatureTypeInfoProvider.cs 中可以看到GetTypeFromDefinition/GetTypeFromReference把 token 换成ModuleLookupMapKind.TypeDefToMethodTable/TypeRefToMethodTable后调用GetModuleLookupMapElementGetGenericMethodParameter通过RuntimeTypeSystem.GetGenericMethodInstantiation(method)[index]取精确类型GetGenericTypeParameter则优先从SignatureTypeContext.OwningType的结构化类型实参中取其次从精确类型的GetInstantiation取。关键设计查表失败也不丢信息。对TypeDef或TypeRef而言lookup map 中可能还没有目标类型句柄类型尚未加载。此时提供者必须保留签名中编码的是ELEMENT_TYPE_CLASS还是ELEMENT_TYPE_VALUETYPE。这个区分足以对引用型实参做分类而无需强制加载类型反过来一个拿不到精确类型句柄的值类型其布局是不确定的indeterminate不能送入需要其大小或字段布局的 ABI 路径。对应实现见 SignatureTypeInfoProvider.GetTypeFromTokenrawTypeKind为SignatureTypeKind.Class时记CorElementType.Class为ValueType时记CorElementType.ValueType仅当两者都不是且句柄可用时才回退到精确类型的分类。签名类型信息模型SignatureTypeInfo每个解码出的类型被表示为一个结构化记录SignatureTypeInfoSignatureTypeInfo.cs包含四类信息信息来源用途外层元素类型Outer element type签名元素类型即使目标类型未完全加载也能对基元、引用、指针、byref 和值类型分类精确类型句柄Exact type handle可用时模块 lookup map、泛型实例化或运行时内部签名元素提供目标内存支撑的运行时布局与分类泛型类型定义Generic type definition可用时解码出的泛型类型当精确构造类型句柄不可用时保留其定义泛型实参Generic arguments递归签名解码为嵌套值类型字段解码提供结构化的泛型上下文其中精确ITypeHandle代表目标内存支撑的MethodTable*或TypeDesc*RuntimeTypeSystem契约中TargetTypeHandle与TypeHandleBits的区分可参见 RuntimeTypeSystem.md。它在这里是故意可选的签名可以在运行时尚未为某个类型产出精确句柄之前就描述它——这正是未加载类型也能参与调用约定计算的核心机制。嵌套字段的泛型替换当遍历需要某个字段的类型时契约从外围类型的模块读取该字段的元数据签名并以包含该类型owning type的结构化信息作为泛型上下文进行解码GetFieldTypeInfoCallingConvention_1.cs。这样即使不存在精确构造的方法表嵌套字段中的泛型参数也能被替换。典型例子Spanint的封闭方法表closed MT尚未加载时其布局可以取自开放泛型SpanT字段层面的 byref/ptr 区分与 T 的具体取值无关。ArgIterator 需要的类型抽象逐项适配ArgIterator共享源码 ArgIterator.cs位于src/coreclr/tools/Common/CallingConvention/同时被 crossgen2 与 cDAC 以文件链接方式共享消费一个很小的类型抽象ITypeHandleITypeHandle.cs来应用目标 ABI。CallingConvention契约将解码出的类型信息与目标契约适配为该接口cDAC 侧的适配器是CdacTypeHandleCdacTypeHandle.cs。契约文档给出了完整的适配表ArgIterator 操作数据来源精确布局不可用时的行为IsNull既无签名元素类型也无精确类型句柄报告无类型GetCorElementType签名元素类型仅当需要把枚举值类型归一化到底层基元时使用精确句柄尽可能使用结构化元素类型IsValueType/IsPointerType签名元素类型精确句柄作为回退使用结构化分类PointerSizeTarget.PointerSize始终可用GetSize/HasIndeterminateSize对精确值类型使用RuntimeTypeSystem.GetBaseSize未解析的值类型大小不确定依赖大小的 ABI 路径被拒绝RequiresAlign8RuntimeTypeSystem.RequiresAlign8无精确句柄时返回 falseIsHomogeneousAggregate/GetHomogeneousAggregateElementSizeRuntimeTypeSystem.TryGetHFAElementSize无精确句柄时返回 falseGetSystemVAmd64PassStructInRegisterDescriptorRuntimeTypeSystem.TryGetSystemVAmd64EightByteClassification无精确句柄时报告结构体不按寄存器分类IsTrivialPointerSizedStruct精确值类型大小 其实例FieldDesc列表与字段签名除非精确布局证明了 x86 特例否则返回 falseGetFpStructInRegistersInfo目标相关的 RISC-V / LoongArch64 ABI 分类尚未实现契约拒绝该 ABI 路径GetFieldAlignment目标相关的 LoongArch64 / WASM 布局尚未实现契约拒绝该 ABI 路径下面逐项给出源码侧印证GetSizeCdacTypeHandle.GetSize引用/指针/byref/数组/类等直接返回PointerSize否则要求精确句柄存在否则抛NotImplementedException即大小不确定 → 拒绝并把GetBaseSize减去对象头与 MethodTable 指针2 * PointerSize换算成非装箱unboxed布局大小——这与运行时MethodTable::BaseSize含对象头/对齐填充的语义一致GetCorElementTypeCdacTypeHandle.GetCorElementType对值类型会镜像MetaSig::PeekArgNormalized——通过GetInternalCorElementType把枚举折叠为其底层基元如 byte 枚举 →U1。注释明确说明共享ArgIterator的 x86IsArgumentInRegister依赖这一归一化否则子指针大小的枚举会被错误地当作栈传参数IsTrivialPointerSizedStructCdacTypeHandle.IsTrivialPointerSizedStruct仅 x86 有意义要求恰好一个实例字段且该字段为指针大小的基元I/U/I4/U4/Ptr/FnPtr或递归地另一个 trivial 结构体该行为与 crossgen2 侧ILCompiler.ReadyToRun的ITypeHandle.IsTrivialPointerSizedStruct对应HFA/SystemV/对齐分别映射到RuntimeTypeSystem.TryGetHFAElementSize、TryGetSystemVAmd64EightByteClassification、GetClassAlignmentRequirement。其中 SystemV 的 eight-byte 分类在运行时中存放于EEClassOptionalFields.EightByteRegistersInfo仅在UNIX_AMD64_ABI构建上填充见 RuntimeTypeSystem.md 的EEClassOptionalFields描述符分类计数为 0 或大于 2 时视为无分类。完成每个参数与返回类型的适配器构造后契约用目标TransitionBlock、方法调用约定ManagedInstance/ManagedStatic、实例/varargs 状态、泛型上下文实参状态初始化ArgIterator然后遍历产生的参数偏移把位置与 GC 分类编码进 GCRefMap blob。TransitionBlock的体系含StructInRegsOffset -2、OffsetOfFirstGCRefMapSlot等架构差异见 TransitionBlock.cs。GCRefMap 编码与运行时字节级兼容GCRefMap是运行时为每个调用点callsite编码GC 摘要的格式。原生侧的权威定义在 gcrefmap.h契约的编码器 GCRefMapEncoder 逐条镜像了它的编码规则流边界与高位置 1 终止编码总是从字节边界开始每个字节的最高位用于表示编码流的结束因此每字节只有 7 个有效位位置增量编码pos槽位位置总是编码为相对前一个位置的增量delta两比特基本单元值 0/1/2 分别表示常见的三种构造——跳过单个槽位SKIP、GC 引用REF、内部指针INTERIOR值 3 表示后面跟扩展编码扩展整数编码以 4 比特块为单位3 个数据位 1 个续位见AppendInt续位用于标记块的结束x86 前缀x86 上编码以callee 弹出栈大小开始用同一套两比特机制编码WriteStackPopCbStackPop来自ArgIteratorvarargs 时 x86 由调用方清理栈故为 0。令牌定义与运行时一致ArgIterator.cs令牌值含义GCREFMAP_SKIP0跳过单个槽位GCREFMAP_REF1槽位是 GC 引用GCREFMAP_INTERIOR2槽位是内部指针byref / 托管指针GCREFMAP_METHOD_PARAM3方法泛型上下文实参InstArgMethodDescGCREFMAP_TYPE_PARAM4类型泛型上下文实参InstArgMethodTableGCREFMAP_VASIG_COOKIE5varargs 的 VASigCookie 槽位cDAC 编码器的工作方式ComputeArgGCRefMapBlobCore用SortedDictionaryint, GCRefMapToken累积(偏移, 令牌)遍历GetArgumentLayout产出的每个实参位置按类型分类打令牌见下一节若无任何 GC 相关实参非 x86 直接返回空 blobx86 仍先写WriteStackPop前缀用GCRefMapPosFromOffset把偏移换算成位置序号x86 上寄存器区与栈区的 pos 顺序与偏移顺序相反故必须按 pos 顺序写出逐个WriteToken超过MaxGCRefMapBlobLength 252即保守拒绝Flush写出结尾字节——镜像原生Flush中挂起字节低 7 位非零或 pos 为 0 才写出的规则。特殊参数与边界情况的令牌规则GetArgumentLayoutCallingConvention_1.cs与令牌编码共同处理了若干运行时特有的隐藏参数this指针hasThis时先登记ThisOffset。值类型this且非 unboxing stub编码为Interior类类型this编码为Ref与ArgDestination.GcMark的语义一致泛型上下文实参param type arg共享泛型方法需要一个隐藏实例化实参。GetGenericContextLoc返回InstArgMethodDesc时打MethodParam令牌InstArgMethodTable时打TypeParam令牌否则跳过。这对应运行时 frames.cpp 中非 dispatch cell 时若RequiresInstArg()则SetHasParamTypeArg()的逻辑async continuation异步方法有一个始终在调用方侧传递的 continuation 实参与实例化实参不同它是调用约定的一部分而不仅是共享泛型实现细节。cDAC 用RuntimeTypeSystem.IsAsyncMethod探测并登记其偏移元素类型记为Objectvarargs镜像运行时FakeGcScanRoots的短路行为——只登记 VASigCookie 槽位GetVASigCookieOffset并停止遍历可变尾部实参在 GC 扫描时由 cookie 指向的签名报告而非由本契约报告x86 下CbStackPop为 0调用方清理SystemV-AMD64 寄存器传结构体GetNextOffset返回StructInRegsOffset特例时读取 eight-byte 分类描述符与ArgLocDesc逐 eight-byte 判断IntegerReference→RefIntegerByRef→InteriorSSE 分类走 XMM 寄存器不前进通用寄存器偏移——镜像 ArgDestination.ReportPointersFromStructInRegistersByRefLike 值类型SpanT等 ref struct若结构体含托管指针字段则沿GetFieldDescList遍历实例字段对每个ELEMENT_TYPE_BYREF字段在结构体内偏移处发Interior令牌并递归进入嵌套的 ByRefLike 值类型字段深度上限MaxByRefLikeRecursionDepth 16防环。镜像的是运行时ByRefPointerOffsetsReportersiginfo.cpp而ELEMENT_TYPE_PTR/IntPtr/void*字段明确不报告对应 QCall 类句柄包装器不计 GC含 GC 指针的传值结构体若精确类型句柄可用且ContainsGCPointers为真则用GetGCDescSeries的(Offset, Size)序列把每个指针槽打Ref令牌偏移以装箱对象起点为参照需减去pointerSize换算到非装箱帧内布局——镜像ReportPointersFromValueTypeArgsiginfo.cpp。正确性验证cdacstress 的 ARGITER 子检查契约正确性的金标准是cdacstress压测设施中的ARGITER子检查cdacstress.cpp。其机制为CDACSTRESS_ARGITER 0x00000200比较CallingConvention的实参枚举结果与运行时ComputeCallRefMap见 cdacstress.cpp对活动线程 transition Frame 上的每一个MethodDesc先由运行时自身通过ComputeCallRefMap(pMD, builder, /*isDispatchCell*/ false)产出权威 blobComputeRuntimeArgGCRefMap处理了签名无法分类返回 -1blob 超过缓冲区返回 -2等边界并显式声明CONTRACT_VIOLATION(ModeViolation | GCViolation)以适配在分配器钩子中触发的 GC 模式差异再请求 cDAC 通过TryComputeArgGCRefMapBlob产出同一 blob两者做逐字节比较不匹配即打印运行时与 crossgen2/cDAC 双方的 blob 十六进制转储并触发断言。因此字节级兼容不是一句口号而是有持续运行的压测作为判定依据。任何对GCRefMapEncoder位编码、WriteStackPop前缀、x86 pos 顺序或某类参数令牌规则的偏离都会被该子检查捕获。与运行时实现的整体对照作为收尾把契约实现与运行时原生侧做一次整体对照可以看到每一处契约逻辑都能在运行时找到镜像契约侧cDAC运行时侧CoreCLRGetArgumentLayoutArgIteratorCdacTypeHandle遍历ComputeCallRefMap中的MetaSigArgIteratorframes.cppGCRefMapEncoderCallingConvention_1.csGCRefMapBuildergcrefmap.hByRefLike 字段 Interior 发射ByRefPointerOffsetsReportersiginfo.cpp传值结构体 GCDesc 序列 Ref 发射ReportPointersFromValueTypeArgsiginfo.cppSystemV 寄存器结构体 eight-byte 令牌ArgDestination::ReportPointersFromStructInRegisters枚举归一化GetInternalCorElementTypeMetaSig::PeekArgNormalized共享泛型实例化实参令牌SetHasParamTypeArgframes.cpp与GCREFMAP_METHOD_PARAM/TYPE_PARAMvarargs VASigCookie 短路FakeGcScanRoots的 varargs 分支frames.cpp这一共享同一套ArgIterator 各自镜像同一份编码规则的架构正是该契约能以较低维护成本保持与运行时字节级一致的关键ABI 的繁复规则集中在一处共享代码cDAC 侧只需把签名 → 类型信息 → ITypeHandle 适配做对即可。延伸阅读数据契约总览与规范RuntimeTypeSystem 契约ITypeHandle、MethodDescHandle、类型布局查询 APILoader 契约GetModuleLookupMapElement与 lookup-map 种类EcmaMetadata 契约MetadataReader获取CLR ABI 规范本文不重述的 ABI 细节共享ArgIteratorsrc/coreclr/tools/Common/CallingConvention/ArgIterator.cs原生 GCRefMap 编解码src/coreclr/inc/gcrefmap.h运行时权威实现src/coreclr/vm/frames.cppcDAC 契约实现src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/逐字节验证设施src/coreclr/vm/cdacstress.cpp【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考