乐趣区

关于运维:eBPF动手实践系列一解构内核源码eBPF样例编译过程

作者:闻茂泉

他山之石

理解和把握纯 c 语言的 ebpf 编译和应用,有助于咱们加深对于 eBPF 技术原理的进一步把握,也有助于开发合乎本人业务需要的高性能的 ebpf 程序。目前常见和支流的纯 c 语言的 ebpf 编译应用办法,次要是两种。一种是内核源码中原生提供的编译形式。另外一种是 libbpf-bootstrap 我的项目中提供的 skeleton 编译形式。libbpf-bootstrap 形式和社区 5.x 以上内核联合的比拟好,当前再做介绍,明天咱们抉择基于 4.18 内核的基于内核源码的原生编译形式做介绍。

在国内学习 ebpf 技术,就不得不提到《Linux 内核观测技术 BPF》书籍译者狄卫华老师。狄老师还有一个网站《深入浅出 eBPF》。在网站里,他专门用一篇文章介绍了基于内核源码形式编译 ebpf 的形式,文章内容叫《【BPF 入门系列 -3】BPF 环境搭建》

网址:https://www.ebpf.top/post/ebpf_c_env/

咱们明天将参考这篇文章内容,对基于内核源码形式的纯 c 语言的 ebpf 编译形式做进一步剖析。

获取内核源码

目前支流的服务器的操作系统环境还是以 8u + 4.18 内核为主。因而,本文以 4.18 版本内核为次要剖析对象。咱们提供如下操作系统环境的获取倡议:

获取操作系统环境

如果你本人有 centos8u 兼容环境操作系统,则能够应用已有的环境。如果没有,能够通过阿里云官网购买阿里云主机,抉择抉择 centos8 或者 anolis8 操作系统环境。

$  cat /etc/centos-release
CentOS Linux release 8.5.2111
$  uname -r
4.18.0-348.7.1.el8_5.x86_64

获取开源的内核源码

能够应用 wget,从 aliyun 官网镜像,获取开源的 4.18 内核源码。

$  cd /tmp/$  
wget https://mirrors.aliyun.com/linux-kernel/v4.x/linux-4.18.
tar.gz$  tar -zxvf linux-4.18.tar.gz$  
cd linux-4.18

下载内核源码肯定要确保内核版本与操作系统的统一。起因是 ebpf 会用到 VERSION、PATCHLEVEL 和 SUBLEVEL 这 3 个宏的值与内核做内核版本校验。如果版本传的不对,ebpf 校验会失败。

$  cat Makefile  | grep -P '^VERSION|^PATCHLEVEL|^SUBLEVEL'   
VERSION = 4
PATCHLEVEL = 18
SUBLEVEL = 0

初始化根底环境

须要装置 ebpf 编译时依赖的 llvm 和 clang 等 rpm 包。此外内核编译还须要依赖 openssl-devel 等 rpm 包。

$  sudo yum install bison flex openssl-devel
$  sudo yum install clang llvm elfutils-libelf-devel

具体每个试验机器的环境可能略有差异,须要依据本人的状况做细节调整。

编译内核源码中 ebpf 程序样例

编译环境初始化

狄老师的文章中这里执行的是 make scripts,在内核源码编译时此步骤前通常还须要执行 make prepare。而 make init 正好蕴含这两步 make prepare && make scripts。因而,咱们将命令依照如下形式优化,根本可能一遍跑过:

$  cd /tmp/linux-4.18
$  make oldconfig && make init  # make oldconfig && make prepare && make scripts
$  make headers_install

编译内核源码样例

终于执行到了内核源码中提供的 ebpf 程序样例的编译。

$  make M=samples/bpf

执行样例程序

咱们能够通过对样例程序的执行,对编译成果进行验证。结果显示执行胜利,狄老师文章中的步骤验证通过,有点小冲动。

$  sudo ./samples/bpf/trace_output
recv 1766352 events per sec

