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…