千里之行,始于足下
理解和把握纯 c 语言的 eBPF 编译和应用,有助于咱们加深对于 eBPF 技术原理的进一步把握,也有助于开发合乎本人业务需要的高性能的 ebpf 程序。上一篇文章《eBPF 入手实际系列一:解构内核源码 eBPF 样例编译过程》中,咱们理解了基于内核源码的 ebpf 程序的编译步骤。其中编译过程对内核源码的依赖的内容,次要体现在对 kernel-devel 和 kernel-headers 两个 rpm 包的文件内容的依赖(centos 环境下)。这给咱们脱离内核源码进行独立的 ebpf 程序编译提供了可能。本文将介绍如何仅依赖于 kernel-devel 和 kernel-headers 等 rpm 包进行纯 c 语言的 eBPF 程序的编译和应用。
eBPF 开发的根底环境筹备
支流的 linux 发行版大多是基于 rpm 包或 deb 包的包管理系统。不同的包管理系统,搭建 eBPF 开发环境时所依赖的包,也略有差异。本文将别离进行介绍。
2.1 rpm 包根底环境初始化
在 centos、fedora 和 anolis 等发行版环境,须要装置一些编译过程的根底包、编译工具包、库依赖包和头文件依赖包等。具体装置步骤如下:
$ yum install git make rsync # 根底包
$ yum install clang llvm elfutils-libelf-devel # 编译工具和依赖库包
$ yum install kernel-headers-$(uname -r) kernel-devel-$(uname -r) # 头文件依赖包
2.2 deb 包根底环境初始化
在 ubuntu、debian 等发行版环境,须要装置一些编译过程的根底包、编译工具包、库依赖包和头文件依赖包等。具体装置步骤如下:
$ apt-get update # 更新 apt 源信息
$ apt install git make rsync # 根底包
$ apt install clang llvm libelf-dev # 编译工具和依赖库包
$ apt install linux-libc-dev linux-headers-$(uname -r) # 头文件依赖包
构建基于纯 C 语言的 eBPF 我的项目
3.1 纯 C 语言编译
在 eBPF 根底环境的筹备实现之后,就能够开始进行纯 C 语言的 eBPF 我的项目的搭建。这里咱们依然抉择应用 centos8u+4.18 内核为例来阐明构建过程。首次构建我的项目环境还须要依赖一次内核源码。下载内核源码,咱们举荐应用阿里云的镜像网站。
$ wget https://mirrors.aliyun.com/linux-kernel/v4.x/linux-4.18.tar.gz
$ tar -zxvf linux-4.18.tar.gz
获取 ebpf_purec_newbie git 我的项目的代码。并且通过其中的 initialize.sh 脚本,初始化 eBPF 我的项目。initialize.sh 脚本须要两个参数。
- 参数 1 用于指定内核源码的门路,
- 参数 2 用于指定新初始化的 ebpf 我的项目的目录,参数 2 可省略,省略后将默认设置为 /tmp/ebpf_project。
$ git clone https://github.com/alibaba/sreworks-ext.git -b master
$ cd sreworks-ext/demos/ebpf_purec_newbie
$ ./initialize.sh ~/linux-4.18 /tmp/ebpf_project
初始化后,就能够进入到 eBPF 我的项目目录,执行 make 命令,对内核源码自带的 eBPF 样例程序 trace_output 进行编译。
$ cd /tmp/ebpf_project
$ make
$ sudo ./trace_output
recv 662097 events per sec
执行 trace_output 命令,对编译后果进行验证,验证完满通过。
3.2 一些非凡状况的解决
这里提供的 ebpf_purec_newbie 的我的项目源码,包含其中的 initialize.sh 脚本,实用于 4.18 及以上各个内核版本。然而其中一些版本的内核源码,也存在一些不欠缺的中央。理论编译或者运行过程中,可能会存在一些问题。现将一些常见问题及解决办法做一些介绍。
3.2.1 函数 test_attr__open 定义相干问题
在 5.4 到 5.9 版本的内核编译时,可能会遇到 undefined reference to `test_attr__open’ 相干的问题。解决办法是关上 Makefile 中的 HAVE_ATTR_TEST 宏。具体可在编译前,执行如下命令批改 Makefile 文件。
$ sed -i '/DHAVE_ATTR_TEST/{s/^#//;}' /tmp/ebpf_project/Makefile
3.2.2 执行 ebpf 程序报 Operation not permitted 谬误
在一些版本的内核,运行编译完的 ebpf 程序 trace_output 时,会报 Operation not permitted 谬误。解决办法是调大过程的 MEMLOCK 资源限度。具体可在 trace_output_user.c 的 main 函数中 snprintf 函数之前,增加如下代码。
+ struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
+ setrlimit(RLIMIT_MEMLOCK, &r);
snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);
````
同时还须要增加相干头文件。
#include <sys/resource.h>
# **ebpf_project 初始化脚本解析 **
计算机技术是一门建设在试验根底上的学科。很多时候,一行 hello world 的胜利输入,打消了咱们对代码的疑虑。有了前文 trace_output 命令胜利运行的根底,咱们能够一鼓作气,深刻代码的细节,探索纯 C 语言的 eBPF 我的项目编译的过程,加深咱们对于 eBPF 技术原理的进一步了解。在 ebpf_purec_newbie 代码我的项目下,只蕴含 3 个文件:initialize.sh、Makefile 和 Makefile.libbpf。```
$ ls ebpf_purec_newbie/initialize.sh Makefile Makefile.libbpf
其中 initialize.sh 脚本是生成新的 eBPF 我的项目 ebpf_project 的脚本。上面介绍 ebpf_project 我的项目各个目录或者文件的起源。
- ebpf_project/tools 目录的内容次要是来自于内核源码目录 linux-4.18/tools。
- ebpf_project/helpers 目录的内容次要是来自于内核源码中 samples/bpf/ 和 tools/testing/selftests/bpf/ 两个目录中的一些 helper 类型的文件。内核源码中将这些 helper 类型的文件和样例文件混淆在一起,给初学者造成一些学习上的凌乱。有鉴于此,咱们对立集中到一个 helpers 目录下。
- trace_output_kern.c和 trace_output_user.c 这两个是 ebpf 样例文件,来自于内核源码的 samples/bpf/ 目录。这两个文件是成对呈现的,需重点关注,后文咱们还会提到。
- ebpf_project/Makefile 文件来自于我的项目 ebpf_purec_newbie/Makefile 文件。
- ebpf_project/tools/lib/bpf/Makefile 文件来自于我的项目 ebpf_purec_newbie/Makefile.libbpf 文件。
以上 ebpf_project 我的项目的内容中,除了两个 Makefile 文件,其余文件都复制于内核源码。这 2 个 Makefile 文件是整个我的项目的菁华所在,也是咱们须要进一步深刻了解的中央。其中 Makefile.libbpf 是用于生成 libbpf.a 动态库。另外一个 Makefile 是我的项目的主 Makefile,用于生成我的项目的可执行文件 trace_output 和内核态 bpf 文件 trace_output_kern.o。下文将别离针对 Makefile.libbpf 和主 Makefile 的代码逻辑进行剖析。
ebpf_project 我的项目 Makefile 解析
5.1 Makefile 解析过程提取
通常状况下理解 Makefile 的解析过程,须要浏览 Makefile 源码,不过本文提出另外一种剖析思路,那就是 奇妙地应用 make 命令的 –debug 选项参数,SHELL 环境变量参数 ,以及 makefile 语法中的 warning 管制函数。 依附这些技巧,咱们能够轻松地对 makefile 的具体解析过程进行提取。
$ cd /tmp/ebpf_project/
$ make clean
$ cd tools/lib/bpf/
$ make --debug=v,m SHELL="bash -x" > libbpf_make.log 2>&1
$ cd /tmp/ebpf_project/
$ make --debug=v,m SHELL="bash -x" > main_make.log 2>&1
````
别离获取了生成 libbpf.a 动态库的日志文件 libbpf_make.log,以及生成 ebpf 可执行程序的主日志文件 main_make.log。### **5.2 生成 libbpf.a 动态库的 Makefile 解析 **
通过对 'Considering target file' 内容的过滤,能够理解到 tools/lib/bpf/Makefile 开展过程。通过这样的一层一层的构建过程,最终将 tools/lib/bpf/ 目录下的几个文件 bpf.c、btf.c、libbpf.c 和 nlattr.c 构建了 libbpf.a 动态库文件。
$ cat libbpf_make.log | grep ‘Considering target file’
Considering target file `all’.
Considering target file `libbpf-in.o’.
Considering target file `precheck'.
Considering target file `force'.
Considering target file `elfdep'.
Considering target file `bpfdep'.
Considering target file `btf.o'.
Considering target file `btf.c'.
Considering target file `libbpf.o'.
Considering target file `libbpf.c'.
Considering target file `nlattr.o'.
Considering target file `nlattr.c'.
Considering target file `bpf.o'.
Considering target file `bpf.c'.
Considering target file `libbpf_errno.o'.
Considering target file `libbpf_errno.c'.
Considering target file `str_error.o'.
Considering target file `str_error.c'.
以 libbpf.o target 为例,能够看到具体一个 target 的残缺解析过程。通常,在“Must remake target”后会有“Invoking recipe from Makefile”,再之后便是咱们最关怀的理论执行的命令(recipe)局部。
Considering target file `libbpf.o’.
File `libbpf.o' does not exist.
Considering target file `libbpf.c'.
Finished prerequisites of target file `libbpf.c'.
No need to remake target `libbpf.c'.
Finished prerequisites of target file `libbpf.o'.
Must remake target `libbpf.o'.
Invoking recipe from Makefile:143 to update target `libbpf.o’.
gcc ‘-DBUILD_STR(s)=#s’ -o libbpf.o -c libbpf.c
Successfully remade target file `libbpf.o'.
最终在 all 这个 target 下,通过 ar rcs libbpf.a libbpf-in.o 这个命令(recipe)生成了 libbpf.a 动态库文件。### **5.3 我的项目主 Makefile 解析 **
这里同样也能够通过对 'Considering target file' 内容的过滤,理解到主 Makefile 的开展过程。每一个 target 局部,也会有与其对应的 "Invoking recipe from Makefile" 局部,以及理论执行的命令(recipe)局部。```
$ cat main_make.log | grep 'Considering target file'
Considering target file `all'.
Considering target file `trace_output'.
Considering target file `bpf_prog'.
Considering target file `verify_target_bpf'.
Considering target file `verify_cmds'.
Considering target file `clang'.
Considering target file `llc'.
Considering target file `trace_output_kern.o'.
Considering target file `trace_output_kern.c'.
Considering target file `tools/lib/bpf/libbpf.a'.
Considering target file `helpers/trace_helpers.o'.
Considering target file `helpers/trace_helpers.c'.
Considering target file `helpers/bpf_load.o'.
Considering target file `helpers/bpf_load.c'.
Considering target file `trace_output_user.o'.
Considering target file `trace_output_user.c'.
以上一层一层的构建步骤,产出指标文件次要是 2 个:trace_output_kern.o 和 trace_output。
- 其中 trace_output_kern.o 指标文件次要由样例文件 trace_output_kern.c 编译产生。
- 而 trace_output 指标文件次要由样例文件 trace_output_user.c,两个 helper 文件 bpf_load.c 和 trace_helpers.c,以及上一步的产物 libbpf.a 动态库编译产生。这里的 target libbpf.a 的局部的 recipe 命令,是最终触发 libbpf Makefile 的 make 构建过程的代码。
要害编译命令的编译参数解析
了解了 makefile 的解析过程,再来看下几个要害编译命令(recipe)的编译参数。在第一篇《解构内核源码 eBPF 样例编译过程》中,咱们曾经初步介绍了一些编译命令的编译参数含意。这里再做一些必要的补充。
6.1 内核态 bpf 程序 (trace_output_kern.o) 编译参数解析
内核态 bpf 程序 trace_output_kern.o 文件,是由样例文件 trace_output_kern.c 文件应用 clang 命令编译产生。
- 编译 trace_output_kern.o 命令的选项参数中,如下 8 个选项参数依赖的文件门路正好是 kernel-devel 这个 rpm 包的内容,这也是咱们脱离内核源码编译的一个中央。
-I/lib/modules/4.18.0-348.7.1.el8_5.x86_64/build/arch/x86/include
-I/lib/modules/4.18.0-348.7.1.el8_5.x86_64/build/arch/x86/include/generated
-I/lib/modules/4.18.0-348.7.1.el8_5.x86_64/build/include
-I/lib/modules/4.18.0-348.7.1.el8_5.x86_64/build/arch/x86/include/uapi
-I/lib/modules/4.18.0-348.7.1.el8_5.x86_64/build/arch/x86/include/generated/uapi
-I/lib/modules/4.18.0-348.7.1.el8_5.x86_64/build/include/uapi
-I/lib/modules/4.18.0-348.7.1.el8_5.x86_64/build/include/generated/uapi
-include /lib/modules/4.18.0-348.7.1.el8_5.x86_64/build//include/linux/kconfig.h
- bpf_helpers.h 是编译内核态 bpf 程序所依赖的要害的 helper 文件。随着内核版本的变动,此文件搁置的地位和内核源码中的样例程序援用的形式也产生了变动。在低版本中,随便的放到了 tools/testing/selftests/bpf 门路,援用形式为 #include “bpf_helpers.h”。而在高版本内核中,搁置的绝对标准一些,bpf_helpers.h 文件被搁置到了 tools/lib/bpf 门路,援用形式也天然改为了 #include <bpf/bpf_helpers.h>。同时,在咱们这里的头文件 include 门路里,也有细微差别。低版本内核,咱们将 bpf_helpers.h 文件规范性的拷贝到到了新我的项目的 helpers 目录,相应的是“-I./helpers”选项参数起作用。而在高版本内核是”-I./tools/lib/”选项参数起了作用。
- 较高版本的 clang 编译器,在增加 - g 选项参数后,会编译出带.BTF 段的指标文件。但较低版本的 clang 却没有这个性能,无奈间接编译出带 BTF 段的指标文件。即便这样,依然能够通过 pahole - J 命令,将指标文件中的 DWARF- 2 信息,转换出 BTF 段信息。
- 较低版本的 clang 编译器,不反对 ’asm goto’ 语法结构。解决办法是通过“-include asm_goto_workaround.h”选项参数,给内核态 bpf 文件被动增加 asm_goto_workaround.h 头文件,绕过这个问题。
6.2 用户态加载程序 (trace_output) 编译参数解析
在用户态指标文件 trace_output 的构建过程中,次要应用的编译命令是 gcc 编译命令。
- 编译 trace_output 的 gcc 命令中,gcc 不必显式的指定零碎头文件列表,gcc 会默认到零碎默认的头文件列表中查找头文件。应用如下命令能够显示零碎默认的头文件蕴含哪些。其中 /usr/include 文件门路正好是 kernel-header 这个 rpm 包的内容所在的目录,这里也是咱们脱离内核源码编译的第二个中央。
$ gcc -xc /dev/null -E -Wp,-v 2>&1 | sed -n 's,^ ,,p'
/usr/lib/gcc/x86_64-redhat-linux/8/include
/usr/local/include
/usr/include
- libbpf.h 头文件是编译 ebpf 用户态程序时,必不可少的头文件依赖。随着内核版本的变动,在内核源码的样例程序中援用的形式也产生了轻微变动。在较低版本内核源码样例中,援用形式是“#include <libbpf.h>”,在较高版本内核源码样例中,援用形式是“#include <bpf/libbpf.h>”。与此同时 libbpf.h 在内核源码中的搁置地位并没有变动,始终都是 tools/lib/bpf/libbpf.h。配合这种援用形式的变动的,是头文件搜寻门路的调整,低版本内核源码样例头文件搜寻门路是“-I./tools/lib/bpf/”,高版本内核源码样例头文件搜寻门路是“-I./tools/lib/”。
进一步摸索
本文为 eBPF 入手实际系列的第二篇,咱们一步一步实现了脱离内核源码后的纯 C 语言 eBPF 我的项目的构建。这个构建计划尽管没有特地思考对 CORE 的适配,然而通用性更强。针对内核态 bpf 程序 (trace_output_kern.o) 和用户态加载程序 (trace_output) 本文仅是从构建过程和编译参数动手,做了一些剖析,下一篇咱们会深刻到这两个要害的样例程序外部的代码逻辑寻根究底,探寻 ebpf 程序的外围逻辑。欢送有想法或者有问题的同学,加群交换 eBPF 技术以及工程实际。
- SREWorks 数智运维工程群 ( 钉钉群号:35853026)
- 跟踪诊断技术 SIG 开发者 & 用户群(钉钉群号:33304007)