對 Linux內核之旅-BPF C編程入門 的筆記
先說一下,這里的BPF實際上是指eBPF,不是傳統的cBPF
1.搭建BPF程序運行環境
1.1.下載內核源碼
下載的內核版本應與你系統的版本一致,查看當前內核版本 uname -r
然后在源碼鏡像站點(http://ftp.sjtu.edu.cn/sites/ftp.kernel.org/pub/linux/kernel)下載對應版本的內核源碼
也可以通過Ubuntu apt倉庫下載。Ubuntu官方自己維護了每個操作系統版本的背后的Linux內核代碼,可以通過以下兩種apt命令方式獲取相關代碼:
# 第一種方式 # 先搜索 > apt-cache search linux-source linux-source - Linux kernel source with Ubuntu patches linux-source-4.15.0 - Linux kernel source for version 4.15.0 with Ubuntu patches linux-source-4.18.0 - Linux kernel source for version 4.18.0 with Ubuntu patches linux-source-5.0.0 - Linux kernel source for version 5.0.0 with Ubuntu patches linux-source-5.3.0 - Linux kernel source for version 5.3.0 with Ubuntu patches # 再安裝 > apt install linux-source-4.15.0 # 第二種方式 > apt-get source linux Reading package lists... Done NOTICE: 'linux' packaging is maintained in the 'Git' version control system at: git://git.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git/bionic Please use: git clone git://git.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git/bionic to retrieve the latest (possibly unreleased) updates to the package. Need to get 167 MB of source archives. Get:2 https://mirrors.ustc.edu.cn/ubuntu bionic-updates/main linux 4.15.0-99.100 (tar) [158 MB] ... # 以上兩種方式,內核源代碼均下載至/usr/src/目錄下
1.2.安裝依賴項
apt install libncurses5-dev flex bison libelf-dev binutils-dev libssl-dev
1.3.安裝Clang和LLVM
然后使用以下兩條命令分別安裝 clang 和 llvm
apt install clangapt install llvm
1.4.配置內核
在源碼根目錄下使用make defconfig生成.config<c/ode>文件
1.5.解決modpost: not found錯誤
因為直接make M=samples/bpf時,會報錯缺少modules的錯誤。修復modpost的錯誤,以下兩種解決方案二選一
make modules_prepare
make script
1.6.關聯內核頭文件
make headers_install
1.7.編譯內核程序樣例
在源碼根目錄下執行make M=samples/bpf,
此時進入linux-source-4.15.0/smaples/bpf中,會看到生成了BPF字節碼文件*_kern.o和用戶態的可執行文件

你可以運行幾個試試,例如sockex1

2. 使用BPF C編寫hello world程序
2.1.先了解一下原理吧

BPF程序經過Clang/LLVM編譯成BPF字節碼,然后通過BPF系統調用的方式加載進內核,然后交給BPF虛擬機來執行,也是JIT的方式動態轉成機器碼
內核有很多hook點,我們在寫BPF程序時也會做事件源配置。當hook點上的事件發生時,就會執行我們的BPF程序。
我們還可以在BPF程序中創建一個Map,把我們想拿到的數據保存在Map中,然后用戶態程序就可以拿到。
總之,就是我們可以通過BPF程序拿到內核的一些數據
2.2.hello world程序

進入samples/bpf目錄,可以利用自帶的Makefile編譯,
編寫hello_kern.c:
#include <linux/bpf.h> #include "bpf_helpers.h" #define SEC(NAME) __attribute__((section(NAME), used)) SEC("tracepoint/syscalls/sys_enter_execve") int bpf_prog(void *ctx){ char msg[] = "Hello World\n"; bpf_trace_printk(msg, sizeof(msg)); return 0; } char _license[] SEC("license") = "GPL";
這個程序的作用就是當發生系統調用(sys_enter_execve)時在終端輸出"Hello World",其實bpf_trace_printk只是將msg寫到一個管道文件中
編寫hello_user.c:
#include <stdio.h> #include "bpf_load.h" int main(int argc, char **argv){ if(load_bpf_file("hello_kern.o")!=0){ printf("The kernel didn't load BPF program\n"); return -1; } read_trace_pipe(); return 0; }
這個程序的作用是將包含BPF的文件hello_kern.o通過系統調用的方式加載進內核,read_trace_pipe()讀取管道文件並打印到終端
2.3.修改Makefile
模仿原有的,有四處需要修改:
# List of programs to build hostprogs-y += hello # Libbpf dependencies hello-objs := bpf_load.o $(LIBBPF) hello_user.o # Tell kbuild to always build the programs always += hello_kern.o HOSTLOADLIBES_hello += -lelf
2.4.編譯
可以返回源碼根目錄用 make M=samples/bpf 或 make samples/bpf/ 編譯
或者直接在當前目錄(samples/bpf) 執行make 編譯
可以查看編譯后的結果,生成了hello可執行文件

2.5.運行

3.進一步
進一步學習BPF程序是如何轉換成字節碼的
3.1.BPF程序中的節(section)


SEC宏會將宏里面的內容(kprobe/sys_write)作為節的名字放到elf文件中,也就是目標文件,可以用readelf工具查看
還用宏生成了一個名字為license的section
3.2.BPF程序中的字節碼(bytecode)
可以用objdump工具查看

可見是將我們的bpf程序編譯到elf文件的某個節中,右邊黃框內就是常說的bpf字節碼,對應左邊灰色內容
接下來講一下,bpf程序是如何轉成字節碼的
3.3.BPF內核輔助函數調用轉換為BPF字節碼的過程

我們用到的BPF內核輔助函數是bpf_trace_printk
bpg_prog是我們的elf函數名字,分析下call 6是怎么得到的?

BPF_FUNC_map_lookup_elem(BPF_FUNC_trace_printk類似)是在bpf.h中定義的,只不過是宏的形式,我們將其展開:

可見BPF_FUNC_trace_printk的相對位置是6,
一般BPF內核輔助函數轉匯編是這樣的:

就是BPF_call id,id就是bpf_func_id中的id;
進一步就是BPF_EMIT_CALL(func name)
例如,在內核中的某一處代碼,調用bpf_map_lookup_elem,在BPF指令集編程中,就是使用BPF_EMIT_CALL來調用的

不難想象,我們調用bpf_trace_printk也是采用同樣的調用方式
BPF_EMIT_CALL(func name)是如何轉化成字節碼的呢?

_bpf_call_base啥也沒做,直接返回0,可見只是需要其地址,而差值就是在enum中的位置
進一步分析

所以,call 6對應的字節碼就是85 00 00 00 06 00 00 00
我們還可以進一步查看JIT前后字節碼的變化:

首先執行objdump -s hello_kern.o 得到JIT之前的字節碼:

在一直運行hello
進入linux-source-4.15.0/tools/bpf/bpftool目錄,make,生成bpftool工具,
通過 ./bpftool prog show 顯示加載了哪些BPF程序:

可見我們的hello程序對應的id為86,鈎子類型為tracepoint
再使用./bpftool prog dump xlated id 86 opcodes 即可查看JIT之后的字節碼:

對比起來看:

其他的沒變,可以看到這個變化,這是因為JIT前call使用的id,JIT后成了調用函數到這個指令的距離
3.4.BPF程序到BPF字節碼的編譯過程:Clang與LLVM


LLVM支持很多后端,通過命令llc -version

bpf target有三種,不指定就根據系統的大小端法
有兩種方式編譯BPF程序:

gcc缺少BPF backend,幸運的是clang支持BPF. 之前的Makefile就是使用clang將hello_kern.c編譯成hello_kern.o
右邊的圖表示一步到位和分布編譯的結果是一樣的,而且是之前用Makefile編譯的也一樣

分步編譯是生成中間IR文件,默認是.ll格式
除視頻外還參考了:
1. https://blog.csdn.net/qq_34258344/article/details/108932912
2. https://cloud.tencent.com/developer/article/1644458
特別是修改Makefile的地方,網上的都只改了三個地方,CSDN殺我¥#……
有個小問題是SEC("kprobe/sys_write")這個hook用不了,我只能換一個試了.
