一文了解 BPF 字節碼

1、什麼是字節碼

相信大家在看有關 BPF 的文章時,都有聽過 “字節碼” 一說,在講字節碼之前,我們先來了解一下,什麼是機器碼?機器碼(machine code),也叫原生碼(native code),就是 CPU 能夠直接讀取並運行的代碼,執行速度最快的代碼,用二進制編碼表示,也叫做機器指令碼。它和 CPU 體系架構強相關。

那麼什麼是字節碼?字節碼(byte code)是一種中間狀態的二進制代碼,是由源碼編譯過來的,可讀性沒有源碼高,而且 CPU 也不能夠直接讀取字節碼。字節碼是一種中間碼,它比機器碼更抽象,需要直譯器轉譯後才能成爲機器碼的中間代碼。

說到字節碼,就不得不提到 JVM 虛擬機,java 源程序經過編譯器編譯後變成字節碼,字節碼由虛擬機解釋執行,虛擬機將每一條要執行的字節碼送給解釋器,解釋器將其翻譯成特定機器上的機器碼,然後在特定機器上運行。它的運行順序是:

java 源代碼 -> 編譯器 -> JVM 可執行的字節碼 -> JVM 中的解釋器 -> 機器可執行的二進制機器碼 -> 程序運行

java 採用字節碼的好處是,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特點。所以 java 程序運行時比較高效,而且由於字節碼並不專對於一種特定的機器,因此 java 程序無需重新編譯便可以在多種不同的計算機上運行。

說完 java 裏面的字節碼以及它的運行邏輯,也瞭解到它的可移植性和執行效率問題,我們很自然的想到,BPF 字節碼,也具有異曲同工之妙。如下圖所示,我們一般將 BPF 程序編譯後生成 BPF 字節碼,然後將 BPF 字節碼注入到內核中,當有事件觸發時,就會執行相應的 BPF 程序。這裏生成字節碼的方式,我們需要用到一個編譯器,比如當前主要是用 C 語言去編寫 BPF 程序,通過 LLVM/Clang 編譯生成 BPF 字節碼,這是一種中間代碼,將這段字節碼加載到內核之後,Linux 的驗證程序確保它可以安全地運行,防止出現可能會使內核崩潰而危及系統的代碼。Linux 內核還爲 BPF 指令集成了即時(JIT)編譯器,JIT 將直接將 BPF 字節碼轉換爲機器碼,從而避免了執行時間的開銷。

我們看到,BPF 字節碼起到了非常關鍵的作用,雖然將用戶 BPF 程序轉成字節碼是通過 Clang 編譯器完成的,如你所願,我們不用關心這個字節碼或者說指令集長得怎麼樣?編譯器替我們幹了這個髒活累活。但是,爲了能提高編寫代碼和執行代碼的效率問題,爲了讓 Linux 內核校驗出錯時,能快速定位出報錯根因,我們有必要了解一下 BPF 的字節碼。

這裏有很多概念,我們再來總結一下:

僞機器碼:假的機器碼,機器碼都是能夠在物理機上直接執行的,僞機器碼不能夠直接執行,需要在虛擬機上執行。BPF 字節碼就是僞機器碼。

BPF 指令集:BPF 字節碼,是一條條的 BPF 指令,BPF 指令集就是僞機器碼,是不能夠在物理機上直接執行的,需要一個虛擬機才能夠執行。我們都知道不同的處理器體系結構有自己的不同指令集,我們所說的 BPF 指令集可以理解爲在 BPF 虛擬機上執行的指令集。

JIT:just in time 的縮寫,我們將編譯好的 BPF 指令集需要在虛擬機上執行,虛擬機需要一條一條的解析爲本機機器碼才能夠執行,所以這個執行效率會很低,但是如果我們的處理器有了 JIT 就能夠將我們 BPF 直接直接編譯爲能夠在機器直接執行的機器碼,這樣大大提高了執行的速度。

在沒有嚴格區分的情況下,我們說 BPF 字節碼和 BPF 指令,是指同一個概念。

2、BPF 指令集

