eBPF 執行原理和流程

ChnMig發表於2024-12-04

前言

極客時間 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 的程式執行分為如下幾步:

  1. 藉助 bpf 系統呼叫,載入 BPF 程式,並記住返回的檔案描述符
  2. 查詢 kprobe 型別的事件編號。BCC 實際上是透過 /sys/bus/event_source/devices/kprobe/type 來查詢的
  3. 呼叫 perf_event_open 建立效能監控事件。比如,事件型別(type 是上一步查詢到的 6)、事件的引數( config1 包含了核心函式 do_sys_openat2 )等
  4. 透過 ioctl 的 PERF_EVENT_IOC_SET_BPF 命令,將 BPF 程式繫結到效能監控事件。

相關文章