揭秘 BPF map 前生今世
1. 前言
眾所周知,map 可用于內(nèi)核 BPF 程序和用戶應(yīng)用程序之間實(shí)現(xiàn)雙向的數(shù)據(jù)交換, 為 BPF 技術(shù)中的重要基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)。
在 BPF 程序中可以通過(guò)聲明 struct bpf_map_def 結(jié)構(gòu)完成創(chuàng)建,這其實(shí)帶給我們一種錯(cuò)覺(jué),感覺(jué)這和普通的 C 語(yǔ)言變量沒(méi)有區(qū)別,然而事實(shí)真的是這樣的嗎?事情遠(yuǎn)沒(méi)有這么簡(jiǎn)單,讀完本文以后相信你會(huì)有更大的驚喜。
struct?bpf_map_def?SEC("maps")?my_map?=?{
?.type?=?BPF_MAP_TYPE_ARRAY,
??//?...
};
我們知道最終 BPF 程序是需要在內(nèi)核中執(zhí)行,但是 map 數(shù)據(jù)結(jié)構(gòu)是用于用戶空間和內(nèi)核 BPF 程序雙向的數(shù)據(jù)結(jié)構(gòu),那么問(wèn)題來(lái)了:
通過(guò)
struct bpf_map_def定義的變量究竟是如何創(chuàng)建的,是在用戶空間創(chuàng)建還是內(nèi)核中直接創(chuàng)建的?如何實(shí)現(xiàn)創(chuàng)建后的 map 的結(jié)構(gòu),在用戶空間與內(nèi)核中 BPF 程序關(guān)聯(lián)?你可能注意到在用戶空間中對(duì)于 map 的訪問(wèn)是通過(guò) map 文件句柄 fd ?完成(類(lèi)型為 int),但是在 BPF 程序中是通過(guò) ?
struct bpf_map *結(jié)構(gòu)完成的。
畢竟數(shù)據(jù)交換跨越了用戶空間和內(nèi)核空間,本文將從深入淺出為各位看官揭開(kāi) map 整個(gè)生命管理的 "大瓜"。
2. 簡(jiǎn)單的使用樣例
本樣例來(lái)自于 samples/bpf/sockex1_user.c[1] 和 sockex1_kern.c[2],略有修改和刪除。
sockex1_user.c[3] 用戶空間程序主要內(nèi)容如下(為方便展示,部分內(nèi)容有刪除和修改):
int?main(int?argc,?char?**argv)
{
?struct?bpf_object?*obj;
?int?map_fd,?prog_fd;
?//?...
??//?加載?BPF?程序至?bpf_object?對(duì)象中,
?bpf_prog_load("sockex_kern.o",?BPF_PROG_TYPE_SOCKET_FILTER,?&obj,?&prog_fd))
?
??//?獲取?my_map?對(duì)應(yīng)的?map_fd?句柄
?map_fd?=?bpf_object__find_map_fd_by_name(obj,?"my_map");?//?==?本次關(guān)注?==
??//?通過(guò)?setsockopt?將?BPF?字節(jié)碼加載到內(nèi)核中
??sock?=?open_raw_sock("lo");
?setsockopt(sock,?SOL_SOCKET,?SO_ATTACH_BPF,?&prog_fd,?sizeof(prog_fd));
?popen("ping?-4?-c5?localhost",?"r");?//?產(chǎn)生報(bào)文
??//?從?my_map?中讀取?5?次?IPPROTO_TCP?的統(tǒng)計(jì)
?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);?//?==?本次關(guān)注?==
??//?...
??sleep(1);
?}
?return?0;
}
sockex1_user.c 文件中的 bpf_map_lookup_elem 調(diào)用的函數(shù)原型如下,定義在文件 tools/lib/bpf/bpf.c[4] 中:
int?bpf_map_lookup_elem(int?fd,?const?void?*key,?void?*value)
函數(shù)底層通過(guò) sys_bpf(cmd=BPF_MAP_LOOKUP_ELEM,...) 實(shí)現(xiàn),為我們方便 map 操作的用戶空間封裝函數(shù), bpf 系統(tǒng)調(diào)用可參考 man 2 bpf[5]。
其中 sockex1_kern.c[6] 主要內(nèi)容如下:
//?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?程序,獲取到報(bào)文協(xié)議類(lèi)型并進(jìn)行計(jì)數(shù)更新
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?對(duì)應(yīng)的值,==?本次關(guān)注?==
?if?(value)
??__sync_fetch_and_add(value,?skb->len);
?return?0;
}
char?_license[]?SEC("license")?=?"GPL";
sockex1_kern.c 文件中的 ?bpf_map_lookup_elem ?函數(shù)為內(nèi)核中提供的 BPF 輔助函數(shù),原型聲明如下,詳情可參考 man 7 bpf-helper[7]:
void?*bpf_map_lookup_elem(struct?bpf_map?*map,?const?void?*key)
用戶空間與內(nèi)核 BPF 輔助函數(shù)參數(shù)對(duì)比
通過(guò)分析 sockex1_user.c 和 sockex1_kern.c 函數(shù)中的 bpf_map_lookup_elem 使用姿勢(shì),這里我們做個(gè)簡(jiǎn)單對(duì)比:
//?用戶空間?map?查詢函數(shù)
int?bpf_map_lookup_elem(int?fd,?const?void?*key,?void?*value)
//?內(nèi)核中?BPF?輔助函數(shù)?map?查詢函數(shù)
void?*bpf_map_lookup_elem(struct?bpf_map?*map,?const?void?*key)
那么如何將 int fd 與 struct bpf_map *map 共同關(guān)聯(lián)一個(gè)對(duì)象呢?這需要我們通過(guò)分析 BPF 字節(jié)碼來(lái)進(jìn)行解密。
3. 深入指令分析
首先我們將 sockex1_kern.c 文件使用 ?llvm/clang 將之編譯成 ELF 的 BPF 字節(jié)碼。對(duì)于生成的 sockex1_kern.o 文件可以用 llvm-objdump 來(lái)查看相對(duì)應(yīng)的文件格式,這里我們僅關(guān)注 map 相關(guān)的部分。
3.1 查看 BPF 指令
$?clang?-O2?-target?bpf?-c?sockex1_kern.c??-o?sockex1_kern.o
$?llvm-objdump?-S?sockex1_kern.o
0000000000000000?:
????//?...
????;??value?=?bpf_map_lookup_elem(&my_map,?&index);?#?備注:編譯的機(jī)器啟用了 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
???????//?...
上述結(jié)果展示了 BPF 程序中 socket1 部分的函數(shù) bpf_prog1 的 BPF 指令,但是其中對(duì)于涉及到的變量 my_map 的引用都未有解決。上述的反匯編部分打印了 map_lookup_elem() 函數(shù)調(diào)用涉及的指令:
根據(jù) BPF 程序調(diào)用的約定,寄存器 r1為函數(shù)調(diào)用的第 1 個(gè)參數(shù),這里即bpf_map_lookup_elem(&my_map, &index)調(diào)用中的my_map。
???????7:?18?01?00?00?00?00?00?00?00?00?00?00?00?00?00?00?r1?=?0?ll??#?64?位直接數(shù)賦值?,?r1?=?0?
???????9:?85?00?00?00?01?00?00?00?call?1?????????????????????????????#?調(diào)用?bpf_map_lookup_elem,編號(hào)為?1
上述 "7:" 行代表了為一條 16 個(gè)字節(jié)的 BPF 指令,表示加載一個(gè) 64 位立即數(shù)。
這里無(wú)需擔(dān)心相關(guān)的 BPF 指令集,后續(xù)我們會(huì)詳細(xì)展開(kāi)解釋。1 個(gè) BPF 指令由 8 個(gè)字節(jié)組成,格式定義如下:
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?*/
};
通過(guò)上述結(jié)構(gòu)對(duì)應(yīng)拆解一下 ”7:“ 行(其中包含了 2 條 BPF 指令,為 BPF 指令中的特殊指令,運(yùn)行時(shí)會(huì)被解析成 1 條指令執(zhí)行) ,第 1 條 BPF 指令詳細(xì)的信息如下:(這里忽略了 off 字段)
opcode為 0x18,即BPF_LD | BPF_IMM | BPF_DW。該 opcode 表示要將一個(gè) 64 位的立即數(shù)加載到目標(biāo)寄存器。dst_reg是 1(4 個(gè) bit 位),代表寄存器r1。src_reg是 0(4 個(gè) bit 位),表示立即數(shù)在指令內(nèi)。imm為 0,因?yàn)?my_map的值在生成 BPF 字節(jié)碼的時(shí)候還未進(jìn)行創(chuàng)建。
第 2 條指令主要負(fù)責(zé)保存 imm 的高 32 位。
3.2 加載器創(chuàng)建 map 對(duì)象
當(dāng)加載器(loader)在加載 ELF 對(duì)象 sockex1_kern.o 時(shí),其首先會(huì)從 ELF 格式的 maps 區(qū)域獲取到定義的 map 對(duì)象 my_map 及相關(guān)的屬性, 然后通過(guò)調(diào)用 bpf() 系統(tǒng)調(diào)用來(lái)創(chuàng)建 my_map 對(duì)象,如果創(chuàng)建成功,那么 bpf() 系統(tǒng)調(diào)用返回一個(gè)文件描述符 (map fd)。
同時(shí),加載器也會(huì)對(duì)于基于 map 元信息(比如名稱(chēng) my_map)與通過(guò) bpf() 系統(tǒng)調(diào)用創(chuàng)建 map 后返回的 map fd 建立起對(duì)應(yīng)關(guān)系,此后用戶空間空間程序就可以使用 my_map 作為關(guān)鍵字獲取到其對(duì)應(yīng)的 fd,具體代碼如下:
map_fd?=?bpf_object__find_map_fd_by_name(obj,?"my_map");
用戶空間獲取到了 map 對(duì)象的 fd,后續(xù)可用于 map_lookup_elem(map_fd, ...) 函數(shù)進(jìn)行 map 的查詢等操作。
3.3 第一次變身:map fd 替換
以上完成了 my_map 對(duì)象的創(chuàng)建,但是在 BPF 字節(jié)碼程序加載到內(nèi)核前,還需要將 map fd 在 BPF 指令集中完成第一次變身,如函數(shù) lib/bpf.c: bpf_apply_relo_map() 的代碼片段所示:
????????prog->insns[insn_off].src_reg?=?BPF_PSEUDO_MAP_FD;?//?值在內(nèi)核中定義為?1
????????prog->insns[insn_off].imm?=?ctx->map_fds[map_idx];?// ctx->map_fds[map_idx]?即為保存的 map fd 值。
這里假設(shè)獲取到的 map 文件描述符為 6,那么在加載的 BPF 程序完成 bpf_apply_relo_map 的替換后上述的指令對(duì)比如下:
ELF 文件中的字節(jié)碼:
???????7:?18?01?00?00?00?00?00?00?00?00?00?00?00?00?00?00?r1?=?0?ll??#?64?位直接數(shù)賦值?,?r1?=?0?
???????9:?85?00?00?00?01?00?00?00?call?1?????????????????????????????#?調(diào)用?bpf_map_lookup_elem,編號(hào)為?1
替換 map fd 后的字節(jié)碼:
???????7:?18?11?00?00?06?00?00?00?00?00?00?00?00?00?00?00?r1?=?0?ll??#?64?位直接數(shù)賦值?,?r1?=?6?
???????9:?85?00?00?00?01?00?00?00?call?1?????????????????????????????#?調(diào)用?bpf_map_lookup_elem,編號(hào)為?1?????????????????????
3.4 第二次變身:map fd 替換成 map 結(jié)構(gòu)指針
當(dāng)上述經(jīng)過(guò)第一次變身的 BPF 字節(jié)碼加載到內(nèi)核后,還需要進(jìn)行一次變身,才能真正在內(nèi)核中工作,這次 BPF 驗(yàn)證器(verifier)扛過(guò)大旗。
驗(yàn)證器將加載器注入到指令中的 map fd 替換成內(nèi)核中的 map 對(duì)象指針。調(diào)用堆棧的情況如下:
????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()
函數(shù) replace_map_fd_with_map_ptr() 通過(guò)以下代碼完成第二次大變身,實(shí)現(xiàn)了內(nèi)核中 BPF 字節(jié)碼的 imm 搖身一變成為 map ptr 地址。
????????f?=?fdget(insn[0].imm);??//?從第?1?條指令中的?imm?字段獲取到加載器設(shè)置的?map?fd
????????map?=?__bpf_map_get(f);??//?基于?map?fd?獲取到?map?對(duì)象指針
????????addr?=?(unsigned?long)map;??
????????insn[0].imm?=?(u32)addr;???//?將 map ?對(duì)象指針低 32 位放入第一條指令中的 imm 字段
????????insn[1].imm?=?addr?>>?32;??//?將?map??對(duì)象指針高?32?位放入第二條指令中的?imm?字段
于此同時(shí),函數(shù) convert_pseudo_ld_imm64() 還需要清理加載器設(shè)置的 ?src_reg = BPF_PSEUDO_MAP_FD 操作( prog->insns[insn_off].src_reg = BPF_PSEUDO_MAP_FD;), 用于表明完成了整個(gè)指令的重寫(xiě)工作:
????????if?(insn->code?==?(BPF_LD?|?BPF_IMM?|?BPF_DW))
????????????????insn->src_reg?=?0;
如果這里的 my_map 在內(nèi)核中 64 位地址為 0xffff8881384aa200,那么驗(yàn)證器完成第二次變身后的 BPF 字節(jié)碼對(duì)比如下。
替換 map fd 后的字節(jié)碼:
???????7:?18?11?00?00?06?00?00?00?00?00?00?00?00?00?00?00?r1?=?0?ll??#?64?位直接數(shù)賦值?,?r1?=?6?
???????9:?85?00?00?00?01?00?00?00?call?1?????????????????????????????#?調(diào)用?bpf_map_lookup_elem,編號(hào)為?1?
替換為 map 對(duì)象指針后的字節(jié)碼如下:
????? 7:18 01 00?00?00 a2 4a 38 00?00?00?00 81 88 ff ff ??????????#?64?位直接數(shù)賦值?,?r1?=?0xffff8881384aa200?
????? 9:85 00?00?00 30 86 01 00??????????????????#?調(diào)用?bpf_map_lookup_elem,編號(hào)為?1
在完成了上述兩次變身后,當(dāng)在內(nèi)核中調(diào)用 map_lookup_elem() 時(shí),第一個(gè)參數(shù) my_map 的值為 0xffff8881384aa200,
從而實(shí)現(xiàn)了從最早的 ELF 中的 0 ,替換成了 map_fd (6),直到最后的 map 對(duì)象 struct bpf_map * (0xffff8881384aa200)。
提示,內(nèi)核中 bpf_map_lookup_elem 輔助函數(shù)的原型定義為:
static?void?*(*bpf_map_lookup_elem)(struct?bpf_map?*map,?void?*key)
4. 整個(gè)流程總結(jié)
通過(guò)上述 map 訪問(wèn)指令的 2 次大變身,我們可以清晰了解 map 創(chuàng)建、map fd 指令重寫(xiě)和 map ptr 對(duì)象的重寫(xiě),也能夠徹底明白用戶空間 map fd 與內(nèi)核中 map 對(duì)象指針的關(guān)聯(lián)關(guān)系。
俗話說(shuō)一圖勝千言,這里我們用一張圖進(jìn)行整個(gè)流程的總結(jié):

原始圖片來(lái)自于這里 [8],略有修改。
參考
Linux bpf map internals[9] eCHO episode 11: Exploring bpftool with Quentin Monnet[10] ebpf: BPF_FUNC_map_lookup_elem calling convention[11] 邊緣網(wǎng)絡(luò) eBPF 超能力:eBPF map 原理與性能解析
參考資料
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
