GSOC 2025: How To Support jump_label For OpenRISC
jump_label是Linux内核中的一种机制,用于优化频繁切换的分支代码(如 if-else、switch 等),特别是在动态启停的调试或性能监控场景中。其核心思想是通过运行时动态修改代码,将条件分支转换为无条件跳转或直接空操作(NOP),从而减少分支预测开销,提升性能(By DeepSeek)。
在过去的几周内,我通过学习jump_label的各种博客,源码剖析以及对应的内核文档。了解到移植并实现jump_label需要的步骤。
- Linux Kernel Documentation about static_key
- 【Linux内核|代码技巧】ARM64 jump label源码分析
- Linux:Jump label 实现简析 - JiMoKuangXiangQu
- Linux内核jump label与static key的原理与示例
- static key & jump label | 属乌鸦的
在内核的文档中,有关于最为基础的jump_label实现顺序:

HAVE_ARCH_JUMP_LABEL是内核中决定是否开启static_key的第一步,这指出当前硬件架构是否实现了jump_label所需的底层支持。而JUMP_LABEL_NOP_SIZE则是为了确保后续跳转指令和NOP指令替换时,指令大小一致(如果不一致,则会导致指令撕裂等问题)。
可能单单讲解概念会比较模糊,我们接下来先看看对应源码的剖析进而来分析如何移植OpenRISC的jump_label实现。
JumpLabel Source Code For RISC-V
要了解内核中jump_label的整体运行流程,我们就需要找到jump_label的运行入口(jump_label_init)。但这在之前,我们还需要了解到一个概念,jump_label实际上是一张表格,为了实现快速跳转,内核将每一个跳转点(也就是static_key)的信息都记录在__jump_table中,分别由__start___jump_table和__stop___jump_table控制起始和结尾地址。
1 |
|
上面的代码是内核对内存布局的指示,其中就包含了jump_table的布局信息。我们可以看到,jump_table以八字节对齐在内存中,并且其中有一个关键信息RO_AFTER_INIT_DATA。这一点是在Patch V2中和Shorne一起检查出的问题。
了解到这一点后(__jump_table),我们现在就可以开始从jump_label_init开始分析了。
JumpLabel Init Stage
在jump_label_init函数中,我们忽视掉其他无关代码(并非说它们不重要,而是去除掉这些代码并不会影响理解)。
1 | void __init jump_label_init(void) |
如上,这就是jump_label_init的核心代码部分。由于jump_label的功能是通过注入汇编实现的,因此在编译阶段jump_label的信息便已经写入到__jump_table中。这里便涉及到上面内核文档中所提到的arch_static_branch和arch_static_branch_jump的实现。(此时,jump_entry的信息都是乱序的)。
对于一个static_key来说,每一个static_key都不止对应了一个jump_entry因此为了防止重复处理以及性能考虑,内核首先需要对jump_entry进行排序(通常是按照jump_entry指向static_key的地址按升序排列),然后才能开始下一步操作。
然后需要对该jump_entry的地址进行检查是否位于.init段内,然后通过jump_entry_set_init对jump_entry->key的倒数第二比特进行配置。
1 | static inline void jump_entry_set_init(struct jump_entry *entry, bool set) |
最后通过static_key_set_entries来确保static_key对应的一组jump_entry被关联到一起。如果想要启用jump_label的功能,则需要通过传入参数或其他方式,触发jump_label_update函数,而update函数则会触发上述文档中关于arch_jump_label_transform的实现。
至此,简单的jump_label的原理便已经阐述完毕。接下来开始陈述我的工作以及对应处理。
Implement JumpLabel For Or1k
PATCH Draft
2025年5月25日凌晨,我对Shorne发送了一个名为《[PATCH] openrisc: tracing: Support the jump_label draft》的邮件。

