每日一博 | 解构内核源码 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
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Google 发布 Fuchsia OS F10
Fuchsia OS 是由 Google 开发的一款全新操作系统,旨在取代 Android 和 chromeOS。Fuchsia OS 使用 Google 自己开发的 Zircon 内核,而不是基于 Linux 内核。 Fuchsia OS 使用 Flutter 作为其主要的应用程序开发框架,这使得应用程序可以在不同类型的设备上无缝运行。该操作系统采用了 Material 设计语言,拥有现代化的用户界面和丰富的动画效果。 Fuchsia OS 目前还处于开发阶段,只能在一些特定的设备(如 Google Nest Hub)上运行。 近日 Google 发布了 Fuchsia F10 版本,该版本实施了以下变更: 组件框架 除了 Opal、Kronk、cast_runner 和 web_instance 之外,大多数组件现在都以 v2 的形式运行。 开发者 Bazel Bazel 工作流中做出了以下改进: 在 Fuchsia 平台之外定义的 FIDL 库不得以 Fuchsia 开头。 fuchsia_package.deps 应更新为 fuchsia_package.components ...
- 下一篇
Compose Multiplatform —— Kotlin 声明式 UI 框架
Compose Multiplatform 是用于跨多个平台共享 UI 的声明式框架,基于 Kotlin 和 Jetpack Compose。 目前支持以下平台: iOS(Alpha) Android(via Jetpack Compose) Desktop(Windows, MacOS, Linux) Web(Experimental) 核心特性 加速 UI 开发 构建一次用户界面,然后在所有平台上使用,包括 Android、iOS、Web 和桌面。无需费力同步不同的 UI,并且可以加快向用户交付应用程序的速度。 适用于其他平台的 Android UI 技能 使用已在 Jetpack Compose 中熟悉的相同 API 为所有平台构建用户界面。 卓越的生态系统 使用各种可以立即投入生产的 Kotlin 库和框架,从一开始便轻松提高工作效率,并从热情、乐于助人的开发者社区获取支持! 与每个平台轻松集成 基于Kotlin Multiplatform构建意味着无需拘泥于仅使用平台特定的功能和 API。轻松使用每个平台的全部功能,就像原生应用程序一样。 组件级重用 使用在所有目标平台上均可...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS8编译安装MySQL8.0.19
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS6,7,8上安装Nginx,支持https2.0的开启