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