作者:聞茂泉
他山之石
了解和掌握純c語言的ebpf編譯和使用,有助于我們加深對于eBPF技術原理的進一步掌握,也有助于開發符合自己業務需求的高性能的ebpf程序。目前常見和主流的純c語言的ebpf編譯使用方法,主要是兩種。一種是內核源碼中原生提供的編譯方式。另外一種是libbpf-bootstrap項目中提供的skeleton編譯方式。libbpf-bootstrap方式和社區5.x以上內核結合的比較好,以后再做介紹,今天我們選擇基于4.18內核的基于內核源碼的原生編譯方式做介紹。
在國內學習ebpf技術,就不得不提到《Linux內核觀測技術BPF》書籍譯者狄衛華老師。狄老師還有一個網站《深入淺出 eBPF》。在網站里,他專門用一篇文章介紹了基于內核源碼方式編譯ebpf的方式,文章內容叫《【BPF入門系列-3】BPF 環境搭建》
網址:https://www.ebpf.top/post/ebpf_c_env/
我們今天將參考這篇文章內容,對基于內核源碼方式的純c語言的ebpf編譯方式做進一步分析。
獲取內核源碼
目前主流的服務器的操作系統環境還是以8u + 4.18內核為主。因此,本文以4.18版本內核為主要分析對象。我們提供如下操作系統環境的獲取建議:
獲取操作系統環境
如果你自己有centos8u兼容環境操作系統,則可以使用已有的環境。如果沒有,可以通過阿里云官網購買阿里云主機,選擇選擇centos8或者anolis8操作系統環境。
$ cat /etc/centos-release
CentOS Linux release 8.5.2111
$ uname -r
4.18.0-348.7.1.el8_5.x86_64
獲取開源的內核源碼
可以使用wget,從aliyun官網鏡像,獲取開源的4.18內核源碼。
$ cd /tmp/$
wget https://mirrors.aliyun.com/linux-kernel/v4.x/linux-4.18.
tar.gz$ tar -zxvf linux-4.18.tar.gz$
cd linux-4.18
下載內核源碼一定要確保內核版本與操作系統的一致。原因是ebpf會用到VERSION、PATCHLEVEL和SUBLEVEL這3個宏的值與內核做內核版本校驗。如果版本傳的不對,ebpf校驗會失敗。
$ cat Makefile | grep -P '^VERSION|^PATCHLEVEL|^SUBLEVEL'
VERSION = 4
PATCHLEVEL = 18
SUBLEVEL = 0
初始化基礎環境
需要安裝ebpf編譯時依賴的llvm和clang等rpm包。此外內核編譯還需要依賴openssl-devel等rpm包。
$ sudo yum install bison flex openssl-devel
$ sudo yum install clang llvm elfutils-libelf-devel
具體每個實驗機器的環境可能略有差別,需要根據自己的情況做細節調整。
編譯內核源碼中ebpf程序樣例
編譯環境初始化
狄老師的文章中這里執行的是make scripts,在內核源碼編譯時此步驟前通常還需要執行make prepare。而make init正好包含這兩步make prepare && make scripts。因此,我們將命令按照如下方式優化,基本能夠一遍跑過:
$ cd /tmp/linux-4.18
$ make oldconfig && make init # make oldconfig && make prepare && make scripts
$ make headers_install
編譯內核源碼樣例
終于執行到了內核源碼中提供的ebpf程序樣例的編譯。
$ make M=samples/bpf
執行樣例程序
我們可以通過對樣例程序的執行,對編譯效果進行驗證。結果顯示執行成功,狄老師文章中的步驟驗證通過,有點小激動。
$ sudo ./samples/bpf/trace_output
recv 1766352 events per sec
內核源碼的ebpf編譯關鍵過程提取
接下去就是本文最重點的部分,對ebpf編譯過程的分析。我們主要分分析headers_install和對samples/bpf目錄的make這2個步驟。
頭文件安裝 make headers_install
重新獲取一個干凈的內核源碼,再次執行上面的編譯步驟。這次我們對編譯過程增加一些觀察步驟。
$ cd /tmp/
$ rm -fr /tmp/linux-4.18
$ tar -zxvf linux-4.18.tar.gz
$ cd /tmp/linux-4.18
$ make oldconfig && make init
$ ls usr/include/
ls: cannot access usr/include/: No such file or directory # 此時include目錄不存在
$ make headers_install
$ ls usr/include/ -R | grep -v -P ':$' | grep -v -P '^$' | wc -l931 # 此時include目錄下有931個文件
$ diff -rs usr/include/ /usr/include/|grep -P '^Files .+ and .+ are identical$'|wc -l677
這說明內核源碼目錄下,headers_install步驟生成的usr/include/目錄下功能900多個文件,其中大多數(677個)文件都能在操作系統環境的/usr/include/下找到完全一摸一樣的同名文件,并且內容也完全相同。
$ rpm -ql kernel-headers | wc -l
964
$ rpm -ql kernel-headers | head
/usr/include/asm
/usr/include/asm-generic
/usr/include/asm-generic/bpf_perf_event.h
而操作系統環境的/usr/include/目錄正好是kernel-headers包的安裝目錄。所以編譯過程中headers_install步驟就是在內核源碼目錄生成了kernel-headers包作用一樣的內容。
eBPF樣例編譯 make M=samples/bpf
ebpf樣例的編譯過程,我們做一下改進,通過SHELL選項打開shell的調試選項。具體命令如下:
$ make M=samples/bpf --debug=v,m SHELL="bash -x" > make.log 2>&1
通過分析make.log,再結合其他一些黑科技,可以大概找出內核源碼樣例中trace_output命令的編譯脈絡。其中用戶態編譯脈絡如下。為了表述上更加突出主題,此處只顯示編譯命令的關鍵信息,下一節會給出完整編譯命令。
$ gcc -g -fPIC -c -o libbpf.o libbpf.c
$ gcc -g -fPIC -c -o bpf.o bpf.c
$ gcc -g -fPIC -c -o btf.o btf.c
$ gcc -g -fPIC -c -o nlattr.o nlattr.c
$ ld -r -o libbpf-in.o libbpf.o bpf.o nlattr.o btf.o
$ ar rcs libbpf.a libbpf-in.o
$ gcc -O2 -std=gnu89 -c -o bpf_load.o bpf_load.c
$ gcc -O2 -std=gnu89 -c -o trace_output_user.o trace_output_user.c
$ gcc -O2 -std=gnu89 -c -o trace_helpers.o trace_helpers.c
$ gcc -o trace_output bpf_load.o trace_output_user.o trace_helpers.o libbpf.a -lelf -lrt
其中內核態編譯脈絡如下:
$ clang -O2 -emit-llvm -c trace_output_kern.c -o -
$ llc -march=bpf -filetype=obj -o trace_output_kern.o
其中前一行最后的橫線 - 表示 這里是輸出給shell管道,所以這兩行實際是可以通過shell管道拼接成一個命令來執行的。
手工編譯內核源碼中的eBPF樣例分析
通過上一節對關鍵步驟make M=samples/bpf的實踐,我們已經可以編譯出內核源碼中提供的ebpf樣例。但這還不夠我們充分地理解這個編譯過程,我們將這編譯過程繼續拆解一下,拆解成可以一步步執行的那種,為了方便大家理解,我將這個過程分解為 A-H 6大手工步驟,里面還會包含一些細分的小步驟:
$ cd /tmp/
$ rm -fr /tmp/linux-4.18$ tar -zxvf linux-4.18.tar.gz
$ cd /tmp/linux-4.18
$ make oldconfig && make init
$ make headers_install
$ cd tools/lib/bpf/
手工步驟A過程解析
手工步驟A1:
$ # gcc -g -fPIC -c -o libbpf.o libbpf.c
$ gcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o libbpf.o libbpf.c
手工步驟A2:
$ # gcc -g -fPIC -c -o bpf.o bpf.c
$ gcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o bpf.o bpf.c
手工步驟A3:
$ # gcc -g -fPIC -c -o btf.o btf.c
$ gcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o btf.o btf.c
手工步驟A4:
$ # gcc -g -fPIC -c -o nlattr.o nlattr.c
$ gcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o nlattr.o nlattr.c
針對手工步驟A1到A4的關鍵編譯選項做一些介紹。
-fPIC,告訴編譯器輸出位置無關目標,為后面生成共享庫埋下伏筆。
-I. 表示需要包含當前目錄下的頭文件。
-I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf,這4個頭文件,是用戶態ebpf程序所依賴tool目錄下的頭文件位置。
手工步驟B過程解析
$ ld -r -o libbpf-in.o libbpf.o bpf.o nlattr.o btf.o
手工步驟B是將步驟A中產生4個.o文件進行鏈接。
手工步驟C過程解析
$ ar rcs libbpf.a libbpf-in.o
手工步驟C是從鏈接后的文件中提取靜態庫文件。
手工步驟D/E/F過程解析
手工步驟D:
$ # gcc -O2 -std=gnu89 -c -o bpf_load.o bpf_load.c
$ gcc -O2 -fomit-frame-pointer -std=gnu89 -I./usr/include -I./tools/lib/ -I./tools/testing/selftests/bpf/ -I./tools/lib/ -I./tools/include -I./tools/perf -I./usr/include -Wno-unused-variable -c -o samples/bpf/bpf_load.o samples/bpf/bpf_load.c
手工步驟E:
$ # gcc -O2 -std=gnu89 -c -o trace_output_user.o trace_output_user.c
$ gcc -O2 -fomit-frame-pointer -std=gnu89 -I./usr/include -I./tools/lib/ -I./tools/testing/selftests/bpf/ -I./tools/lib/ -I./tools/include -I./tools/perf -I./tools/lib/bpf/ -c -o samples/bpf/trace_output_user.o samples/bpf/trace_output_user.c
手工步驟F:
$ # gcc -O2 -std=gnu89 -c -o trace_helpers.o trace_helpers.c
$ gcc -O2 -fomit-frame-pointer -std=gnu89 -I./usr/include -I./tools/lib/ -I./tools/testing/selftests/bpf/ -I./tools/lib/ -I./tools/include -I./tools/perf -I./tools/lib/bpf/ -c -o samples/bpf/../../tools/testing/selftests/bpf/trace_helpers.o samples/bpf/../../tools/testing/selftests/bpf/trace_helpers.c
針對手工步驟E的關鍵編譯選項做一些介紹。手工步驟D和手工步驟F與此類似。
O2 和 -std=gnu89 是兩個核心選項。
include選項,一共有6個,我們將其分為3組。第一組是-I./usr/include ,這表示包含等同于kernel-headers的內容。● 第二組是-I./tools/lib/, -I./tools/include,-I./tools/perf,-I./tools/lib/bpf/● 第三組是-I./tools/testing/selftests/bpf/。之所以把這一組單獨獨立出來,是因為它和樣例代碼處于同樣的路徑。
手工步驟G過程解析
$ # gcc -o trace_output bpf_load.o trace_output_user.o trace_helpers.o libbpf.a -lelf -lrt
$ gcc -o samples/bpf/trace_output samples/bpf/bpf_load.o samples/bpf/trace_output_user.o samples/bpf/../../tools/testing/selftests/bpf/trace_helpers.o /tmp/linux-4.18/samples/bpf/../../tools/lib/bpf/libbpf.a -lelf -lrt
針對手工步驟G的關鍵編譯選項做一些介紹。
-lelf -lrt鏈接兩個類庫
libbpf.a表示以靜態鏈接庫的方式鏈接libbpf的類庫。● 最關鍵的是,沒有添加-static選項,沒有添加-static選項,沒有添加-static選項,重要的事情說三遍。
手工步驟H過程解析
$ clang -nostdinc -isystem /usr/lib/gcc/x86_64-redhat-linux/8/include -I./arch/x86/include -I./arch/x86/include/generated -I./include -I./arch/x86/include/uapi -I./arch/x86/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/kconfig.h -Isamples/bpf -I./tools/testing/selftests/bpf/ -D__KERNEL__ -D__BPF_TRACING__ -D__TARGET_ARCH_x86 -O2 -emit-llvm -c samples/bpf/trace_output_kern.c -o - | llc -march=bpf -filetype=obj -o samples/bpf/trace_output_kern.o
針對手工步驟H的關鍵編譯選項做一些介紹。
-nostdinc -isystem /usr/lib/gcc/x86_64-redhat-linux/8/include,這2個選項是一組。nostdinc表示屏蔽掉系統默認的include環境,替換成當前gcc編譯器自帶的include頭文件環境。
-I./arch/x86/include,-I./arch/x86/include/generated,-I./include,-I./arch/x86/include/uapi,-I./arch/x86/include/generated/uapi,-I./include/uapi,-I./include/generated/uapi。這7個頭文件很關鍵,是內核態ebpf程序所依賴的絕大多數頭文件的位置。● -include ./include/linux/kconfig.h,這個頭文件也很關鍵,是讓上面7個頭文件生效的前提條件。● -I samples/bpf 和 -I ./tools/testing/selftests/bpf/,這2個頭文件是和ebpf樣例所處位置相同,單獨獨立出來看。● llc是llvm的連接器。內核是將clang的編譯和llc的鏈接獨立成兩步完成,在llc步驟才指定-march=bpf。對編譯結果進行驗證,完美驗證通過,第二次有點小激動。
$ sudo ./samples/bpf/trace_outputrecv 1760674 events per sec
關鍵步驟抽取不是最終目的,根本目的是能讓我們實現脫離內核源碼進行獨立的純C語言編譯。我們將在后續的文章中進一步闡述。
關于4.9版本內核
按照內核的原生步驟,對4.9內核進行一次編譯,我們會發現對應手工步驟E的這一步,編譯代碼有點不一樣,具體代碼如下。
$ gcc -o samples/bpf/trace_output samples/bpf/bpf_load.o samples/bpf/libbpf.o samples/bpf/trace_output_user.o -lelf -lrt
其中沒有了對libbpf.a靜態庫的鏈接,但卻多了一個libbpf.o文件的鏈接。
$ cd /tmp/linux-4.9/
$ find . -name libbpf.c
./samples/bpf/libbpf.c
./tools/lib/bpf/libbpf.c
查詢內核源碼,可以發現,在4.9內核下,有2個libbpf.c文件,分別處于./tools/lib/bpf/目錄和./samples/bpf/目錄。而內核ebpf樣例暫時使用的還是老的./samples/bpf/libbpf.c文件。
進一步探索
本文為eBPF動手實踐系列的第一篇,我們實現了基于內核源碼框架的一步一步的純C語言編譯,下一篇我們會對這個編譯過程繼續深入探索,實現脫離內核源碼后的純C語言編譯。
歡迎有想法或者有問題的同學,加群交流eBPF技術以及工程實踐。
SREWorks數智運維工程群(釘釘群號:35853026)
附錄: eBPF手工純C編譯完整命令清單
cd /tmp/rm -fr /tmp/linux-4.18tar -zxvf linux-4.18.tar.gz cd /tmp/linux-4.18make oldconfig && make initmake headers_installcd tools/lib/bpf/
# 步驟A1# gcc -g -fPIC -c -o libbpf.o libbpf.cgcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o libbpf.o libbpf.c
# 步驟A2# gcc -g -fPIC -c -o bpf.o bpf.cgcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o bpf.o bpf.c
# 步驟A3# gcc -g -fPIC -c -o btf.o btf.cgcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o btf.o btf.c
# 步驟A4# gcc -g -fPIC -c -o nlattr.o nlattr.cgcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o nlattr.o nlattr.c
# 步驟Bld -r -o libbpf-in.o libbpf.o bpf.o nlattr.o btf.o
# 步驟Car rcs libbpf.a libbpf-in.o
cd /tmp/linux-4.18/
# 步驟D# gcc -O2 -std=gnu89 -c -o bpf_load.o bpf_load.cgcc -O2 -fomit-frame-pointer -std=gnu89 -I./usr/include -I./tools/lib/ -I./tools/testing/selftests/bpf/ -I./tools/lib/ -I./tools/include -I./tools/perf -I./usr/include -Wno-unused-variable -c -o samples/bpf/bpf_load.o samples/bpf/bpf_load.c
# 步驟E# gcc -O2 -std=gnu89 -c -o trace_output_user.o trace_output_user.cgcc -O2 -fomit-frame-pointer -std=gnu89 -I./usr/include -I./tools/lib/ -I./tools/testing/selftests/bpf/ -I./tools/lib/ -I./tools/include -I./tools/perf -I./tools/lib/bpf/ -c -o samples/bpf/trace_output_user.o samples/bpf/trace_output_user.c
# 步驟F# gcc -O2 -std=gnu89 -c -o trace_helpers.o trace_helpers.cgcc -O2 -fomit-frame-pointer -std=gnu89 -I./usr/include -I./tools/lib/ -I./tools/testing/selftests/bpf/ -I./tools/lib/ -I./tools/include -I./tools/perf -I./tools/lib/bpf/ -c -o samples/bpf/../../tools/testing/selftests/bpf/trace_helpers.o samples/bpf/../../tools/testing/selftests/bpf/trace_helpers.c
# 步驟G# gcc -o trace_output bpf_load.o trace_output_user.o trace_helpers.o libbpf.a -lelf -lrtgcc -o samples/bpf/trace_output samples/bpf/bpf_load.o samples/bpf/trace_output_user.o samples/bpf/../../tools/testing/selftests/bpf/trace_helpers.o /tmp/linux-4.18/samples/bpf/../../tools/lib/bpf/libbpf.a -lelf -lrt
# 步驟Hclang -nostdinc -isystem /usr/lib/gcc/x86_64-redhat-linux/8/include -I./arch/x86/include -I./arch/x86/include/generated -I./include -I./arch/x86/include/uapi -I./arch/x86/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/kconfig.h -Isamples/bpf -I./tools/testing/selftests/bpf/ -D__KERNEL__ -D__BPF_TRACING__ -D__TARGET_ARCH_x86 -O2 -emit-llvm -c samples/bpf/trace_output_kern.c -o - | llc -march=bpf -filetype=obj -o samples/bpf/trace_output_kern.o