01
欲窮千里目,更上一層樓
在上一篇文章《eBPF動手實踐系列二:構(gòu)建基于純C語言的eBPF項目》中,我們初步實現(xiàn)了脫離內(nèi)核源碼進行純C語言eBPF項目的構(gòu)建。libbpf庫在早期和內(nèi)核源碼結(jié)合的比較緊密,如今的libbpf庫更加成熟,已經(jīng)完全脫離內(nèi)核源碼獨立發(fā)展。
為了更加具體的理解linux內(nèi)核版本演進和libbpf版本演進的關(guān)系,本文在“附錄A”中總結(jié)了各個內(nèi)核版本源碼示例中所依賴的libbpf庫的對應(yīng)版本信息。
大部分版本的內(nèi)核獲取libbpf版本的方法如下,從libbpf庫目錄的libbpf.map文件中提取最大的版本號信息。這里的"source"為內(nèi)核源碼所在目錄。
$ cat ./source/tools/lib/bpf/libbpf.map | grep -oE '^LIBBPF_([0-9.]+)' | sort -rV | head -n1 | cut -d'_' -f2
較早版本的內(nèi)核在./tools/lib/bpf/Makefile文件中直接定義了libbpf的版本信息。
$ cat ./source/tools/lib/bpf/Makefile BPF_VERSION = 0 BPF_PATCHLEVEL = 0 BPF_EXTRAVERSION = 2? ? ?02
eBPF編程方案簡介
為了簡化 eBPF程序的開發(fā)流程,降低開發(fā)者在使用 libbpf 庫時的入門難度,libbpf-bootstrap 框架應(yīng)運而生?;趌ibbpf-bootstrap框架的編程方案是目前網(wǎng)絡(luò)上看到的最主流編程方案。此外,網(wǎng)絡(luò)上也偶見比較古老的僅依賴一個bpf_load.c文件的C語言編程方案,這個方案并不需要依賴libbpf庫的支持。
主流的C語言實現(xiàn)的eBPF編程方案,大體上就是以下三種,筆者總共將其歸納為3代。
除了經(jīng)典的C語言編程方案,一些編程框架還選擇使用Python語言,Go語言,或者Rust語言作為用戶態(tài)加載的實現(xiàn)語言。
盡管libbpf-bootstrap骨架C語言方案、基于libbpfgo庫的go語言方案等已經(jīng)被大家廣泛使用和接受。但筆者認為基于原生libbpf庫的eBPF編程方案仍然具備很多獨特的優(yōu)勢。以下是原生libbpf庫eBPF編程方案的一些獨特優(yōu)勢:
更深的控制和靈活性:直接使用原生libbpf 庫的方案意味著可以與更底層交互,實現(xiàn)更多的控制,定制加載和管理 eBPF 程序和 maps 過程,滿足更復(fù)雜的需求。
更好的學(xué)習(xí)和理解:libbpf-bootstrap封裝抽象屏蔽了很多細節(jié),直接使用原生libbpf可以對 eBPF 子系統(tǒng)有更深入的理解,有利于開發(fā)者對 eBPF 內(nèi)部工作原理的理解。
更細粒度的依賴管理:直接使用原生libbpf庫能夠指定依賴的 libbpf 庫版本和功能,進而更精細化地管理項目依賴關(guān)系。
更好的低版本內(nèi)核適應(yīng)性:基于原生libbpf庫的方案,在低版本操作系統(tǒng)發(fā)行版和低版本內(nèi)核上可以有更好的兼容性。
本文將由淺入深介紹第 2 代原生libbpf庫的eBPF編程方案,并提出一種改進思路。
03
準備eBPF開發(fā)的基礎(chǔ)環(huán)境
主流的linux發(fā)行版大多是基于rpm包或deb包的包管理系統(tǒng)。不同的包管理系統(tǒng),初始化eBPF開發(fā)環(huán)境時所依賴的包,也略有差別。本文將分別進行介紹。
3.1? rpm包基礎(chǔ)環(huán)境初始化
在RPM包發(fā)行版環(huán)境,需要安裝一些編譯過程的基礎(chǔ)包、編譯工具包、庫依賴包和頭文件依賴包等。我們推薦使用如下一些發(fā)行版及其兼容環(huán)境:Anolis 8.8、Kylin V10、CentOS ?8.5、和 Fedora 39 等。
詳細安裝步驟如下:
$ yum install git make # 基礎(chǔ)包 $ yum install kernel-headers-$(uname -r) # 頭文件依賴包 $ yum install clang llvm elfutils-libelf-devel # 編譯工具和依賴庫包 ## 依次選擇如下命令之一,安裝bpftool工具 $ yum install bpftool-$(uname -r) $ yum install bpftool?3.2? deb包基礎(chǔ)環(huán)境初始化
在 DEB 包發(fā)行版環(huán)境,需要安裝一些編譯過程的基礎(chǔ)包、編譯工具包、庫依賴包和頭文件依賴包等。推薦使用Ubuntu 22.04 或Debian 12 等發(fā)行版及其兼容環(huán)境。
詳細安裝步驟如下:
$ apt-get update # 更新apt源信息 $ apt install git make # 基礎(chǔ)包 $ apt install linux-libc-dev # 頭文件依賴包 $ apt install clang llvm libelf-dev # 編譯工具和依賴庫包 ## 依次選擇如下命令之一,安裝bpftool工具 $ apt install linux-tools-common linux-tools-$(uname -r) $ apt install linux-tools-common linux-tools-generic $ apt install linux-tools-$(uname -r) linux-cloud-tools-$(uname -r) $ apt install bpftool? ? ?04
構(gòu)建基于原生libbpf庫的eBPF項目
本文的目的是向大家分享一個以第2代 ebpf 編程方案為基礎(chǔ)的改進ebpf編譯構(gòu)建方案。本節(jié)先用一些篇幅內(nèi)容,對第2代方案本身的構(gòu)建編譯過程做一些介紹。
libbpf庫具有一定的向下兼容能力,可以選擇使用截至目前最新的歸檔版本libbpf-1.3.0來搭建編程環(huán)境。以 libbpf-1.3.0版本libbpf庫為基礎(chǔ),下文會提供若干實例代碼,來剖析ebpf構(gòu)建原理。完成了基礎(chǔ)環(huán)境的初始化,就可以開始搭建我們的eBPF項目。所有的代碼示例都可以通過如下git項目獲取。為了后面訪問方便,這里用一個shell變量NATIVE_LIBBPF用來存儲工作目錄。
$ cd ~ $ git clone https://github.com/alibaba/sreworks-ext.git $ NATIVE_LIBBPF=~/sreworks-ext/demos/native_libbpf_guide/?
4.1? 初步構(gòu)建基于原生libbpf庫的eBPF項目
首先來看一個基于原生libbpf庫的第2代eBPF構(gòu)建實例。ebpf初學(xué)者,可以考慮選擇跟蹤 execve 系統(tǒng)調(diào)用產(chǎn)生的事件。
$ cd $NATIVE_LIBBPF # 返回工作目錄 $ cd trace_execve_libbpf130 # 進入項目目錄 $ make $ sudo ./trace_execve trace_execve 15836221 5501 bash 1534 bash 0 /usr/bin/ls trace_execve 15914126 5502 bash 1534 bash 0 /usr/bin/ps $ make clean
?
執(zhí)行trace_execve命令,對編譯結(jié)果進行驗證,完美驗證通過。
4.2? eBPF項目的目錄結(jié)構(gòu)解析
介紹下trace_execve_libbpf130的目錄結(jié)構(gòu)。
?
trace_execve_libbpf130目錄 | 說明 |
./ | 項目用戶態(tài)代碼和主Makefile |
./progs | 項目內(nèi)核態(tài)bpf程序代碼 |
./include | 項目的業(yè)務(wù)代碼相關(guān)的頭文件 |
./helpers | 非來自于libbpf庫的一些helpler文件 |
./tools/lib/bpf/ | 來自于libbpf-1.3.0/src/ |
./tools/include/ | 來自于libbpf-1.3.0/include/ |
./tools/build/ | 項目構(gòu)建時一些feature探測代碼 |
./tools/scripts/ | 項目Makefile所依賴的一些功能函數(shù) |
?
再介紹下本項目trace_execve_libbpf130和libbpf-1.3.0庫的對應(yīng)關(guān)系。下載libbpf-1.3.0庫解壓后,使用diff命令進行目錄對比。
目錄native_libbpf_guide/trace_execve_libbpf130/tools/lib/bpf/內(nèi)容,除Makefile內(nèi)容外都來自目錄~/libbpf-1.3.0/src/。
目錄native_libbpf_guide/trace_execve_libbpf130/tools/include/來自目錄~/libbpf-1.3.0/include/,所有內(nèi)容都完全一致。
除以上兩部分來自libbpf-1.3.0庫以外的文件,其余都由本項目原創(chuàng)貢獻。
$ cd ~ $ wget http://github.com/libbpf/libbpf/archive/refs/tags/v1.3.0.tar.gz $ tar -zxvf v1.3.0.tar.gz $ diff -qr $NATIVE_LIBBPF/trace_execve_libbpf130/tools/lib/bpf/ ~/libbpf-1.3.0/src/ Only in ~/libbpf-1.3.0/src/: .gitignore Files ~/native_libbpf_guide/trace_execve_libbpf130/tools/lib/bpf/Makefile and ~/libbpf-1.3.0/src/Makefile differ $ diff -qr $NATIVE_LIBBPF/trace_execve_libbpf130/tools/include/ ~/libbpf-1.3.0/include/
在這個項目中添加ebpf的代碼,可以遵循這樣的目錄結(jié)構(gòu)。用戶態(tài)加載文件放到根目錄下,內(nèi)核態(tài)bpf文件放到progs目錄下,用戶態(tài)和內(nèi)核態(tài)公共的頭文件放到include目錄下。
?
?
$ cd $NATIVE_LIBBPF # 返回工作目錄 $ cd trace_execve_libbpf130 # 進入項目目錄 $ find . -name "trace_execve*" ./trace_execve.c ./progs/trace_execve.bpf.c ./include/trace_execve.h?
?
?
4.3? eBPF項目的Makefile解析
?
?
$ cd $NATIVE_LIBBPF # 返回工作目錄 $ cd trace_execve_libbpf130 # 進入項目目錄 $ find . -name Makefile ./Makefile ./progs/Makefile ./tools/lib/bpf/Makefile ./tools/build/feature/Makefile
?
?
trace_execve_libbpf130項目有4個Makefile,分別如下:
./Makefile是主文件,用于生成用戶態(tài)eBPF程序trace_execve。
./progs/Makefile 用于生成內(nèi)核態(tài)BPF程序trace_execve.bpf.o。
./tools/lib/bpf/Makefile 用于生成libbpf.a靜態(tài)庫。
./tools/build/feature/Makefile 用于一些feature的探測。
在項目空間的根目錄運行make命令進行項目構(gòu)建時,會首先執(zhí)行Makefile文件。在Makefile文件中會通過make的-C選項間接觸發(fā)progs/Makefile和tools/lib/bpf/Makefile的執(zhí)行。
感興趣的同學(xué)可以通過上一章節(jié)中提到的make --debug=v,m SHELL="bash -x" 命令逐步debug這些makefile的執(zhí)行過程。
下文重點分析下編譯過程的一些編譯參數(shù),讓我們加深對eBPF構(gòu)建過程的理解。
4.4? ?C語言編譯器的目錄搜索選項
在開始分析eBPF程序的編譯參數(shù)之前,先要簡單說一下C語言編譯器(gcc/clang)的目錄搜索選項。C語言的頭文件都需要按照目錄搜索選項的指引,才能正確找到它所在位置。
除了日常我們熟知的-I選項,clang/gcc的目錄搜索選項還有很多,它們優(yōu)先級的順序依次如下:
頭文件引用方式include "myheader.h",則在當前文件所在目錄查找myheader.h頭文件。
頭文件引用方式include "myheader.h",如果有-iquote mydir選項,則在mydir目錄查找myheader.h頭文件。
頭文件引用方式include
頭文件引用方式include
頭文件引用方式include
頭文件引用方式include
4.5? 內(nèi)核態(tài)bpf程序編譯參數(shù)解析
內(nèi)核態(tài)bpf程序trace_execve.bpf.o文件,是由 bpf 文件trace_execve.bpf.c使用clang命令編譯產(chǎn)生。trace_execve.bpf.c文件的頭文件依賴如下。
?
?
$ cat progs/trace_execve.bpf.c // SPDX-License-Identifier: GPL-2.0 #include#include #include #include "common.h" #include "trace_execve.h"
?
?
從前面項目構(gòu)建過程中,可以提取出完整的內(nèi)核態(tài)bpf程序的編譯命令。
?
?
$ clang -iquote ./../include/ -iquote ./../helpers -I./../tools/lib/ -I./../tools/include/uapi -idirafter /usr/lib64/clang/15.0.7/include -idirafter /usr/include -idirafter /usr/include/x86_64-linux-gnu/ -DENABLE_ATOMICS_TESTS -D__KERNEL__ -D__BPF_TRACING__ -D__TARGET_ARCH_x86 -g -Werror -O2 -mlittle-endian -target bpf -mcpu=v3 -c trace_execve.bpf.c -o trace_execve.bpf.o
?
?
下面對一些關(guān)鍵環(huán)節(jié)做一些解析:
頭文件vmlinux.h由bpftool工具在編譯時動態(tài)生成,vmlinux.h包含了絕大多數(shù)bpf程序的內(nèi)核態(tài)和用戶態(tài)(uapi)依賴。通過編譯選項-I./../tools/lib/可以搜索到vmlinux.h頭文件。
通過-I./../tools/lib/編譯選項,可以在./tools/lib/目錄下的bpf子目錄中查找到bpf_helpers.h和bpf_tracing.h頭文件,這些頭文件都是對vmlinux.h頭文件內(nèi)核態(tài)依賴的補充。
通過-iquote ./../include/編譯選項,可以在./include/目錄中查找到trace_execve.h和common.h頭文件。
在上面這些頭文件依賴的預(yù)處理過程中,會依賴一些宏變量來決定預(yù)處理的展開邏輯。上面編譯命令中的宏就是起這些作用,-DENABLE_ATOMICS_TESTS -D__KERNEL__ -D__BPF_TRACING__ -D__TARGET_ARCH_x86。比如在bpf_tracing.h頭文件中,就有#if defined(__TARGET_ARCH_x86)的宏判斷語句,來決定預(yù)處理展開邏輯走x86分支。
編譯選項-target bpf,指示Clang將代碼生成為針對eBPF目標的目標代碼。編譯選項-mcpu=v3,指示Clang生成針對v3版本的eBPF處理器的目標代碼。編譯選項-mlittle-endian:指示Clang生成適用于小端序處理器的目標代碼。
通過-I./../tools/include/uapi編譯選項,可以在./tools/include/uapi/目錄下的linux子目錄中查找到bpf.h頭文件。同時kernel-headers包引入的/usr/include/linux/目錄下也有bpf.h,./tools/include/uapi下的bpf.h優(yōu)先級會覆蓋它。此外,目錄./tools/include/uapi/linux下的頭文件和vmlinux.h頭文件存在一定的重疊,通常情況下同時加載會出現(xiàn)編譯沖突。如果在一些簡單的 ebpf 使用場景,可以使用
4.6? 用戶態(tài)加載程序編譯參數(shù)解析
用戶態(tài)eBPF程序trace_execve文件,是由源文件trace_execve.c文件使用gcc命令編譯。trace_execve.c文件的頭文件依賴如下。
?
?
$ cat trace_execve.c // from kernel-headers #include#include #include #include #include #include #include // from libbpf #include #include #include "common.h" #include "trace_execve.h"
?
?
從前面項目構(gòu)建過程中,也可以提取出完整的用戶態(tài)程序的編譯命令。
?
?
gcc -iquote ./helpers/ -iquote ./include/ -I./tools/lib/ -I./tools/include/ -g -c -o trace_execve.o trace_execve.c
?
?
通過-I./tools/include/編譯選項,可以在./tools/include/目錄下的linux子目錄中查找到
通過-I./tools/lib/編譯選項,可以在./tools/lib/目錄下的bpf子目錄中查找到
通過-iquote ./include/編譯選項,可以在./include/目錄中查找到trace_execve.h和common.h頭文件。
其他頭文件都可以在由kerne-headers包提供的標準系統(tǒng)目錄(Standard system directories)的/usr/include/目錄及子目錄中查找到。所以,
4.7? libbpf.a靜態(tài)庫編譯解析
關(guān)于libbpf.a靜態(tài)庫的編譯過程,上一篇文章已經(jīng)有所介紹。這里僅再次強調(diào)下,在本項目中,我們完全實現(xiàn)了libbpf庫的自主可控,可控源代碼,可控編譯構(gòu)建過程。這至少給我們帶來如下兩方面好處:
對于一些ebpf的資深人士,可以自主修改libbpf庫中不盡如人意的地方,實現(xiàn)滿足自己業(yè)務(wù)需求的優(yōu)化。
對于一些ebpf的初學(xué)者,完全可以在libbpf庫中任意感興趣的地方,通過插入printf或其他斷點方式,深入學(xué)習(xí)libbpf庫的原理。
05
改進基于原生libbpf庫的eBPF項目構(gòu)建
5.1? 傳統(tǒng)方案美中不足
在上文中,我們初步實現(xiàn)了基于libbpf庫的第 2 代 eBPF項目的構(gòu)建。但截止到目前,此方案還有一個明顯的缺陷。讓我們繼續(xù)上一篇的案例來分析,在搭建完開發(fā)環(huán)境后執(zhí)行如下步驟。
?
?
$ cd $NATIVE_LIBBPF # 返回工作目錄 $ cd trace_execve_libbpf130 # 進入項目目錄 $ make clean $ make $ sudo ./trace_execve trace_execve 160646349 5503 sa1 1 systemd 0 /usr/lib64/sa/sa1 trace_execve 160646371 5503 sa1 1 systemd 0 /usr/lib64/sa/sadc $ mv progs/trace_execve.bpf.o progs/trace_execve.bpf.o.bak $ sudo ./trace_execve libbpf: elf: failed to open progs/trace_execve.bpf.o: No such file or directory ERROR: failed to open prog: 'No such file or directory' $ mv progs/trace_execve.bpf.o.bak progs/trace_execve.bpf.o $ sudo ./trace_execve trace_execve 190767474 5566 crond 5565 crond 0 /bin/bash trace_execve 190767486 5566 bash 5565 crond 0 /bin/run-parts
?
?
從實驗結(jié)果可以看出,當我們把bpf目標文件trace_execve.bpf.o改名為trace_execve.bpf.o.bak后,trace_execve程序執(zhí)行會報錯,提示讀取trace_execve.bpf.o文件不存在。而當我們再次將備份后的bpf目標文件trace_execve.bpf.o.bak改回原名trace_execve.bpf.o后,重新執(zhí)行trace_execve程序又一切正常了。這說明,當前方案構(gòu)建后,需要將trace_execve程序和bpf目標文件trace_execve.bpf.o這一組文件一起進行分發(fā),才能正常執(zhí)行。這給我們在工程的實現(xiàn)上帶來了很大的挑戰(zhàn)。
為了解決上面提到的問題,第 3 代 ebpf 編程方案 libbpf-bootstrap框架發(fā)明了skeleton骨架,即使用*.skel.h頭文件的方式,將bpf目標文件trace_execve.bpf.o的內(nèi)容編譯進trace_execve程序。這樣后續(xù)只需分發(fā)trace_execve二進制程序文件即可。
如果不依賴libbpf-bootstrap編程框架,繼續(xù)僅依賴 libbpf 庫是否可以做到這一點呢?答案是可以的,本文獨辟蹊徑,給大家分享一個使用hexdump命令輕松實現(xiàn)*.skel.h頭文件的方式。
5.2? 使用hexdump生成skel.h頭文件
簡單歸納一下使用libbpf-bootstrap框架編程過程中的構(gòu)建步驟。
?
步驟 | libbpf-bootstrap框架構(gòu)建 | 可改進機會點 |
1 | bpftool btf dump file vmlinux format c > vmlinux.h | ? |
2 | clang -O2 -target bpf -c trace_execve.bpf.c -o trace_execve.bpf.o | ? |
3 | bpftool gen skeleton trace_execve.bpf.o > trace_execve.skel.h | 此步驟用hexdump替換bpftool |
4 | gcc -o trace_execve trace_execve.c -lbpf ?-lelf | 此步驟更改加載函數(shù)為libbpf標準函數(shù) |
?
分析libbpf-bootstrap編程框架的實現(xiàn)原理,可以了解到。在第3步會依靠bpftool工具將trace_execve.bpf.o這個目標文件轉(zhuǎn)換成十六進制格式的文本,并將這個文本內(nèi)容作為trace_execve.skel.h頭文件中的一個變量的值,最后還需要讓trace_execve.c用戶態(tài)加載文件包含這個trace_execve.skel.h頭文件。這其中將bpf目標文件轉(zhuǎn)換成十六進制文本并生成skel.h頭文件的過程最為關(guān)鍵。
libbpf-bootstrap編程框架非常成熟,但方案使用中必須遵循他的一些規(guī)則,比如頭文件trace_execve.skel.h的命令必須包含程序的關(guān)鍵詞trace_execve,再比如加載函數(shù)trace_execve_bpf__load()也必須包含程序的關(guān)鍵詞trace_execve。如何能不依賴這個規(guī)范,實現(xiàn)一個更加輕量級的編程方案呢?這讓我們想到了hexdump命令,可以用它替換bpftool工具,并且生成符合自己期望的頭文件。
?
?
$ hexdump -v -e '"\x" 1/1 "%02x"' trace_execve.bpf.o > trace_execve.hex?
?
?
5.3? 深入構(gòu)建基于原生libbpf庫的eBPF項目
下面我們就嘗試依靠hexdump命令實現(xiàn)一個單一可執(zhí)行文件的解決方案。開始體驗我們基于第 2 代編程方案改進的eBPF項目,進入項目代碼。
?
?
$ cd $NATIVE_LIBBPF # 返回工作目錄 $ cd hexdump_skel_libbpf130 # 進入項目目錄 $ make $ sudo ./trace_execve trace_execve bash su 74113 74112 0 /usr/bin/bash trace_execve bash su 74113 74112 0 /usr/bin/bash $ sudo ./probe_execve probe_execve 19076757 5572 0anacron 5570 0anacron 0 probe_execve 19076758 5573 0anacron 5570 0anacron 0
?
?
分別執(zhí)行trace_execve和probe_execve兩個命令,對編譯結(jié)果進行驗證,均完美驗證通過。這里我們在trace_execve實例基礎(chǔ)上又增加了一個probe_execve實例,說明hexdump_skel_libbpf130項目是支持多實例編譯的。
下面我們來驗證下本文開頭的情況,看看沒有了bpf目標文件時的情形。
?
?
$ cd $NATIVE_LIBBPF # 返回工作目錄 $ cd hexdump_skel_libbpf130 # 進入項目目錄 $ rm -fr progs/trace_execve.bpf.o progs/probe_execve.bpf.o $ sudo ./trace_execve trace_execve 19076759 5574 run-parts 5566 run-parts 0 /bin/basename trace_execve 19076760 5575 run-parts 5566 run-parts 0 /bin/logger $ sudo ./probe_execve probe_execve sh python 78841 78838 0 probe_execve sh python 78841 78838 0
?
?
從運行結(jié)果看,雖然刪除了兩個bpf目標文件trace_execve.bpf.o和probe_execve.bpf.o,僅僅依靠trace_execve和probe_execve兩個文件即可成功執(zhí)行??梢栽賴L試將trace_execve 可執(zhí)行文件拷貝到其他目錄,結(jié)果依然可行。
5.4? 改進的eBPF項目Makefile解析
hexdump_skel_libbpf130項目也是同樣的4個Makefile,其中將bpf目標文件編譯到用戶態(tài)加載進程中的環(huán)節(jié)主要在項目的主Makefile中實現(xiàn)。還是老辦法獲取make構(gòu)建的詳細過程。
?
?
$ cd $NATIVE_LIBBPF # 返回工作目錄 $ cd hexdump_skel_libbpf130 # 進入項目目錄 $ make clean $ make --debug=v,m SHELL="bash -x" > make.log 2>&1
?
?
對于構(gòu)建日志的分析可以參考前面文章,我們把關(guān)鍵環(huán)節(jié)提取出來。
?
?
$ cat make.log | grep -n "Considering target file" 14:Considering target file 'all'. 16: Considering target file 'tools/lib/bpf/libbpf.a'. 21: Considering target file 'helpers/uprobe_helper.o'. 23: Considering target file 'helpers/uprobe_helper.c'. 31: Considering target file 'probe_execve'. 33: Considering target file 'probe_execve.o'. 35: Considering target file 'probe_execve.c'. 38: Considering target file 'probe_execve.skel.h'. 40: Considering target file 'probe_execve.hex'. 42: Considering target file 'progs/probe_execve.bpf.o'. 44: Considering target file 'progs/probe_execve.bpf.c'. 145: Considering target file 'trace_execve'. 147: Considering target file 'trace_execve.o'. 149: Considering target file 'trace_execve.c'. 152: Considering target file 'trace_execve.skel.h'. 154: Considering target file 'trace_execve.hex'. 156: Considering target file 'progs/trace_execve.bpf.o'. 158: Considering target file 'progs/trace_execve.bpf.c'.
?
?
從關(guān)鍵構(gòu)建步驟中,我們可以了解到:
probe_execve和trace_execve兩個target都是all目標的下級目標,并且probe_execve和trace_execve是串行的。這個里隱含的一個意思是,當trace_execve開始構(gòu)建的時候,probe_execve已經(jīng)完全構(gòu)建完畢,probe_execve這個最終可執(zhí)行文件已經(jīng)生成完畢。此時,probe_execve構(gòu)建過程中所依賴的所有中間文件都不再需要了。所以,probe_execve和trace_execve構(gòu)建過程中依賴的中間文件是可以重名的。
tools/lib/bpf/libbpf.a和helpers/uprobe_helper.o已經(jīng)提前編譯好了,就不再做過多的說明了。最終的用戶態(tài)可執(zhí)行加載程序的主要依賴鏈條如下。
?
?
trace_execve ├── trace_execve.o │ ├── trace_execve.c │ ├── trace_execve.skel.h │ │ ├── trace_execve.hex │ │ │ ├──progs/trace_execve.bpf.o │ │ │ │ └── progs/trace_execve.bpf.c
?
?
再看一下主Makefile的源碼,為了實現(xiàn)以上的目標依賴,我們連用了5個靜態(tài)模式規(guī)則(Static Pattern Rules)。
?
?
$(HELPER_OBJECTS): %.o:%.c $(BPF_OBJECT):./progs/%.bpf.o:./progs/%.bpf.c $(HEX_OBJECT):%.hex:./progs/%.bpf.o $(SKEL_OBJECT):%.skel.h:%.hex $(USER_OBJECT):%.o:%.c %.skel.h $(LOADER_OBJECT): %:%.o
?
?
其中任何一個靜態(tài)模式規(guī)則的目標集合,都是通過項目根目錄下*.c文件的集合,進行局部字符串替換獲得。
?
?
SOURCES := $(wildcard *.c) HELPER_OBJECTS := $(patsubst %.c,%.o,$(wildcard $(HELPERS_PATH)/*.c)) LOADER_OBJECT := $(patsubst %.c,%,$(SOURCES)) USER_OBJECT := $(patsubst %.c,%.o,$(SOURCES)) SKEL_OBJECT := $(patsubst %.c,%.skel.h,$(SOURCES)) HEX_OBJECT := $(patsubst %.c,%.hex,$(SOURCES)) BPF_OBJECT := $(patsubst %.c,./progs/%.bpf.o,$(SOURCES))?
?
?
5.5? 從file到memory實現(xiàn)讀取elf的轉(zhuǎn)變
本方案的主要邏輯是在主Makefile中實現(xiàn),但也需要c代碼中做一些調(diào)整。bpf文件trace_execve.bpf.c并不需要任何修改,只需要在用戶態(tài)加載程序trace_execve.c做一些調(diào)整。
傳統(tǒng)的讀取bpf目標文件方式,相關(guān)代碼如下:
?
?
char filename[256] = "progs/trace_execve.bpf.o"; struct bpf_object * bpf_obj = bpf_object__open_file(filename, NULL);
?
?
改進后的讀取memory方式,相關(guān)代碼如下:
?
?
#include "skeleton.skel.h" struct bpf_object * bpf_obj = bpf_object__open_mem(obj_buf, obj_buf_sz, NULL);
?
?
很明顯libbpf庫提供了bpf_object__open_file(bpf_object__open)和bpf_object__open_mem兩個函數(shù)用于讀取elf格式的bpf目標文件trace_execve.bpf.o。區(qū)別是bpf_object__open_file是在trace_execve運行時,再去讀取trace_execve.bpf.o文件內(nèi)容,而bpf_object__open_mem是在編譯時,已經(jīng)把elf內(nèi)容編譯進trace_execve程序。至于bpf_object__open函數(shù)在libbpf庫的libbpf.c文件中是對bpf_object__open_file函數(shù)的封裝。
這兩個libbpf庫函數(shù),最終都是調(diào)用elf標準庫函數(shù)實現(xiàn)了相關(guān)功能,具體代碼實現(xiàn)是在libbpf庫的libbpf.c文件中的bpf_object__elf_init函數(shù)中,代碼如下:
?
?
static int bpf_object__elf_init(struct bpf_object *obj){ ...... if (obj->efile.obj_buf_sz > 0) { elf = elf_memory((char *)obj->efile.obj_buf, obj->efile.obj_buf_sz); } else { obj->efile.fd = open(obj->path, O_RDONLY | O_CLOEXEC); ...... elf = elf_begin(obj->efile.fd, ELF_C_READ_MMAP, NULL); } ...... }
?
?
可以看出,bpf_object__open_mem函數(shù)的最終實現(xiàn)是elf的elf_memory函數(shù),bpf_object__open_file函數(shù)的最終實現(xiàn)是elf的elf_begin函數(shù)。
5.6? 原生libbpf庫與libbpf-bootstrap的若干區(qū)別
相比較第3代的 libbpf-bootstrap框架方案和第2代的傳統(tǒng)libbpf庫方案,使用hexdump命令的原生libbpf庫第 2 代改進方案方案在實現(xiàn)方法上,有一些獨特的優(yōu)勢。
這里將這三種方案的主要區(qū)別歸納總結(jié)如下:
這里補充下,trace_execve_bpf__open()函數(shù)的實現(xiàn),也是間接通過libbpf庫的bpf_object__open_skeleton()函數(shù),最終也調(diào)用了bpf_object__open_mem()函數(shù)。
5.7? 使用attach_tracepoint替代attach
在ebpf用戶態(tài)程序的加載過程中,有一個attach的步驟。細心的讀者應(yīng)該已經(jīng)發(fā)現(xiàn)了,在trace_execve_libbpf130項目中,我們使用的是bpf_program__attach()函數(shù)實現(xiàn)的靜態(tài)探針點的attach。而在hexdump_skel_libbpf130項目中,我們使用的卻是bpf_program__attach_tracepoint()函數(shù)實現(xiàn)的靜態(tài)探針點的attach。區(qū)別是bpf_program__attach_tracepoint函數(shù)的參數(shù)中會指定靜態(tài)探針點的具體信息,而bpf_program__attach不用指定靜態(tài)探針點的信息。進一步閱讀bpf_program__attach函數(shù)的源代碼可以了解到,它是依靠內(nèi)核態(tài)的bpf目標文件中SEC的節(jié)名稱信息來獲取和確定靜態(tài)探針點的信息的??偨Y(jié)這兩種方法如下:
?
? | trace_execve.c中相關(guān)代碼 | trace_execve.bpf.c中相關(guān)代碼 |
attach方案A | bpf_program__attach(bpf_prog) | SEC("tracepoint/syscalls/sys_enter_execve") |
attach方案B | bpf_program__attach_tracepoint(bpf_prog, "syscalls", "sys_enter_execve") | SEC("tracepoint") |
?
很明顯,在trace_execve.c和trace_execve.bpf.c的代碼中,只要有一處設(shè)置靜態(tài)探針點即可。如果兩處都設(shè)置,而且兩處設(shè)置的靜態(tài)探針點信息沖突的情況下,會以用戶態(tài)的bpf_program__attach_tracepoint函數(shù)設(shè)置的信息為準。
libbpf庫中的bpf_link__destroy()函數(shù)是負責(zé)對attach函數(shù)生成的link進行銷毀的函數(shù)。attach和destroy的過程實際上就是對內(nèi)核靜態(tài)探針點開啟和關(guān)閉的過程。
在這里特別推薦使用方案B中的bpf_program__attach_tracepoint替代方案A中的bpf_program__attach方法,這樣方便我們在用戶態(tài)代碼中靈活的開關(guān)ebpf的采集。除了專門用于靜態(tài)探針點的bpf_program__attach_tracepoint()函數(shù),還有適用于其他類型的專用的attach函數(shù),例如bpf_program__attach_kprobe()、bpf_program__attach_kprobe()、bpf_program__attach_uprobe()和bpf_program__attach_usdt()等。
5.8? 使用by_name替代by_title
在稍早一些libbpf庫中提供2個函數(shù)用于獲取bpf progam 類型數(shù)據(jù),分別是bpf_object__find_program_by_name()函數(shù)和bpf_object__find_program_by_title()函數(shù)。以trace_execve_libbpf130項目的 bpf代碼為例。
?
?
SEC("tracepoint/syscalls/sys_enter_execve") int trace_execve_enter(struct syscalls_enter_execve_args *ctx){ ...... }
?
?
其中tracepoint/syscalls/sys_enter_execve這個字符串就稱為title,trace_execve_enter這個函數(shù)名就稱為name。結(jié)合上文的結(jié)論,后續(xù)推薦bpf內(nèi)核態(tài)代碼中都使用SEC("tracepoint")的語法格式,那么使用by_title函數(shù)將不再能做出區(qū)分。因此這里特別推薦大家今后使用by_name的函數(shù)替代by_titile的函數(shù)。而且,在最新版的libbpf庫中,也徹底移除了bpf_object__find_program_by_title()函數(shù)。
06
基于原生libbpf庫改進方案構(gòu)建USDT和Uprobe項目
基于hexdump命令的改進型原生libbpf庫編程方案不但在內(nèi)核態(tài)跟蹤診斷上表現(xiàn)完美,在用戶態(tài)應(yīng)用進程的跟蹤診斷上依然可以表現(xiàn)得非常出色。本節(jié)內(nèi)容將在上文的基礎(chǔ)上,繼續(xù)分析如何使用原生libbpf庫開發(fā)和構(gòu)建USDT和Uprobe項目。
6.1? 用戶態(tài)模擬程序
用戶態(tài)應(yīng)用程序的ebpf,還需要準備一個模擬程序。尤其是針對USDT類型,還需要在模擬程序中進行靜態(tài)打點。本小節(jié)將提供一個如何打USDT跟蹤點的實例。
?
?
$ cd $NATIVE_LIBBPF # 返回工作目錄 $ cd mark_usdt_uprobe # 進入項目目錄 $ make $ sudo cp umark /usr/bin/ $ sudo umark >/dev/null 2>/dev/null & $ make clean
?
?
執(zhí)行完以上步驟,就啟動了用戶態(tài)模擬程序umark,后續(xù)即可通過USDT和Uprobe方式,追蹤umark進程的運行情況。
下面初步對umark模擬程序的代碼做一些介紹。
?
?
$ ls Makefile README.md sdt.h umark.c $ cat umark.c #include#include //#include #include "sdt.h" unsigned long long int func_uprobe1(unsigned long long int x){ return x + 1; } unsigned long long int func_uprobe2(unsigned long long int x, unsigned long long int y){ return x + y; } int main(int argc, char const *argv[]) { unsigned long long int i; int var1 = 10, var2 = 20, var3 = 30; for (i = 0; i < 86400000; i++) { sleep(1); DTRACE_PROBE1(groupa, probe1, var1); DTRACE_PROBE2(groupb, probe2, var2, var3); printf("hit uprobe1 %llu ", func_uprobe1(i)); printf("hit uprobe2 %llu ", func_uprobe2(i + 3, i + 8)); } return 0; }
?
?
其中func_uprobe1和func_uprobe2是兩個C語言函數(shù)用于下文的uprobe跟蹤實例的追蹤。DTRACE_PROBE1和DTRACE_PROBE2是兩個宏函數(shù),用于在umark.c程序中打USDT的靜態(tài)跟蹤點。最多支持傳入12個跟蹤點參數(shù),即DTRACE_PROBE1、DTRACE_PROBE2,一直到DTRACE_PROBE12。probe1和probe2是這個靜態(tài)跟蹤點的name,groupa和groupb是跟蹤點name的分組名,可以省略。
DTRACE_PROBE1宏函數(shù)定義在std.h頭文件內(nèi),需要提前安裝頭文件所在包。
在rpm包環(huán)境,sdt.h頭文件屬于systemtap-sdt-devel這個rpm包。
?
?
$ find /usr/include/ -name sdt.h /usr/include/sys/sdt.h $ rpm -qf /usr/include/sys/sdt.h systemtap-sdt-devel-4.8-2.0.2.al8.x86_64
?
?
在deb包環(huán)境,sdt.h頭文件屬于systemtap-sdt-dev這個deb包。
?
?
$ find /usr/include/ -name sdt.h /usr/include/x86_64-linux-gnu/sys/sdt.h $ dpkg -S /usr/include/x86_64-linux-gnu/sys/sdt.h systemtap-sdt-dev:amd64: /usr/include/x86_64-linux-gnu/sys/sdt.h
?
?
令人欣慰的是,這個sdt.h頭文件并無太多額外依賴,簡單修改后,可以獨立維護。于是,我們可以將其拷貝到本項目根目錄。并將
6.2? 構(gòu)建基于libbpf庫的USDT和Uprobe項目
下面我們就進一步介紹下使用第 2 代改進編程方案的ebpf跟蹤用戶態(tài)進程的解決方案。開始體驗我們的eBPF項目trace_user_libbpf130,進入項目代碼。
?
?
$ cd $NATIVE_LIBBPF # 返回工作目錄 $ cd trace_user_libbpf130 # 進入項目目錄 $ make $ sudo ./uprobe_test func_uprobe1 2374242 4604 umark 1534 bash 0 23368 23373 func_uprobe2 2374242 4604 umark 1534 bash 0 23371 23376 $ sudo ./usdt_test func_usdt1 2375442 4604 umark 1534 bash 0 10 17 func_usdt2 2375442 4604 umark 1534 bash 0 20 30
?
?
分別執(zhí)行uprobe_test和usdt_test兩個命令,對編譯結(jié)果進行驗證,均完美驗證通過。
trace_user_libbpf130項目的構(gòu)建和編譯過程與前面項目hexdump_skel_libbpf130無太多差異,不再做過多贅述。下文將著重對本項目中USDT和Uprobe的相關(guān)C語言源碼進行解析。
6.3? USDT代碼解析
trace_user_libbpf130項目中的USDT部分,開啟了2個usdt靜態(tài)探針點的跟蹤,這2個靜態(tài)探針點分別是probe1和probe2。
第一個靜態(tài)探針點實例,選擇在attach時,通過bpf_program__attach_usdt函數(shù)的參數(shù)指定靜態(tài)探針點的相關(guān)信息。包括跟蹤的進程信息"/usr/bin/umark",usdt組名信息"groupa",usdt名稱信息"probe1"等,代碼如下:
?
?
bpf_program__attach_usdt(bpf_prog1, -1, "/usr/bin/umark", "groupa", "probe1", NULL);
?
?
?第二個靜態(tài)探針點實例,選擇在bpf目標文件中,通過SEC宏的方式指定靜態(tài)探針點的相關(guān)信息。包括跟蹤的進程信息"/usr/bin/umark",usdt組名信息"groupb",usdt名稱信息"probe2"等,代碼如下:
?
?
SEC("usdt//usr/bin/umarkprobe2")?
?
?
6.4? BPF_USDT宏函數(shù)解析
目前主流的USDT類型的ebpf代碼實例,在bpf目標文件中都使用BPF_USDT宏函數(shù)來定義ebpf的處理函數(shù),例如本項目實例中。
?
?
int BPF_USDT(usdt_probe1, int x)
?
?
在這里,宏函數(shù)BPF_USDT的第1個參數(shù)"usdt_probe1"才是真正的函數(shù)名,也就是前文所述by_name的name信息。宏函數(shù)的第2個參數(shù)"int x"才是usdt_probe1函數(shù)的第一個參數(shù),依次類推。
各種USDT類型的ebpf代碼實例中,很少見到對這個宏函數(shù)BPF_USDT原理的分析。此處,我們借助第二個USDT靜態(tài)探針點在bpf目標文件中的使用來解析它。代碼實例的關(guān)鍵部分如下:
?
?
int usdt_probe2(struct pt_regs *ctx); static inline __attribute__((always_inline)) typeof(usdt_probe2(0)) ____usdt_probe2(struct pt_regs *ctx, int x, int y); typeof(usdt_probe2(0)) usdt_probe2(struct pt_regs *ctx) { return ____usdt_probe2(ctx, ({ long _x; bpf_usdt_arg(ctx, 0, &_x); (void *)_x; }), ({ long _x; bpf_usdt_arg(ctx, 1, &_x); (void *)_x; })); } static inline __attribute__((always_inline)) typeof(usdt_probe2(0)) ____usdt_probe2(struct pt_regs *ctx, int x, int y) { ...... }
?
?
這4行代碼,前兩行是函數(shù)聲明,后兩行是函數(shù)定義。usdt_probe2函數(shù)內(nèi)部調(diào)用了____usdt_probe2函數(shù)。一些代碼解讀:
always_inline,意味著無論優(yōu)化設(shè)置如何,編譯器都應(yīng)該始終將這個函數(shù)內(nèi)聯(lián)到任何調(diào)用它的地方。
typeof(usdt_probe2(0)) 用于確定 usdt_probe2 的返回類型,從而確保 ____usdt_probe2 與 usdt_probe2 有相同的返回類型。
({ long _x; bpf_usdt_arg(ctx, 0, &_x); (void *)_x; }) 這個復(fù)合語句用于獲取USDT探針的參數(shù)值。
使用 bpf_usdt_arg 輔助函數(shù)來獲取探針的第一個參數(shù),并將其存儲到局部變量 _x 中。再將 _x 強制轉(zhuǎn)換為 void * 類型并傳遞給 ____usdt_probe2 函數(shù)。同樣的操作也對第二個參數(shù) y 進行。
特別強調(diào)一下bpf_usdt_arg輔助函數(shù)來自于usdt.bpf.h頭文件,但本項目有2個usdt.bpf.h頭文件,其中一個在libbpf庫中,另外一個在./helpers/目錄下,helpers 目錄下的是經(jīng)過本項目改造過的。此示例中生效的是./helpers/目錄下的。
?
?
$ cd $NATIVE_LIBBPF # 返回工作目錄 $ cd trace_user_libbpf130 # 進入項目目錄 $ find . -name usdt.bpf.h ./tools/lib/bpf/usdt.bpf.h ./helpers/usdt.bpf.h?
?
?
6.5? Uprobe代碼解析
trace_user_libbpf130項目中的Uprobe部分,開啟了2個uprobe類型探針點的跟蹤,這2個uprobe探針點分別是probe1和probe2。
第一個uprobe探針點實例,選擇在attach時,通過bpf_program__attach_uprobe函數(shù)的參數(shù)指定uprobe探針點的相關(guān)信息。包括uprobe的類型(0表示函數(shù)進入時,1表示函數(shù)返回時),跟蹤的進程信息"/usr/bin/umark",被跟蹤的函數(shù)在進程中的偏移量 func_off1等。需要提前通過get_elf_func_offset()函數(shù)計算出這個偏移量,此函數(shù)定義在了helpers/uprobe_helper.c文件內(nèi)。相關(guān)代碼如下:
?
?
func_off1 = get_elf_func_offset("/usr/bin/umark", "func_uprobe1"); bpf_program__attach_uprobe(bpf_prog1, 0, -1, "/usr/bin/umark", func_off1);
?
?
第二個uprobe探針點實例,選擇在bpf目標文件中,通過SEC宏的方式指定uprobe探針點的相關(guān)信息。包括跟蹤的進程信息"/usr/bin/umark",被跟蹤的應(yīng)用進程中的函數(shù)"func_uprobe2"等。此種情況,libbpf庫會自動計算這個偏移量。代碼如下:
?
?
SEC("uprobe//usr/bin/umark:func_uprobe2")?
?
?
6.6? BPF_KPROBE宏函數(shù)解析
目前主流的Uprobe類型的ebpf代碼實例,在bpf目標文件中都使用BPF_KPROBE宏函數(shù)來定義ebpf的處理函數(shù),例如本項目實例中。
?
?
int BPF_KPROBE(user_probe1, unsigned long long int x)
?
?
?在這里,宏函數(shù)BPF_KPROBE的第1個參數(shù)"user_probe1"才是真正的函數(shù)名,也就是前文所述by_name的name信息。宏函數(shù)的第2個參數(shù)"unsigned long long int x"才是user_probe1函數(shù)的第一個參數(shù),依次類推。
各種Uprobe類型的ebpf代碼實例中,也同樣很少見到對這個宏函數(shù)BPF_KPROBE原理的分析。此處,我們借助第二個Uprobe探針點在bpf目標文件中的使用來解析它。關(guān)鍵的代碼實例如下:
?
?
long user_probe2(struct pt_regs *ctx); inline typeof(user_probe2(0)) ____user_probe2(struct pt_regs *ctx, unsigned long long int x, unsigned long long int y); inline typeof(user_probe2(0)) ____user_probe2(struct pt_regs *ctx, unsigned long long int x, unsigned long long int y) { ...... } typeof(user_probe2(0)) user_probe2(struct pt_regs *ctx) { return ____user_probe2(ctx, (unsigned long long int)PT_REGS_PARM1(ctx), (unsigned long long int)PT_REGS_PARM2(ctx)); }
?
?
這4行代碼,前兩行是函數(shù)聲明,后兩行是函數(shù)定義。user_probe2函數(shù)內(nèi)部調(diào)用了____user_probe2函數(shù)。一些代碼解讀:
inline typeof(user_probe2(0)) ____user_probe2(struct pt_regs *ctx, unsigned long long int x, unsigned long long int y); ?這是內(nèi)聯(lián)函數(shù)____user_probe2的聲明。
typeof(user_probe2(0))用于確定____user_probe2函數(shù)的返回類型,保證與user_probe2函數(shù)的返回類型一致。
typeof(user_probe2(0)) user_probe2(struct pt_regs *ctx) { return ____user_probe2(ctx, (unsigned long long int)PT_REGS_PARM1(ctx), (unsigned long long int)PT_REGS_PARM2(ctx)); } ?這是user_probe2函數(shù)的定義。它使用PT_REGS_PARM1(ctx)和PT_REGS_PARM2(ctx)宏來獲取用戶空間探針傳遞給eBPF程序的前兩個參數(shù)。
如果對于以上的代碼解讀如果還有不明白的地方,可以嘗試問問GPT。
審核編輯:黃飛
?
評論
查看更多