BPF 指令集是一個通用的 RISC 指令集,指令集由指令操作碼和寄存器組成。我們知道在 1992 年誕生了 BPF 技術,當時的寄存器和指令數目非常有限,到後來 eBPF 技術發展起來,寄存器和指令數目多了很多。爲了區別,原來的 BPF 又稱之爲 classic BPF(cBPF)。

我們先從功能上,對比一下 cBPF 和 eBPF:

一、cBPF 支持的功能比較單一,常用在網絡的數據包過濾,比如大名鼎鼎的 tcpdump。而 eBPF 除了能夠支持網絡的數據包的過濾外,也支持其他的事件類型,如 XDP、Perf Event、kprobe、tracepoint 等等。

二、eBPF 引入 Map 機制。在 cBPF 我們通過接收隊列將過濾後數據獲取出來,但是在 eBPF 我們可以將數據放到 Map 空間中。Map 空間是用戶空間和內核空間共享的,所以一般是在內核中將數據存入到 Map 空間中,然後在用戶空間取出數據。或者用戶空間寫入一些控制邏輯,內核空間根據它進行分支選擇。

三、eBPF 指令集變得更復雜了,以便支持更多功能。與此同時,有了專門的用於編譯 BPF 字節碼的編譯器 clang/llvm,這樣我們就可以基於 c 語言等進行 BPF 程序的開發,而不是直接寫 BPF 彙編。

四、還有在安全機制方面等等一些改變。

2.1 tcpdump 和 cBPF 指令碼

在講 BPF 指令集前,我們先看一下,大家非常熟悉的 tcpdump,這是一個通過輸入表達式(其實是一些過濾規則)進行網絡抓包的工具,然後通過 libpcap 把這個表達式轉換成 cBPF 的字節碼的。

這個字節碼或者指令可以通過 - d(-dd 可以看到具體的指令格式) 命令去查看,比如:

#tcpdump -d -i eth0 tcp and port 80
(000) ldh      [12]
(001) jeq      #0x86dd          jt 2	jf 8
(002) ldb      [20]
(003) jeq      #0x6             jt 4	jf 19
(004) ldh      [54]
(005) jeq      #0x50            jt 18	jf 6
(006) ldh      [56]
(007) jeq      #0x50            jt 18	jf 19
(008) jeq      #0x800           jt 9	jf 19
(009) ldb      [23]
(010) jeq      #0x6             jt 11	jf 19
(011) ldh      [20]
(012) jset     #0x1fff          jt 19	jf 13
(013) ldxb     4*([14]&0xf)
(014) ldh      [x + 14]
(015) jeq      #0x50            jt 18	jf 16
(016) ldh      [x + 16]
(017) jeq      #0x50            jt 18	jf 19
(018) ret      #262144
(019) ret      #0

#tcpdump -dd -i eth0 tcp and port 80
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 6, 0x000086dd },
{ 0x30, 0, 0, 0x00000014 },
{ 0x15, 0, 15, 0x00000006 },
{ 0x28, 0, 0, 0x00000036 },
{ 0x15, 12, 0, 0x00000050 },
{ 0x28, 0, 0, 0x00000038 },
{ 0x15, 10, 11, 0x00000050 },
{ 0x15, 0, 10, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 0, 8, 0x00000006 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x00000050 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00000050 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },

cBPF 架構的基本元素如下,以下內容來源於 linux 內核目錄下的:

Documentation/networking/filter.rst

1bY7a5

cBPF 彙編的一條指令爲 64 字節, 在頭文件 <usr/include/linux/filter.h> 中有定義。如下所示。

16bit 的 code 表示具體的操作類型,有加載 / 存儲,跳轉,運算等類型;

8 bit 的 jt 和 jf 是用於提供代碼的跳轉偏移量,jt 爲真跳轉,jf 爲假跳轉;

32bit 的 k 爲通用值,根據指令類型有不同含義。

struct sock_filter {	/* Filter block */
	__u16	code;   /* Actual filter code */
	__u8	jt;	/* Jump true */
	__u8	jf;	/* Jump false */
	__u32	k;      /* Generic multiuse field */
};

