jump label机制进入Linux内核已经很多很多年了,它的目的是 消除分支。 为了达到这个目的,jump label的手段是 修改分支处的代码。

~把代码当做数据,代码和数据在冯诺伊曼计算机中得到了统一~

本质上,jump label作用于下面的逻辑:

var = false;
...
if vardo_true
elsedo_false

静态拆分成了下面的两个逻辑,其一是:

jmp l_true
do_false
ret
l_true:do_true

或者,其二是:

nop
do_false
ret
l_true:do_true

但二者不能同时共存。 显然,这破坏了通用性和灵活性,带来了高效!

这相当于一个硬熔断,具体详情参见:
https://blog.csdn.net/dog250/article/details/6123517
【PS:这篇文章是我上周找到的,看完了才发现,竟然是我自己写的】


本文来一点可以看得见的东西,演示一下真实的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   2struct 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;
}
#endifint main(int argc, char **argv)
{int E1, E2;E1 = atoi(argv[1]);E2 = atoi(argv[2]);
#ifdef JUMP_LABELint e1 = 0x11223344;int e2 = 0xaabbccdd;printf("Just Jump label\n");if (E1) {update_branch(e1);}if (E2) {update_branch(e2);}
#endif#ifdef JUMP_LABELif (static_branch_true(e1)) {#elseif (E1) {#endifprintf("condition 1 is true\n");} else {printf("condition 1 is false\n");}
#ifdef JUMP_LABELif (static_branch_true(e2)) {#elseif (E2) {#endifprintf("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,%edi0x0000000000400673 <+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 0Breakpoint 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   0x40066e0x0000000000400667 <+79>: jmpq   0x40066e <main+86>0x000000000040066c <+84>:    jmp    0x40067a <main+98>0x000000000040066e <+86>:    mov    $0x400750,%edi0x0000000000400673 <+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判断。

周末了,杭州的纯流民要回嘉定了,大巴上写点东西。


浙江温州皮鞋湿,下雨进水不会胖!

Linux内核jump label与static key的原理与示例相关推荐

  1. 怎样搞懂Linux内核内存管理中的KASAN实现原理

    前言 KASAN是一个动态检测内存错误的工具.KASAN可以检测全局变量.栈.堆分配的内存发生越界访问等问题.功能比SLUB DEBUG齐全并且支持实时检测.越界访问的严重性和危害性通过我之前的文章( ...

  2. linux内核实现ipsec,基于IPv6的IPSec原理分析和在Linux内核中的实现

    The Analysis of IPsec and It's Implement in Linux Kernel Based on IPv6 Sun Shanpeng 1 孙善鹏(1987-),男,硕 ...

  3. Linux内核学习四库全书

    关于内核学习我建议不要上来就读内核而是先了解内核的构成和特性,然后通过思考发现疑问这时再去读内核源码.即先了解概貌在读局部细节.而且内核分成好多部分,不要只是按照顺序去读,应该针对某一部分比如内存管理 ...

  4. Linux内核参数优化网络带宽,基于Linux内核的网络带宽管理

    摘要: 随着多媒体技术和高速网络技术的发展,网络应用的不断增多,用户对带宽的需求不断增加,从而使得网络中经常产生拥塞.而且一些对带宽资质占用比较高的应用,不仅占用的网络带宽资源多,同时要求低延迟和低抖 ...

  5. Linux内核源码分析《进程管理》

    Linux内核源码分析<进程管理> 前言 1. Linux 内核源码分析架构 2. 进程原理分析 2.1 进程基础知识 2.2 Linux进程四要素 2.3 进程描述符 task_stru ...

  6. Linux内核怎么学?看这一份书单足够!

    Linux内核长什么样?这幅漫画是以一个房子的侧方刨面图来绘画的.使用这样的一个房子来代表 Linux 内核.你能给这幅漫画分析一下读图路径吗? 读完这么路径清晰的图,让我们看一下刚刚上架的内核新书及 ...

  7. Linux内核深度解析

    本书基于 4.x 版本的 Linux 内核,介绍了 Linux 内核的若干关键子系统的技术原理.本书主要内 容包括内核的引导过程.内核管理和调度进程的技术原理.内核管理虚拟内存和物理内存的技术原 理. ...

  8. [转载] Linux内核学习书籍

    转自 https://blog.csdn.net/qq_34870631/article/details/83013431 关于内核学习我建议不要上来就读内核而是先了解内核的构成和特性,然后通过思考发 ...

  9. Linux内核学习全书

    关于内核学习我建议不要上来就读内核而是先了解内核的构成和特性,然后通过思考发现疑问这时再去读内核源码.即先了解概貌在读局部细节.而且内核分成好多部分,不要只是按照顺序去读,应该针对某一部分比如内存管理 ...

最新文章

  1. 谈钱太俗!难道开源软件只能讲道义?
  2. mutt msmtp
  3. 封装(私有化成员变量,获取变量值)
  4. 周围剃光头顶留长发型_为啥很多头顶光光的人,宁愿留周围一圈头发,也不剃成光头呢?...
  5. 【Machine Learning 二】单变量线性回归,代价函数,梯度下降
  6. Java 操作 HBase 教程
  7. Word宏的利用学习
  8. 二元函数连续与偏导数存在的关系_偏导数存在(二元函数连续性怎么判断)
  9. @keyframes详解
  10. IT软件下载地址大全
  11. 产品设计方法论:用户体验五要素
  12. keras进行时间序列预测
  13. 黑科技网站第三弹 怀旧游戏集锦
  14. linux文件赋予用户权限,Linux 给用户赋予操作权限
  15. Android开发:使用EasyPay打造全能移动支付框架
  16. json转换工具Fastjson
  17. 蓝牙学习笔记之建立蓝牙连接的过程
  18. ZLG7290应用注意事项及检测步骤
  19. System.Web.Services.Protocols.SoapException: 服务器无法处理请求
  20. WP 平衡球游戏开发教程(二) -在XNA 渲染Farseer物理对象

热门文章

  1. CentOS7克隆虚拟机需要修改的配置
  2. Excel技巧大杂烩
  3. 英国《物理世界》杂志评选出世界十大物理学家
  4. 激光雷达赛道“白刃战”?硅光芯片级FMCW技术进入量产周期
  5. OpenCV+反色处理
  6. 学习笔记:Unity CustomSRP-5-Baked Light
  7. 求助:电压跟随器的输入电压问题
  8. 博通服务器网卡型号区别,博通BROADCOM 网卡型号汇总
  9. php先乘除还是先加减,加减乘除运算法则是什么?
  10. 使用anaconda 要用conda 方式更新各个软件,不要用pip