内核源码的 ebpf 编译要害过程提取

接下去就是本文最重点的局部,对 ebpf 编译过程的剖析。咱们次要分剖析 headers_install 和对 samples/bpf 目录的 make 这 2 个步骤。

头文件装置 make headers_install

从新获取一个洁净的内核源码,再次执行下面的编译步骤。这次咱们对编译过程减少一些察看步骤。

$  cd  /tmp/
$  rm -fr /tmp/linux-4.18
$  tar -zxvf linux-4.18.tar.gz 
$  cd /tmp/linux-4.18
$  make oldconfig && make init
$  ls usr/include/
ls: cannot access usr/include/: No such file or directory      # 此时 include 目录不存在
$  make headers_install
$  ls usr/include/ -R  | grep -v -P ':$' | grep -v -P '^$' | wc -l931                                                      #  此时 include 目录下有 931 个文件
$  diff -rs usr/include/ /usr/include/|grep -P '^Files .+ and .+ are identical$'|wc -l677

这阐明内核源码目录下,headers_install 步骤生成的 usr/include/ 目录下性能 900 多个文件,其中大多数(677 个)文件都能在操作系统环境的 /usr/include/ 下找到齐全一摸一样的同名文件,并且内容也完全相同。

$  rpm -ql kernel-headers | wc -l
964  
$  rpm -ql kernel-headers | head 
/usr/include/asm
/usr/include/asm-generic
/usr/include/asm-generic/bpf_perf_event.h

而操作系统环境的 /usr/include/ 目录正好是 kernel-headers 包的装置目录。所以编译过程中 headers_install 步骤就是在内核源码目录生成了 kernel-headers 包作用一样的内容。

eBPF 样例编译 make M=samples/bpf

ebpf 样例的编译过程,咱们做一下改良,通过 SHELL 选项关上 shell 的调试选项。具体命令如下:

$  make M=samples/bpf --debug=v,m SHELL="bash -x" > make.log 2>&1

通过剖析 make.log,再联合其余一些黑科技,能够大略找出内核源码样例中 trace_output 命令的编译脉络。其中用户态编译脉络如下。为了表述上更加突出主题,此处只显示编译命令的要害信息,下一节会给出残缺编译命令。

$  gcc -g -fPIC -c -o libbpf.o libbpf.c
$  gcc -g -fPIC -c -o bpf.o bpf.c
$  gcc -g -fPIC -c -o btf.o btf.c
$  gcc -g -fPIC -c -o nlattr.o nlattr.c
$  ld -r -o libbpf-in.o libbpf.o bpf.o nlattr.o btf.o
$  ar rcs libbpf.a libbpf-in.o
$  gcc -O2 -std=gnu89 -c -o bpf_load.o bpf_load.c
$  gcc -O2 -std=gnu89 -c -o trace_output_user.o trace_output_user.c
$  gcc -O2 -std=gnu89 -c -o trace_helpers.o trace_helpers.c
$  gcc -o trace_output bpf_load.o trace_output_user.o trace_helpers.o libbpf.a -lelf -lrt

其中内核态编译脉络如下:

$  clang -O2 -emit-llvm -c trace_output_kern.c -o - 
$  llc -march=bpf -filetype=obj -o trace_output_kern.o

其中前一行最初的横线 – 示意 这里是输入给 shell 管道,所以这两行理论是能够通过 shell 管道拼接成一个命令来执行的。

手工编译内核源码中的 eBPF 样例剖析

通过上一节对关键步骤 make M=samples/bpf 的实际,咱们曾经能够编译出内核源码中提供的 ebpf 样例。但这还不够咱们充沛地了解这个编译过程,咱们将这编译过程持续拆解一下,拆解成能够一步步执行的那种,为了不便大家了解,我将这个过程合成为 A-H 6 大手工步骤,外面还会蕴含一些细分的小步骤:

