前言
極客時間 eBPF 核心技術與實戰 的學習筆記.
本章說一下 ebpf 的執行原理, 本章有些內容是直接 copy 自課程原文
eBPF虛擬機器(執行器)包含了什麼
官方的說話, eBPF 是執行在 eBPF 虛擬機器中, 而不是直接作用於系統.
但是也有人說, eBPF執行系統更應該稱之為 執行器, 因為他並不如 虛擬機器, 他們的差異如下:
- eBPF只提供了有限的指令集, 並不像虛擬機器一樣是一個完整的系統, 提供了完整的指令集, 這是因為 eBPF 不能影響到系統的穩定性, 所以只提供了一些處理過的指令集.
在前一章中, 我們可以發現核心態程式碼部分, 使用 C 直接呼叫了輔助函式, 這是 eBPF 為了提高開發效率有意為之的.
那麼我們看一下 eBPF 虛擬機器包含了哪些部分
eBPF輔助函式
就像上一章的 bpf_get_current_pid_tgid()
, eBPF為我們提供了許多輔助函式, 透過這些輔助函式來呼叫到系統的若干執行資訊
這些函式實際上是幫我們呼叫了核心的其他模組, 但是需要注意的是, eBPF提供的輔助函式並不是全部可用的, 能夠呼叫的函式由 eBPF 的程式型別決定
eBPF驗證器
用於確保 eBPF 程式碼的安全, 驗證器會將需要執行的指令建立成有向無環圖, 確保執行的指令都是可達的, 再模擬執行指令, 確保指令不是無效的
儲存模組
11 個 64 位暫存器、一個程式計數器和一個 512 位元組的棧組成的儲存模組. 這裡控制eBPF程式的執行. 這樣的設計, 導致了程式的若干限制:
- 函式的呼叫只能有一個返回值
- 函式呼叫引數不能超過5個
- 棧儲存不能超過512位元組
即時編譯器
將 eBPF 程式編譯成位元組碼執行
BPF 對映(map)
大塊儲存, 可以讓使用者態程式訪問, 來進行資料的讀取
BPF指令長什麼樣
需要先安裝 bpftool
apt install bpftool
然後執行
bpftool prog list
會輸出當前執行的 bpf 程式, 列印類似於
root@VM-4-12-debian:~# bpftool prog list
3: cgroup_device name sd_devices tag 3650d9673c54ce30 gpl
loaded_at 2024-11-30T14:06:29+0800 uid 0
xlated 504B jited 310B memlock 4096B
其中, 3 是這個 eBPF 程式的編號, cgroup_device 是這個程式的型別, sd_devices 是這個 程式的名字
我們可以再開一個命令列, 執行上一章的 helloworld.py, 在執行時再次執行 bpftool prog list
42: kprobe name hello_world tag 38dd440716c4900f gpl
loaded_at 2024-12-04T21:12:45+0800 uid 0
xlated 104B jited 71B memlock 4096B
btf_id 85
發現這次多了一條, 名稱就是我們定義的 hello_world, 而我們的程式型別是 kprobe
, 編號是 42, 知道了 編號 後, 我們可以檢視這個程式的詳細指令(42注意替換為你的 編號)
root@VM-4-12-debian:~# sudo bpftool prog dump xlated id 42
int hello_world(void * ctx):
; int hello_world(void *ctx)
0: (b7) r1 = 33
; ({ char _fmt[] = "Hello, World!"; bpf_trace_printk_(_fmt, sizeof(_fmt)); });
1: (6b) *(u16 *)(r10 -4) = r1
2: (b7) r1 = 1684828783
3: (63) *(u32 *)(r10 -8) = r1
4: (18) r1 = 0x57202c6f6c6c6548
6: (7b) *(u64 *)(r10 -16) = r1
7: (bf) r1 = r10
;
8: (07) r1 += -16
; ({ char _fmt[] = "Hello, World!"; bpf_trace_printk_(_fmt, sizeof(_fmt)); });
9: (b7) r2 = 14
10: (85) call bpf_trace_printk#-61424
; return 0;
11: (b7) r0 = 0
12: (95) exit
其中, ;
開頭的行是我們編寫的程式碼, 其他行是轉換的指令
就拿 0: (b7) r1 = 33
舉例, 0
是指令的行數, (b7)
是十六進位制值, 代表BPF 指令碼, 具體的碼和含義可以參考 bpf-docs/eBPF.md at master · iovisor/bpf-docs , 這裡的 b7
代表 64位暫存器賦值, r1 = 33
則是BPF 指令的虛擬碼
所以上面的詳細指令可以翻譯成:
- 第 0-8 行,藉助 R10 暫存器從棧中把字串 “Hello, World!” 讀出來,並放入 R1 暫存器中
- 第 9 行,向 R2 暫存器寫入字串的長度 14(即程式碼註釋裡面的
sizeof(_fmt)
) - 第 10 行,呼叫 BPF 輔助函式 bpf_trace_printk 輸出字串
- 第 11 行,向 R0 暫存器寫入 0,表示程式的返回值是 0
- 最後一行,程式執行成功退出
這些指令先透過 R1 和 R2 暫存器設定了 bpf_trace_printk 的引數, 然後呼叫 bpf_trace_printk 函式輸出字串, 最後再透過 R0 暫存器返回成功.
而BPF 虛擬機器在接受到這些指令後, 經過校驗, 會透過即時編譯器模組編譯成本地機器指令執行.
使用命令檢視編譯後的本地機器指令
bpftool prog dump jited id 42
如果報錯說不支援, 是因為核心預設不開啟檢視機器指令的功能, 可自行搜尋解決辦法
BPF程式什麼時候執行
需要安裝模組 strace
apt install strace
使用 strace 工具來檢視 hello.py 的執行過程
# -ebpf表示只跟蹤bpf系統呼叫
sudo strace -v -f -ebpf ./hello.py
輸出
bpf(BPF_PROG_LOAD,
{
prog_type=BPF_PROG_TYPE_KPROBE,
insn_cnt=13,
insns=[
{code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0x21},
{code=BPF_STX|BPF_H|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-4, imm=0},
{code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0x646c726f},
{code=BPF_STX|BPF_W|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-8, imm=0},
{code=BPF_LD|BPF_DW|BPF_IMM, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0x6c6c6548},
{code=BPF_LD|BPF_W|BPF_IMM, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0x57202c6f},
{code=BPF_STX|BPF_DW|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-16, imm=0},
{code=BPF_ALU64|BPF_X|BPF_MOV, dst_reg=BPF_REG_1, src_reg=BPF_REG_10, off=0, imm=0},
{code=BPF_ALU64|BPF_K|BPF_ADD, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0xfffffff0},
{code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_2, src_reg=BPF_REG_0, off=0, imm=0xe},
{code=BPF_JMP|BPF_K|BPF_CALL, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0x6},
{code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0},
{code=BPF_JMP|BPF_K|BPF_EXIT, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0}
],
prog_name="hello_world",
...
},
128) = 4
可以看到呼叫 bpf
函式, 傳入了 3 個引數, 實際上 bpf
函式只需要3個引數, 那麼這裡,這三個引數的含義是:
- 第一個引數是 BPF_PROG_LOAD, 表示載入 BPF 程式
- 第二個引數是 bpf_attr 型別的結構體, 表示 BPF 程式的屬性. 其中, 有幾個需要你留意的引數, 比如: prog_type 表示 BPF 程式的型別, 是 BPF_PROG_TYPE_KPROBE , 跟我們 Python 程式碼中的 attach_kprobe 一致. insn_cnt (instructions count) 表示指令條數, insns (instructions) 包含了具體的每一條指令, 這兒的 13 條指令跟我們前面 bpftool prog dump 的結果是一致的
- prog_name 則表示 BPF 程式的名字, 即 hello_world. 第三個引數 128 表示屬性的大小.
而在第一章中, 我們就說了, eBPF程式並不是一直執行, 而是指定的事件發生後才觸發執行.
我們的 hello.py
中程式碼寫明瞭呼叫了 attach_kprobe
進行事件的註冊
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")
為了驗證這個結果, 我們使用 strace 再次獲取一下, 這次獲取全部的流程而不是隻是 ebpf
strace -v -f ./hello.py
會發現呼叫如下
...
/* 1) 載入BPF程式 */
bpf(BPF_PROG_LOAD,...) = 4
...
/* 2)查詢事件型別 */
openat(AT_FDCWD, "/sys/bus/event_source/devices/kprobe/type", O_RDONLY) = 5
read(5, "6\n", 4096) = 2
close(5) = 0
...
/* 3)建立效能監控事件 */
perf_event_open(
{
type=0x6 /* PERF_TYPE_??? */,
size=PERF_ATTR_SIZE_VER7,
...
wakeup_events=1,
config1=0x7f275d195c50,
...
},
-1,
0,
-1,
PERF_FLAG_FD_CLOEXEC) = 5
/* 4)繫結BPF到kprobe事件 */
ioctl(5, PERF_EVENT_IOC_SET_BPF, 4) = 0
...
所以, 其實eBPF 的程式執行分為如下幾步:
- 藉助 bpf 系統呼叫,載入 BPF 程式,並記住返回的檔案描述符
- 查詢 kprobe 型別的事件編號。BCC 實際上是透過 /sys/bus/event_source/devices/kprobe/type 來查詢的
- 呼叫 perf_event_open 建立效能監控事件。比如,事件型別(type 是上一步查詢到的 6)、事件的引數( config1 包含了核心函式 do_sys_openat2 )等
- 透過 ioctl 的 PERF_EVENT_IOC_SET_BPF 命令,將 BPF 程式繫結到效能監控事件。