對於網絡報文的過濾,有一個例子是直接將 struct sock_filter 數組的指針通過 setsockopt(2) 傳遞給內核:

    /* From the example above: tcpdump -i em1 port 22 -dd */
    struct sock_filter code[] = {
        { 0x28,  0,  0, 0x0000000c },
        { 0x15,  0,  8, 0x000086dd },
        { 0x30,  0,  0, 0x00000014 },
        { 0x15,  2,  0, 0x00000084 },
        { 0x15,  1,  0, 0x00000006 },
        { 0x15,  0, 17, 0x00000011 },
        { 0x28,  0,  0, 0x00000036 },
        { 0x15, 14,  0, 0x00000016 },
        { 0x28,  0,  0, 0x00000038 },
        { 0x15, 12, 13, 0x00000016 },
        { 0x15,  0, 12, 0x00000800 },
        { 0x30,  0,  0, 0x00000017 },
        { 0x15,  2,  0, 0x00000084 },
        { 0x15,  1,  0, 0x00000006 },
        { 0x15,  0,  8, 0x00000011 },
        { 0x28,  0,  0, 0x00000014 },
        { 0x45,  6,  0, 0x00001fff },
        { 0xb1,  0,  0, 0x0000000e },
        { 0x48,  0,  0, 0x0000000e },
        { 0x15,  2,  0, 0x00000016 },
        { 0x48,  0,  0, 0x00000010 },
        { 0x15,  0,  1, 0x00000016 },
        { 0x06,  0,  0, 0x0000ffff },
        { 0x06,  0,  0, 0x00000000 },
    };

    struct sock_fprog bpf = {
        .len = ARRAY_SIZE(code),
        .filter = code,
    };

    sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (sock < 0)
        /* ... bail out ... */

    ret = setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));
    if (ret < 0)
        /* ... bail out ... */

    /* ... */
    close(sock);

由於性能有限, 因此後面 cBPF 由發展成爲 eBPF, 有新的指令和架構。原始的 BPF 指令會被自動翻譯爲新的 eBPF 指令,目前在 Linux 內核裏,bpf_convert_filter() 函數在做這個轉換。

2.2 eBPF 指令集

接下來我們重點來介紹 eBPF 的指令集。參考自:

Documentation/bpf/instruction-set.rst

Documentation/bpf/classic_vs_extended.rst

eBPF 的寄存器

eBPF 由 11 個 64 位寄存器、一個程序計數器 PC 和一個 512 字節的大 BPF 堆棧空間組成。寄存器被命名爲 r0- r10。操作模式默認爲 64 位。64 位的寄存器也可作 32 位子寄存器使用,它們只能通過特殊的 ALU(算術邏輯單元)操作訪問,使用低 32 位,高 32 位使用零填充。

寄存器的使用約定如下:

hqCl35

在加載和存儲指令中,寄存器 R6 是一個隱式輸入,必須包含指向 sk_buff(ctx)的指針。寄存器 R0 是一個隱式輸出,它包含從數據包中獲取的數據。

eBPF 的指令格式

struct bpf_insn 結構體用來表示 eBPF 具體的指令格式:

struct bpf_insn {
 __u8 code;  /* opcode */
 __u8 dst_reg:4; /* dest register */
 __u8 src_reg:4; /* source register */
 __s16 off;  /* signed offset */
 __s32 imm;  /* signed immediate constant */
};

其中的 code 字段,如下:

  +----------------+--------+--------------------+
  |   4 bits       |  1 bit |   3 bits           |
  | operation code | source | instruction lass  |
  +----------------+--------+--------------------+
  (MSB)                                      (LSB)

opcode 字段的低 3 位,決定指令類型。指令類型包含:加載與存儲指令、運算指令、跳轉指令。

LZTreA

eBPF 把 BPF_RET 和 BPF_MISC 指令去掉了,換成了 BPF_JMP32 和 BPF_ALU64,提供更大範圍的跳轉和 64 位場景下的運算操作。

