服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

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

日期: 来源:阿里智能运维收集编辑:闻茂泉


1

他山之石

了解和掌握纯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编译方式做进一步分析。

2

获取内核源码

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

2.1  获取操作系统环境

如果你自己有centos8u兼容环境操作系统,则可以使用已有的环境。如果没有,可以通过阿里云官网购买阿里云主机,选择选择centos8或者anolis8操作系统环境。
$  cat /etc/centos-releaseCentOS Linux release 8.5.2111$  uname -r4.18.0-348.7.1.el8_5.x86_64

2.2  获取开源的内核源码

可以使用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 = 4PATCHLEVEL = 18SUBLEVEL = 0

3

初始化基础环境

需要安装ebpf编译时依赖的llvm和clang等rpm包。此外内核编译还需要依赖openssl-devel等rpm包。
$  sudo yum install bison flex openssl-devel$  sudo yum install clang llvm elfutils-libelf-devel

具体每个实验机器的环境可能略有差别,需要根据自己的情况做细节调整。

4

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

4.1  编译环境初始化

狄老师的文章中这里执行的是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

4.2  编译内核源码样例

终于执行到了内核源码中提供的ebpf程序样例的编译。
$  make M=samples/bpf

4.3  执行样例程序

我们可以通过对样例程序的执行,对编译效果进行验证。结果显示执行成功,狄老师文章中的步骤验证通过,有点小激动。
$  sudo ./samples/bpf/trace_outputrecv 1766352 events per sec

5

内核源码的ebpf编译关键过程提取

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

5.1  头文件安装 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 -l964  $  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包作用一样的内容。

5.2  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管道拼接成一个命令来执行的。

6

手工编译内核源码中的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/

6.1  手工步骤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目录下的头文件位置。

6.2  手工步骤B过程解析

$  ld -r -o libbpf-in.o  libbpf.o bpf.o nlattr.o btf.o
手工步骤B是将步骤A中产生4个.o文件进行链接。

6.3  手工步骤C过程解析

$  ar rcs libbpf.a libbpf-in.o
手工步骤C是从链接后的文件中提取静态库文件。

6.4  手工步骤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/。之所以把这一组单独独立出来,是因为它和样例代码处于同样的路径。

6.5  手工步骤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选项,重要的事情说三遍。

6.6  手工步骤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语言编译。我们将在后续的文章中进一步阐述。

7

关于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文件。

8

进一步探索

本文为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
/ END /

更多推荐

点击「阅读原文」查看SREWorks开源地址!

相关阅读

  • 【技术分享】从0到1——Hook内核系统调用

  • 本文对kernel双机调试环境搭建、基础LKM编写、内核Hook系统调用、内核模块在不同机器上的加载进行了简单的介绍。前言先介绍一下背景吧,目前IOT设备发展可谓如火如荼,在研究
  • 我用 Dubbo 传输文件,差点被开除。。。

  • 原价999元,现在参加拼团活动立享优惠价仅 149 元,赶快一起参团吧!《从0开始带你读Dubbo3.0核心源码》=== 课程介绍 ===本课程基于最新的 Dubbo3.0 讲解,将从零开始,一步步画图带
  • Windows内核漏洞分析与EXP编写技巧

  • 内核,是一个操作系统的核心。是基于硬件的第一层软件扩充,提供操作系统的最基本的功能,是操作系统工作的基础,它负责管理系统的进程、内存、设备驱动程序、文件和网络系统,决定着
  • 外行人都能看懂的SpringCloud,错过了血亏!

  • 原价799元,现在参加拼团活动立享优惠价仅 199 元,赶快一起参团吧!《SpringCloudAlibaba源码剖析专家课》=== 课程内容 ===现在大部分的公司在选择微服务技术框架的使用都会用sp
  • 地球内核正在相对反转

  • 图片来源:pixabay撰文 | 二七 在地球中心,一层熔融金属构成的液态圈层内,悬浮着一个主要由铁组成的固态内核。随着地球自转,内核也在同步转动,然而它的旋转速度与外部圈层并不一

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • AppLovin2023Q1大会回顾:活动云集,收获满满!

  • 将AppLovin微信公众号设为星标, 获取移动行业最新资讯在2023年的第一季度,AppLovin参加了众多行业活动,与我们的客户、潜在客户和合作伙伴建立了更深度的连接。在本文中,我们将
  • 天津中海·云麓公馆广告创意鉴赏

  • 关注「地产全案」公众号,每天收看全国顶尖创意作品案例~ 作品档案 项目名称:天津中海·云麓公馆创作公司:天津知行互动广告有限公司| 策略思考项目位于天津市南开区与红桥区