一文了解 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 支持的功能比較單一,常用在網絡的數據包過濾,比如大名鼎鼎的 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
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 位使用零填充。
寄存器的使用約定如下:
在加載和存儲指令中,寄存器 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 位,決定指令類型。指令類型包含:加載與存儲指令、運算指令、跳轉指令。
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