BPF_LD 和 BPF_LDX: 兩個類都用於加載操作。BPF_LD 用於加載雙字。後者是從 cBPF 繼承而來的,主要是爲了保持 cBPF 到 eBPF 的轉換效率,因爲它們優化了 JIT 代碼。

BPF_ST 和 BPF_STX: 兩個類都用於存儲操作,用於將數據從寄存器到存儲器中。

BPF_ALU 和 BPF_ALU64: 分別是 32 位和 64 位下的 ALU 操作。

BPF_JMP 和 BPF_JMP32:跳轉指令。JMP32 的跳轉範圍是 32 位大小 (一個字)

運算和跳轉指令

當 BPF_CLASS(code) == BPF_ALU 或 BPF_JMP 時,code 字段可分爲三部分,如下所示:

+----------------+--------+--------------------+
|   4 bits       |  1 bit |   3 bits           |
| operation code | source | instruction class  |
+----------------+--------+--------------------+
(MSB)                                      (LSB)

其中的第四位 source,可以爲 0 或者 1,在 linux 中,使用如下宏定義:

BPF_K     0x00
BPF_X     0x08

在 cBPF 中,表示:

BPF_SRC(code) == BPF_X - use register X as source operand
BPF_SRC(code) == BPF_K - use 32-bit immediate as source operand

在 eBPF 中,這意味着:

BPF_SRC(code) == BPF_X - use 'src_reg' register as source operand
BPF_SRC(code) == BPF_K - use 32-bit immediate as source operand

也就是說,操作數的選擇上,BPF_K 代表使用立即數,BPF_X 代表使用源寄存器的內容。

如果 BPF_CLASS(code) 等於 BPF_ALU 或 BPF_ALU64,則 BPF_OP(code) 是以下之一:

  BPF_ADD   0x00
  BPF_SUB   0x10
  BPF_MUL   0x20
  BPF_DIV   0x30
  BPF_OR    0x40
  BPF_AND   0x50
  BPF_LSH   0x60
  BPF_RSH   0x70
  BPF_NEG   0x80
  BPF_MOD   0x90
  BPF_XOR   0xa0
  BPF_MOV   0xb0  /* eBPF only: mov reg to reg */
  BPF_ARSH  0xc0  /* eBPF only: sign extending shift right */
  BPF_END   0xd0  /* eBPF only: endianness conversion */

如果 BPF_CLASS(code) 等於 BPF_JMP 或 BPF_JMP32,則 BPF_OP(code) 是以下之一:

  BPF_JA    0x00  /* BPF_JMP only */
  BPF_JEQ   0x10
  BPF_JGT   0x20
  BPF_JGE   0x30
  BPF_JSET  0x40
  BPF_JNE   0x50  /* eBPF only: jump != */
  BPF_JSGT  0x60  /* eBPF only: signed '>' */
  BPF_JSGE  0x70  /* eBPF only: signed '>=' */
  BPF_CALL  0x80  /* eBPF BPF_JMP only: function call */
  BPF_EXIT  0x90  /* eBPF BPF_JMP only: function return */
  BPF_JLT   0xa0  /* eBPF only: unsigned '<' */
  BPF_JLE   0xb0  /* eBPF only: unsigned '<=' */
  BPF_JSLT  0xc0  /* eBPF only: signed '<' */
  BPF_JSLE  0xd0  /* eBPF only: signed '<=' */

加載和存儲指令

當 BPF_CLASS(code) 等於 BPF_LD 或 BPF_ST 時,op 字段可分爲三部分,如下所示:

 +--------+--------+-------------------+
  | 3 bits | 2 bits |   3 bits          |
  |  mode  |  size  | instruction class |
  +--------+--------+-------------------+
  (MSB)                             (LSB)

其中的 size 定義如下:

BPF_W   0x00    /* word=4 byte */
BPF_H   0x08    /* half word */
BPF_B   0x10    /* byte */
BPF_DW  0x18    /* eBPF only, double word */
 B  - 1 byte
 H  - 2 byte
 W  - 4 byte
 DW - 8 byte (eBPF only)

