eBPF 入門教程
作者:Adrian Ratiu
譯者:狄衛(wèi)華
1. 前言
有興趣了解更多關(guān)于 eBPF 技術(shù)的底層細(xì)節(jié)?那么請(qǐng)繼續(xù)移步,我們將深入研究 eBPF 的底層細(xì)節(jié),從其虛擬機(jī)機(jī)制和工具,到在遠(yuǎn)程資源受限的嵌入式設(shè)備上運(yùn)行跟蹤。
注意:本系列博客文章將集中在 eBPF 技術(shù),因此對(duì)于我們來(lái)講,文中 BPF 和 eBPF 等同,可相互使用。BPF 名字/縮寫已經(jīng)沒(méi)有太大的意義,因?yàn)檫@個(gè)項(xiàng)目的發(fā)展遠(yuǎn)遠(yuǎn)超出了它最初的范圍。BPF 和 eBPF 在該系列中會(huì)交替使用。
第 1 部分[1]和第 2 部分[2] 為新人或那些希望通過(guò)深入了解 eBPF 技術(shù)棧的底層技術(shù)來(lái)進(jìn)一步了解 eBPF 技術(shù)的人提供了深入介紹。 第 3 部分[3]是對(duì)用戶空間工具的概述,旨在提高生產(chǎn)力,建立在第 1 部分和第 2 部分中介紹的底層虛擬機(jī)機(jī)制之上。 第 4 部分[4]側(cè)重于在資源有限的嵌入式系統(tǒng)上運(yùn)行 eBPF 程序,在嵌入式系統(tǒng)中完整的工具鏈技術(shù)棧(BCC/LLVM/python 等)是不可行的。我們將使用占用資源較小的嵌入式工具在 32 位 ARM 上交叉編譯和運(yùn)行 eBPF 程序。只對(duì)該部分感興趣的讀者可選擇跳過(guò)其他部分。 第 5 部分[5]是關(guān)于用戶空間追蹤。到目前為止,我們的努力都集中在內(nèi)核追蹤上,所以是時(shí)候我們關(guān)注一下用戶進(jìn)程了。
如有疑問(wèn)時(shí),可使用該流程圖:

