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/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用不了,我只能换一个试了.