mode 定義如下:

BPF_IMM     0x00  /* used for 32-bit mov in classic BPF and 64-bit in eBPF */
BPF_ABS     0x20
BPF_IND     0x40
BPF_MEM     0x60
BPF_LEN     0x80  /* classic BPF only, reserved in eBPF */
BPF_MSH     0xa0  /* classic BPF only, reserved in eBPF */
BPF_ATOMIC  0xc0  /* eBPF only, atomic operations */

3、eBPF 彙編

前面我們介紹了 eBPF 和 cBPF 的基礎指令碼,接下來一起看看 eBPF 的指令構成是什麼樣子的,這有助於我們去分析 verifier 出錯時的一些根因定位。以 x86_64 爲例,先介紹一下 eBPF 使用到的幾個寄存器和 x86_64 的映射關係:

    R0 - rax
    R1 - rdi
    R2 - rsi
    R3 - rdx
    R4 - rcx
    R5 - r8
    R6 - rbx
    R7 - r13
    R8 - r14
    R9 - r15
    R10 - rbp

rdi、rsi、rdx、rcx 是傳遞的參數和順序。

下面是一段 eBPF 的僞代碼:

  Then the following eBPF pseudo-program::

    bpf_mov R6, R1 /* save ctx */
    bpf_mov R2, 2
    bpf_mov R3, 3
    bpf_mov R4, 4
    bpf_mov R5, 5
    bpf_call foo
    bpf_mov R7, R0 /* save foo() return value */
    bpf_mov R1, R6 /* restore ctx for next call */
    bpf_mov R2, 6
    bpf_mov R3, 7
    bpf_mov R4, 8
    bpf_mov R5, 9
    bpf_call bar
    bpf_add R0, R7
    bpf_exit

上面僞代碼包括寄存器賦值,運算和跳轉,以及返回。

如果機器上開啓了相關架構的 jit 功能,會轉成對應架構的彙編指令:

    push %rbp
    mov %rsp,%rbp
    sub $0x228,%rsp
    mov %rbx,-0x228(%rbp)
    mov %r13,-0x220(%rbp)
    mov %rdi,%rbx
    mov $0x2,%esi
    mov $0x3,%edx
    mov $0x4,%ecx
    mov $0x5,%r8d
    callq foo
    mov %rax,%r13
    mov %rbx,%rdi
    mov $0x6,%esi
    mov $0x7,%edx
    mov $0x8,%ecx
    mov $0x9,%r8d
    callq bar
    add %r13,%rax
    mov -0x228(%rbp),%rbx
    mov -0x220(%rbp),%r13
    leaveq
    retq

大概對應到的 c 程序:

    u64 bpf_filter(u64 ctx)
    {
    return foo(ctx, 2, 3, 4, 5) + bar(ctx, 6, 7, 8, 9);
    }

具體彙編分析

爲了加深對前面所介紹的基礎指令的認識,我們分析一下這段代碼:

sample/bpf/sock_example.c

本節來自於網絡。

抽取關鍵的這個信息:

 struct bpf_insn prog[] = {
  BPF_MOV64_REG(BPF_REG_6, BPF_REG_1), /* R6 = R1*/ /* R6指向數據包地址 */
  BPF_LD_ABS(BPF_B, ETH_HLEN + offsetof(struct iphdr, protocol) /* R0 = ip->proto */), /*R6作爲隱式輸入,R0作爲隱式輸出。結果R0報錯IP協議值*/
  BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4), /* *(u32 *)(fp - 4) = r0 */ /* 將協議值保存在棧中*/
  BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), /*R10只讀寄存器,指向棧幀。複製一份到R2中*/
  BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */ /* 內核bpf_map_lookup_elem函數的第二個參數key的內存地址放在R2中 */
  BPF_LD_MAP_FD(BPF_REG_1, map_fd), /* 內核bpf_map_lookup_elem函數的第一個參數map_fd放在R1中 */
  BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), /* 函數的返回值爲value所在內存的地址,放在R0寄存器中*/
  BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 2), /* 如果返回的內存地址爲0,則向下跳兩個指令 */
  BPF_MOV64_IMM(BPF_REG_1, 1), /* r1 = 1 */
  BPF_RAW_INSN(BPF_STX | BPF_XADD | BPF_DW, BPF_REG_0, BPF_REG_1, 0, 0), /* xadd r0 += r1 */ /* value的值加一;結果R0存儲1,R1存儲value地址 */
  BPF_MOV64_IMM(BPF_REG_0, 0), /* r0 = 0 */
  BPF_EXIT_INSN(), /* R0作爲返回值,返回零 */
 };