在这一份dratf patch中,我针对于OpenRISC架构的jump_label进行移植和实现,但始终卡在了虚拟机启动界面。其中,一开始我一直认为是text_patch的问题,因为当时我的patch看上去只有text_patch没有实现(因为OpenRISC目前确实还没有text_patch feature)。因此我还有了以下的一个提问:
《Need help implementing text_patch for JUMP_LABEL》
但在随后与Shorne的讨论中,发现这并不是一个重点(或许说,这不是一个关键因素)。
Shorne: Note I was reading other jump_label implementation, the text patching feature seems options. but it would be good to implement a clean text patching API
Shorne: But not required
2025年5月25日下午,Shorne对我的draft patch进行阅读,然后给出了许多提问:
1 | > --- a/arch/openrisc/Kconfig |
这里是关于HAVE_ARCH_JUMP_LABEL和CONFIG_JUMP_LABEL的添加位置的提问,Shorne希望我与其他内核风格保持一直,因此我立刻修改了。
1 | > +#define WASM(inst) "l." #inst |
针对于这个问题,实际上是我参考了arm的风格所导致的,因为arm有如下代码:
1 |
但仔细想想之后,直接硬编码进去可能更好一些。所以这一点也进行了修改。
1 | > + offset = jump_entry_target(entry) - jump_entry_code(entry); |
这里需要进行详细解释了。在一开始,我参考了arm架构的jump_label的移植实现。而arm架构的b.w的格式如下:

在b.w中,arm提供了24位的立即数,也就是对应了b.w label。而对于arm架构的实际立即数计算为:1 << 24 << 2。然后换算为有符号数即为:-33554432 ~ 33554428。因此,我在draft patch中直接沿用了这一数据(因为OpenRISC的l.j的立即数是26位的,和1 << 24 << 2相同)。
但是我忽略了一点,arm之所以后面需要<< 2,是因为需要扩展对齐;在OpenRISC中也是如此:

