eBPF编程入门学习笔记–C编程
前言
目前的BPF编程方式:
其中ply目前也应该已经比较完善了,他的仓库是:https://github.com/iovisor/ply
可以看出目前BPF编程主要分成3种方式,指令集编程、C编程和BPF前端。
指令集编程就是直接写BPF的指令集,我们通过单个指令的组合进行编程,比较困难。可参考这篇文章。
C编程 编出就是本文下面的内容,我们需要实现 kern.c(bpf程序) 和 user.c(加载器)。
BPF前端,更友好的编程方式,提供了编程的框架,将一些操作封装成了函数,方便开发。详细的区别将在后续学习中进行记录。
参考资料
环境配置
使用ubuntu20版本
1.1.下载内核源码
下载的内核版本应与你系统的版本一致,查看当前内核版本 uname -r
然后在源码镜像站点(http://ftp.sjtu.edu.cn/sites/ftp.kernel.org/pub/linux/kernel)下载对应版本的内核源码
我是 5.13.0版本,所以下载对应的源码:
http://ftp.sjtu.edu.cn/sites/ftp.kernel.org/pub/linux/kernel/v5.x/linux-5.13.tar.gz
解压到/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
这个下面几个过程中可能会遇到各种 fatal error 错误,基本上是依赖缺失导致的,关键报错搜索就有解决办法,安装对应缺失的文件即可
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和用户态的可执行文件
我们可以运行demo试一下:
开始编写程序
hello-world
第一个实例程序
编写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()读取管道文件并打印到终端
修改Makefile文件,这里有点坑,不同内核版本的Makefile文件有些差异,网上不同文章的版本可能不同,不能完全按照进行修改,所以依据我当前的Makefile文件进行修改。
# List of programs to build
tprogs-y += hello
xxx
编译
返回源码根目录用 make M=samples/bpf
或 make samples/bpf/
编译
或者直接在当前目录(samples/bpf) 执行make 编译
这里遇到如下报错:
/usr/src/linux-5.13/samples/bpf/hello_user.c:3:10: fatal error: bpf_load.h: No such file or directory
3 | #include "bpf_load.h"
| ^~~~~~~~~~~~
compilation terminated.
make[2]: *** [/usr/src/linux-5.13/samples/bpf/Makefile.target:75: /usr/src/linux-5.13/samples/bpf/hello_user.o] Error 1
make[1]: *** [Makefile:1847: /usr/src/linux-5.13/samples/bpf] Error 2
make[1]: Leaving directory '/usr/src/linux-5.13'
make: *** [Makefile:269: all] Error 2
可以看到,提示缺失 bpf_load.h 头文件,这个头文件是内核给做的示范文件中一个,在较新版本的内核中没有了。 bpf_load 被认为是bpf loader并在2020年一次更新中删除了:https://www.spinics.net/lists/xdp-newbies/msg01866.html
不过还有很多可以学习的东西值得记录。
Makefile文件分析
来学习一下Makefile文件有什么内容,什么作用,make怎么工作的。
不同内核版本的Makefile内容会有差异,但总体逻辑是一致的。
- Make命令教程
- make命令,利用的是根目录下的Makefile,完成“生成头文件”和“生成.config文件”
关键内容分析:
- hostprogs -y 或者新版本中是 tprogs -y
它是Makefile文件中最开始的一段变量,官方的注释是List of programs to build,就是说明要构建的目标可执行程序,比如我们要构建新的hello程序,就添加一条 hostprogs-y += hello
。kbuild默认会去同一个目录下查找名为hello.c作为构建这个可执行文件的源文件。
之后将显式依赖关系添加到可执行文件中,一种是为Makefile中某个target添加这个可执行文件,作为prerequisites,形成依赖关系,这样就可以触发这个可执行文件的构建任务,另一种是直接利用变量 always,即无需指定第一种方式中的依赖关系,只要Makefile被执行,变量always中包含的可执行文件都会被构建。文件中有如下内容:
always := $(hostprogs-y)
- 变量
<executeable>-objs
LIBBPF := ../../tools/lib/bpf/bpf.o
CGROUP_HELPERS := ../../tools/testing/selftests/bpf/cgroup_helpers.o
test_lru_dist-objs := test_lru_dist.o $(LIBBPF)
sock_example-objs := sock_example.o $(LIBBPF)
fds_example-objs := bpf_load.o $(LIBBPF) fds_example.o
sockex1-objs := bpf_load.o $(LIBBPF) sockex1_user.o
sockex2-objs := bpf_load.o $(LIBBPF) sockex2_user.o
sockex3-objs := bpf_load.o $(LIBBPF) sockex3_user.o
如上代码,前两行是声明变量,在后续会进行引用。后续的变量名称和定义都有着这样的规律:<executeable>-objs
,右边是多个.o文件, 可执行文件可以由多个其他文件复合组成,通过<executeable>-objs
这样的语法,可以列出并指定所有用于生成最终可执行文件(命名为executeable)的文件清单。以如下代码为例,可执行文件sockex1是由bpf_load.o、bpf.o和sockex1_usr.o链接生成的。
sockex1-objs := bpf_load.o $(LIBBPF) sockex1_user.o
- 变量HOSTCFLAGS和HOSTLOADLIBES
HOSTCFLAGS += -I$(objtree)/usr/include
HOSTCFLAGS += -I$(srctree)/tools/lib/
HOSTCFLAGS += -I$(srctree)/tools/testing/selftests/bpf/
HOSTCFLAGS += -I$(srctree)/tools/lib/ -I$(srctree)/tools/include
HOSTCFLAGS += -I$(srctree)/tools/perf
HOSTCFLAGS_bpf_load.o += -I$(objtree)/usr/include -Wno-unused-variable
HOSTLOADLIBES_fds_example += -lelf
HOSTLOADLIBES_sockex1 += -lelf
HOSTLOADLIBES_sockex2 += -lelf
HOSTLOADLIBES_sockex3 += -lelf
...
HOSTLOADLIBES_tracex4 += -lelf -lrt
...
- 变量HOSTCFLAGS, 它是在编译host program(即可执行文件)时,为编译操作指定的特殊选项,如上面代码中使用-I参数指定依赖的头文件所在目录。默认情况下,这个变量的配置会作用到当前Makefile涉及的所有host program。如果你想为某个host program单独指定一个编译选项,可以像上文的这行代码:(只为bpf_load.o这个object文件指定特殊选项。)
HOSTCFLAGS_bpf_load.o += -I$(objtree)/usr/include -Wno-unused-variable
- HOSTLOADLIBES是用于链接(link)操作时指定的特殊选项,如上面代码中使用两个library(因为代码中使用了相关的函数),通过选项-l加到最终生成的可执行文件中:
- libelf,这个库用来管理elf格式的文件,bpf程序一般都会使用elf作为最终格式,因此需要加载这个library。
- librt,这个库其实很常用,一般含有#include<time.h>头文件的代码,都需要加载这个library,用来支持real time相关功能。
- 如何编译BPF程序源文件
# Trick to allow make to be run from this directory
all:
$(MAKE) -C ../../ $(CURDIR)/
...
$(obj)/%.o: $(src)/%.c
$(CLANG) $(NOSTDINC_FLAGS) $(LINUXINCLUDE) $(EXTRA_CFLAGS) -I$(obj) \
-I$(srctree)/tools/testing/selftests/bpf/ \
-D__KERNEL__ -Wno-unused-value -Wno-pointer-sign \
-D__TARGET_ARCH_$(ARCH) -Wno-compare-distinct-pointer-types \
-Wno-gnu-variable-sized-type-not-at-end \
-Wno-address-of-packed-member -Wno-tautological-compare \
-Wno-unknown-warning-option $(CLANG_ARCH_ARGS) \
-O2 -emit-llvm -c $< -o -| $(LLC) -march=bpf -filetype=obj -o $@
其中有两个系统变量:第一个 $@
代表的是target所指的文件名;第二个 $<
代表的是第一个prerequisite的文件名。这里看过前面提到的阮一峰的Make教程的话就知道这两个变量的意思,他们属于自动变量:
$@
指代当前目标,就是Make命令当前构建的那个目标。比如,make foo的$@
就指代foa.txt b.txt: touch $@
等同于下面的写法:
a.txt: touch a.txt b.txt: touch b.txt
$<
指代第一个前置条件。比如,规则为t: p1 p2
,那么$<
就指代p1。综上:
a.txt: b.txt c.txt cp $< $@
等同于:
a.txt: b.txt c.txt cp b.txt a.txt
上面的编译代码抽取出核心部分如下:
$(obj)/%.o: $(src)/%.c
clang -I $(srctree)/tools/testing/selftests/bpf/ \
-O2 -emit-llvm -c $< -o -| \
llc -march=bpf -filetype=obj -o $@
可以看出最后一行make命令的本质,就是把所有.c源代码文件,通过clang全部编译成.o目标文件。其中通配符匹配的 .o 文件和 .c 文件分别是Makefile规则 <target> : <prerequisites>
中的两个参数。
小结
对samples/bpf/Makefile这个文件执行make命令的本质就是:
- 为运行在内核空间的示例源代码(一般文件名称后缀为kern.c),编译生成.o后缀的目标文件,以便加载到对应BPF提供的hook中去。
- 为运行在用户空间的示例源代码(一般文件文件后缀为user.c),编译生成可以在本机直接运行的可执行文件,以便用户可以直接运行测试。
总结
在学习这部分内容的文章和课程的时候,发现新版本的内核源码已经有了一些变化,其中 load.h 的缺失也是在发展过程被抛弃的,虽然导致了编译的问题。 但是也学习了关于编译过程进行了哪些工作和 Makefile 的规范,以及 BPF C编程的一些入门。
这里后续我转为使用libbpf-bootstrap进行编程工作。本章节学习结束,后续会在libbpf-bootstrap的学习基础上进行文章分享。