下面我們對上面代碼逐行指令進行梳理:

第一條指令

/* Short form of mov, dst_reg = src_reg */

#define BPF_MOV64_REG(DST, SRC)           \
  ((struct bpf_insn) {                    \
    .code  = BPF_ALU64 | BPF_MOV | BPF_X, \
    .dst_reg = DST,                       \
    .src_reg = SRC,                       \
    .off     = 0,                         \
    .imm     = 0 })

BPF_MOV64_REG 這條指令是將源寄存器 R1 的值移動到 R6 寄存器中。其中,R1 指向數據包的起始地址,一般是 skb 指針。

第二條指令

/* Direct packet access, R0 = *(uint *) (skb->data + imm32) */

#define BPF_LD_ABS(SIZE, IMM)                     \
  ((struct bpf_insn) {                            \
    .code    = BPF_LD | BPF_SIZE(SIZE) | BPF_ABS, \
    .dst_reg = 0,                                 \
    .src_reg = 0,                                 \
    .off     = 0,                                 \
    .imm     = IMM })

在加載和存儲指令中,寄存器 R6 是一個隱式輸入,寄存器 R0 是一個隱式輸出。

根據偏移量,讀取 IP 協議類型,例如,TCP 的協議號爲 6,UDP 的協議號爲 17,ICMP 的協議號爲 1。其中,協議字段佔 8 位。

所以,BPF_LD_ABS 這條指令表示,將 IP 協議值放入 R0 寄存器。

第三條指令

/* Memory store, *(uint *) (dst_reg + off16) = src_reg */

#define BPF_STX_MEM(SIZE, DST, SRC, OFF)         \
  ((struct bpf_insn) {                           \
    .code  = BPF_STX | BPF_SIZE(SIZE) | BPF_MEM, \
    .dst_reg = DST,                              \
    .src_reg = SRC,                              \
    .off     = OFF,                              \
    .imm     = 0 })

R10 是唯一的只讀寄存器,包含用於訪問 BPF 堆棧空間的幀指針地址。(關於棧幀結構可以參考:gdb 調試之棧幀信息)

這條指令意思是將 R0 寄存器中的內容 (上一步保存了協議類型),保存到棧中。需要注意的是,這裏是 BPF_W,只保存了 R0 寄存器中的第 32 位。

第四條指令

BPF_MOV64_REG(BPF_REG_2, BPF_REG_10)

因爲棧向下生長了。所以這裏使用了 R2 寄存器指向棧頂。

BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4)

/* ALU ops on immediates, bpf_add|sub|...: dst_reg += imm32 */

#define BPF_ALU64_IMM(OP, DST, IMM)				\
	((struct bpf_insn) {					\
		.code  = BPF_ALU64 | BPF_OP(OP) | BPF_K,	\
		.dst_reg = DST,					\
		.src_reg = 0,					\
		.off   = 0,					\
		.imm   = IMM })

上面的指令展開,便是一個 64 位的二進制數,r2-4 的結果放在 r2 裏。展開可以在 samples/bpf/bpf_insn.h 和 include/uapi/linux/bpf.h 中查看。

第五條指令

/* BPF_LD_IMM64 macro encodes single 'load 64-bit immediate' insn */

#define BPF_LD_IMM64(DST, IMM)           \
  BPF_LD_IMM64_RAW(DST, 0, IMM)