$  cd  /tmp/
$  rm -fr /tmp/linux-4.18$  tar -zxvf linux-4.18.tar.gz 
$  cd /tmp/linux-4.18
$  make oldconfig && make init
$  make headers_install
$  cd tools/lib/bpf/

手工步骤 A 过程解析

手工步骤 A1:

$  # gcc -g -fPIC -c -o libbpf.o libbpf.c
$  gcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o libbpf.o libbpf.c

手工步骤 A2:

$ # gcc -g -fPIC -c -o bpf.o bpf.c
$  gcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o bpf.o bpf.c

手工步骤 A3:

$ # gcc -g -fPIC -c -o btf.o btf.c
$  gcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o btf.o btf.c

手工步骤 A4:

$ # gcc -g -fPIC -c -o nlattr.o nlattr.c
$  gcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o nlattr.o nlattr.c

针对手工步骤 A1 到 A4 的要害编译选项做一些介绍。

  • -fPIC,通知编译器输入地位无关指标,为前面生成共享库埋下伏笔。
  • -I. 示意须要蕴含当前目录下的头文件。
  • -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf,这 4 个头文件,是用户态 ebpf 程序所依赖 tool 目录下的头文件地位。

手工步骤 B 过程解析

$  ld -r -o libbpf-in.o  libbpf.o bpf.o nlattr.o btf.o

手工步骤 B 是将步骤 A 中产生 4 个.o 文件进行链接。

手工步骤 C 过程解析

$  ar rcs libbpf.a libbpf-in.o

手工步骤 C 是从链接后的文件中提取动态库文件。

手工步骤 D /E/ F 过程解析

手工步骤 D:

$  # gcc -O2 -std=gnu89 -c -o bpf_load.o bpf_load.c
$  gcc -O2 -fomit-frame-pointer -std=gnu89 -I./usr/include -I./tools/lib/ -I./tools/testing/selftests/bpf/ -I./tools/lib/ -I./tools/include -I./tools/perf -I./usr/include -Wno-unused-variable -c -o samples/bpf/bpf_load.o samples/bpf/bpf_load.c

手工步骤 E:

$  # gcc -O2 -std=gnu89 -c -o trace_output_user.o trace_output_user.c
$  gcc -O2 -fomit-frame-pointer -std=gnu89 -I./usr/include -I./tools/lib/ -I./tools/testing/selftests/bpf/ -I./tools/lib/ -I./tools/include -I./tools/perf -I./tools/lib/bpf/ -c -o samples/bpf/trace_output_user.o samples/bpf/trace_output_user.c

手工步骤 F:

$  # gcc -O2 -std=gnu89 -c -o trace_helpers.o trace_helpers.c
$  gcc -O2 -fomit-frame-pointer -std=gnu89 -I./usr/include -I./tools/lib/ -I./tools/testing/selftests/bpf/ -I./tools/lib/ -I./tools/include -I./tools/perf -I./tools/lib/bpf/ -c -o samples/bpf/../../tools/testing/selftests/bpf/trace_helpers.o samples/bpf/../../tools/testing/selftests/bpf/trace_helpers.c

针对手工步骤 E 的要害编译选项做一些介绍。手工步骤 D 和手工步骤 F 与此相似。

  • O2 和 -std=gnu89 是两个外围选项。
  • include 选项,一共有 6 个,咱们将其分为 3 组。第一组是 -I./usr/include,这示意蕴含等同于 kernel-headers 的内容。● 第二组是 -I./tools/lib/,-I./tools/include,-I./tools/perf,-I./tools/lib/bpf/● 第三组是 -I./tools/testing/selftests/bpf/。之所以把这一组独自独立进去,是因为它和样例代码处于同样的门路。

手工步骤 G 过程解析

