资讯动态

dotnet/runtime RyuJIT 反汇编与 JIT Dump 观察指南:DOTNET_JitDisasm 配置变量、方法匹配规则与 Late Disassembler 全流程

发布时间:2026/9/17 20:42:28 来源:尧图企业网站定制
dotnet/runtime RyuJIT 反汇编与 JIT Dump 观察指南DOTNET_JitDisasm 配置变量、方法匹配规则与 Late Disassembler 全流程【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime本文基于 dotnet/runtime 仓库的 viewing-jit-dumps.md 编写系统讲解如何查看 RyuJIT 为托管程序生成的机器码反汇编、GC 信息与 JIT 内部转储JIT dump从搭建演示环境、设置DOTNET_JitDisasm等配置变量到 Debug/Checked 构建下独有的 GC/调试信息输出、Late Disassembler 字节级验证以及 crossgen2 原生映像反汇编读者可完整掌握定位某段托管代码到底生成了什么机器码的全套方法论。一、搭建观察环境一个用于反汇编的托管示例程序JIT 输出分两类已发布产品Release 构建即可用的输出以及仅在 Debug 或 Checked 构建的运行时中才存在的输出。开始前先准备一个待反汇编的 .NET 控制台应用如果还没有一个安装 .NET SDK在目标目录执行dotnet new console创建项目编辑Program.cs把想反汇编的方法放在Main直接或间接调用到的路径上。官方文档给出的示例方法InefficientJoin有意使用字符串拼接低效地拼接参数列表并加上[MethodImpl(MethodImplOptions.NoInlining)]防止它与Main的其余代码被内联混在一起从而保证反汇编输出聚焦于目标方法本身using System; using System.Collections.Generic; using System.Runtime.CompilerServices; namespace ConsoleApplication { public class Program { public static void Main(string[] args) { Console.WriteLine(InefficientJoin(args)); } // Add NoInlining to prevent this from getting // mixed up with the rest of the code in Main [MethodImpl(MethodImplOptions.NoInlining)] private static string InefficientJoin(IEnumerablestring args) { var result string.Empty; foreach (var arg in args) result (arg ); return result.Substring(0, Math.Max(0, result.Length - 1)); } } }设置好下文介绍的环境变量后运行应用例如C:\testset DOTNET_JitDisasmInefficientJoin C:\testdotnet run -c Release ; Assembly listing for method ConsoleApplication.Program:InefficientJoin(System.Collections.Generic.IEnumerable1[System.String]):System.String (Instrumented Tier0) ; Emitting BLENDED_CODE for X64 with AVX512 - Windows ; Instrumented Tier0 code ; rbp based frame ; fully interruptible G_M000_IG01: ;; offset0x0000 push rbp sub rsp, 208 lea rbp, [rsp0xD0] xor eax, eax mov qword ptr [rbp-0x98], rax vxorps xmm4, xmm4, xmm4 ... G_M000_IG02: ;; offset0x0048 mov rcx, 0x22680000008 mov gword ptr [rbp-0x40], rcx ... call [r11]System.Collections.Generic.IEnumerable1[System.__Canon]:GetEnumerator():System.Collections.Generic.IEnumerator1[System.__Canon]:this ...输出头部的注释行;开头携带了大量有用信息方法完整签名、(Instrumented Tier0)表示这是带插桩的分层编译 Tier0 代码、Emitting BLENDED_CODE for X64 with AVX512 - Windows说明目标架构与指令集能力、rbp based frame与fully interruptible描述帧布局与可中断性。每个指令块以G_M000_IGxx标签开头并附带十六进制偏移offset0x...。关于dotnet run的注意事项dotnet run会拉起大量代码包括 MSBuild、NuGet、Roslyn 编译器等等而你设置的环境变量对它们全部生效。因此更推荐的做法是先用dotnet build或dotnet publish构建好应用再设置配置变量、最后运行产物避免无关进程的编译输出干扰结果。构建 Debug/Checked 版运行时如果你需要访问只在 Debug/Checked 构建中可用的配置变量例如下一节会介绍的DOTNET_JitDisasmWithGC就需要自行构建 runtime 仓库构建步骤见 docs/workflow/README.md。构建完成后可以按 Dogfooding 每日构建 的方式安装一个 dogfood 版 .NET 运行时也可以只使用构建产物中的corerun工具直接运行测试。推荐的工作流是安装一个 dogfood 构建然后同时构建 Debug 和 Release 两种版本的仓库——除 JIT 之外的运行时部分全部使用 Release 版只把 JITclrjit替换为你本地构建的 Debug 版。具体步骤按上文用dotnet new console创建测试程序修改csproj在RuntimeIdentifier标签中指定与当前系统对应的 RIDruntime ID。例如 Windows x64 机器上Project SdkMicrosoft.NET.Sdk PropertyGroup OutputTypeExe/OutputType TargetFrameworknet9.0/TargetFramework RuntimeIdentifierwin-x64/RuntimeIdentifier /PropertyGroup /Project完整 RID 与操作系统的对应关系可查阅 .NET 官方 RID 目录。编辑完代码后执行dotnet restore和dotnet publish -c Release所有运行所需二进制会落在bin/Release/tfm/rid/publish目录用本地构建的 CLR dll 覆盖发布产物中的运行时 dll。命令行方式如下# Windows robocopy /e runtime-repo path\artifacts\bin\coreclr\windows.arch.Release app root\bin\Release\tfm\rid\publish NUL copy /y runtime-repo path\artifacts\bin\coreclr\windows.arch.Debug\clrjit.dll app root\bin\Release\tfm\rid\publish NUL # Unix cp -rT runtime-repo path/artifacts/bin/coreclr/OS.arch.Release app root/bin/Release/tfm/rid/publish cp runtime-repo path/artifacts/bin/coreclr/OS.arch.Debug/libclrjit.so app root/bin/Release/tfm/rid/publish注意两个构建目录的差异OS.arch.Release目录整体复制提供 Release 运行时再单独把OS.arch.Debug目录下的clrjit.dllWindows/libclrjit.soUnix拷贝进去即只替换 JIT 本体。二、JIT 配置变量总览声明位置、类型与设置方式JIT 的行为由一系列配置变量configuration variables控制。从源码结构看这些变量集中声明在两个头文件中两者的分工与文档描述一致src/coreclr/jit/jitconfigvalues.h —— JIT 自身读取和处理的配置变量如 JitLateDisasm 的声明L268-L270、JitDisasm 一族L336-L366、JitTimeLogCsv / JitTimeLogFileL653-L656、JitRawHexCode 一族L892-L893src/coreclr/inc/clrconfigvalues.h —— 部分变量由VM 而非 JIT读取处理例如 JitName / JitPath / AltJit / AltJitNameL287-L292其中AltJit用于指定替代 JIT 编译器后文 Late Disassembler 章节会用到。配置字符串名通常带DOTNET_前缀一般通过环境变量DOTNET_name设置。例如让所有名为Main的方法输出反汇编# Windows set DOTNET_JitDisasmMain # Powershell $env:DOTNET_JitDisasmMain # Unix export DOTNET_JitDisasmMain配置变量只有三种取值类型类型说明示例字符串string文件路径、汇编名列表等DOTNET_JitStdOutFiledisasm.txt整数integer常为 0/1 开关。注意整数按十六进制解析DOTNET_JitDisasmSummary1方法列表method list空格分隔的方法名模式集合DOTNET_JitDisasmMain Test1 Test2变量未显式指定时使用默认值通常默认值语义为关闭/禁用/false取方法列表的变量默认为不指定任何方法。跨编译器ReadyToRun / NativeAOT的传参方式不同向 crossgen2ReadyToRun或 ilcNativeAOT传递 JIT 配置变量不能通过环境变量必须在命令行用--codegenopt开关传入配置名不带DOTNET_前缀详见 debugging-aot-compilers.md。SuperPMI 的传参方式JIT 开发者使用 superpmi.exe 时通过-jitoption/-jit2option开关向 JIT 传配置变量向 superpmi.py 则使用-jitoption/-base_jit_option/-diff_jit_option开关两种情况下变量名同样不带DOTNET_前缀。从源码看这些配置变量在编译入口处的实际判断逻辑见 compiler.cpp 中对 JitDisasm / JitLateDisasm 的检查L2573-L2607编译每个方法时JIT 用JitConfig.JitDisasm().contains(...)/JitConfig.JitLateDisasm().contains(...)判断当前方法是否命中方法列表命中即打开对应输出。方法名匹配规则method list 详解DOTNET_JitDisasm等变量接受空格分隔的方法名模式列表匹配规则取决于模式中的字符最简单的是仅写方法名不含类名例如Main也可以列出多个简单方法名例如Main Test1 Test2若模式含!字符被匹配的字符串会加上程序集名 感叹号前缀。例testassembly!*指定名为testassembly的程序集中的所有方法若模式含:字符被匹配的字符串会加上类名 冒号前缀。例TestClass:Main指定类TestClass中名为Main的单个方法若模式含(字符被匹配的字符串会追加方法签名参数类型后缀若类名部分冒号之前含[则匹配内容包含该类的泛型实例化若方法名部分冒号与左括号之间含[则匹配内容包含该方法的泛型实例化模式可使用两个通配符*匹配任意字符和?匹配任意单个字符。最常用、最简单的模式就是*含义是所有方法例如DOTNET_JitDisasm*TestClass:*匹配TestClass中所有方法*Main*匹配名字中含Main子串的所有方法包括恰好叫Main的方法。关键结论匹配所针对的字符串格式为[ClassName[Instantiation]:]MethodName[Instantiation][(types)]该格式与 JIT 打印方法签名的格式完全一致所以你可以把反汇编输出中打印的签名直接复制粘贴进环境变量。以文档中的泛型示例为例namespace MyNamespace { public class CT1, T2 { [MethodImpl(MethodImplOptions.NoInlining)] public void MT3, T4(T1 arg1, T2 arg2, T3 arg3, T4 arg4) { } } } new Csbyte, string().Mint, object(default, default, default, default); // compilation 1 new Cint, int().Mint, int(default, default, default, default); // compilation 2DOTNET_JitDisasmSummary会打印出这两个实例化的完整名字MyNamespace.C2[byte,System.__Canon]:Mint,System.__Canon MyNamespace.C2[int,int]:Mint,int注意C2这个形式含反引号与数字后缀是 Roslyn 写入元数据的类型名并非 RyuJIT 添加的。对于 PowerShell 用户要额外小心反引号是转义字符需要写双反引号转义。命中两个编译的模式有M *C2:M *C2[*]:M[*](https://link.gitcode.com/i/d278ffbe2e24dcd69b48ef1386d128b7) MyNamespace.C2:M仅命中第一个编译的模式有M[int,*Canon] MyNamespace.C2[byte,*]:M M(*Canon)三、反汇编输出Release 版可用变量以下配置变量在发布版产品与内部 Debug/Checked 构建中均可用对应 jitconfigvalues.h 中的 RELEASE_CONFIG 声明L336-L366变量取值作用DOTNET_JitDisasmSummary1 或 0设为 1 时打印所有被 JIT 编译的函数列表DOTNET_JitDisasmmethod-list输出指定函数的反汇编如DOTNET_JitDisasmMain、DOTNET_JitDisasmMain Test1 Test2、DOTNET_JitDisasm*全部函数DOTNET_JitDisasmDiffable1 或 0设为 1 使生成的代码可 diff把输出中的指针值替换为相同的、固定的众所周知值使不同运行间的输出可逐字比较DOTNET_JitDisasmWithAlignmentBoundaries1 或 0设为 1 时在生成代码中显示对齐边界DOTNET_JitDisasmOnlyOptimized1 或 0设为 1 时隐藏未优化代码的反汇编DOTNET_JitDisasmWithCodeBytes1 或 0设为 1 时除文本反汇编外还显示实际的机器码字节与DOTNET_JitDisasmDiffable1冲突勿同时使用DOTNET_JitStdOutFile文件名未设置时所有 JIT 输出走标准输出设置后 JIT 输出写入该文件仅 Debug/Checked 构建可用的反汇编选项变量取值作用DOTNET_JitDisasmWithGC1 或 0设为 1 时把 GC 信息GC 信息块交错显示在文本反汇编中DOTNET_JitDisasmWithDebugInfo1 或 0设为 1 时把调试信息变量活跃区间 live ranges交错显示在文本反汇编中DOTNET_JitDisasmAssemblies以分号分隔的程序集名列表限制JitDisasm及其他 JIT 输出只作用于列表中的程序集内的函数如DOTNET_JitDisasmAssembliesMyAssembly1;MyAssembly2此外源码中还有文档未展开提及的相关开关可从 jitconfigvalues.hL355-L366 确认JitDisasmSpilled发生寄存器溢出时显示原生代码与JitDisasmWithAddress在反汇编每条指令旁打印进程地址。多线程应用的注意事项JIT 输出在多线程并发编译时不做同步。也就是说若设置DOTNET_JitDisasm*且多个函数正在多个线程上同时编译输出会相互交错大概率完全无法阅读。规避手段有三尽可能让测试用例以单线程运行使用DOTNET_JitStdOutFile把输出写入文件而非标准输出避免与程序自身输出交错把方法列表缩小到只针对一个函数。始终可用的计时日志选项变量作用DOTNET_JitTimeLogFile指定写入各方法计时详细信息的日志文件DOTNET_JitTimeLogCsv指定写入 CSV 格式汇总计时信息的日志文件四、Late Disassembler对最终机器码字节的二次反汇编DOTNET_JitDisasm显示的反汇编是基于JIT 内部表示IR/emit 阶段记录打印的。Late Disassembler晚期反汇编器则是另一个反汇编器它反汇编的是最终生成的代码字节通过 coredistools 包来解释这些字节。这就提供了一种验证手段核对 JIT 内部表示产生的反汇编是否与第三方反汇编器对同一组代码字节的反汇编结果一致。Late Disassembler 目前仅在 Debug/Checked 构建中可用——jit.hL326 的注释亦说明它如需也可以在非 DEBUG 构建中编入。补充背景以当前仓库为准coredistools 当前版本为 1.6.0基于 LLVM 20.1.0源码位于 dotnet 组织的 jitutils 仓库。调用方式与输出目的地优先级DOTNET_JitLateDisasm{method-list} 输出指定函数的晚期反汇编如 DOTNET_JitLateDisasmMain、DOTNET_JitLateDisasm* DOTNET_JitLateDisasmTo{file name} 可选指定晚期反汇编写入的文件晚期反汇编输出发送到下列第一个命中的位置DOTNET_JitLateDisasmTo指定的文件若已设置DOTNET_JitStdOutFile指定的文件若已设置标准输出。从源码看这条优先级链与实现严格对应disasm.cpp 的 disAsmCodeL1493-L1505 中先在 DEBUG 构建下读取JitConfig.JitLateDisasmTo()fopen_utf8(fileName, a)打开失败或未设置时回退到jitstdout()注释还指出该输出写入共享文件、因此不是可重入的且非 DEBUG 构建下disDiffable恒为 true总是 diffable 输出。而doLateDisasm标志在 compiler.cppL2542、L2605-L2607 中依据方法是否命中JitLateDisasm列表被置位。用 Late Disassembler 配合 emitter 单元测试Late Disassembler 的一个典型用途当你向 JIT emitter添加新指令时前提 coredistools 已认识这些新指令可以为每条新指令编写单元测试——把测试加到CodeGen::genEmitterUnitTests()或它调用的函数中。然后写一个带Main函数的 Hello World 程序并设置DOTNET_JitDisasmMain DOTNET_JitLateDisasmMain DOTNET_JitLateDisasmTolatedis.asm 可选把 JitLateDisasm 输出写到单独文件 DOTNET_JitEmitUnitTestsMain DOTNET_JitEmitUnitTestsSectionssve设置这些环境变量后运行程序Main函数会被填入大量额外的单元测试指令并由两套途径输出反汇编——内置的JitDisasm反汇编打印器与 coredistools 的JitLateDisasm反汇编器——对比二者即可验证 emitter 输出与字节解释是否一致。还可以追加DOTNET_JitRawHexCodeMain DOTNET_JitRawHexCodeFilerawhex.txt 可选否则输出到 JitStdOutFile 位置或标准输出然后使用能读取所写文本十六进制码字节如F3E4C10F的外部反汇编器例如 capstone将其反汇编为文本形式与JitDisasm和/或JitLateDisasm的输出交叉比对。用 SuperPMI 跑 emitter 单元测试使用单元测试时哪个函数被编译其实无关紧要因此可以选或新建一个 SuperPMI MCH 文件然后只带单元测试编译单个函数例如superpmi.exe -c 1 -target arm64 -jitoption JitDisasm* -jitoption JitLateDisasm* -jitoption JitEmitUnitTests* -jitoption JitEmitUnitTestsSectionssve benchmarks.run.windows.arm64.checked.mch这对使用交叉编译器时很有用另一条途径是使用 clrconfigvalues.h 中声明的L290-L292DOTNET_AltJit/DOTNET_AltJitName配置变量让替代 JIT交叉编译器来生成反汇编。五、JIT DumpJIT 开发者的内部数据转储JIT dump 是对 JIT 内部数据结构以及编译过程中动作的详细verbose展示供 JIT 开发者使用仅在 JIT 的 Debug/Checked 构建中可用。如何分析这份输出参见 ryujit-overview.md 的 Reading a JitDump 一节。控制 JIT dump 输出的变量声明见 jitconfigvalues.h 的 JitDump 区块L241-L277变量作用DOTNET_JitDumpmethod-list为指定函数开启 JIT dumpDOTNET_JitDumpASCII1 或 0指定 JIT dump 是否仅用 ASCII 字符默认 1关闭后可得到可读性更好的表达式树DOTNET_JitGCDumpmethod-list转储 GC 信息DOTNET_JitUnwindDumpmethod-list转储 unwind 表栈展开信息DOTNET_JitEHDumpmethod-list转储异常处理表同一区块中还有不少与 JitDump 配套的调参开关可供 JIT 开发者深挖例如JitDumpTreeIDsdump 中打印树 ID默认 1、JitDumpBeforeAfterMorph显示 morphing 前后的每棵树、JitHashDump按方法哈希等价触发 JitDump、JitDumpToDebugger把 dump 输出到调试器以及JitDumpFg一族用于导出指定方法的 XML/DOT 流程图含JitDumpFgPhase、JitDumpFgLoops、JitDumpFgEH等细粒度选项。六、反汇编 Native Imagecrossgen2 / ReadyToRun如果你跟着上面的教程运行了示例应用可能会疑惑为什么Substring这类方法的反汇编没有出现在输出里原因是Substring位于 System.Private.CoreLib.dll而该程序集默认已经通过 crossgen2 做了 AOT 预编译ReadyToRun 原生映像运行期 JIT 不再编译它。让 crossgen2 转储信息的做法略有不同——配置必须在命令行指定用--codegenopt参数、且配置名不带DOTNET_前缀。更多细节参见 debugging-aot-compilers.md。七、补充Visual Studio Disasmo 插件如果不习惯纯命令行查看 JIT 生成的反汇编和其他输出的一个简便途径是在 Visual Studio 中使用第三方Disasmo插件它可以配合运行中的进程呈现 JIT 反汇编视图免去手工设置环境变量与解析标准输出的麻烦。八、速查本文涉及的关键文件文件作用src/coreclr/jit/jitconfigvalues.hJIT 侧全部配置变量声明JitDisasm / JitLateDisasm / JitDump / 计时日志等src/coreclr/inc/clrconfigvalues.hVM 侧读取的 JIT 相关配置JitName / JitPath / AltJit 等src/coreclr/jit/disasm.cpp反汇编器实现含 Late DisassemblerdisAsmCode等src/coreclr/jit/compiler.cpp编译入口依据方法列表判定是否开启 JitDisasm / JitLateDisasmdocs/workflow/debugging/coreclr/debugging-aot-compilers.mdcrossgen2 / ilc 的--codegenopt调试方法docs/design/coreclr/jit/ryujit-overview.mdRyuJIT 架构总览含 Reading a JitDump 分析指南适用前提小结DOTNET_JitDisasm、DOTNET_JitDisasmSummary、DOTNET_JitDisasmDiffable、DOTNET_JitDisasmWithAlignmentBoundaries、DOTNET_JitDisasmOnlyOptimized、DOTNET_JitDisasmWithCodeBytes、DOTNET_JitStdOutFile、DOTNET_JitTimeLogFile、DOTNET_JitTimeLogCsv在已发布的 .NET 中即可使用而DOTNET_JitDisasmWithGC、DOTNET_JitDisasmWithDebugInfo、DOTNET_JitDisasmAssemblies、DOTNET_JitLateDisasm、DOTNET_JitLateDisasmTo、DOTNET_JitEmitUnitTests、DOTNET_JitDump、DOTNET_JitGCDump、DOTNET_JitUnwindDump、DOTNET_JitEHDump均要求 Debug/Checked 构建的 JIT对 crossgen2 / ilc 的 ReadyToRun 与 NativeAOT 场景则一律改用命令行--codegenopt传参。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价