#define BPF_LD_IMM64_RAW(DST, SRC, IMM)  \
  ((struct bpf_insn) {                   \
    .code    = BPF_LD | BPF_DW | BPF_IMM,\
    .dst_reg = DST,                      \
    .src_reg = SRC,                      \
    .off     = 0,                        \
    .imm     = (__u32) (IMM) }),         \
  ((struct bpf_insn) {                   \
    .code    = 0,                        \
    .dst_reg = 0,                        \
    .src_reg = 0,                        \
    .off     = 0,                        \
    .imm     = ((__u64) (IMM)) >> 32 })

#ifndef BPF_PSEUDO_MAP_FD
# define BPF_PSEUDO_MAP_FD 1
#endif

/* pseudo BPF_LD_IMM64 insn used to refer to process-local map_fd */
#define BPF_LD_MAP_FD(DST, MAP_FD)    \
  BPF_LD_IMM64_RAW(DST, BPF_PSEUDO_MAP_FD, MAP_FD)

可以看到,這條指令是將 map_fd 的值,保存到 R1 寄存器中。這時候,我們可能會好奇,這中間有 src_reg 什麼事情?

上面我們可以看到,如果只是單純將一個立即數保存到寄存器中,則 src_reg=0;如果這個立即數表示是一個 map_fd,則則 src_reg=1;

這樣我們便可以區分指令中的立即數是否表示一個 map_fd。後面 replace_map_fd_with_map_ptr 函數會用到這個性質。

第六條指令

/* Raw code statement block */

#define BPF_RAW_INSN(CODE, DST, SRC, OFF, IMM)  \
  ((struct bpf_insn) {                          \
    .code    = CODE,                            \
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off     = OFF,                             \
    .imm     = IMM })

其中 BPF_FUNC_map_lookup_elem 的宏展開爲 1。至於跳轉到 1 的位置,在 verifier 後是 bpf_map_lookup_elem 這個函數,則是後續的問題了。可以參考:fixup_bpf_calls

這裏,可以從宏的名稱看出是是跳轉到 bpf_map_lookup_elem 函數位置。

第七條指令

/* Conditional jumps against immediates, if (dst_reg 'op' imm32) goto pc + off16 */

#define BPF_JMP_IMM(OP, DST, IMM, OFF)     \
  ((struct bpf_insn) {                     \
    .code  = BPF_JMP | BPF_OP(OP) | BPF_K, \
    .dst_reg = DST,                        \
    .src_reg = 0,                          \
    .off     = OFF,                        \
    .imm     = IMM })

這條指令表示,R0 寄存器 等於 0,則向下跳過兩個指令。

R0 寄存器 這裏存儲的是協議號,根據 IP 協議號列表可知,但 IP 數據包中的協議爲 “IPv6 逐跳選項”,則向下跳過兩個指令。

第八條指令

BPF_RAW_INSN(BPF_STX | BPF_XADD | BPF_DW, BPF_REG_0, BPF_REG_1, 0, 0)

xadd - 交換相加。

第九條指令

BPF_MOV64_IMM(BPF_REG_0, 0), /* r0 = 0 */

R0 是包含 BPF 程序退出值的寄存器,設置返回值 R0=0。

第十條指令

/* Program exit */

#define BPF_EXIT_INSN()                  \
    ((struct bpf_insn) {                 \
        .code  = BPF_JMP | BPF_EXIT,     \
        .dst_reg = 0,                    \
        .src_reg = 0,                    \
        .off     = 0,                    \
        .imm     = 0 })

程序退出指令,使用 BPF_EXIT

4、總結

前面我們介紹了 eBPF 的字節碼和指令架構的定義,也拿了一個具體的例子進行了分析,通過 eBPF 的字節碼(指令集)有助於我們理解 eBPF 程序。在進行 eBPF 程序開發時,會遇到很多 verifier 的報錯,經過學習本文後,通過讀報錯信息就可以大概知道什麼地方的問題了。

大家可以參考《收藏:eBPF verifier 常見錯誤整理》

https://mp.weixin.qq.com/s/1za4Xk_dgafd_kMOkhLw1w

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/RB-q8IAz3L-OSmkygMTBEg