$  # gcc -o trace_output bpf_load.o trace_output_user.o trace_helpers.o libbpf.a -lelf -lrt
$  gcc -o samples/bpf/trace_output samples/bpf/bpf_load.o samples/bpf/trace_output_user.o samples/bpf/../../tools/testing/selftests/bpf/trace_helpers.o /tmp/linux-4.18/samples/bpf/../../tools/lib/bpf/libbpf.a -lelf -lrt

针对手工步骤 G 的要害编译选项做一些介绍。

  • -lelf -lrt 链接两个类库
  • libbpf.a 示意以动态链接库的形式链接 libbpf 的类库。● 最要害的是,没有增加 -static 选项,没有增加 -static 选项,没有增加 -static 选项,重要的事件说三遍。

手工步骤 H 过程解析

$  clang -nostdinc -isystem /usr/lib/gcc/x86_64-redhat-linux/8/include -I./arch/x86/include -I./arch/x86/include/generated -I./include -I./arch/x86/include/uapi -I./arch/x86/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/kconfig.h -Isamples/bpf -I./tools/testing/selftests/bpf/ -D__KERNEL__ -D__BPF_TRACING__ -D__TARGET_ARCH_x86 -O2 -emit-llvm -c samples/bpf/trace_output_kern.c -o - | llc -march=bpf -filetype=obj -o samples/bpf/trace_output_kern.o

针对手工步骤 H 的要害编译选项做一些介绍。

  • -nostdinc -isystem /usr/lib/gcc/x86_64-redhat-linux/8/include,这 2 个选项是一组。nostdinc 示意屏蔽掉零碎默认的 include 环境,替换成以后 gcc 编译器自带的 include 头文件环境。
  • -I./arch/x86/include,-I./arch/x86/include/generated,-I./include,-I./arch/x86/include/uapi,-I./arch/x86/include/generated/uapi,-I./include/uapi,-I./include/generated/uapi。这 7 个头文件很要害,是内核态 ebpf 程序所依赖的绝大多数头文件的地位。● -include ./include/linux/kconfig.h,这个头文件也很要害,是让下面 7 个头文件失效的前提条件。● -I samples/bpf 和 -I ./tools/testing/selftests/bpf/,这 2 个头文件是和 ebpf 样例所处地位雷同,独自独立进去看。● llc 是 llvm 的连接器。内核是将 clang 的编译和 llc 的链接独立成两步实现,在 llc 步骤才指定 -march=bpf。对编译后果进行验证,完满验证通过,第二次有点小冲动。
$  sudo ./samples/bpf/trace_outputrecv 1760674 events per sec

关键步骤抽取不是最终目标,基本目标是能让咱们实现脱离内核源码进行独立的纯 C 语言编译。咱们将在后续的文章中进一步论述。

对于 4.9 版本内核

依照内核的原生步骤,对 4.9 内核进行一次编译,咱们会发现对应手工步骤 E 的这一步,编译代码有点不一样,具体代码如下。

$  gcc -o samples/bpf/trace_output samples/bpf/bpf_load.o samples/bpf/libbpf.o samples/bpf/trace_output_user.o -lelf -lrt

其中没有了对 libbpf.a 动态库的链接,但却多了一个 libbpf.o 文件的链接。

$  cd /tmp/linux-4.9/
$  find . -name libbpf.c
./samples/bpf/libbpf.c
./tools/lib/bpf/libbpf.c

查问内核源码,能够发现,在 4.9 内核下,有 2 个 libbpf.c 文件,别离处于./tools/lib/bpf/ 目录和./samples/bpf/ 目录。而内核 ebpf 样例临时应用的还是老的./samples/bpf/libbpf.c 文件。

进一步摸索

本文为 eBPF 入手实际系列的第一篇,咱们实现了基于内核源码框架的一步一步的纯 C 语言编译,下一篇咱们会对这个编译过程持续深刻摸索,实现脱离内核源码后的纯 C 语言编译。

欢送有想法或者有问题的同学,加群交换 eBPF 技术以及工程实际。

SREWorks 数智运维工程群 ( 钉钉群号:35853026)

