BPF簡介
BPF,全稱是Berkeley Packet Filter(伯克利數(shù)據(jù)包過濾器)的縮寫。其誕生于1992年,最初的目的是提升網(wǎng)絡(luò)包過濾工具的性能。后面,隨著這個工具重新實現(xiàn)BPF的內(nèi)核補丁和不斷完善代碼,BPF程序變成了一個更通用的執(zhí)行引擎,可以完成多種任務(wù)。簡單來說,BPF提供了一種在各種內(nèi)核時間和應(yīng)用程序事件發(fā)生時運行一小段程序的機制。其允許內(nèi)核在系統(tǒng)和應(yīng)用程序事件發(fā)生時運行一小段程序,這樣就將內(nèi)核變得完全可編程,允許用戶定制和控制他們的系統(tǒng)。
BPF其有指令集、存儲對象和輔助函數(shù)等幾部分組成。由于它采取了虛擬指令集規(guī)范,因此也可將其視為一種虛擬機實現(xiàn)。當Linux指定的時候,其會提供兩種執(zhí)行機制:一個解釋器和一個將BPF指令動態(tài)轉(zhuǎn)換為本地化指令的即時編程器。在實際執(zhí)行之前,BPF指令必須先通過驗證器的安全性檢查,以確保BPF程序自身不會崩潰或者損壞內(nèi)核。
注:擴展后的BPF通??s寫為eBPF,但官方縮寫仍然是BPF。在內(nèi)核之中只有一個執(zhí)行引擎,其同時支持eBPF和經(jīng)典BPF程序。
BPF驗證器
BPF允許任何人在Linux內(nèi)核之中執(zhí)行任意的代碼,這聽起來的十分危險,但是由于有著BPF驗證器使得這一過程變的相當?shù)陌踩?。BPF時內(nèi)核的一個模塊,所有的BPF程序都必須經(jīng)過它的審查才能夠被加載到內(nèi)核之中去運行。
驗證器執(zhí)行的第一項檢查就是對BPF虛擬機加載的代碼進行靜態(tài)分析。這一步的目的是保證程序可以按照預(yù)期去結(jié)束,而不會產(chǎn)生死循環(huán)拜拜浪費系統(tǒng)資源。驗證器會創(chuàng)建一個DAG(有向無環(huán)圖),將BPF程序的每個執(zhí)行首位相連之后去執(zhí)行DFS(深度優(yōu)先遍歷),當且僅當每個路徑都能達到DAG的底部才會通過驗證。
之后其會執(zhí)行第二項檢查,也就是對BPF程序執(zhí)行預(yù)執(zhí)行處理。這個時候驗證器會去分析程序執(zhí)行的每條指令,確保不會執(zhí)行無效的指令。同時也會檢查所有內(nèi)存指針是否可以正確訪問和解引用。
尾部調(diào)用
BPF程序可以使用尾部調(diào)用來調(diào)用其他BPF程序,這是個強大的功能。其允許通過組合比較小的BPF功能來實現(xiàn)更為復(fù)雜的程序。當從一個BPF程序調(diào)用另外一個BPF程序的時候,內(nèi)核會完全重置程序上下文。這意味著如果想要在多個BPF程序之中共享信息這是做不到的。為了解決程序間共享信息的問題,BPF引入了BPF映射的機制來解決這個問題,我們會在后面詳細的介紹BPF映射機制。
注:內(nèi)核5.2 版本之前BPF只允許執(zhí)行4096條指令,所以才有了尾部調(diào)用這個特性。從5.2開始,指令限制擴展到了100w條,尾部調(diào)用的遞歸層次也有了32次的限制。
BPF 環(huán)境配置
內(nèi)核升級
BPF程序在4系內(nèi)核之后就已經(jīng)成為了內(nèi)核的頂級子系統(tǒng),但是為了讓我們的系統(tǒng)能夠穩(wěn)定運行BPF程序,還是推薦安裝5系內(nèi)核。首先,我們可以使用如下的命令獲取當前系統(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)過升級了,如果沒有經(jīng)歷過升級,可以按照如下的命令獲取系統(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
之后的過程,同學(xué)們可以百度相應(yīng)的教程獲取安裝,本文章將專注于BPF技術(shù)的使用。
安裝好相應(yīng)內(nèi)核之后,為了讓我們在開發(fā)的時候更為容易,推薦這里將內(nèi)核源碼單獨編譯一下,方便我們鏈接:
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)境安裝
升級好內(nèi)核環(huán)境之后,我們還需要安裝BPF程序的依賴環(huán)境,主要可以分為三個部分:
- BCC 工具包:通過github 獲取相應(yīng)的源碼進行安裝
- LLVM 編譯器:訪問官網(wǎng)可獲取安裝教程
- 其他依賴程序:
sudo dnf install make glibc-devel.i686 elfutils-libelf-devel wget tar clang bcc strace kernel-devel -y
運行第一個BPF程序
在安裝好上述程序之后,我們使用如下的代碼可以來測試我們的環(huán)境是否配置完成。BPF程序可以由C語言來編寫,之后由LLVM編譯,其可以將C語言寫的程序編譯成能夠加載到內(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)
程序源碼有兩個,一個是bpf_program.c這里面存放的是要執(zhí)行的BPF源碼,其會被編譯成為一個.o文件。
在這里我們使用BPF提供的SEC屬性告知BPF虛擬機在何時運行此程序。下面的代碼會在execve系統(tǒng)調(diào)用跟蹤點被執(zhí)行的時候運行BPF程序。當內(nèi)核檢測到execve的時候,BPF程序被執(zhí)行時,我們會看到輸出消息"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
文件會被下面的這個由loader.c
編譯成為的moniter-exec
程序去執(zhí)行。其會把BPF程序加載到內(nèi)核之中去運行,這里依賴的就是我們使用的load_bpf_file
,其將會獲取一個二進制文件并把它加載到內(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
# 運行以下程序
sudo ./loader
BPF映射
BPF映射以的形式會被保存到內(nèi)核之中,其可以被任何其他的BPF程序訪問。用戶空間的程序也可以通過文件描述符訪問BPF映射。BPF映射之中可以保存事先指定大小的任何類型的數(shù)據(jù)。內(nèi)核會將數(shù)據(jù)看作二進制塊,這意味著內(nèi)核并不關(guān)系BPF映射保存的具體內(nèi)容。
此內(nèi)容會存在較多的代碼,這里會將相關(guān)所需要的MakeFile文件內(nè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 # 要刪除的BPF模塊
build: # 填寫要編譯的 BPF程序模塊
.DEFAULT_GOAL := build
創(chuàng)建BPF映射
創(chuàng)建BPF映射的最值方式就是使用bpf_create_map
系統(tǒng)調(diào)用。這個函數(shù)需要傳入五個參數(shù):
- map_type:map的類型,如果設(shè)置為
BPF_MAP_CREATE
,則表示創(chuàng)建一個新的映射。 - key_size: key的字節(jié)數(shù)
- value_size:value的字節(jié)數(shù)
- max_entries:最大的鍵值對數(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)建成功,這個接口會返回一個指向這個map的文件描述符。如果創(chuàng)建失敗,將返回-1。失敗會有三種原因,我們可以通過errno
來進行區(qū)分。
- 如果屬性無效,內(nèi)核將errnor變量設(shè)置為
EINVAL
; - 如果用戶權(quán)限不夠,內(nèi)核將errno變量設(shè)置為
EPERM
; - 如果沒有足夠的內(nè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");
}
我們在一開始提到的MakeFile文件之中添加如下信息即可編譯上述代碼:
create: map_create.c
clang -o create -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: create
最后運行編譯后的程序:
sudo ./create
Create BPF map success!
BPF映射類型
在Demo之中我們使用到了BPF_MAP_TYPE_HASH
這個map類型,其表示在內(nèi)核空間之中創(chuàng)建一個哈希表映射。除此之外,BPF還支持如下的Map類型:
- BPF_MAP_TYPE_HASH: 哈希表映射,和我們熟知的哈希表是類似的。該映射可以使用任意大小的Key和Value,內(nèi)核會按照需求分配和釋放他們。當在哈希表映射上使用更新操作的時候,內(nèi)核會自動的更新元素。
- BPF_MAP_TYPE_ARRAY:數(shù)據(jù)映射,在對數(shù)據(jù)初始化的時候,所有元素在內(nèi)存之中將預(yù)分配空間并且設(shè)置為0。數(shù)據(jù)映射的Key必須是4字節(jié)的,而且使用數(shù)組映射的一個缺點是映射之中的元素不能夠被刪除,這使得無法使數(shù)據(jù)變小。如果在數(shù)組上執(zhí)行刪除操作,那么用戶將得到一個EINVAL錯誤。
- BPF_MAP_TYPE_PROG_ARRAY:程序數(shù)組映射,這種類型保存對BPF程序的引用(其他BPF程序的文件描述符),程序數(shù)據(jù)映射類型可以使用bpf_tail_call來執(zhí)行剛剛提到的尾部調(diào)用。
- BPF_MAP_TYPE_PERF_EVENT_AYYAY:Perf事件數(shù)組映射,該映射將perf_events數(shù)據(jù)存儲在環(huán)形緩存區(qū),用于BPF程序和用戶空間程序進行實時通信。其可以將內(nèi)核跟蹤工具發(fā)出的事件轉(zhuǎn)發(fā)給用戶空間程序,使很多可觀測工具的基礎(chǔ)。
- BPF_MAP_TYPE_PERCUP_HASH:哈希表映射的改進版本,我們可以將此哈希表分配給單個獨立的CPU(每個CPU都有自己獨立的哈希表),而不是多個CPU共享一個哈希表。
- BPF_MAP_TYPE_PRECPU_ARRAY:數(shù)據(jù)映射的改進版本,也是每個CPU擁有自己獨立的數(shù)組。
- BPF_MAP_TYPE_STACK_TRACE:棧跟蹤信息,可以結(jié)合內(nèi)核開發(fā)人員添加的幫助函數(shù)bpf_get_stackid將棧跟蹤信息寫入到該映射。
持久化BPF MAP
BPF映射的基本特征使基于文件描述符的,這意味著關(guān)閉文件描述符后,映射及其所保存的所有信息都會消失。這意味著我們無法獲取已經(jīng)結(jié)束的BPF程序保存在映射之中的信息,在Linux 內(nèi)核4.4 版本之后,引入了兩個新的系統(tǒng)調(diào)用,bpf_obj_pin用來固定(固定后不可更改)和bpf_obj_get獲取來自BPF虛擬文件系統(tǒng)的映射和BPF程序。
BPF虛擬文件系統(tǒng)的默認目錄使/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;
}
我們在一開始提到的MakeFile文件之中添加如下信息即可編譯上述代碼:
save: map_save.c
clang -o save -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: save
之后,我們可以查看這個目錄查看是否固定成功了:
sudo ls /sys/fs/bpf/
my_hash
對BPF 元素進行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)核程序訪問的函數(shù)簽名和用戶空間之不同的。當然,訪問的行為也是不同的:內(nèi)核程序可以原子的執(zhí)行更新操作,用戶空間則需要發(fā)送消息到內(nèi)核,之后先復(fù)制值,然后再進行更新映射。這意味著更新操作不是原子性的。
下面使這個函數(shù)的函數(shù)原型,如果執(zhí)行成功,該函數(shù)返回0;如果失敗,則將返回復(fù)數(shù)并且把失敗的原因?qū)懭肴肿兞縠rrno之中。
- file_fd:map的文件描述符表示
- key:指向key的指針
- value:指向value的指針
- type:表示更新映射的方式。
- 如果傳入0,表示元素存在則更新,不存在則創(chuàng)建;
- 如果傳入1,表示在元素不存在的時候,內(nèi)核創(chuàng)建元素
- 如果傳入2,表示元素存在的時候,內(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;
}
我們在一開始提到的MakeFile文件之中添加如下信息即可編譯上述代碼:
update: map_update.c
clang -o update -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: update
最后運行編譯后的程序:
sudo ./update -k 1 -v 9
set key: 1
set value: 9
Fetch
當新元素寫入到map之后,我們可以使用bpf_map_lookup_elem
系統(tǒng)調(diào)用來讀取map之中的元素,其函數(shù)原型如下:
下面使這個函數(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;
}
我們在一開始提到的MakeFile文件之中添加如下信息即可編譯上述代碼:
fetch: map_fetch.c
clang -o fetch -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: fetch
最后運行編譯后的程序:
sudo ./update -k 1 -v 9
set key: 1
set value: 9
Delete
當新元素寫入到map之后,我們可以使用bpf_map_delete_elem
系統(tǒng)調(diào)用來刪除map之中的元素,其函數(shù)原型如下:
下面使這個函數(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;
}
我們在一開始提到的MakeFile文件之中添加如下信息即可編譯上述代碼:
delete: map_delete.c
clang -o delete -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: delete
最后運行編譯后的程序:
sudo ./delete -k 1
delte key:1 success!
Iter
假設(shè)我們寫入了很多元素到map之后,我們可以使用bpf_map_get_next_key
系統(tǒng)調(diào)用來遍歷map之中的元素,其函數(shù)原型如下:
下面使這個函數(shù)的函數(shù)原型,如果執(zhí)行成功,該函數(shù)返回0;如果失敗,則將返回復(fù)數(shù)并且把失敗的原因?qū)懭肴肿兞縠rrno之中。
- file_fd:map的文件描述符表示
- key:指向key的指針
- next_key:指向下個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
最后運行編譯后的程序:
[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跟蹤
跟蹤使一種為了進行分析和調(diào)試工作的數(shù)據(jù)收集行為,通過有效的利用BPF來使得我們可以以盡可能小的代價來訪問Linux內(nèi)核和應(yīng)用程序的任何信息。
探針
探針使一種探測程序,其會傳遞程序執(zhí)行時環(huán)境的相關(guān)信息,我們通過BPF探針收集系統(tǒng)之中的數(shù)據(jù)以方便我們后續(xù)進行探索分析。在BPF之中,主要會提供以下四種探針:
- 內(nèi)核探針:提供對內(nèi)核中內(nèi)部組件的動態(tài)訪問能力;
- 跟蹤點:提供對內(nèi)核中內(nèi)部組件的靜態(tài)訪問能力;
- 用戶空間探針:提供對用戶空間運行的程序的動態(tài)訪問能力;
- 用戶靜態(tài)定義跟蹤點:提供對用戶空間運行的程序的靜態(tài)訪問能力;
內(nèi)核探針
內(nèi)核探針提供了對幾乎任何內(nèi)核指令設(shè)置動態(tài)標記和中斷的能力。當內(nèi)核到達這些標志的時候,附加到探針的代碼就會被執(zhí)行,之后內(nèi)核將恢復(fù)到正常運行的模式。
注:這里指的注意的是,內(nèi)核探針沒有穩(wěn)定的應(yīng)用程序二進制接口(ABI),其會隨著內(nèi)核版本的演進而更改。
內(nèi)核探針可以分為兩類:
- kprobes:kprobes允許在執(zhí)行任何內(nèi)核指令之前插入BPF程序。我們首先可以指定一個要探測的程序,之后當內(nèi)核執(zhí)行到設(shè)置探針的指令的時候,它將會從代碼處開始執(zhí)行我們編寫的BPF程序,在BPF程序執(zhí)行完之后繼續(xù)執(zhí)行原有的程序。
下面的例子是個簡單的Demo:
我們首先在python之中插入C代碼,其主要工作就是獲取當前內(nèi)核正在運行的命令名稱。之后使用python 的BPF加載此C代碼,并將此代碼和execve
系統(tǒng)調(diào)用相關(guān)聯(lián)起來,也就是當execve系統(tǒng)調(diào)用被觸發(fā)之后,會先去執(zhí)行我們指定的用戶代碼。
from bcc import BPF
bpf_source = """
#include < uapi/linux/ptrace.h >
int do_sys_execve(struct pt_regs *ctx) {
char comm[16];
//獲得當前內(nèi)核正在運行的命令名
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)核指令有返回值時插入BPF程序
下面是一個使用kretprobs的例子,其會在execve
系統(tǒng)調(diào)用之后開始執(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'
跟蹤點
跟蹤點時內(nèi)核代碼的靜態(tài)標記,可用于將代碼附加在運行的內(nèi)核中。跟蹤點和kprobes的主要區(qū)別在于跟蹤點由內(nèi)核開發(fā)人員在內(nèi)核中編寫和修改。由于其是靜態(tài)存在的,所以跟蹤點的ABI會更加的穩(wěn)定。我們可以查看/sys/kernel/debug/tracing/events目錄下的內(nèi)容,這里是系統(tǒng)之中所有可用的跟蹤點,在筆者的電腦上,跟蹤點如下:
[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
這里我們可以看到由兩個額外的文件:
- enable:表示允許啟用和禁用BPF子系統(tǒng)的所有跟蹤點。如果該文件的內(nèi)容為0,表示禁用跟蹤點;如果該文件的內(nèi)容為1,表示跟蹤點已啟用
我們可以用以下命令去啟用跟蹤點:
- filter:用來編寫表達式,定義內(nèi)核跟蹤子系統(tǒng)過濾事件。
下面是一個使用BPF程序跟蹤系統(tǒng)加載其他BPF程序的Demo。我們定義我們的BPF程序,其會在執(zhí)行到跟蹤點的時候,執(zhí)行我們的BPF程序,這里我們指定了跟蹤點為net_dev_xmit,其會在執(zhí)行這個跟蹤點的之后,執(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 才是具體的跟蹤點
上面的函數(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'
用戶空間探針
用戶空間探針允許也在用戶空間運行的程序中設(shè)置動態(tài)標志。它們等同于內(nèi)核探針,用戶空間探針是運行在用戶空間的監(jiān)測程序。當我們定義uprobe
的時候,內(nèi)核會在附加的指令上創(chuàng)建陷阱。當程序執(zhí)行到該指令的時候,內(nèi)核將觸發(fā)事件以回調(diào)函數(shù)的方式調(diào)用探針函數(shù)。
跟內(nèi)核探針類似,用戶探針也分為兩類:
- uprobes:其是內(nèi)核在程序特定指令執(zhí)行之前插入該指令集的鉤子。下面是個示例代碼:
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語言寫了個程序用于打印"Hello, BPF",之后我們指定BPF程序,其會在執(zhí)行main函數(shù)的時候打印一個提示信息。下面是這個程序執(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并行探針,用于用戶空間程序,其會將BPF程序附加到指令返回值上,允許通過BPF代碼從寄存器中訪問返回值,下面是這個程序示例:
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()
上面的程序會統(tǒng)計man函數(shù)開始和結(jié)束的時間,其會將開始時間放到BPF映射之中,然后再結(jié)束的時候從映射之中讀取這個一開始的值,得到程序的執(zhí)行時間:
sudo python3 example.py
b' main-39066 [005] d..31 31384.927590: bpf_trace_printk: Function call duration: 52049'
b''
FQA
Q:使用python作為bcc前端的時候遇到報錯:“ 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
# 解決上述報錯
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)注
37文章
3268瀏覽量
57715 -
BPF
+關(guān)注
關(guān)注
0文章
25瀏覽量
4006 -
解釋器
+關(guān)注
關(guān)注
0文章
103瀏覽量
6513
發(fā)布評論請先 登錄
相關(guān)推薦
評論