The immediate value is shifted left two bits, sign-extended to program counter width, and
then added to the address of the jump instruction. The result is the effective address of the
jump.
因此,实际上的OpenRISC对应的数值应该按照26比特再向左位移两个比特的有符号扩展进行计算:
1 | > + /* |
而最后一个问题,Shorne指出:HAVE_ARCH_JUMP_LABEL_RELATIVE也应被实现。
我查阅了主流架构(x86、arm64、RISC-V),发现它们确实都实现了HAVE_ARCH_JUMP_LABEL_RELATIVE这一个feature。因此,我在后续的patch中进行了添加。
HAVE_ARCH_JUMP_LABEL_RELATIVE会影响两个地方:
- struct jump_label
- arch_static_branch和arch_static_branch_jump
1 | > SHould this be added last? Maybe its better to have in in the same location of |
到现在,OpenRISC的jump_label的实现就已经有了大致雏形了。如下是无法开机的界面:

PATCH V1
2025年6月6日,自draft patch的完成已经过去快两周,因为毕业答辩和毕业论文的事情,因此之前都没有进展。当天我突然发现一个现象:

当我把这一段代码的jump_entry写入的逻辑注释掉后,我发现内核可以正常启动。我立即将这一现象告诉Shorne,但Shorne问我: “Do you understand this section of code and what it does?“
当时我回答了正确了,但是后面回来一想,确实我当时对这一段代码的理解不够深入。还记得HAVE_ARCH_JUMP_LABEL_RELATIVE这个flag吗?HAVE_ARCH_JUMP_LABEL_RELATIVE在这里就起了作用:
1 | ".long 1b - ., " label " - . \n\t" |
如果没有HAVE_ARCH_JUMP_LABEL_RELATIVE,那么实际上就应该如同arm架构一样的编写格式:
1 | ".word 1b, " label ", " key "\n\t" |
也就是说,HAVE_ARCH_JUMP_LABEL_RELATIVE实际上决定了jump_label的所有字段是保存偏移量的,如果没有这个flag,则就会类似于arm这样保存绝对地址。
然后我就卡在这里了,过了几天后,Shorne问我:”do you need my help to get it working?“
YES, I NEED. 然后我就把第一版也就是PATCH V1发送给了Shorne。
PATCH V2
两天后,2025年6月13日,我和Shorne开始一起调试。一开始,Shorne提供了一个关键信息:
1 | [ 0.000000] jump_label_cmp: a: c0988d24 vs b: c0988d30 keya: c09cc614 vs keyb: c09cc634 |
貌似之前的崩溃是因为jump_label_init在排序时出了问题。然后Shorne指出: “Maybe this is the issue, the jump_table is stored in the .rodata section which is read only data.”
1 | c00f6de8 <jump_label_swap>: |
在这一点上,Shorne和我达成了一致:一定是因为jump_label_init在排序时,对.rodata的数据进行了修改。但是其他架构的排序也是发生在.rodata内,因此肯定有一种机制使得.rodata当时是允许修改的。
在和Shorne的交流中,他的一句话引起了我的注意:”but according the the failure it is not, we should if the page table is marking it as read only during setup“。确实,.rodata按理来说是无法被修改的,但是如果在启动时,.rodata还未被标记为只读状态,那么是否这时候的.rodata是能够被修改的呢?
答案是肯定的,接近二十分钟后,我找到了答案。我惊喜的和Shorne说,”wow, look this! linux kernel arch riscv setup.c#L312“。在RISC-V架构中,setup_arch函数(每一个架构都会进行实现)中在最开始就会调用一个setup_initial_init_mm和paging_init函数。当我转过头去查看OpenRISC的对应函数时,发现了惊喜。
在OpenRISC的paging_init中有这样的一个调用:
1 | extern const char _s_kernel_ro[], _e_kernel_ro[]; |
当我将jump_label_init在paging_init之前进行调用时,就能够发现原本不会输出任何信息的终端开始有日志输出了。
不过当时,Shorne和我说,”I don’t think so, did you see the last message I pasted about mm being null, causing the failure? it was not the read only issue.“。然后转而对dtlb_miss_handler和itlb_miss_handler进行研究。
但随后,Shorne同意了我的结果:
1 | > readelf -s vmlinux | grep -e _kernel_ro -e jump_table |
在这里就可以清晰的看到,_s_kernel_ro和_e_kernel_ro被写入到.rodata中了。但是我们发现,经过这样处理后还是会出现失败的情况,然后我对jump_label_init进行了分析,增加了一条测试代码:
1 | + printk("static_key_initialized: %d", static_key_initialized); |
最后在日志中可以发现,jump_label_init实际上会调用两次,第一次在setup_arch中(位于paging_init之前)被调用,在这次调用的时候,__jump_table的内容是可写的,因此成功解决了jump_label_sort_entries的崩溃问题。需要注意,第一次运行时,如果成功了会将static_key_initialized设置为1。因此在第二次执行时,这里就直接返回,不再进行初始化。因此崩溃的实际上在另外的地方。
然后,Shorne对控制台的权限进行了获取,这样能够看到更多的日志输出:
1 | 0xefc68a77: "" |
事实证明了,jump_label_init成功,这里是位于arch_jump_label_transform_queue出现了问题。当时我始终认为text_patch是必须的,因此在这里加了一个日志,如果运行到这里就会输出entry wrong place。
“because missing the text_patch to flush?“,我对Shorne说,然后Shorne对这块进行了补全,并且验证了这个PATCH V2确实可行。
最终,Shorne通过参考RISC-V架构的text_patch,发现RISC-V会通过patch_map对.rodata的数据进行映射,这样就不会引发paging_init后的jump_label_update的非法地址访问。
1 | waddr = patch_map(addr, FIX_TEXT_POKE0); |
然后,Shorne发送了PATCH V2给我。
1 | Initial support for OpenRISC jumplabel support. Currently causes |

PATCH V3
当天深夜,Shorne突然和我说,
“I think I figured out what the failure is., it boots now. You made a basic mistake, sending patch.“
我当时十分震惊,心想是什么错误。然后Shorne说,
“You were missing l.nop in the branch delay slots“。
是的,OpenRISC需要延迟槽。这里简单解释下延迟槽:
延迟槽(Delay Slot)是RISC(精简指令集计算机)架构(如MIPS、SPARC)中的一种特殊设计,用于优化流水线执行效率。它的核心思想是:在分支指令(如跳转、调用)生效之前,允许执行紧随其后的下一条指令,从而减少流水线停顿(Pipeline Stall)—— by dpsk(deepseek)
1 | j target # jump to target |
解决完这个问题后,Shorne给我发送了PATCH V3,应该说,这就是我的jump_label first PATCH。

当然,这个PATCH V3并没有那么完美,还有很多事情需要补充。但是,终于可以开机了!

PATCH MORE
TO BE DONE…