BPF C編程入門


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 clang
apt 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/bpfmake 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用不了,我只能換一個試了.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM