Linux 内核 jump label 与 static key 的原理与示例
↓推荐关注↓
jump label机制进入Linux内核已经很多很多年了,它的目的是 消除分支。 为了达到这个目的,jump label的手段是 修改分支处的代码。
~把代码当做数据,代码和数据在冯诺伊曼计算机中得到了统一~
本质上,jump label作用于下面的逻辑:
var = false;
...
if var
do_true
else
do_false
静态拆分成了下面的两个逻辑,其一是:
jmp l_true
do_false
ret
l_true:
do_true
或者,其二是:
nop
do_false
ret
l_true:
do_true
但二者不能同时共存。显然,这破坏了通用性和灵活性,带来了高效! 这相当于一个硬熔断
本文来一点可以看得见的东西,演示一下真实的jump label & static key。
先看下面的C代码:
#include <stdio.h>
int main(int argc, char **argv)
{
int E1, E2;
E1 = atoi(argv[1]);
E2 = atoi(argv[2]);
if (E1) {
printf("condition 1 is true\n");
} else {
printf("condition 1 is false\n");
}
if (E2) {
printf("condition 2 is true\n");
} else {
printf("condition 2 is false\n");
}
return 0;
}
很简单的代码,也很正确。然而, 如果main函数是一个高频调用的函数,并且在E1,E2是不随着代码逻辑而发生变化,仅仅参数设定的情况下, 那么if语句尽量消除以消除不必要的分支预测,而这正是jump label的用武之地!
我们下面用jump label机制来重写上面的代码,请看:
// jump_label_demo.c
// gcc -DJUMP_LABEL -O jump_label_demo.c -o demo -g
#include <stdio.h>
#include <sys/mman.h>
#ifdef JUMP_LABEL
struct entry {
unsigned long code;
unsigned long target;
unsigned long key;
};
#define MAX 2
struct entry base __attribute__ ((section ("__jump_table"))) = {0};
void update_branch(int key)
{
int i;
char *page;
struct entry *e = (struct entry *)((char *)&base - MAX*sizeof(struct entry));
for (i = 0; i < MAX; i++) {
e = e + i;
if (e->key == key) {
// 修改代码段
unsigned int *code = (int *)((char *)e->code + 1);
unsigned int offset = (unsigned int)(e->target - e->code - 5);
page = (char *)((unsigned long)code & 0xffffffffffff1000);
mprotect((void *)page, 4096, PROT_WRITE|PROT_READ|PROT_EXEC);
*code = offset;
mprotect((void *)page, 4096, PROT_READ|PROT_EXEC);
break;
}
}
}
#define STATIC_KEY_INITIAL_NOP ".byte 0xe9 \n\t .long 0\n\t"
static __attribute__((always_inline)) inline static_branch_true(int enty)
{
int ent = enty;
asm goto ("1:"
STATIC_KEY_INITIAL_NOP
".pushsection __jump_table, \"aw\" \n\t"
// 定义三元组{本函数内联后标号1的地址,本函数内联后标号l_yes的地址,参数enty}
".quad 1b, %l[l_yes], %c0\n\t"
".popsection \n\t"
:
: "i"(ent)
:
: l_yes);
return 0;
l_yes:
return 1;
}
#endif
int main(int argc, char **argv)
{
int E1, E2;
E1 = atoi(argv[1]);
E2 = atoi(argv[2]);
#ifdef JUMP_LABEL
int e1 = 0x11223344;
int e2 = 0xaabbccdd;
printf("Just Jump label\n");
if (E1) {
update_branch(e1);
}
if (E2) {
update_branch(e2);
}
#endif
#ifdef JUMP_LABEL
if (static_branch_true(e1)) {
#else
if (E1) {
#endif
printf("condition 1 is true\n");
} else {
printf("condition 1 is false\n");
}
#ifdef JUMP_LABEL
if (static_branch_true(e2)) {
#else
if (E2) {
#endif
printf("condition 2 is true\n");
} else {
printf("condition 2 is false\n");
}
return 0;
}
定义JUMP_LABEL宏编译之,看看效果:
[root@localhost checker]# gcc -DJUMP_LABEL -O jump_label_demo.c -o demo -g
[root@localhost checker]# ./demo 1 0
Just Jump label
condition 1 is true
condition 2 is false
[root@localhost checker]# ./demo 0 1
Just Jump label
condition 1 is false
condition 2 is true
如何做到的呢?static_branch_true内联函数是如何判断true or false的呢?
事实上,jump label逻辑修改了代码段,取消了条件判断!这一切都是在update_branch中发生的。我们看下update_branch调用之前,main函数的汇编码:
(gdb) disassemble main
Dump of assembler code for function main:
...
0x0000000000400662 <+74>: callq 0x4005ad <update_branch>
// 0x0000000000400667 <+79> 记住这里的指令吧!
0x0000000000400667 <+79>: jmpq 0x40066c <main+84>
0x000000000040066c <+84>: jmp 0x40067a <main+98>
0x000000000040066e <+86>: mov $0x400750,%edi
0x0000000000400673 <+91>: callq 0x400470 <puts@plt>
在执行了update_branch之后,main函数发生了变化:
(gdb) b main
Breakpoint 1 at 0x400618: file jump_label_demo.c, line 56.
(gdb) r 1 0
Starting program: /root/checker/./demo 1 0
Breakpoint 1, main (argc=3, argv=0x7fffffffe428) at jump_label_demo.c:56
56 {
(gdb) next
59 E1 = atoi(argv[1]);
(gdb) next
60 E2 = atoi(argv[2]);
(gdb)
65 printf("Just Jump label\n");
(gdb)
Just Jump label
66 if (E1) {
(gdb)
67 update_branch(e1);
(gdb)
69 if (E2) {
(gdb) disassemble main
Dump of assembler code for function main:
...
0x0000000000400662 <+74>: callq 0x4005ad <update_branch>
// 0x0000000000400667 <+79> 指令已经被修改为jmpq 0x40066e
0x0000000000400667 <+79>: jmpq 0x40066e <main+86>
0x000000000040066c <+84>: jmp 0x40067a <main+98>
0x000000000040066e <+86>: mov $0x400750,%edi
0x0000000000400673 <+91>: callq 0x400470 <puts@plt>
看样子就是这么回事!
之所以这件事可以发生得如此简单,多亏了一个新的section,即__jump_table,我们通过objdump看看__jump_table的内容:
Contents of section __jump_table:
601040 67064000 00000000 6e064000 00000000 g.@.....n.@.....
601050 44332211 00000000 84064000 00000000 D3".......@.....
601060 8b064000 00000000 ddccbbaa ffffffff ..@.............
601070 00000000 00000000 00000000 00000000 ................
601080 00000000 00000000
通过jump_label_demo.c的struct entry结构体,我们直到这个section中包含了多个3元组,包含3个字段:
需要修改的代码地址。 需要jmp到的代码地址。 匹配健。
我们看67064000 00000000按照小端就是0x400667,它就是需要修改的代码地址,而6e064000 00000000按照小端则是0x40066e:
400667: e9 00 00 00 00 jmpq 40066c <main+0x54>
40066c: eb 0c jmp 40067a <main+0x62>
40066e: bf 50 07 40 00 mov $0x400750,%edi
看来,这个__jump_table的item会将jmpq 40066c修改为jmpq 40066e,从而实现了 永久静态分支。
最后,__jump_table的内容就是在每一个内联的static_branch_true函数中被填充的,该参数的参数是一个key,它指示了branch entry三元组中的最后一个字段。
static_branch_true函数的内联非常重要,它实现了将branch entry三元组数据直接插入到__jump_table section,而不是共享同一个函数体。
总之,如果你看代码还是觉得别扭,手敲一遍我上面的示例程序,就理解了,内核里面的也就这么回事,总结一句话:
依靠运行时修改代码而不是依靠状态数据来控制执行流。
我不知道这对于所谓的 通用计算机程序设计 是不是反其道而行之,但在效果上,它确实是一匹好马。不禁感叹, 硬编码读起来是丑陋的,但执行起来却是高效的!
灵活性换高效率,得不偿失,我是这样以为。jump label的本质在于, 将同时刻存在的一套代码沿着时间线在可预期的固定时间点上分割成逻辑相反的两套代码。
硬件性能的提升将会证明jump label就是个笑话。
说两句好话,Linux内核参数,sysctl变量基本上就可以通过jump label来运作,从而替代if判断。
- EOF -
看完本文有收获?请分享给更多人
推荐关注「Linux 爱好者」,提升Linux技能
点赞和在看就是最大的支持❤️