2. eBPF 是什么?
eBPF 是一個(gè)基于寄存器的虛擬機(jī),使用自定義的 64 位 RISC 指令集,能夠在 Linux 內(nèi)核內(nèi)運(yùn)行即時(shí)本地編譯的 "BPF 程序",并能訪問(wèn)內(nèi)核功能和內(nèi)存的一個(gè)子集。這是一個(gè)完整的虛擬機(jī)實(shí)現(xiàn),不要與基于內(nèi)核的虛擬機(jī)(KVM)相混淆,后者是一個(gè)模塊,目的是使 Linux 能夠作為其他虛擬機(jī)的管理程序。eBPF 也是主線內(nèi)核的一部分,所以它不像其他框架那樣需要任何第三方模塊(LTTng[6] 或 SystemTap[7]),而且?guī)缀跛械?Linux 發(fā)行版都默認(rèn)啟用。熟悉 DTrace 的讀者可能會(huì)發(fā)現(xiàn) DTrace/BPFtrace 對(duì)比[8]非常有用。
在內(nèi)核內(nèi)運(yùn)行一個(gè)完整的虛擬機(jī)主要是考慮便利和安全。雖然 eBPF 程序所做的操作都可以通過(guò)正常的內(nèi)核模塊來(lái)處理,但直接的內(nèi)核編程是一件非常危險(xiǎn)的事情 - 這可能會(huì)導(dǎo)致系統(tǒng)鎖定、內(nèi)存損壞和進(jìn)程崩潰,從而導(dǎo)致安全漏洞和其他意外的效果,特別是在生產(chǎn)設(shè)備上(eBPF 經(jīng)常被用來(lái)檢查生產(chǎn)中的系統(tǒng)),所以通過(guò)一個(gè)安全的虛擬機(jī)運(yùn)行本地 JIT 編譯的快速內(nèi)核代碼對(duì)于安全監(jiān)控和沙盒、網(wǎng)絡(luò)過(guò)濾、程序跟蹤、性能分析和調(diào)試都是非常有價(jià)值的。部分簡(jiǎn)單的樣例可以在這篇優(yōu)秀的 eBPF 參考[9]中找到。
基于設(shè)計(jì),eBPF 虛擬機(jī)和其程序有意地設(shè)計(jì)為不是圖靈完備的:即不允許有循環(huán)(正在進(jìn)行的工作是支持有界循環(huán)【譯者注:已經(jīng)支持有界循環(huán),#pragma unroll 指令】),所以每個(gè) eBPF 程序都需要保證完成而不會(huì)被掛起、所有的內(nèi)存訪問(wèn)都是有界和類型檢查的(包括寄存器,一個(gè) MOV 指令可以改變一個(gè)寄存器的類型)、不能包含空解引用、一個(gè)程序必須最多擁有 BPF_MAXINSNS 指令(默認(rèn) 4096)、"主"函數(shù)需要一個(gè)參數(shù)(context)等等。當(dāng) eBPF 程序被加載到內(nèi)核中,其指令被驗(yàn)證模塊解析為有向環(huán)狀圖,上述的限制使得正確性可以得到簡(jiǎn)單而快速的驗(yàn)證。
譯者注:BPF_MAXINSNS 這個(gè)限制已經(jīng)被放寬至 100 萬(wàn)條指令(BPF_COMPLEXITY_LIMIT_INSNS),但是非特權(quán)執(zhí)行的 BPF 程序這個(gè)限制仍然會(huì)保留。
歷史上,eBPF (cBPF) 虛擬機(jī)只在內(nèi)核中可用,用于過(guò)濾網(wǎng)絡(luò)數(shù)據(jù)包,與用戶空間程序沒(méi)有交互,因此被稱為 "伯克利數(shù)據(jù)包過(guò)濾器"【譯者注:早期的 BPF 實(shí)現(xiàn)被稱為經(jīng)典 cBPF】。從內(nèi)核 v3.18(2014 年)開始,該虛擬機(jī)也通過(guò) bpf() syscall[10] 和uapi/linux/bpf.h[11] 暴露在用戶空間,這導(dǎo)致其指令集在當(dāng)時(shí)被凍結(jié),成為公共 ABI,盡管后來(lái)仍然可以(并且已經(jīng))添加新指令。
因?yàn)閮?nèi)核內(nèi)的 eBPF 實(shí)現(xiàn)是根據(jù) GPLv2 授權(quán)的,它不能輕易地被非 GPL 用戶重新分發(fā),所以也有一個(gè)替代的 Apache 授權(quán)的用戶空間 eBPF 虛擬機(jī)實(shí)現(xiàn),稱為 "uBPF"。撇開法律條文不談,基于用戶空間的實(shí)現(xiàn)對(duì)于追蹤那些需要避免內(nèi)核-用戶空間上下文切換成本的性能關(guān)鍵型應(yīng)用很有用。
3. eBPF 是怎么工作的?
eBPF 程序在事件觸發(fā)時(shí)由內(nèi)核運(yùn)行,所以可以被看作是一種函數(shù)掛鉤或事件驅(qū)動(dòng)的編程形式。從用戶空間運(yùn)行按需 eBPF 程序的價(jià)值較小,因?yàn)樗械陌葱栌脩粽{(diào)用已經(jīng)通過(guò)正常的非 VM 內(nèi)核 API 調(diào)用("syscalls")來(lái)處理,這里 VM 字節(jié)碼帶來(lái)的價(jià)值很小。事件可由 kprobes/uprobes、tracepoints、dtrace probes、socket 等產(chǎn)生。這允許在內(nèi)核和用戶進(jìn)程的指令中鉤住(hook)和檢查任何函數(shù)的內(nèi)存、攔截文件操作、檢查特定的網(wǎng)絡(luò)數(shù)據(jù)包等等。一個(gè)比較好的參考是 Linux 內(nèi)核版本對(duì)應(yīng)的 BPF 功能[12]。
如前所述,事件觸發(fā)了附加的 eBPF 程序的執(zhí)行,后續(xù)可以將信息保存至 map 和環(huán)形緩沖區(qū)(ringbuffer)或調(diào)用一些特定 API 定義的內(nèi)核函數(shù)的子集。一個(gè) eBPF 程序可以鏈接到多個(gè)事件,不同的 eBPF 程序也可以訪問(wèn)相同的 map 以共享數(shù)據(jù)。一個(gè)被稱為 "program array" 的特殊讀/寫 map 存儲(chǔ)了對(duì)通過(guò) bpf() 系統(tǒng)調(diào)用加載的其他 eBPF 程序的引用,在該 map 中成功的查找則會(huì)觸發(fā)一個(gè)跳轉(zhuǎn),而且并不返回到原來(lái)的 eBPF 程序。這種 eBPF 嵌套也有限制,以避免無(wú)限的遞歸循環(huán)。
運(yùn)行 eBPF 程序的步驟:
用戶空間將字節(jié)碼和程序類型一起發(fā)送到內(nèi)核,程序類型決定了可以訪問(wèn)的內(nèi)核區(qū)域【譯者注:主要是 BPF 幫助函數(shù)的各種子集】。 內(nèi)核在字節(jié)碼上運(yùn)行驗(yàn)證器,以確保程序可以安全運(yùn)行(kernel/bpf/verifier.c)。 內(nèi)核將字節(jié)碼編譯為本地代碼,并將其插入(或附加到)指定的代碼位置。【譯者注:如果啟用了 JIT 功能,字節(jié)碼編譯為本地代碼】。 插入的代碼將數(shù)據(jù)寫入環(huán)形緩沖區(qū)或通用鍵值 map。 用戶空間從共享 map 或環(huán)形緩沖區(qū)中讀取結(jié)果值。
map 和環(huán)形緩沖區(qū)結(jié)構(gòu)是由內(nèi)核管理的(就像管道和 FIFO 一樣),獨(dú)立于掛載的 eBPF 或訪問(wèn)它們的用戶程序。對(duì) map 和環(huán)形緩沖區(qū)結(jié)構(gòu)的訪問(wèn)是異步的,通過(guò)文件描述符和引用計(jì)數(shù)實(shí)現(xiàn),可確保只要有至少一個(gè)程序還在訪問(wèn),結(jié)構(gòu)就能夠存在。加載的 JIT 后代碼通常在加載其的用戶進(jìn)程終止時(shí)被刪除,盡管在某些情況下,它仍然可以在加載進(jìn)程的生命期之后繼續(xù)存在。
為了方便編寫 eBPF 程序和避免進(jìn)行原始的 bpf()系統(tǒng)調(diào)用,內(nèi)核提供了方便的 libbpf 庫(kù)[13],包含系統(tǒng)調(diào)用函數(shù)包裝器,如bpf_load_program[14] 和結(jié)構(gòu)定義(如 bpf_map[15]),在 LGPL 2.1 和 BSD 2-Clause 下雙重許可,可以靜態(tài)鏈接或作為 DSO。內(nèi)核代碼也提供了一些使用 libbpf 簡(jiǎn)潔的例子,位于目錄 samples/bpf/[16] 中。
4. 樣例學(xué)習(xí)
內(nèi)核開發(fā)者非常可憐,因?yàn)閮?nèi)核是一個(gè)獨(dú)立的項(xiàng)目,因而沒(méi)有用戶空間諸如 Glibc、LLVM、JavaScript 和 WebAssembly 諸如此類的好東西! - 這就是為什么內(nèi)核中 eBPF 例子中會(huì)包含原始字節(jié)碼或通過(guò) libbpf 加載預(yù)組裝的字節(jié)碼文件。我們可以在 sock_example.c[17] 中看到這一點(diǎn),這是一個(gè)簡(jiǎn)單的用戶空間程序,使用 eBPF 來(lái)計(jì)算環(huán)回接口上統(tǒng)計(jì)接收到 ?TCP、UDP 和 ICMP 協(xié)議包的數(shù)量。
我們跳過(guò)微不足道的的 main[18] 和 open_raw_sock[19] 函數(shù),而專注于神奇的代碼 test_sock[20]。
static?int?test_sock(void)
{
?int?sock?=?-1,?map_fd,?prog_fd,?i,?key;
?long?long?value?=?0,?tcp_cnt,?udp_cnt,?icmp_cnt;
?map_fd?=?bpf_create_map(BPF_MAP_TYPE_ARRAY,?sizeof(key),?sizeof(value),?256,?0);
?if?(map_fd?0)?{printf("failed?to?create?map'%s'\n",?strerror(errno));
??goto?cleanup;
?}
?struct?bpf_insn?prog[]?=?{BPF_MOV64_REG(BPF_REG_6,?BPF_REG_1),
??BPF_LD_ABS(BPF_B,?ETH_HLEN?+?offsetof(struct?iphdr,?protocol)?/*?R0?=?ip->proto?*/),
??BPF_STX_MEM(BPF_W,?BPF_REG_10,?BPF_REG_0,?-4),?/*?*(u32?*)(fp?-?4)?=?r0?*/
??BPF_MOV64_REG(BPF_REG_2,?BPF_REG_10),
??BPF_ALU64_IMM(BPF_ADD,?BPF_REG_2,?-4),?/*?r2?=?fp?-?4?*/
??BPF_LD_MAP_FD(BPF_REG_1,?map_fd),
??BPF_RAW_INSN(BPF_JMP?|?BPF_CALL,?0,?0,?0,?BPF_FUNC_map_lookup_elem),
??BPF_JMP_IMM(BPF_JEQ,?BPF_REG_0,?0,?2),
??BPF_MOV64_IMM(BPF_REG_1,?1),?/*?r1?=?1?*/
??BPF_RAW_INSN(BPF_STX?|?BPF_XADD?|?BPF_DW,?BPF_REG_0,?BPF_REG_1,?0,?0),?/*?xadd?r0?+=?r1?*/
??BPF_MOV64_IMM(BPF_REG_0,?0),?/*?r0?=?0?*/
??BPF_EXIT_INSN(),};
?size_t?insns_cnt?=?sizeof(prog)?/?sizeof(struct?bpf_insn);
?prog_fd?=?bpf_load_program(BPF_PROG_TYPE_SOCKET_FILTER,?prog,?insns_cnt,
???????"GPL",?0,?bpf_log_buf,?BPF_LOG_BUF_SIZE);
?if?(prog_fd?0)?{printf("failed?to?load?prog'%s'\n",?strerror(errno));
??goto?cleanup;
?}
?sock?=?open_raw_sock("lo");
?if?(setsockopt(sock,?SOL_SOCKET,?SO_ATTACH_BPF,?&prog_fd,?sizeof(prog_fd))?0)?{printf("setsockopt?%s\n",?strerror(errno));
??goto?cleanup;
?}
首先,通過(guò) libbpf API 創(chuàng)建一個(gè) BPF map,該行為就像一個(gè)最大 256 個(gè)元素的固定大小的數(shù)組。按 IPROTO_*[21] 定義的鍵索引網(wǎng)絡(luò)協(xié)議(2 字節(jié)的 word),值代表各自的數(shù)據(jù)包計(jì)數(shù)(4 字節(jié)大小)。除了數(shù)組,eBPF 映射還實(shí)現(xiàn)了其他數(shù)據(jù)結(jié)構(gòu)類型[22],如棧或隊(duì)列。
接下來(lái),eBPF 的字節(jié)碼指令數(shù)組使用方便的內(nèi)核宏[23]進(jìn)行定義。在這里,我們不會(huì)討論字節(jié)碼的細(xì)節(jié)(這將在第 2 部分描述機(jī)器后進(jìn)行)。更高的層次上,字節(jié)碼從數(shù)據(jù)包緩沖區(qū)中讀取協(xié)議字,在 map 中查找,并增加特定的數(shù)據(jù)包計(jì)數(shù)。
然后 BPF 字節(jié)碼被加載到內(nèi)核中,并通過(guò) libbpf 的 bpf_load_program 返回 fd 引用來(lái)驗(yàn)證正確/安全。調(diào)用指定了 eBPF 程序類型[24],這決定了它可以訪問(wèn)哪些內(nèi)核子集。因?yàn)闃永且粋€(gè) SOCKET_FILTER 類型,因此提供了一個(gè)指向當(dāng)前網(wǎng)絡(luò)包的參數(shù)。最后,eBPF 的字節(jié)碼通過(guò)套接字層被附加到一個(gè)特定的原始套接字上,之后在原始套接字上接受到的每一個(gè)數(shù)據(jù)包運(yùn)行 eBPF 字節(jié)碼,無(wú)論協(xié)議如何。
剩余的工作就是讓用戶進(jìn)程開始輪詢共享 map 的數(shù)據(jù)。
?for?(i?=?0;?i?10;?i++)?{
??key?=?IPPROTO_TCP;
??assert(bpf_map_lookup_elem(map_fd,?&key,?&tcp_cnt)?==?0);
??key?=?IPPROTO_UDP;
??assert(bpf_map_lookup_elem(map_fd,?&key,?&udp_cnt)?==?0);
??key?=?IPPROTO_ICMP;
??assert(bpf_map_lookup_elem(map_fd,?&key,?&icmp_cnt)?==?0);
??printf("TCP?%lld?UDP?%lld?ICMP?%lld?packets\n",
?????????tcp_cnt,?udp_cnt,?icmp_cnt);
??sleep(1);
?}
}
5. 總結(jié)
第 1 部分介紹了 eBPF 的基礎(chǔ)知識(shí),我們通過(guò)如何加載字節(jié)碼和與 eBPF 虛擬機(jī)通信的例子進(jìn)行了講述。由于篇幅限制,編譯和運(yùn)行例子作為留給讀者的練習(xí)。我們也有意不去分析具體的 eBPF 字節(jié)碼指令,因?yàn)檫@將是第 2 部分的重點(diǎn)。在我們研究的例子中,用戶空間通過(guò) libbpf 直接用 C 語(yǔ)言從內(nèi)核虛擬機(jī)中讀取 eBPF map 值(使用 10 次 1 秒的睡眠!),這很笨重,而且容易出錯(cuò),而且很快就會(huì)變得很復(fù)雜,所以在第 3 部分,我們將研究更高級(jí)別的工具,通過(guò)腳本或特定領(lǐng)域的語(yǔ)言自動(dòng)與虛擬機(jī)交互。
繼續(xù)閱讀(eBPF 概述,第 2 部分:機(jī)器和字節(jié)碼)[25]...。
引用鏈接
第 1 部分: https://www.collabora.com/news-and-blog/blog/2019/04/05/an-ebpf-overview-part-1-introduction/
[2]第 2 部分: https://www.collabora.com/news-and-blog/blog/2019/04/15/an-ebpf-overview-part-2-machine-and-bytecode/
[3]第 3 部分: https://www.collabora.com/news-and-blog/blog/2019/04/26/an-ebpf-overview-part-3-walking-up-the-software-stack/
[4]第 4 部分: https://www.collabora.com/news-and-blog/blog/2019/05/06/an-ebpf-overview-part-4-working-with-embedded-systems/
[5]第 5 部分: https://www.collabora.com/news-and-blog/blog/2019/05/14/an-ebpf-overview-part-5-tracing-user-processes/
[6]LTTng: https://lttng.org/docs/v2.10/#doc-lttng-modules
[7]SystemTap: https://kernelnewbies.org/SystemTap
[8]DTrace/BPFtrace 對(duì)比: http://www.brendangregg.com/blog/2018-10-08/dtrace-for-linux-2018.html
[9]eBPF 參考: http://www.brendangregg.com/ebpf.html
[10]bpf() syscall: https://github.com/torvalds/linux/blob/v4.20/tools/lib/bpf
[11]uapi/linux/bpf.h: https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/bpf.h
[12]Linux 內(nèi)核版本對(duì)應(yīng)的 BPF 功能: https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md
[13]libbpf 庫(kù): https://github.com/torvalds/linux/blob/v4.20/tools/lib/bpf
[14]bpf_load_program: https://github.com/torvalds/linux/blob/v4.20/tools/lib/bpf/bpf.c#L214
[15]bpf_map: https://github.com/torvalds/linux/blob/v4.20/tools/lib/bpf/libbpf.c#L157
[16]samples/bpf/: https://github.com/torvalds/linux/blob/v4.20/samples/bpf/
[17]sock_example.c: https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c
[18]main: https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c#L98
[19]open_raw_sock: https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.h#L13
[20]test_sock: https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c#L35
[21]IPROTO_*: https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/in.h#L28
[22]其他數(shù)據(jù)結(jié)構(gòu)類型: https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/bpf.h#L113
[23]內(nèi)核宏: https://github.com/torvalds/linux/blob/v4.20/samples/bpf/bpf_insn.h
[24]程序類型: https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/bpf.h#L138
[25]繼續(xù)閱讀(eBPF 概述,第 2 部分:機(jī)器和字節(jié)碼): https://www.collabora.com/news-and-blog/blog/2019/04/15/an-ebpf-overview-part-2-machine-and-bytecode/
