BPF簡(jiǎn)介
BPF,全稱是Berkeley Packet Filter(伯克利數(shù)據(jù)包過(guò)濾器)的縮寫(xiě)。其誕生于1992年,最初的目的是提升網(wǎng)絡(luò)包過(guò)濾工具的性能。后面,隨著這個(gè)工具重新實(shí)現(xiàn)BPF的內(nèi)核補(bǔ)丁和不斷完善代碼,BPF程序變成了一個(gè)更通用的執(zhí)行引擎,可以完成多種任務(wù)。簡(jiǎn)單來(lái)說(shuō),BPF提供了一種在各種內(nèi)核時(shí)間和應(yīng)用程序事件發(fā)生時(shí)運(yùn)行一小段程序的機(jī)制。其允許內(nèi)核在系統(tǒng)和應(yīng)用程序事件發(fā)生時(shí)運(yùn)行一小段程序,這樣就將內(nèi)核變得完全可編程,允許用戶定制和控制他們的系統(tǒng)。
BPF其有指令集、存儲(chǔ)對(duì)象和輔助函數(shù)等幾部分組成。由于它采取了虛擬指令集規(guī)范,因此也可將其視為一種虛擬機(jī)實(shí)現(xiàn)。當(dāng)Linux指定的時(shí)候,其會(huì)提供兩種執(zhí)行機(jī)制:一個(gè)解釋器和一個(gè)將BPF指令動(dòng)態(tài)轉(zhuǎn)換為本地化指令的即時(shí)編程器。在實(shí)際執(zhí)行之前,BPF指令必須先通過(guò)驗(yàn)證器的安全性檢查,以確保BPF程序自身不會(huì)崩潰或者損壞內(nèi)核。
注:擴(kuò)展后的BPF通??s寫(xiě)為eBPF,但官方縮寫(xiě)仍然是BPF。在內(nèi)核之中只有一個(gè)執(zhí)行引擎,其同時(shí)支持eBPF和經(jīng)典BPF程序。
BPF驗(yàn)證器
BPF允許任何人在Linux內(nèi)核之中執(zhí)行任意的代碼,這聽(tīng)起來(lái)的十分危險(xiǎn),但是由于有著B(niǎo)PF驗(yàn)證器使得這一過(guò)程變的相當(dāng)?shù)陌踩?。BPF時(shí)內(nèi)核的一個(gè)模塊,所有的BPF程序都必須經(jīng)過(guò)它的審查才能夠被加載到內(nèi)核之中去運(yùn)行。
驗(yàn)證器執(zhí)行的第一項(xiàng)檢查就是對(duì)BPF虛擬機(jī)加載的代碼進(jìn)行靜態(tài)分析。這一步的目的是保證程序可以按照預(yù)期去結(jié)束,而不會(huì)產(chǎn)生死循環(huán)拜拜浪費(fèi)系統(tǒng)資源。驗(yàn)證器會(huì)創(chuàng)建一個(gè)DAG(有向無(wú)環(huán)圖),將BPF程序的每個(gè)執(zhí)行首位相連之后去執(zhí)行DFS(深度優(yōu)先遍歷),當(dāng)且僅當(dāng)每個(gè)路徑都能達(dá)到DAG的底部才會(huì)通過(guò)驗(yàn)證。
之后其會(huì)執(zhí)行第二項(xiàng)檢查,也就是對(duì)BPF程序執(zhí)行預(yù)執(zhí)行處理。這個(gè)時(shí)候驗(yàn)證器會(huì)去分析程序執(zhí)行的每條指令,確保不會(huì)執(zhí)行無(wú)效的指令。同時(shí)也會(huì)檢查所有內(nèi)存指針是否可以正確訪問(wèn)和解引用。
尾部調(diào)用
BPF程序可以使用尾部調(diào)用來(lái)調(diào)用其他BPF程序,這是個(gè)強(qiáng)大的功能。其允許通過(guò)組合比較小的BPF功能來(lái)實(shí)現(xiàn)更為復(fù)雜的程序。當(dāng)從一個(gè)BPF程序調(diào)用另外一個(gè)BPF程序的時(shí)候,內(nèi)核會(huì)完全重置程序上下文。這意味著如果想要在多個(gè)BPF程序之中共享信息這是做不到的。為了解決程序間共享信息的問(wèn)題,BPF引入了BPF映射的機(jī)制來(lái)解決這個(gè)問(wèn)題,我們會(huì)在后面詳細(xì)的介紹BPF映射機(jī)制。
注:內(nèi)核5.2 版本之前BPF只允許執(zhí)行4096條指令,所以才有了尾部調(diào)用這個(gè)特性。從5.2開(kāi)始,指令限制擴(kuò)展到了100w條,尾部調(diào)用的遞歸層次也有了32次的限制。
BPF 環(huán)境配置
內(nèi)核升級(jí)
BPF程序在4系內(nèi)核之后就已經(jīng)成為了內(nèi)核的頂級(jí)子系統(tǒng),但是為了讓我們的系統(tǒng)能夠穩(wěn)定運(yùn)行BPF程序,還是推薦安裝5系內(nèi)核。首先,我們可以使用如下的命令獲取當(dāng)前系統(tǒng)的版本:
uname -a
Linux localhost 5.0.9 #2 SMP PREEMPT Mon Feb 27 00:00:23 CST 2023 x86_64 x86_64 x86_64 GNU/Linux
筆者這里的系統(tǒng)已經(jīng)經(jīng)過(guò)升級(jí)了,如果沒(méi)有經(jīng)歷過(guò)升級(jí),可以按照如下的命令獲取系統(tǒng)的源碼:
# 獲取相應(yīng)版本的內(nèi)核源碼
cd /tmp
wget -c https://mirrors.aliyun.com/linux-kernel//v5.x/linux-5.0.9.tar.gz -O - | tar -xz
之后的過(guò)程,同學(xué)們可以百度相應(yīng)的教程獲取安裝,本文章將專注于BPF技術(shù)的使用。
安裝好相應(yīng)內(nèi)核之后,為了讓我們?cè)陂_(kāi)發(fā)的時(shí)候更為容易,推薦這里將內(nèi)核源碼單獨(dú)編譯一下,方便我們鏈接:
tar -xvf linux-5.0.9.tar.gz
sudo mv linux-5.0.9 /kernel-src
cd /kernel-src/tools/lib/bpf
sudo make && sudo make install prefix=/
依賴環(huán)境安裝
升級(jí)好內(nèi)核環(huán)境之后,我們還需要安裝BPF程序的依賴環(huán)境,主要可以分為三個(gè)部分:
- BCC 工具包:通過(guò)github 獲取相應(yīng)的源碼進(jìn)行安裝
- LLVM 編譯器:訪問(wèn)官網(wǎng)可獲取安裝教程
- 其他依賴程序:
sudo dnf install make glibc-devel.i686 elfutils-libelf-devel wget tar clang bcc strace kernel-devel -y
運(yùn)行第一個(gè)BPF程序
在安裝好上述程序之后,我們使用如下的代碼可以來(lái)測(cè)試我們的環(huán)境是否配置完成。BPF程序可以由C語(yǔ)言來(lái)編寫(xiě),之后由LLVM編譯,其可以將C語(yǔ)言寫(xiě)的程序編譯成能夠加載到內(nèi)核執(zhí)行的匯編代碼。
# 指定編譯器為clang
CLANG = clang
# 編譯完后的程序名稱
EXECABLE = monitor-exec
# 源碼名稱
BPFCODE = bpf_program
# BPF依賴地址
BPFTOOLS = /kernel-src/samples/bpf
BPFLOADER = $(BPFTOOLS)/bpf_load.c
# 指定頭文件
CCINCLUDE += -I/kernel-src/tools/testing/selftests/bpf
LOADINCLUDE += -I/kernel-src/samples/bpf
LOADINCLUDE += -I/kernel-src/tools/lib
LOADINCLUDE += -I/kernel-src/tools/perf
LOADINCLUDE += -I/kernel-src/tools/include
LIBRARY_PATH = -L/usr/local/lib64
BPFSO = -lbpf
CFLAGS += $(shell grep -q "define HAVE_ATTR_TEST 1" /kernel-src/tools/perf/perf-sys.h
&& echo "-DHAVE_ATTR_TEST=0")
.PHONY: clean $(CLANG) bpfload build
clean:
rm -f *.o *.so $(EXECABLE)
build: ${BPFCODE.c} ${BPFLOADER}
$(CLANG) -O2 -target bpf -c $(BPFCODE:=.c) $(CCINCLUDE) -o ${BPFCODE:=.o}
bpfload: build
# 編譯程序
clang $(CFLAGS) -o $(EXECABLE) -lelf $(LOADINCLUDE) $(LIBRARY_PATH) $(BPFSO)
$(BPFLOADER) loader.c
$(EXECABLE): bpfload
.DEFAULT_GOAL := $(EXECABLE)
程序源碼有兩個(gè),一個(gè)是bpf_program.c這里面存放的是要執(zhí)行的BPF源碼,其會(huì)被編譯成為一個(gè).o文件。
在這里我們使用BPF提供的SEC屬性告知BPF虛擬機(jī)在何時(shí)運(yùn)行此程序。下面的代碼會(huì)在execve系統(tǒng)調(diào)用跟蹤點(diǎn)被執(zhí)行的時(shí)候運(yùn)行BPF程序。當(dāng)內(nèi)核檢測(cè)到execve的時(shí)候,BPF程序被執(zhí)行時(shí),我們會(huì)看到輸出消息"Hello, World, BPF!"
#include < linux/bpf.h >
#define SEC(NAME) __attribute__((section(NAME), used))
static int (*bpf_trace_printk)(const char *fmt, int fmt_size,
...) = (void *)BPF_FUNC_trace_printk;
SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx) {
char msg[] = "Hello, World, BPF!";
bpf_trace_printk(msg, sizeof(msg));
return 0;
}
// 程序許可證,linux內(nèi)核只允許加載GPL許可的程序
char _license[] SEC("license") = "GPL";
上面的.o
文件會(huì)被下面的這個(gè)由loader.c
編譯成為的moniter-exec
程序去執(zhí)行。其會(huì)把BPF程序加載到內(nèi)核之中去運(yùn)行,這里依賴的就是我們使用的load_bpf_file
,其將會(huì)獲取一個(gè)二進(jìn)制文件并把它加載到內(nèi)核之中。
#include "bpf_load.h"
#include < stdio.h >
int main(int argc, char **argv) {
if (load_bpf_file("bpf_program.o") != 0) {
printf("The kernel didn't load the BPF programn");
return -1;
}
read_trace_pipe();
return 0;
}
之后我們執(zhí)行如下的命令去編譯上述的代碼:
make
# 運(yùn)行以下程序
sudo ./loader
BPF映射
BPF映射以的形式會(huì)被保存到內(nèi)核之中,其可以被任何其他的BPF程序訪問(wèn)。用戶空間的程序也可以通過(guò)文件描述符訪問(wèn)BPF映射。BPF映射之中可以保存事先指定大小的任何類型的數(shù)據(jù)。內(nèi)核會(huì)將數(shù)據(jù)看作二進(jìn)制塊,這意味著內(nèi)核并不關(guān)系BPF映射保存的具體內(nèi)容。
此內(nèi)容會(huì)存在較多的代碼,這里會(huì)將相關(guān)所需要的MakeFile文件內(nèi)容展示出來(lái):
CLANG = clang
INCLUDE_PATH += -I/kernel-src/tools/lib/bpf
INCLUDE_PATH += -I/kernel-src/tools/**
LIBRARY_PATH = -L/usr/local/lib64
BPFSO = -lbpf
.PHONY: clean
clean:
rm -f # 要?jiǎng)h除的BPF模塊
build: # 填寫(xiě)要編譯的 BPF程序模塊
.DEFAULT_GOAL := build
創(chuàng)建BPF映射
創(chuàng)建BPF映射的最值方式就是使用bpf_create_map
系統(tǒng)調(diào)用。這個(gè)函數(shù)需要傳入五個(gè)參數(shù):
- map_type:map的類型,如果設(shè)置為
BPF_MAP_CREATE
,則表示創(chuàng)建一個(gè)新的映射。 - key_size: key的字節(jié)數(shù)
- value_size:value的字節(jié)數(shù)
- max_entries:最大的鍵值對(duì)數(shù)量
- map_flags:map創(chuàng)建行為的參數(shù),0表示不預(yù)先分配內(nèi)存
int bpf_create_map(bpf_map_type map_type, int key_size, int value_size, int max_entries, int map_flags);
如果創(chuàng)建成功,這個(gè)接口會(huì)返回一個(gè)指向這個(gè)map的文件描述符。如果創(chuàng)建失敗,將返回-1。失敗會(huì)有三種原因,我們可以通過(guò)errno
來(lái)進(jìn)行區(qū)分。
- 如果屬性無(wú)效,內(nèi)核將errnor變量設(shè)置為
EINVAL
; - 如果用戶權(quán)限不夠,內(nèi)核將errno變量設(shè)置為
EPERM
; - 如果沒(méi)有足夠的內(nèi)存來(lái)保存映射的話,內(nèi)核將errno變量設(shè)置為
ENOMEM
;
Demo
#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < stdlib.h >
#include < unistd.h >
int main(int argc, char **argv) {
//# create
int fd = bpf_create_map(BPF_MAP_TYPE_HASH, sizeof(int), sizeof(int), 100, 0);
if (fd < 0) {
printf("Failed to create map: %d (%s)n", fd, strerror(errno));
return -1;
}
printf("Create BPF map success!n");
}
我們?cè)谝婚_(kāi)始提到的MakeFile文件之中添加如下信息即可編譯上述代碼:
create: map_create.c
clang -o create -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: create
最后運(yùn)行編譯后的程序:
sudo ./create
Create BPF map success!
BPF映射類型
在Demo之中我們使用到了BPF_MAP_TYPE_HASH
這個(gè)map類型,其表示在內(nèi)核空間之中創(chuàng)建一個(gè)哈希表映射。除此之外,BPF還支持如下的Map類型:
- BPF_MAP_TYPE_HASH: 哈希表映射,和我們熟知的哈希表是類似的。該映射可以使用任意大小的Key和Value,內(nèi)核會(huì)按照需求分配和釋放他們。當(dāng)在哈希表映射上使用更新操作的時(shí)候,內(nèi)核會(huì)自動(dòng)的更新元素。
- BPF_MAP_TYPE_ARRAY:數(shù)據(jù)映射,在對(duì)數(shù)據(jù)初始化的時(shí)候,所有元素在內(nèi)存之中將預(yù)分配空間并且設(shè)置為0。數(shù)據(jù)映射的Key必須是4字節(jié)的,而且使用數(shù)組映射的一個(gè)缺點(diǎn)是映射之中的元素不能夠被刪除,這使得無(wú)法使數(shù)據(jù)變小。如果在數(shù)組上執(zhí)行刪除操作,那么用戶將得到一個(gè)EINVAL錯(cuò)誤。
- BPF_MAP_TYPE_PROG_ARRAY:程序數(shù)組映射,這種類型保存對(duì)BPF程序的引用(其他BPF程序的文件描述符),程序數(shù)據(jù)映射類型可以使用bpf_tail_call來(lái)執(zhí)行剛剛提到的尾部調(diào)用。
- BPF_MAP_TYPE_PERF_EVENT_AYYAY:Perf事件數(shù)組映射,該映射將perf_events數(shù)據(jù)存儲(chǔ)在環(huán)形緩存區(qū),用于BPF程序和用戶空間程序進(jìn)行實(shí)時(shí)通信。其可以將內(nèi)核跟蹤工具發(fā)出的事件轉(zhuǎn)發(fā)給用戶空間程序,使很多可觀測(cè)工具的基礎(chǔ)。
- BPF_MAP_TYPE_PERCUP_HASH:哈希表映射的改進(jìn)版本,我們可以將此哈希表分配給單個(gè)獨(dú)立的CPU(每個(gè)CPU都有自己獨(dú)立的哈希表),而不是多個(gè)CPU共享一個(gè)哈希表。
- BPF_MAP_TYPE_PRECPU_ARRAY:數(shù)據(jù)映射的改進(jìn)版本,也是每個(gè)CPU擁有自己獨(dú)立的數(shù)組。
- BPF_MAP_TYPE_STACK_TRACE:棧跟蹤信息,可以結(jié)合內(nèi)核開(kāi)發(fā)人員添加的幫助函數(shù)bpf_get_stackid將棧跟蹤信息寫(xiě)入到該映射。
持久化BPF MAP
BPF映射的基本特征使基于文件描述符的,這意味著關(guān)閉文件描述符后,映射及其所保存的所有信息都會(huì)消失。這意味著我們無(wú)法獲取已經(jīng)結(jié)束的BPF程序保存在映射之中的信息,在Linux 內(nèi)核4.4 版本之后,引入了兩個(gè)新的系統(tǒng)調(diào)用,bpf_obj_pin用來(lái)固定(固定后不可更改)和bpf_obj_get獲取來(lái)自BPF虛擬文件系統(tǒng)的映射和BPF程序。
BPF虛擬文件系統(tǒng)的默認(rèn)目錄使/sys/fs/bpf,如果Linux系統(tǒng)內(nèi)核不支持BPF,可以使用mount命令掛載此文件系統(tǒng):
mount -t bpf /sys/fs/bpf /sys/fs/bpf
BPF固定的系統(tǒng)調(diào)用為bpf_obj_pin
,其函數(shù)原型如下:
- file_fd:表示map的文件描述符
- file_path:要固定到的文件路徑
int bpf_obj_pin(int file_fd, const char* file_path)
Demo
#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < string.h >
#include < unistd.h >
#include < stdlib.h >
#include
static const char *file_path = "/sys/fs/bpf/my_hash";
int main(int argc, char **argv) {
//# create
int fd = bpf_create_map(BPF_MAP_TYPE_HASH, sizeof(int), sizeof(int), 100, 0);
if (fd < 0) {
printf("Failed to create map: %d (%s)n", fd, strerror(errno));
return -1;
}
int pinned = bpf_obj_pin(fd, file_path);
if (pinned < 0) {
printf("Failed to pin map to the file system: %d (%s)n", pinned,
strerror(errno));
return -1;
}
return 0;
}
我們?cè)谝婚_(kāi)始提到的MakeFile文件之中添加如下信息即可編譯上述代碼:
save: map_save.c
clang -o save -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: save
之后,我們可以查看這個(gè)目錄查看是否固定成功了:
sudo ls /sys/fs/bpf/
my_hash
對(duì)BPF 元素進(jìn)行CRUD
Update
我們可以使用bpf_map_update_elem系統(tǒng)調(diào)用去插入元素到剛創(chuàng)建的map之中。內(nèi)核程序需要從bpf/bpf_helpers.h文件加載此函數(shù),而用戶空間程序則需要從tools/lib/bpf/bpf.h文件加載,所以內(nèi)核程序訪問(wèn)的函數(shù)簽名和用戶空間之不同的。當(dāng)然,訪問(wèn)的行為也是不同的:內(nèi)核程序可以原子的執(zhí)行更新操作,用戶空間則需要發(fā)送消息到內(nèi)核,之后先復(fù)制值,然后再進(jìn)行更新映射。這意味著更新操作不是原子性的。
下面使這個(gè)函數(shù)的函數(shù)原型,如果執(zhí)行成功,該函數(shù)返回0;如果失敗,則將返回復(fù)數(shù)并且把失敗的原因?qū)懭肴肿兞縠rrno之中。
- file_fd:map的文件描述符表示
- key:指向key的指針
- value:指向value的指針
- type:表示更新映射的方式。
- 如果傳入0,表示元素存在則更新,不存在則創(chuàng)建;
- 如果傳入1,表示在元素不存在的時(shí)候,內(nèi)核創(chuàng)建元素
- 如果傳入2,表示元素存在的時(shí)候,內(nèi)核更新元素
int bpf_map_update_elem(int file_fd, void* key, void* value, int type);
Demo
#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < stdlib.h >
#include < string.h >
#include < unistd.h >
#include "bpf.h"
extern char *optarg;
extern int optind;
extern int opterr;
extern int optopt;
static const char *file_path = "/sys/fs/bpf/my_hash";
int main(int argc, char **argv) {
char ch;
int key;
int value;
while ((ch = getopt(argc, argv, "k:v:")) != -1) {
switch (ch) {
case 'k':
printf("set key: %sn", optarg);
key = atoi(optarg);
break;
case 'v':
printf("set value: %sn", optarg);
value = atoi(optarg);
break;
}
}
int fd, added, pinned;
//# open
fd = bpf_obj_get(file_path);
if (fd < 0) {
printf("Failed to fetch the map: %d (%s)n", fd, strerror(errno));
return -1;
}
added = bpf_map_update_elem(fd, &key, &value, BPF_ANY);
if (added < 0) {
printf("Failed to update map: %d (%s)n", added, strerror(errno));
return -1;
}
return 0;
}
我們?cè)谝婚_(kāi)始提到的MakeFile文件之中添加如下信息即可編譯上述代碼:
update: map_update.c
clang -o update -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: update
最后運(yùn)行編譯后的程序:
sudo ./update -k 1 -v 9
set key: 1
set value: 9
Fetch
當(dāng)新元素寫(xiě)入到map之后,我們可以使用bpf_map_lookup_elem
系統(tǒng)調(diào)用來(lái)讀取map之中的元素,其函數(shù)原型如下:
下面使這個(gè)函數(shù)的函數(shù)原型,如果執(zhí)行成功,該函數(shù)返回0;如果失敗,則將返回復(fù)數(shù)并且把失敗的原因?qū)懭肴肿兞縠rrno之中。
- file_fd:map的文件描述符表示
- key:指向key的指針
- value:指向value的指針
int bpf_map_lookp_elem(int file_fd, void* key, void* value);
Demo
#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < string.h >
#include "bpf.h"
#include < unistd.h >
#include < stdlib.h >
#include "bpf.h"
extern char* optarg;
extern int optind;
extern int opterr;
extern int optopt;
static const char *file_path = "/sys/fs/bpf/my_hash";
int main(int argc, char **argv) {
char ch;
int key;
int value;
while ((ch = getopt(argc, argv, "k:v:")) != -1)
{
switch (ch)
{
case 'k':
key = atoi(optarg);
break;
}
}
int fd, result;
fd = bpf_obj_get(file_path);
if (fd < 0) {
printf("Failed to fetch the map: %d (%s)n", fd, strerror(errno));
return -1;
}
result = bpf_map_lookup_elem(fd, &key, &value);
if (result < 0) {
printf("Failed to read value from the map: %d (%s)n", result,
strerror(errno));
return -1;
}
printf("Value read from the key %d: '%d'n", key,value);
return 0;
}
我們?cè)谝婚_(kāi)始提到的MakeFile文件之中添加如下信息即可編譯上述代碼:
fetch: map_fetch.c
clang -o fetch -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: fetch
最后運(yùn)行編譯后的程序:
sudo ./update -k 1 -v 9
set key: 1
set value: 9
Delete
當(dāng)新元素寫(xiě)入到map之后,我們可以使用bpf_map_delete_elem
系統(tǒng)調(diào)用來(lái)刪除map之中的元素,其函數(shù)原型如下:
下面使這個(gè)函數(shù)的函數(shù)原型,如果執(zhí)行成功,該函數(shù)返回0;如果失敗,則將返回復(fù)數(shù)并且把失敗的原因?qū)懭肴肿兞縠rrno之中。
- file_fd:map的文件描述符表示
- key:指向key的指針
int bpf_map_delete_elem(int file_fd, void* key);
Demo
#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < string.h >
#include "bpf.h"
#include < unistd.h >
#include < stdlib.h >
#include "bpf.h"
extern char* optarg;
extern int optind;
extern int opterr;
extern int optopt;
static const char *file_path = "/sys/fs/bpf/my_hash";
int main(int argc, char **argv) {
char ch;
int key;
int value;
while ((ch = getopt(argc, argv, "k:v:")) != -1)
{
switch (ch)
{
case 'k':
key = atoi(optarg);
break;
}
}
int fd,result;
fd = bpf_obj_get(file_path);
if (fd < 0) {
printf("Failed to fetch the map: %d (%s)n", fd, strerror(errno));
return -1;
}
key = 1;
result = bpf_map_delete_elem(fd, &key);
if (result < 0) {
printf("Failed to delete value from the map: %d (%s)n", fd,
strerror(errno));
return -1;
}
printf("delte key:%d success!n", key);
return 0;
}
我們?cè)谝婚_(kāi)始提到的MakeFile文件之中添加如下信息即可編譯上述代碼:
delete: map_delete.c
clang -o delete -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: delete
最后運(yùn)行編譯后的程序:
sudo ./delete -k 1
delte key:1 success!
Iter
假設(shè)我們寫(xiě)入了很多元素到map之后,我們可以使用bpf_map_get_next_key
系統(tǒng)調(diào)用來(lái)遍歷map之中的元素,其函數(shù)原型如下:
下面使這個(gè)函數(shù)的函數(shù)原型,如果執(zhí)行成功,該函數(shù)返回0;如果失敗,則將返回復(fù)數(shù)并且把失敗的原因?qū)懭肴肿兞縠rrno之中。
- file_fd:map的文件描述符表示
- key:指向key的指針
- next_key:指向下個(gè)key的指針
int bpf_map_get_next_key(int file_fd, void* key, void* next_key);
Demo
#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < stdlib.h >
#include < string.h >
#include < unistd.h >
#include "bpf.h"
extern char *optarg;
extern int optind;
extern int opterr;
extern int optopt;
static const char *file_path = "/sys/fs/bpf/my_hash";
int main(int argc, char **argv) {
int fd, value, result;
fd = bpf_obj_get(file_path);
if (fd < 0) {
printf("Failed to fetch the map: %d (%s)n", fd, strerror(errno));
return -1;
}
int start_key = -1;
int next_key;
while (bpf_map_get_next_key(fd, &start_key, &next_key) == 0) {
start_key = next_key;
printf("Key read from the map: '%d'n", next_key);
}
return 0;
}
Demo
iter: map_iter.c
clang -o iter -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: iter
最后運(yùn)行編譯后的程序:
[ik@localhost chapter-3]$ sudo ./iter
Key read from the map: '2'
Key read from the map: '8'
Key read from the map: '10'
Key read from the map: '5'
Key read from the map: '6'
Key read from the map: '3'
Key read from the map: '4'
Key read from the map: '9'
Key read from the map: '7'
Key read from the map: '11'
BPF跟蹤
跟蹤使一種為了進(jìn)行分析和調(diào)試工作的數(shù)據(jù)收集行為,通過(guò)有效的利用BPF來(lái)使得我們可以以盡可能小的代價(jià)來(lái)訪問(wèn)Linux內(nèi)核和應(yīng)用程序的任何信息。
探針
探針使一種探測(cè)程序,其會(huì)傳遞程序執(zhí)行時(shí)環(huán)境的相關(guān)信息,我們通過(guò)BPF探針收集系統(tǒng)之中的數(shù)據(jù)以方便我們后續(xù)進(jìn)行探索分析。在BPF之中,主要會(huì)提供以下四種探針:
- 內(nèi)核探針:提供對(duì)內(nèi)核中內(nèi)部組件的動(dòng)態(tài)訪問(wèn)能力;
- 跟蹤點(diǎn):提供對(duì)內(nèi)核中內(nèi)部組件的靜態(tài)訪問(wèn)能力;
- 用戶空間探針:提供對(duì)用戶空間運(yùn)行的程序的動(dòng)態(tài)訪問(wèn)能力;
- 用戶靜態(tài)定義跟蹤點(diǎn):提供對(duì)用戶空間運(yùn)行的程序的靜態(tài)訪問(wèn)能力;
內(nèi)核探針
內(nèi)核探針提供了對(duì)幾乎任何內(nèi)核指令設(shè)置動(dòng)態(tài)標(biāo)記和中斷的能力。當(dāng)內(nèi)核到達(dá)這些標(biāo)志的時(shí)候,附加到探針的代碼就會(huì)被執(zhí)行,之后內(nèi)核將恢復(fù)到正常運(yùn)行的模式。
注:這里指的注意的是,內(nèi)核探針沒(méi)有穩(wěn)定的應(yīng)用程序二進(jìn)制接口(ABI),其會(huì)隨著內(nèi)核版本的演進(jìn)而更改。
內(nèi)核探針可以分為兩類:
- kprobes:kprobes允許在執(zhí)行任何內(nèi)核指令之前插入BPF程序。我們首先可以指定一個(gè)要探測(cè)的程序,之后當(dāng)內(nèi)核執(zhí)行到設(shè)置探針的指令的時(shí)候,它將會(huì)從代碼處開(kāi)始執(zhí)行我們編寫(xiě)的BPF程序,在BPF程序執(zhí)行完之后繼續(xù)執(zhí)行原有的程序。
下面的例子是個(gè)簡(jiǎn)單的Demo:
我們首先在python之中插入C代碼,其主要工作就是獲取當(dāng)前內(nèi)核正在運(yùn)行的命令名稱。之后使用python 的BPF加載此C代碼,并將此代碼和execve
系統(tǒng)調(diào)用相關(guān)聯(lián)起來(lái),也就是當(dāng)execve系統(tǒng)調(diào)用被觸發(fā)之后,會(huì)先去執(zhí)行我們指定的用戶代碼。
from bcc import BPF
bpf_source = """
#include < uapi/linux/ptrace.h >
int do_sys_execve(struct pt_regs *ctx) {
char comm[16];
//獲得當(dāng)前內(nèi)核正在運(yùn)行的命令名
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("executing program: %s
", comm);
return 0;
}
"""
# 加載BPF程序到內(nèi)核
bpf = BPF(text=bpf_source)
# 將BPF程序和execve系統(tǒng)調(diào)用關(guān)聯(lián)
execve_function = bpf.get_syscall_fnname("execve")
# 由于不同內(nèi)核版本提供的ABI不同,bcc工具包提供了獲得函數(shù)簽名的接口
bpf.attach_kprobe(event=execve_function, fn_name="do_sys_execve")
# 輸出跟蹤日志
bpf.trace_print()
上面的代碼最終執(zhí)行效果如下:
sudo python3 example.py
b' node-35560 [005] d..31 26011.217315: bpf_trace_printk: executing program: node'
b''
b' sh-35562 [007] d..31 26011.219055: bpf_trace_printk: executing program: sh'
b''
b' node-35563 [006] d..31 26011.221001: bpf_trace_printk: executing program: node'
b''
b' sh-35563 [007] d..31 26011.222363: bpf_trace_printk: executing program: sh'
b''
b' node-35564 [007] d..31 26011.233929: bpf_trace_printk: executing program: node'
b''
b' sh-35564 [007] d..31 26011.235267: bpf_trace_printk: executing program: sh'
b''
b' cpuUsage.sh-35565 [002] d..31 26011.236663: bpf_trace_printk: executing program: cpuUsage.sh'
kretprobes:kretprobes是在內(nèi)核指令有返回值時(shí)插入BPF程序
下面是一個(gè)使用kretprobs的例子,其會(huì)在execve
系統(tǒng)調(diào)用之后開(kāi)始執(zhí)行我們的指定的BPF程序。
from bcc import BPF
bpf_source = """
#include < uapi/linux/ptrace.h >
int ret_sys_execve(struct pt_regs *ctx) {
int return_value;
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
//獲取返回值 PT_REGS_RC 獲取上下文之中寄存器的返回值
return_value = PT_REGS_RC(ctx);
bpf_trace_printk("program: %s, return: %d
", comm, return_value);
return 0;
}
"""
bpf = BPF(text=bpf_source)
execve_function = bpf.get_syscall_fnname("execve")
bpf.attach_kretprobe(event=execve_function, fn_name="ret_sys_execve")
bpf.trace_print()
上面的程序執(zhí)行效果如下:
sudo python3 example.py
b' sh-35856 [000] d..31 26366.112370: bpf_trace_printk: program: sh, return: 0'
b''
b' which-35858 [007] d..31 26366.114034: bpf_trace_printk: program: which, return: 0'
b''
b' sh-35859 [007] d..31 26366.116329: bpf_trace_printk: program: sh, return: 0'
b''
b' ps-35859 [007] d..31 26366.117328: bpf_trace_printk: program: ps, return: 0'
b''
b' sh-35860 [007] d..31 26366.129422: bpf_trace_printk: program: sh, return: 0'
b''
b' cpuUsage.sh-35860 [007] d..31 26366.130579: bpf_trace_printk: program: cpuUsage.sh, return: 0'
跟蹤點(diǎn)
跟蹤點(diǎn)時(shí)內(nèi)核代碼的靜態(tài)標(biāo)記,可用于將代碼附加在運(yùn)行的內(nèi)核中。跟蹤點(diǎn)和kprobes的主要區(qū)別在于跟蹤點(diǎn)由內(nèi)核開(kāi)發(fā)人員在內(nèi)核中編寫(xiě)和修改。由于其是靜態(tài)存在的,所以跟蹤點(diǎn)的ABI會(huì)更加的穩(wěn)定。我們可以查看/sys/kernel/debug/tracing/events目錄下的內(nèi)容,這里是系統(tǒng)之中所有可用的跟蹤點(diǎn),在筆者的電腦上,跟蹤點(diǎn)如下:
[ik@localhost kretprobes]$ sudo ls /sys/kernel/debug/tracing/events
alarmtimer devlink gvt iomap mdio nmi rcu sunrpc workqueue
avc dma_fence hda iommu mei oom regmap swiotlb writeback
block drm hda_controller io_uring migrate page_isolation resctrl syscalls x86_fpu
bpf_test_run enable hda_intel irq mmap pagemap rpm task xdp
bpf_trace error_report header_event irq_matrix mmap_lock page_pool rseq tcp xen
bridge exceptions header_page irq_vectors mmc percpu rtc thermal xfs
cfg80211 fib huge_memory kmem module power sched timer xhci-hcd
cgroup fib6 hwmon kvm mptcp printk scsi tlb
clk filelock hyperv kvmmmu msr pwm signal ucsi
compaction filemap i2c kyber napi qdisc skb udp
context_tracking fs_dax i915 libata neigh random smbus vmscan
cpuhp ftrace initcall mac80211 net ras sock vsyscall
dev gpio intel_iommu mce netlink raw_syscalls spi wbt
這里我們可以看到由兩個(gè)額外的文件:
- enable:表示允許啟用和禁用BPF子系統(tǒng)的所有跟蹤點(diǎn)。如果該文件的內(nèi)容為0,表示禁用跟蹤點(diǎn);如果該文件的內(nèi)容為1,表示跟蹤點(diǎn)已啟用
我們可以用以下命令去啟用跟蹤點(diǎn):
- filter:用來(lái)編寫(xiě)表達(dá)式,定義內(nèi)核跟蹤子系統(tǒng)過(guò)濾事件。
下面是一個(gè)使用BPF程序跟蹤系統(tǒng)加載其他BPF程序的Demo。我們定義我們的BPF程序,其會(huì)在執(zhí)行到跟蹤點(diǎn)的時(shí)候,執(zhí)行我們的BPF程序,這里我們指定了跟蹤點(diǎn)為net_dev_xmit,其會(huì)在執(zhí)行這個(gè)跟蹤點(diǎn)的之后,執(zhí)行我們的BPF程序trace_net_dev_xmit
from bcc import BPF
bpf_source = """
int trace_net_dev_xmit(struct pt_regs *ctx) {
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("%s is loading a BPF program", comm);
return 0;
}
"""
bpf = BPF(text = bpf_source)
bpf.attach_tracepoint(tp = "net:net_dev_xmit", fn_name = "trace_net_dev_xmit")
bpf.trace_print()
注:這里的net表示跟蹤子系統(tǒng),net_dev_xmit 才是具體的跟蹤點(diǎn)
上面的函數(shù)執(zhí)行結(jié)果如下:
sudo python3 example.py
b' node-34494 [005] d..31 27609.874798: bpf_trace_printk: node is loading a BPF program'
b' sshd-34382 [007] d..31 27609.874937: bpf_trace_printk: sshd is loading a BPF program'
b' node-34494 [005] d..31 27609.876698: bpf_trace_printk: node is loading a BPF program'
b' sshd-34382 [007] d..31 27609.876769: bpf_trace_printk: sshd is loading a BPF program'
b' irq/129-iwlwifi-847 [006] d.s61 27609.877073: bpf_trace_printk: irq/129-iwlwifi is loading a BPF program'
b' irq/129-iwlwifi-847 [006] d.s61 27609.877078: bpf_trace_printk: irq/129-iwlwifi is loading a BPF program'
b' irq/129-iwlwifi-847 [006] d.s61 27609.877079: bpf_trace_printk: irq/129-iwlwifi is loading a BPF program'
用戶空間探針
用戶空間探針允許也在用戶空間運(yùn)行的程序中設(shè)置動(dòng)態(tài)標(biāo)志。它們等同于內(nèi)核探針,用戶空間探針是運(yùn)行在用戶空間的監(jiān)測(cè)程序。當(dāng)我們定義uprobe
的時(shí)候,內(nèi)核會(huì)在附加的指令上創(chuàng)建陷阱。當(dāng)程序執(zhí)行到該指令的時(shí)候,內(nèi)核將觸發(fā)事件以回調(diào)函數(shù)的方式調(diào)用探針函數(shù)。
跟內(nèi)核探針類似,用戶探針也分為兩類:
- uprobes:其是內(nèi)核在程序特定指令執(zhí)行之前插入該指令集的鉤子。下面是個(gè)示例代碼:
package main
import "fmt"
func main() {
fmt.Println("Hello, BPF")
}
from bcc import BPF
bpf_source = """
int trace_go_main(struct pt_regs *ctx) {
u64 pid = bpf_get_current_pid_tgid();
bpf_trace_printk("New main process running with PID: %d
", pid);
return 0;
}
"""
bpf = BPF(text = bpf_source)
bpf.attach_uprobe(name = "./main", sym = "main.main", fn_name = "trace_go_main")
bpf.trace_print()
在這里我們用go語(yǔ)言寫(xiě)了個(gè)程序用于打印"Hello, BPF",之后我們指定BPF程序,其會(huì)在執(zhí)行main函數(shù)的時(shí)候打印一個(gè)提示信息。下面是這個(gè)程序執(zhí)行的示例:
sudo python3 example.py
b' main-38680 [004] d..31 31093.647465: bpf_trace_printk: New main process running with PID: 38680'
b''
- uretprobes:uretprobes是kretprobes并行探針,用于用戶空間程序,其會(huì)將BPF程序附加到指令返回值上,允許通過(guò)BPF代碼從寄存器中訪問(wèn)返回值,下面是這個(gè)程序示例:
from bcc import BPF
bpf_source = """
BPF_HASH(cache, u64, u64);
int trace_start_time(struct pt_regs *ctx) {
u64 pid = bpf_get_current_pid_tgid();
u64 start_time_ns = bpf_ktime_get_ns();
cache.update(&pid, &start_time_ns);
return 0;
}
"""
bpf_source += """
int print_duration(struct pt_regs *ctx) {
u64 pid = bpf_get_current_pid_tgid();
u64 *start_time_ns = cache.lookup(&pid);
if (start_time_ns == 0) {
return 0;
}
u64 duration_ns = bpf_ktime_get_ns() - *start_time_ns;
bpf_trace_printk("Function call duration: %d
", duration_ns);
return 0;
}
"""
bpf = BPF(text = bpf_source)
bpf.attach_uprobe(name = "./main", sym = "main.main", fn_name = "trace_start_time")
bpf.attach_uretprobe(name = "./main", sym = "main.main", fn_name = "print_duration")
bpf.trace_print()
上面的程序會(huì)統(tǒng)計(jì)man函數(shù)開(kāi)始和結(jié)束的時(shí)間,其會(huì)將開(kāi)始時(shí)間放到BPF映射之中,然后再結(jié)束的時(shí)候從映射之中讀取這個(gè)一開(kāi)始的值,得到程序的執(zhí)行時(shí)間:
sudo python3 example.py
b' main-39066 [005] d..31 31384.927590: bpf_trace_printk: Function call duration: 52049'
b''
FQA
Q:使用python作為bcc前端的時(shí)候遇到報(bào)錯(cuò):“ Option ‘openmp-ir-builder-optimistic-attributes’ registered more than once!”
A: 重新編譯一遍BCC,使用如下命令:
# 編譯bcc模塊
git clone https://github.com/iovisor/bcc.git
mkdir bcc/build; cd bcc/build
sudo cmake ..
sudo make
sudo make install
# 解決上述報(bào)錯(cuò)
sudo cmake -DENABLE_LLVM_SHARED=1 ..
sudo make
sudo make install
# 編譯python3依賴
sudo cmake -DPYTHON_CMD=python3 .. # build python3 binding
pushd src/python/
sudo make
sudo make install
popd
-
應(yīng)用程序
+關(guān)注
關(guān)注
38文章
3316瀏覽量
58560 -
BPF
+關(guān)注
關(guān)注
0文章
25瀏覽量
4216 -
解釋器
+關(guān)注
關(guān)注
0文章
103瀏覽量
6663
發(fā)布評(píng)論請(qǐng)先 登錄
關(guān)于 eBPF 安全可觀測(cè)性,你需要知道的那些事兒
TCP-IP詳解卷2_BPF:BSD 分組過(guò)濾程序
Linux內(nèi)核GPIO操作函數(shù)的詳解分析
保證BPF程序安全的BPF驗(yàn)證器介紹

教你們?nèi)绾问褂胑BPF追蹤LINUX內(nèi)核

如何使用BPF對(duì)Linux內(nèi)核進(jìn)行實(shí)時(shí)跟蹤

BPF系統(tǒng)調(diào)用與Tracing類型的BPF程序
BPF ring buffer解決的問(wèn)題及背后的設(shè)計(jì)
BPF編程的環(huán)境搭建方法

BPF為內(nèi)核編程提供了一個(gè)新的參考模型
Linux內(nèi)核觀測(cè)技術(shù)eBPF中文入門(mén)指南
BPF如何在Unix內(nèi)核實(shí)現(xiàn)網(wǎng)絡(luò)數(shù)據(jù)包過(guò)濾

Linux內(nèi)核革命性技術(shù)之BPF的前世今生

評(píng)論