.NET Runtime cDAC 数据契约解读:Notifications 调试通知的解码机制与源码实践

发布时间:2026/9/17 14:04:33
.NET Runtime cDAC 数据契约解读:Notifications 调试通知的解码机制与源码实践 .NET Runtime cDAC 数据契约解读Notifications 调试通知的解码机制与源码实践【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime本文基于 dotnet/runtime 仓库中的 Notifications 数据契约文档系统讲解 .NET 运行时CoreCLR如何通过 cDACData Contract Reader合约把运行时抛出的调试器通知debugger notification解码为类型化对象。你将掌握SetGcNotification/TryParseNotification两个合约 API 的语义与实现理解通知从原生运行时产生、经异常机制传递、再到 cDAC 解析的完整链路并能在调试器/SOS 等诊断工具开发中正确消费这些通知。一、背景为什么需要 Notifications 合约.NET 运行时在运行过程中会产生大量与调试器相关的事件例如模块加载/卸载、方法被 JIT 编译、异常抛出、GC 标记阶段结束、进入异常 catch 子句等。传统的 DACData Access Component通过特定的通知机制把这些事件传给调试器侧代码而在新一代 cDACsrc/native/managed/cdac架构中这些事件以数据契约的形式被抽象为合约接口。Notifications合约的核心职责非常聚焦解码由运行时发起的调试器通知。它只负责读——把一段携带通知信息的TargetPointer数组解析成类型化的NotificationData对象至于写——比如管理 JIT 代码通知的白名单allowlist——则属于另一个独立合约CodeNotifications见 CodeNotifications 文档。两合约在功能上互补但边界清晰Notifications只解码运行时已产生的事件不向目标进程写入任何数据CodeNotifications向目标进程写入 JIT 通知白名单可按需惰性分配表结构。二、合约 API 总览Notifications合约版本 c1对外暴露两个 API定义于 INotifications.cs// 为被判定的各代condemned generation设置 GC 通知 // 参数是一个位掩码第 i 位置位表示第 i 代 void SetGcNotification(int condemnedGeneration); // 将异常信息数组解析为类型化的通知对象。 // 若通知类型未知则返回 false。对结果做模式匹配以访问通知特有字段。 bool TryParseNotification(ReadOnlySpanTargetPointer exceptionInformation, out NotificationData? notification);两个 API 的性质完全不同API方向作用SetGcNotification写向目标进程写入GcNotificationFlags全局变量登记希望收到 GC 通知的代数generationTryParseNotification读把从目标进程读取的原始通知参数数组解码为类型化NotificationData版本 c1 的依赖清单文档给出了版本 1 的自动生成清单由!-- BEGIN GENERATED --标注数据描述符Data descriptors使用无全局变量Global variables使用GlobalTypeMeaningGcNotificationFlagspointer存储 GC 通知数据的全局标志使用的其他合约无。也就是说Notifications合约只依赖一个全局变量GcNotificationFlags。在原生侧它对应src/coreclr/vm/util.cpp中的g_gcNotificationFlags并在 datadescriptor.inc 中通过CDAC_GLOBAL_POINTER(GcNotificationFlags, ::g_gcNotificationFlags)暴露给 cDAC 读取。三、通知的产生端原生运行时如何发出通知要正确理解解码逻辑必须先看清原生侧的通知产生链路。整个机制的核心实现在 util.cpp 的DACNotify系列函数中。3.1 通知参数数组与异常机制运行时产生通知时并不是通过普通的函数调用把数据传给调试器而是把通知参数写进一个全局数组g_clrNotificationArguments容量上限为MAX_CLR_NOTIFICATION_ARGS 3见 util.hpp然后通过RaiseException(CLRDATA_NOTIFY_EXCEPTION, ...)抛出一次伪异常由调试器通过既有的异常机制捕获// src/coreclr/vm/util.cpp void DACNotifyExceptionHelper(TADDR *args, UINT argCount) { _ASSERTE(argCount MAX_CLR_NOTIFICATION_ARGS); // 仅当存在原生调试器且托管调试器未附加时发送 if (minipal_is_native_debugger_present() !CORDebuggerAttached()) { CrstHolder lh(g_clrNotificationCrst); for (UINT i 0; i argCount; i) g_clrNotificationArguments[i] args[i]; DACRaiseException(args, argCount); // 内部调用 RaiseException(CLRDATA_NOTIFY_EXCEPTION, ...) g_clrNotificationArguments[0] 0; } }注意两个关键前提条件源码可验证见 util.cppminipal_is_native_debugger_present()—— 必须有原生调试器在场!CORDebuggerAttached()—— 托管调试器CorDebug未附加时才走这条通知路径。g_clrNotificationArguments数组本身也通过CDAC_GLOBAL_POINTER(ClrNotificationArguments, ::g_clrNotificationArguments)datadescriptor.inc暴露给 cDAC。3.2 各类通知的参数布局参数数组的约定是第一个元素是通知类型枚举值其后各元素是该类型的载荷。原生侧的类型枚举定义在 util.hppMODULE_LOAD_NOTIFICATION 1, MODULE_UNLOAD_NOTIFICATION 2, JIT_NOTIFICATION 3, // 旧版cDAC 不处理 EXCEPTION_NOTIFICATION 5, GC_NOTIFICATION 6, CATCH_ENTER_NOTIFICATION 7, JIT_NOTIFICATION2 8,各通知的产生点分别对应通知触发时机产生函数MODULE_LOAD_NOTIFICATION模块加载完成DACNotify::DoModuleLoadNotification由 assembly.cpp 调用MODULE_UNLOAD_NOTIFICATION模块卸载DACNotify::DoModuleUnloadNotification由 ceeload.cpp 调用JIT_NOTIFICATION2方法被 JIT 编译出原生代码DACNotify::DoJITNotification由 prestub.cpp 调用EXCEPTION_NOTIFICATION异常抛出DACNotify::DoExceptionNotificationGC_NOTIFICATIONGC 标记阶段MarkEnd结束DACNotify::DoGCNotificationutil.cppCATCH_ENTER_NOTIFICATION进入异常 catch 子句DACNotify::DoExceptionCatcherEnterNotification由 exinfo.cpp 调用例如 GC 通知和 catch 进入通知的参数构造// GC 通知Args { GC_NOTIFICATION, typ, condemnedGeneration } TADDR Args[3] { GC_NOTIFICATION, (TADDR) args.typ, (TADDR) args.condemnedGeneration }; DACNotifyExceptionHelper(Args, 3); // 进入 catchArgs { CATCH_ENTER_NOTIFICATION, MethodDescPtr, nativeOffset } TADDR Args[3] { CATCH_ENTER_NOTIFICATION, (TADDR) MethodDescPtr, (TADDR)nativeOffset }; DACNotifyExceptionHelper(Args, 3);某些通知还受g_dacNotificationFlags的开关控制例如DoExceptionCatcherEnterNotification只有在g_dacNotificationFlags CLRDATA_NOTIFY_ON_EXCEPTION_CATCH_ENTER置位时才发送见 util.cpp。四、消费端cDAC 合约实现 Notifications_1cDAC 侧的完整实现位于 Notifications_1.cs。它内部维护一个与原生枚举一一对应的私有枚举private enum NotificationType_1 : uint { ModuleLoad 1, ModuleUnload 2, Exception 5, Gc 6, ExceptionCatcherEnter 7, Jit2 8, }与原生 util.hpp 中的值完全一致。其中原生仍存在但 cDAC 明确不处理的JIT_NOTIFICATION 3未出现在该枚举中——单元测试的注释也印证了这一点// JIT_NOTIFICATION (legacy, not handled)。4.1 SetGcNotification写入 GC 通知掩码void INotifications.SetGcNotification(int condemnedGeneration) { TargetPointer pGcNotificationFlags _target.ReadGlobalPointer(Constants.Globals.GcNotificationFlags); uint currentFlags _target.Readuint(pGcNotificationFlags); if (condemnedGeneration 0) _target.Writeuint(pGcNotificationFlags, 0); else { _target.Writeuint(pGcNotificationFlags, currentFlags | (uint)condemnedGeneration); } }语义要点参数是位掩码第 i 位代表第 i 代generation。例如传入2二进制10表示关注第 1 代传入4100表示关注第 2 代condemnedGeneration 0时清零即不再关注任何代非零时按位或合并多次调用会累积设置不会覆盖已有的关注位。该逻辑与原生侧GcNotifications::SetNotificationutil.cpp完全对应VOID SetNotification(GcEvtArgs ev) { if (ev.typ GC_MARK_END) { if (ev.condemnedGeneration 0) g_gcNotificationFlags 0; else g_gcNotificationFlags (DWORD)(g_gcNotificationFlags | ev.condemnedGeneration); } }注意原生侧只有当事件类型为GC_MARK_END时才更新标志而 cDAC 侧的SetGcNotification是调试器主动发起的写操作两者共享同一份g_gcNotificationFlags状态。GC 侧的实际消费点在 gcenv.ee.cppGC 引擎通过GCToEEInterface::AnalyzeSurvivorsRequested/AnalyzeSurvivorsFinished构造GcEvtArgs gea { GC_MARK_END, { (1condemnedGeneration) } }先调用GcNotifications::GetNotification(gea)判断该代是否被登记关注命中后再调用DACNotify::DoGCNotification(gea)发出通知。GetNotification的判断逻辑为// 关注 condemnedGeneration 0或该代的位被设置 if (ev.typ GC_MARK_END (ev.condemnedGeneration 0 || (g_gcNotificationFlags ev.condemnedGeneration) ! 0)) return TRUE;4.2 TryParseNotification统一解码入口bool INotifications.TryParseNotification(ReadOnlySpanTargetPointer exceptionInformation, [NotNullWhen(true)] out NotificationData? notification) { notification null; if (exceptionInformation.IsEmpty) return false; notification (NotificationType_1)(uint)exceptionInformation[0].Value switch { NotificationType_1.ModuleLoad new ModuleLoadNotificationData(exceptionInformation[1]), NotificationType_1.ModuleUnload new ModuleUnloadNotificationData(exceptionInformation[1]), NotificationType_1.Jit2 new JitNotificationData(exceptionInformation[1], exceptionInformation[2]), NotificationType_1.Exception new ExceptionNotificationData(exceptionInformation[1]), NotificationType_1.Gc ParseGcNotification(exceptionInformation), NotificationType_1.ExceptionCatcherEnter new ExceptionCatcherEnterNotificationData(exceptionInformation[1], (uint)exceptionInformation[2].Value), _ null, }; return notification is not null; }解码规则总结通知类型首元素载荷布局结果类型ModuleLoad[1] 模块地址ModuleLoadNotificationData(ModuleAddress)ModuleUnload[1] 模块地址ModuleUnloadNotificationData(ModuleAddress)Jit2[1] MethodDesc 地址[2] 原生代码地址JitNotificationData(MethodDescAddress, NativeCodeAddress)Exception[1] 线程地址ExceptionNotificationData(ThreadAddress)Gc交给ParseGcNotification二次解析GcNotificationDataExceptionCatcherEnter[1] MethodDesc 地址[2] 原生偏移ExceptionCatcherEnterNotificationData(MethodDescAddress, NativeOffset)两个边界行为值得注意均有单元测试覆盖空数组exceptionInformation.IsEmpty时直接返回falsenotification为null未知类型switch 的_分支返回null最终返回false。原生遗留类型JIT_NOTIFICATION 3以及任意未定义值都走该路径。五、数据类型体系NotificationData 层级结构文档定义了一套 C# record 类型体系接口定义见 INotifications.cspublic enum GcEventType { MarkEnd 1, } public record struct GcEventData(GcEventType EventType, int CondemnedGeneration); public enum NotificationType { Unknown 0, ModuleLoad, ModuleUnload, Jit2, Exception, Gc, ExceptionCatcherEnter, } public abstract record NotificationData(NotificationType Type); public record ModuleLoadNotificationData(TargetPointer ModuleAddress) : NotificationData(NotificationType.ModuleLoad); public record ModuleUnloadNotificationData(TargetPointer ModuleAddress) : NotificationData(NotificationType.ModuleUnload); public record JitNotificationData(TargetPointer MethodDescAddress, TargetPointer NativeCodeAddress) : NotificationData(NotificationType.Jit2); public record ExceptionNotificationData(TargetPointer ThreadAddress) : NotificationData(NotificationType.Exception); public record GcNotificationData(GcEventData EventData, bool IsSupportedEvent) : NotificationData(NotificationType.Gc); public record ExceptionCatcherEnterNotificationData(TargetPointer MethodDescAddress, uint NativeOffset) : NotificationData(NotificationType.ExceptionCatcherEnter);设计要点NotificationData是抽象基类Type属性标识通知种类消费方使用 C# 模式匹配is/switch对派生类型分派访问各自特有字段TargetPointer表示目标进程地址避免在调试器进程与目标进程之间混用地址空间概念。GC 通知的特殊处理GC 通知的载荷不是简单的一个值而是一个事件结构因此单独抽出了ParseGcNotificationprivate static GcNotificationData ParseGcNotification(ReadOnlySpanTargetPointer exceptionInformation) { GcEventType eventType (GcEventType)(uint)exceptionInformation[1].Value; GcEventData eventData new(eventType, (int)(uint)exceptionInformation[2].Value); return new GcNotificationData(eventData, IsSupportedEvent: eventType GcEventType.MarkEnd); }布局为[0] Gc(6)、[1] GcEventType、[2] condemnedGeneration。其中GcEventType目前只有MarkEnd 1一个值对应原生GC_MARK_ENDIsSupportedEvent标记该事件类型是否为当前合约支持的版本——eventType GcEventType.MarkEnd时为true其余为false。即使事件类型未知合约也会返回GcNotificationDataTryParseNotification返回true只是IsSupportedEvent为false由消费方自行决定如何处理这是一种向前兼容的渐进式解码策略。原生侧 util.cpp 的DACNotify::ParseGCNotification采用同样的思路仅对GC_MARK_END提取condemnedGeneration其他GcEvt_t返回FALSE。六、与 CodeNotifications 合约的分工文档明确指出JIT 代码通知白名单的管理是另一个独立合约见 CodeNotifications 文档。两者对比维度NotificationsCodeNotifications方向只解码运行时已产生的事件向目标进程写入通知白名单写目标GcNotificationFlags全局变量g_pNotificationTableJITNotification 数组惰性分配不需要需要SetCodeNotification首次写入非零标志时经Target.AllocateMemory分配表版本c1c1CodeNotifications合约管理一张JITNotification数组索引 0 为簿记槽MethodToken字段存当前条目数实际条目从索引 1 开始容量由JITNotificationTableSize全局常量原生侧为JIT_NOTIFICATION_TABLE_SIZE 1000见 util.hpp决定。其 cDAC 实现 CodeNotifications_1.cs 中有一个值得注意的细节当表满时抛出COMExceptionHResult E_FAIL对齐传统 DAC 的失败路径而在批量清除时只修剪尾部空闲条目刻意区别于原生JITNotifications::SetAllNotificationsutil.cpp的算法避免把其他模块仍活跃的条目孤立在长度之外。而Notifications合约本身不写这张表、不分配内存二者的边界由此清晰分开。七、单元测试验证合约行为有完整的单元测试覆盖见 NotificationsTests.cs测试通过TestPlaceholderTarget构造 mock 目标进程并以c1版本注册合约。可验证的行为包括测试验证点TryParseNotification_EmptySpan_ReturnsFalse空数组返回falseTryParseNotification_UnknownType_ReturnsFalse类型0、遗留的3JIT_NOTIFICATION以及任意值99均返回falseTryParseNotification_ModuleLoad/ModuleUnload解码出模块地址并正确设置TypeTryParseNotification_Jit解码出 MethodDesc 地址与原生代码地址TryParseNotification_Exception解码出线程地址TryParseNotification_Gc_SupportedEventMarkEnd 代 2 时IsSupportedEvent trueTryParseNotification_Gc_UnsupportedEvent未知 GC 事件类型时IsSupportedEvent false但解析仍成功TryParseNotification_ExceptionCatcherEnter解码出 MethodDesc 地址与原生偏移测试常量与原生枚举一一对应ModuleLoad 1 ... Jit2 8可作为实现正确性的对照基准。八、应用场景与阅读指引Notifications合约面向的是 .NET 诊断工具链调试器、SOS、cDAC 消费者开发者。典型使用流程为调试器捕获目标进程抛出的CLRDATA_NOTIFY_EXCEPTION从ClrNotificationArguments全局数组或异常参数读取原始TargetPointer数组调用TryParseNotification得到类型化NotificationData模式匹配派生类型读取模块地址、MethodDesc 地址、GC 代数等字段。若需要主动订阅 JIT 事件让运行时在编译/丢弃方法时发通知则应配合CodeNotifications合约的SetCodeNotification/SetAllCodeNotifications使用。关键源码索引合约文档Notifications.md、CodeNotifications.md合约接口定义INotifications.cscDAC 实现Notifications_1.cs、CodeNotifications_1.cs单元测试NotificationsTests.cs原生通知产生端util.cppDACNotify系列、GcNotifications、util.hpp通知类型枚举、JITNotification结构GC 侧消费点gcenv.ee.cpp全局变量暴露datadescriptor.inc结语Notifications是 cDAC 数据契约体系中最小巧、最聚焦的合约之一它不分配内存、不写目标进程GcNotificationFlags除外、不依赖其他合约只用两个 API 完成把运行时通知解码为类型化对象这一件事。理解它的关键在于把握三层对应关系原生枚举值 ↔ cDAC 枚举值 ↔NotificationType托管枚举以及参数数组的布局约定首元素为类型其后为载荷。沿着 Notifications.md 与上述源码路径即可完整追踪一条调试通知从RaiseException到GcNotificationData的全生命周期。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考