揭祕 BPF map 前生今世
1. 前言
衆所周知,map 可用於內核 BPF 程序和用戶應用程序之間實現雙向的數據交換, 爲 BPF 技術中的重要基礎數據結構。
在 BPF 程序中可以通過聲明 struct bpf_map_def
結構完成創建,這其實帶給我們一種錯覺,感覺這和普通的 C 語言變量沒有區別,然而事實真的是這樣的嗎?事情遠沒有這麼簡單,讀完本文以後相信你會有更大的驚喜。
struct bpf_map_def SEC("maps") my_map = {
.type = BPF_MAP_TYPE_ARRAY,
// ...
};
我們知道最終 BPF 程序是需要在內核中執行,但是 map 數據結構是用於用戶空間和內核 BPF 程序雙向的數據結構,那麼問題來了:
-
通過
struct bpf_map_def
定義的變量究竟是如何創建的,是在用戶空間創建還是內核中直接創建的? -
如何實現創建後的 map 的結構,在用戶空間與內核中 BPF 程序關聯?你可能注意到在用戶空間中對於 map 的訪問是通過 map 文件句柄 fd 完成(類型爲 int),但是在 BPF 程序中是通過
struct bpf_map *
結構完成的。
畢竟數據交換跨越了用戶空間和內核空間,本文將從深入淺出爲各位看官揭開 map 整個生命管理的 "大瓜"。
2. 簡單的使用樣例
本樣例來自於 samples/bpf/sockex1_user.c[1] 和 sockex1_kern.c[2],略有修改和刪除。
sockex1_user.c[3] 用戶空間程序主要內容如下(爲方便展示,部分內容有刪除和修改):
int main(int argc, char **argv)
{
struct bpf_object *obj;
int map_fd, prog_fd;
// ...
// 加載 BPF 程序至 bpf_object 對象中,
bpf_prog_load("sockex_kern.o", BPF_PROG_TYPE_SOCKET_FILTER, &obj, &prog_fd))
// 獲取 my_map 對應的 map_fd 句柄
map_fd = bpf_object__find_map_fd_by_name(obj, "my_map"); // == 本次關注 ==
// 通過 setsockopt 將 BPF 字節碼加載到內核中
sock = open_raw_sock("lo");
setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd));
popen("ping -4 -c5 localhost", "r"); // 產生報文
// 從 my_map 中讀取 5 次 IPPROTO_TCP 的統計
for (i = 0; i < 5; i++) {
long long tcp_cnt;
int key = IPPROTO_TCP;
assert(bpf_map_lookup_elem(map_fd, &key, &tcp_cnt) == 0); // == 本次關注 ==
// ...
sleep(1);
}
return 0;
}
sockex1_user.c 文件中的 bpf_map_lookup_elem
調用的函數原型如下,定義在文件 tools/lib/bpf/bpf.c[4] 中:
int bpf_map_lookup_elem(int fd, const void *key, void *value)
函數底層通過 sys_bpf(cmd=BPF_MAP_LOOKUP_ELEM,...)
實現,爲我們方便 map 操作的用戶空間封裝函數, bpf 系統調用可參考 man 2 bpf[5]。
其中 sockex1_kern.c[6] 主要內容如下:
// map 定義
struct bpf_map_def SEC("maps") my_map = {
.type = BPF_MAP_TYPE_ARRAY,
.key_size = sizeof(u32),
.value_size = sizeof(long),
.max_entries = 256,
};
// BPF 程序,獲取到報文協議類型並進行計數更新
SEC("socket1")
int bpf_prog1(struct __sk_buff *skb)
{
int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol));
long *value;
value = bpf_map_lookup_elem(&my_map, &index); // 查找索引並更新 map 對應的值,== 本次關注 ==
if (value)
__sync_fetch_and_add(value, skb->len);
return 0;
}
char _license[] SEC("license") = "GPL";
sockex1_kern.c 文件中的 bpf_map_lookup_elem
函數爲內核中提供的 BPF 輔助函數,原型聲明如下,詳情可參考 man 7 bpf-helper[7]:
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)
用戶空間與內核 BPF 輔助函數參數對比
通過分析 sockex1_user.c 和 sockex1_kern.c 函數中的 bpf_map_lookup_elem
使用姿勢,這裏我們做個簡單對比:
// 用戶空間 map 查詢函數
int bpf_map_lookup_elem(int fd, const void *key, void *value)
// 內核中 BPF 輔助函數 map 查詢函數
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)
那麼如何將 int fd
與 struct bpf_map *map
共同關聯一個對象呢?這需要我們通過分析 BPF 字節碼來進行解密。
3. 深入指令分析
首先我們將 sockex1_kern.c 文件使用 llvm/clang
將之編譯成 ELF 的 BPF 字節碼。對於生成的 sockex1_kern.o
文件可以用 llvm-objdump
來查看相對應的文件格式,這裏我們僅關注 map 相關的部分。
3.1 查看 BPF 指令
$ clang -O2 -target bpf -c sockex1_kern.c -o sockex1_kern.o
$ llvm-objdump -S sockex1_kern.o
0000000000000000 <bpf_prog1>:
// ...
; value = bpf_map_lookup_elem(&my_map, &index); # 備註:編譯的機器啓用了 BTF
7: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
9: 85 00 00 00 01 00 00 00 call 1
// ...
上述結果展示了 BPF 程序中 socket1
部分的函數 bpf_prog1
的 BPF 指令,但是其中對於涉及到的變量 my_map
的引用都未有解決。上述的反彙編部分打印了 map_lookup_elem()
函數調用涉及的指令:
- 根據 BPF 程序調用的約定,寄存器
r1
爲函數調用的第 1 個參數,這裏即bpf_map_lookup_elem(&my_map, &index)
調用中的my_map
。
7: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll # 64 位直接數賦值 , r1 = 0
9: 85 00 00 00 01 00 00 00 call 1 # 調用 bpf_map_lookup_elem,編號爲 1
上述 "7:" 行代表了爲一條 16 個字節的 BPF 指令,表示加載一個 64 位立即數。
這裏無需擔心相關的 BPF 指令集,後續我們會詳細展開解釋。1 個 BPF 指令由 8 個字節組成,格式定義如下:
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 */
};
通過上述結構對應拆解一下 ”7:“ 行(其中包含了 2 條 BPF 指令,爲 BPF 指令中的特殊指令,運行時會被解析成 1 條指令執行) ,第 1 條 BPF 指令詳細的信息如下:(這裏忽略了 off 字段)
-
opcode
爲 0x18,即BPF_LD | BPF_IMM | BPF_DW
。該 opcode 表示要將一個 64 位的立即數加載到目標寄存器。 -
dst_reg
是 1(4 個 bit 位),代表寄存器r1
。 -
src_reg
是 0(4 個 bit 位),表示立即數在指令內。 -
imm
爲 0,因爲my_map
的值在生成 BPF 字節碼的時候還未進行創建。
第 2 條指令主要負責保存 imm 的高 32 位。
3.2 加載器創建 map 對象
當加載器(loader)在加載 ELF 對象 sockex1_kern.o
時,其首先會從 ELF 格式的 maps
區域獲取到定義的 map 對象 my_map
及相關的屬性, 然後通過調用 bpf()
系統調用來創建 my_map
對象,如果創建成功,那麼 bpf()
系統調用返回一個文件描述符 (map fd)。
同時,加載器也會對於基於 map 元信息(比如名稱 my_map
)與通過 bpf()
系統調用創建 map 後返回的 map fd 建立起對應關係,此後用戶空間空間程序就可以使用 my_map
作爲關鍵字獲取到其對應的 fd,具體代碼如下:
map_fd = bpf_object__find_map_fd_by_name(obj, "my_map");
用戶空間獲取到了 map 對象的 fd,後續可用於 map_lookup_elem(map_fd, ...)
函數進行 map 的查詢等操作。
3.3 第一次變身:map fd 替換
以上完成了 my_map 對象的創建,但是在 BPF 字節碼程序加載到內核前,還需要將 map fd 在 BPF 指令集中完成第一次變身,如函數 lib/bpf.c: bpf_apply_relo_map()
的代碼片段所示:
prog->insns[insn_off].src_reg = BPF_PSEUDO_MAP_FD; // 值在內核中定義爲 1
prog->insns[insn_off].imm = ctx->map_fds[map_idx]; // ctx->map_fds[map_idx] 即爲保存的 map fd 值。
這裏假設獲取到的 map 文件描述符爲 6,那麼在加載的 BPF 程序完成 bpf_apply_relo_map
的替換後上述的指令對比如下:
ELF 文件中的字節碼:
7: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll # 64 位直接數賦值 , r1 = 0
9: 85 00 00 00 01 00 00 00 call 1 # 調用 bpf_map_lookup_elem,編號爲 1
替換 map fd 後的字節碼:
7: 18 11 00 00 06 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll # 64 位直接數賦值 , r1 = 6
9: 85 00 00 00 01 00 00 00 call 1 # 調用 bpf_map_lookup_elem,編號爲 1
3.4 第二次變身:map fd 替換成 map 結構指針
當上述經過第一次變身的 BPF 字節碼加載到內核後,還需要進行一次變身,才能真正在內核中工作,這次 BPF 驗證器(verifier)扛過大旗。
驗證器將加載器注入到指令中的 map fd 替換成內核中的 map 對象指針。調用堆棧的情況如下:
sys_bpf()
--> bpf_prog_load()
--> bpf_check()
--> replace_map_fd_with_map_ptr()
--> do_check()
--> check_ld_imm()
==> check_func_arg()
--> convert_pseudo_ld_imm64()
函數 replace_map_fd_with_map_ptr()
通過以下代碼完成第二次大變身,實現了內核中 BPF 字節碼的 imm
搖身一變成爲 map ptr
地址。
f = fdget(insn[0].imm); // 從第 1 條指令中的 imm 字段獲取到加載器設置的 map fd
map = __bpf_map_get(f); // 基於 map fd 獲取到 map 對象指針
addr = (unsigned long)map;
insn[0].imm = (u32)addr; // 將 map 對象指針低 32 位放入第一條指令中的 imm 字段
insn[1].imm = addr >> 32; // 將 map 對象指針高 32 位放入第二條指令中的 imm 字段
於此同時,函數 convert_pseudo_ld_imm64()
還需要清理加載器設置的 src_reg = BPF_PSEUDO_MAP_FD
操作( prog->insns[insn_off].src_reg = BPF_PSEUDO_MAP_FD;
), 用於表明完成了整個指令的重寫工作:
if (insn->code == (BPF_LD | BPF_IMM | BPF_DW))
insn->src_reg = 0;
如果這裏的 my_map
在內核中 64 位地址爲 0xffff8881384aa200
,那麼驗證器完成第二次變身後的 BPF 字節碼對比如下。
替換 map fd 後的字節碼:
7: 18 11 00 00 06 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll # 64 位直接數賦值 , r1 = 6
9: 85 00 00 00 01 00 00 00 call 1 # 調用 bpf_map_lookup_elem,編號爲 1
替換爲 map 對象指針後的字節碼如下:
7:18 01 00 00 00 a2 4a 38 00 00 00 00 81 88 ff ff # 64 位直接數賦值 , r1 = 0xffff8881384aa200
9:85 00 00 00 30 86 01 00 # 調用 bpf_map_lookup_elem,編號爲 1
在完成了上述兩次變身後,當在內核中調用 map_lookup_elem()
時,第一個參數 my_map
的值爲 0xffff8881384aa200
,
從而實現了從最早的 ELF 中的 0 ,替換成了 map_fd (6),直到最後的 map 對象 struct bpf_map * (0xffff8881384aa200)
。
提示,內核中 bpf_map_lookup_elem
輔助函數的原型定義爲:
static void *(*bpf_map_lookup_elem)(struct bpf_map *map, void *key)
4. 整個流程總結
通過上述 map 訪問指令的 2 次大變身,我們可以清晰瞭解 map 創建、map fd 指令重寫和 map ptr 對象的重寫,也能夠徹底明白用戶空間 map fd 與內核中 map 對象指針的關聯關係。
俗話說一圖勝千言,這裏我們用一張圖進行整個流程的總結:
原始圖片來自於這裏 [8],略有修改。
參考
-
Linux bpf map internals[9]
-
eCHO episode 11: Exploring bpftool with Quentin Monnet[10]
-
ebpf: BPF_FUNC_map_lookup_elem calling convention[11]
參考資料
[1] samples/bpf/sockex1_user.c: https://elixir.bootlin.com/linux/v5.13/source/samples/bpf/sockex1_user.c
[2] sockex1_kern.c: https://elixir.bootlin.com/linux/v5.13/source/samples/bpf/sockex1_kern.c
[3] sockex1_user.c: https://elixir.bootlin.com/linux/v5.13/source/samples/bpf/sockex1_user.c
[4] tools/lib/bpf/bpf.c: https://elixir.bootlin.com/linux/v5.13/source/tools/lib/bpf/bpf.c
[5] man 2 bpf: https://man7.org/linux/man-pages/man2/bpf.2.html
[6] sockex1_kern.c: https://elixir.bootlin.com/linux/v5.13/source/samples/bpf/sockex1_kern.c
[7] man 7 bpf-helper: https://man7.org/linux/man-pages/man7/bpf-helpers.7.html
[8] 這裏 : https://github.com/qmonnet/echo-bpftool/blob/main/slides/loading.svg
[9] Linux bpf map internals: https://mechpen.github.io/posts/2019-08-03-bpf-map/index.html
[10] eCHO episode 11: Exploring bpftool with Quentin Monnet: https://www.youtube.com/watch?v=1EOLh3zzWP4&t=1527s
[11] ebpf: BPF_FUNC_map_lookup_elem calling convention: https://stackoverflow.com/questions/67440821/ebpf-bpf-func-map-lookup-elem-calling-convention
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/pKDn9DrvKYwDlu88Ojg5Pg