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

但二者不能同時共存。 顯然,這破壞了通用性和靈活性,帶來了高效!

這相當於一個硬熔斷,具體詳情參見:
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	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判斷。

週末了,杭州的純流民要回嘉定了,大巴上寫點東西。


浙江溫州皮鞋溼,下雨進水不會胖!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章