千里之行,始于足下

理解和把握纯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_outputrecv 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.ctrace_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命令编译产生。

  1. 编译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
  1. 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/”选项参数起了作用。
  2. 较高版本的clang编译器,在增加-g选项参数后,会编译出带.BTF段的指标文件。但较低版本的clang却没有这个性能,无奈间接编译出带BTF段的指标文件。即便这样,依然能够通过pahole -J命令,将指标文件中的DWARF-2信息,转换出BTF段信息。
  3. 较低版本的clang编译器,不反对'asm goto'语法结构。解决办法是通过“-include asm_goto_workaround.h”选项参数,给内核态bpf文件被动增加asm_goto_workaround.h头文件,绕过这个问题。

6.2  用户态加载程序(trace_output)编译参数解析

在用户态指标文件trace_output的构建过程中,次要应用的编译命令是gcc编译命令。

  1. 编译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
  1. 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