經典BPF與eBPF¶
eBPF被設計為一對一的JIT編譯,這也可以為GCC/LLVM編譯器透過eBPF後端生成最佳化的eBPF程式碼提供可能性,使其效能幾乎與原生編譯的程式碼一樣快。
eBPF格式相對於經典BPF的一些核心變化
暫存器數量從2個增加到10個
舊格式有兩個暫存器A和X,以及一個隱藏的幀指標。新佈局將其擴充套件為10個內部暫存器和一個只讀幀指標。由於64位CPU透過暫存器傳遞函式引數,從eBPF程式到核心函式的引數數量限制為5個,並且一個暫存器用於接收核心函式的返回值。原生情況下,x86_64在暫存器中傳遞前6個引數,aarch64/sparcv9/mips64有7-8個暫存器用於引數;x86_64有6個被呼叫者儲存暫存器,而aarch64/sparcv9/mips64有11個或更多被呼叫者儲存暫存器。
因此,所有eBPF暫存器在x86_64、aarch64等架構上都與硬體暫存器一對一對映,並且eBPF呼叫約定直接對映到64位架構上核心使用的ABI。
在32位架構上,JIT可以對映只使用32位算術的程式,並允許更復雜的程式被解釋執行。
R0 - R5是臨時暫存器,eBPF程式在呼叫時需要根據需要進行溢位/填充。注意,只有一個eBPF程式(== 一個eBPF主例程),它不能呼叫其他eBPF函式,它只能呼叫預定義的核心函式。
暫存器寬度從32位增加到64位
儘管如此,原始32位ALU操作的語義透過32位子暫存器得以保留。所有eBPF暫存器都是64位的,其中32位低位子暫存器在寫入時會零擴充套件為64位。這種行為直接對映到x86_64和arm64的子暫存器定義,但會使其他JIT更難實現。
32位架構透過直譯器執行64位eBPF程式。它們的JIT可以將只使用32位子暫存器的BPF程式轉換為原生指令集,其餘部分則被解釋執行。
操作是64位的,因為在64位架構上,指標也是64位寬,我們希望在核心函式中傳遞64位值,因此32位eBPF暫存器否則將需要定義暫存器對ABI,這樣就無法使用直接的eBPF暫存器到硬體暫存器對映,JIT將需要為進出函式的每個暫存器執行組合/拆分/移動操作,這既複雜又容易出錯且速度慢。另一個原因是使用原子64位計數器。
條件jt/jf目標被jt/fall-through替代
雖然原始設計有諸如
if (cond) jump_true; else jump_false;的構造,但它們正在被替換為替代構造,如if (cond) jump_true; /* else fall-through */。引入bpf_call指令和暫存器傳遞約定,以實現與其他核心函式的零開銷呼叫
在呼叫核心函式之前,eBPF程式需要將函式引數放入R1到R5暫存器以滿足呼叫約定,然後直譯器將從暫存器中獲取它們並傳遞給核心函式。如果R1 - R5暫存器對映到給定架構上用於引數傳遞的CPU暫存器,JIT編譯器無需發出額外的移動指令。函式引數將位於正確的暫存器中,BPF_CALL指令將被JIT編譯為單個“call”硬體指令。選擇此呼叫約定是為了在不影響效能的情況下覆蓋常見的呼叫情況。
核心函式呼叫後,R1 - R5被重置為不可讀,R0包含函式的返回值。由於R6 - R9是被呼叫者儲存的,它們的狀態在呼叫後得以保留。
例如,考慮三個C函式
u64 f1() { return (*_f2)(1); } u64 f2(u64 a) { return f3(a + 1, a); } u64 f3(u64 a, u64 b) { return a - b; }GCC可以將f1, f3編譯為x86_64
f1: movl $1, %edi movq _f2(%rip), %rax jmp *%rax f3: movq %rdi, %rax subq %rsi, %rax reteBPF中的函式f2可能看起來像
f2: bpf_mov R2, R1 bpf_add R1, 1 bpf_call f3 bpf_exit如果f2被JIT編譯並且指標儲存在
_f2中。呼叫f1 -> f2 -> f3和返回將是無縫的。如果沒有JIT,則需要使用__bpf_prog_run()直譯器來呼叫f2。出於實際原因,所有eBPF程式只有一個引數“ctx”,它已放置在R1中(例如在__bpf_prog_run()啟動時),並且程式可以呼叫最多5個引數的核心函式。目前不支援6個或更多引數的呼叫,但如果將來有必要,這些限制可以解除。
在64位架構上,所有暫存器都與硬體暫存器一一對映。例如,x86_64 JIT編譯器可以這樣對映它們...
R0 - rax R1 - rdi R2 - rsi R3 - rdx R4 - rcx R5 - r8 R6 - rbx R7 - r13 R8 - r14 R9 - r15 R10 - rbp
...因為x86_64 ABI要求rdi、rsi、rdx、rcx、r8、r9用於引數傳遞,而rbx、r12 - r15是被呼叫者儲存的暫存器。
那麼下面的eBPF偽程式
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編譯為x86_64後可能看起來像
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); }原型為:u64 (*)(u64 arg1, u64 arg2, u64 arg3, u64 arg4, u64 arg5); 的核心函式 foo() 和 bar() 將在正確的暫存器中接收引數,並將它們的返回值放入
%rax,這在 eBPF 中是 R0。序言和尾聲由 JIT 發出,並且在直譯器中是隱式的。R0-R5 是臨時暫存器,因此 eBPF 程式需要根據呼叫約定在呼叫期間保留它們。例如,以下程式是無效的
bpf_mov R1, 1 bpf_call foo bpf_mov R0, R1 bpf_exit
呼叫後,暫存器R1-R5包含垃圾值,無法讀取。核心中的eBPF驗證器用於驗證eBPF程式。
在新設計中,eBPF也限制為4096條指令,這意味著任何程式都將快速終止,並且只調用固定數量的核心函式。原始BPF和eBPF都是雙運算元指令,這有助於在JIT期間實現eBPF指令和x86指令之間的一對一對映。
用於呼叫直譯器函式的輸入上下文指標是通用的,其內容由特定的用例定義。對於seccomp,暫存器R1指向seccomp_data,對於轉換後的BPF過濾器,R1指向skb。
一個內部翻譯的程式由以下元素組成
op:16, jt:8, jf:8, k:32 ==> op:8, dst_reg:4, src_reg:4, off:16, imm:32
到目前為止,已實現了87條eBPF指令。8位“op”操作碼欄位還有空間用於新指令。其中一些可能使用16/24/32位元組編碼。新指令必須是8位元組的倍數以保持向後相容性。
eBPF是一個通用的RISC指令集。並非所有暫存器和所有指令都在從原始BPF到eBPF的轉換過程中使用。例如,套接字過濾器不使用exclusive add指令,但跟蹤過濾器可能會使用它來維護事件計數器。暫存器R9也不被套接字過濾器使用,但更復雜的過濾器可能會耗盡暫存器,不得不訴諸於棧的溢位/填充。
eBPF可用作通用匯編器,用於最後一步的效能最佳化,套接字過濾器和seccomp將其用作彙編器。跟蹤過濾器可將其用作彙編器,從核心生成程式碼。在核心使用中可能不受安全考慮的限制,因為生成的eBPF程式碼可能最佳化內部程式碼路徑,而不暴露給使用者空間。eBPF的安全性可以來自於eBPF驗證器。在上述用例中,它可用作安全的指令集。
就像原始BPF一樣,eBPF在受控環境中執行,是確定性的,並且核心可以輕鬆證明這一點。程式的安全性可以透過兩個步驟確定:第一步進行深度優先搜尋,以禁止迴圈和其他CFG驗證;第二步從第一條指令開始,遍歷所有可能的路徑。它模擬每條指令的執行,並觀察暫存器和棧的狀態變化。
操作碼編碼¶
eBPF重新使用了經典BPF的大部分操作碼編碼,以簡化經典BPF到eBPF的轉換。
對於算術和跳轉指令,8位“code”欄位分為三部分
+----------------+--------+--------------------+
| 4 bits | 1 bit | 3 bits |
| operation code | source | instruction class |
+----------------+--------+--------------------+
(MSB) (LSB)
三個LSB位儲存指令類別,是以下之一
經典BPF類別
eBPF類別
BPF_LD 0x00
BPF_LD 0x00
BPF_LDX 0x01
BPF_LDX 0x01
BPF_ST 0x02
BPF_ST 0x02
BPF_STX 0x03
BPF_STX 0x03
BPF_ALU 0x04
BPF_ALU 0x04
BPF_JMP 0x05
BPF_JMP 0x05
BPF_RET 0x06
BPF_JMP32 0x06
BPF_MISC 0x07
BPF_ALU64 0x07
第4位編碼源運算元...
BPF_K 0x00 BPF_X 0x08
在經典BPF中,這意味著
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
...以及四個MSB位儲存操作碼。
如果BPF_CLASS(code) == BPF_ALU 或 BPF_ALU64 [ 在 eBPF 中 ],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 [ 在 eBPF 中 ],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_ADD | BPF_X | BPF_ALU在經典BPF和eBPF中都表示32位加法。經典BPF中只有兩個暫存器,所以它表示A += X。在eBPF中它表示dst_reg = (u32) dst_reg + (u32) src_reg; 類似地,BPF_XOR | BPF_K | BPF_ALU在經典BPF中表示A ^= imm32,在eBPF中表示src_reg = (u32) src_reg ^ (u32) imm32。
經典BPF浪費了整個BPF_RET類來表示一個單獨的ret操作。經典BPF_RET | BPF_K表示將imm32複製到返回暫存器並執行函式退出。eBPF被建模為與CPU匹配,因此eBPF中的BPF_JMP | BPF_EXIT僅表示函式退出。eBPF程式需要在執行BPF_EXIT之前將返回值儲存到暫存器R0中。eBPF中的類6被用作BPF_JMP32,表示與BPF_JMP完全相同的操作,但比較的運算元是32位寬。
經典BPF浪費了整個BPF_RET類來表示一個單獨的 ret 操作。經典 BPF_RET | BPF_K 意味著將 imm32 複製到返回暫存器並執行函式退出。eBPF 被建模為與 CPU 匹配,因此 eBPF 中的 BPF_JMP | BPF_EXIT 僅意味著函式退出。eBPF 程式需要在執行 BPF_EXIT 之前將返回值儲存到暫存器 R0 中。eBPF 中的類 6 用作 BPF_JMP32,表示與 BPF_JMP 完全相同的操作,但比較的運算元是 32 位寬。
對於載入和儲存指令,8位“code”欄位被劃分為
+--------+--------+-------------------+
| 3 bits | 2 bits | 3 bits |
| mode | size | instruction class |
+--------+--------+-------------------+
(MSB) (LSB)
大小修飾符是其中之一...
BPF_W 0x00 /* word */
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)
模式修飾符是其中之一
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 */