附录: eBPF 手工纯 C 编译残缺命令清单

cd  /tmp/rm -fr /tmp/linux-4.18tar -zxvf linux-4.18.tar.gz cd /tmp/linux-4.18make oldconfig && make initmake headers_installcd tools/lib/bpf/
# 步骤 A1# gcc -g -fPIC -c -o libbpf.o libbpf.cgcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o libbpf.o libbpf.c
# 步骤 A2# gcc -g -fPIC -c -o bpf.o bpf.cgcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o bpf.o bpf.c
# 步骤 A3# gcc -g -fPIC -c -o btf.o btf.cgcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o btf.o btf.c
# 步骤 A4# gcc -g -fPIC -c -o nlattr.o nlattr.cgcc -g -DHAVE_LIBELF_MMAP_SUPPORT -DCOMPAT_NEED_REALLOCARRAY -fPIC -I. -I/tmp/linux-4.18/tools/include -I/tmp/linux-4.18/tools/arch/x86/include/uapi -I/tmp/linux-4.18/tools/include/uapi -I/tmp/linux-4.18/tools/perf -D"BUILD_STR(s)=#s" -c -o nlattr.o nlattr.c
# 步骤 Bld -r -o libbpf-in.o  libbpf.o bpf.o nlattr.o btf.o
# 步骤 Car rcs libbpf.a libbpf-in.o 
cd /tmp/linux-4.18/
# 步骤 D# gcc -O2 -std=gnu89 -c -o bpf_load.o bpf_load.cgcc -O2 -fomit-frame-pointer -std=gnu89 -I./usr/include -I./tools/lib/ -I./tools/testing/selftests/bpf/ -I./tools/lib/ -I./tools/include -I./tools/perf -I./usr/include -Wno-unused-variable -c -o samples/bpf/bpf_load.o samples/bpf/bpf_load.c
# 步骤 E# gcc -O2 -std=gnu89 -c -o trace_output_user.o trace_output_user.cgcc -O2 -fomit-frame-pointer -std=gnu89 -I./usr/include -I./tools/lib/ -I./tools/testing/selftests/bpf/ -I./tools/lib/ -I./tools/include -I./tools/perf -I./tools/lib/bpf/ -c -o samples/bpf/trace_output_user.o samples/bpf/trace_output_user.c
# 步骤 F# gcc -O2 -std=gnu89 -c -o trace_helpers.o trace_helpers.cgcc -O2 -fomit-frame-pointer -std=gnu89 -I./usr/include -I./tools/lib/ -I./tools/testing/selftests/bpf/ -I./tools/lib/ -I./tools/include -I./tools/perf -I./tools/lib/bpf/ -c -o samples/bpf/../../tools/testing/selftests/bpf/trace_helpers.o samples/bpf/../../tools/testing/selftests/bpf/trace_helpers.c
# 步骤 G# gcc -o trace_output bpf_load.o trace_output_user.o trace_helpers.o libbpf.a -lelf -lrtgcc -o samples/bpf/trace_output samples/bpf/bpf_load.o samples/bpf/trace_output_user.o samples/bpf/../../tools/testing/selftests/bpf/trace_helpers.o /tmp/linux-4.18/samples/bpf/../../tools/lib/bpf/libbpf.a -lelf -lrt
# 步骤 Hclang -nostdinc -isystem /usr/lib/gcc/x86_64-redhat-linux/8/include -I./arch/x86/include -I./arch/x86/include/generated -I./include -I./arch/x86/include/uapi -I./arch/x86/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/kconfig.h -Isamples/bpf -I./tools/testing/selftests/bpf/ -D__KERNEL__ -D__BPF_TRACING__ -D__TARGET_ARCH_x86 -O2 -emit-llvm -c samples/bpf/trace_output_kern.c -o - | llc -march=bpf -filetype=obj -o samples/bpf/trace_output_kern.o
退出移动版