让你的JIT编译代码被系统感知:方法一览
作者:Timo Paulssen | 2026年3月10日 | 阅读时间约12分钟
原文标题:Making your JITted Code known: Let me count the ways
AOT编译器与元数据
长期以来,编译器产生的机器码都会附带元数据,供目标系统的各个部分使用——这是再正常不过的事情。这些元数据包括函数的符号名称、带有行号标注的调试数据,以及异常处理表(exception handling tables)。
对于像 C++ 这样支持异常(exceptions)的语言,编译器中存在专门的表,记录了运行时所需的全部信息:当异常被抛出时,栈上的每一帧(frame)应当如何处理。这一过程被称为"栈展开"(stack unwinding)——即移除栈帧,就好像代码已经从这些函数中返回了一样。
MoarVM 与 longjmp 问题
MoarVM 是 Raku 编程语言的运行时,使用 C 语言编写。它采用 longjmp 来实现异常处理。当一个操作码(opcode)遇到问题时,它会调用 MVM_exception_throw_adhoc,从 C 栈的任意深度直接跳回解释器循环(interpreter loop)。
Windows 上 MSVC 的一个变动导致 longjmp 总是逐帧完全展开整个栈。对于纯 C 代码来说,这通常是不必要的,因为 C 本身没有每函数级(per-function)的清理操作。但问题在于,MoarVM 拥有一个 JIT 编译器。在 longjmp 发生时,JIT 编译的代码可能已经在栈上留有帧,而 JIT 编译器并没有提供任何数据让 Microsoft 的 longjmp 正确处理这些帧,从而导致崩溃。
修复方案涉及为 JIT 编译的代码生成展开信息(unwind information),并在 Windows 上通过 RtlAddFunctionTable 注册这些信息——或者采用 Patrick Böker 发现的更简便方法。
让 JIT 编译代码被系统感知的几种方法
又快又简单:Perf Map
最简单的方法。编写一个文本文件,每行对应一个 JIT 函数:地址、长度、名称。MoarVM 从 2018 年 9 月起就支持此功能。它可以与 Linux 的 perf 工具配合使用,但 perf 无法显示这些函数的汇编代码。
详尽完备:完整的可加载共享对象
创建一个完整的 ELF/DLL/dylib 文件,写入磁盘,然后通过 dlopen 加载。这种方法可以与所有外部工具配合使用,但需要保留文件在磁盘上,并且需要手写正确的 ELF 结构。
RtlAddFunctionTable 与 _U_dyn_register
RtlAddFunctionTable(Windows)和 libunwind 的 _U_dyn_register 允许你提供展开信息(unwind info)。它们可以为函数赋予名称并提供展开指令,但并非总能完美运行。
JITDUMP 格式
专为 Linux 的 perf 工具设计。提供函数名称、展开表(unwind table)以及行号标注。需要额外进行后处理步骤。
GDB 的 JIT 调试信息读取器
允许你向 GDB 提供任意信息,但操作非常繁琐,全部需要手动完成。
为什么这很重要
当 JIT 编译器在运行时生成机器码时,操作系统和调试器并不会自动知道这些代码的存在。如果没有正确的注册机制,栈展开(stack unwinding)将会失败,性能分析工具(profiler)无法将 CPU 时间归因到特定的 JIT 函数,调试器无法设置断点,崩溃报告中也只会显示无意义的地址而非函数名称。
对于任何带有 JIT 编译器的语言运行时来说,向系统注册 JIT 代码对于可调试性(debuggability)、性能分析(profiling)和可靠性(reliability)都是至关重要的。具体选择哪种方法,取决于目标平台、工具链需求以及性能约束。
原文出处:https://wakelift.de/2026/03/09/making-your-jitted-code-known-let-me-count-the-ways/