赞
踩
在上一篇文章《eBPF动手实践系列二:构建基于纯C语言的eBPF项目》中,我们初步实现了脱离内核源码进行纯C语言eBPF项目的构建。libbpf库在早期和内核源码结合的比较紧密,如今的libbpf库更加成熟,已经完全脱离内核源码独立发展。
为了更加具体的理解linux内核版本演进和libbpf版本演进的关系,本文在“附录A”中总结了各个内核版本源码示例中所依赖的libbpf库的对应版本信息。
大部分版本的内核获取libbpf版本的方法如下,从libbpf库目录的libbpf.map文件中提取最大的版本号信息。这里的"source"为内核源码所在目录。
$ cat ./source/tools/lib/bpf/libbpf.map | grep -oE '^LIBBPF_([0-9.]+)' | sort -rV | head -n1 | cut -d'_' -f2
较早版本的内核在./tools/lib/bpf/Makefile文件中直接定义了libbpf的版本信息。
- $ cat ./source/tools/lib/bpf/Makefile
- BPF_VERSION = 0
- BPF_PATCHLEVEL = 0
- BPF_EXTRAVERSION = 2
为了简化 eBPF程序的开发流程,降低开发者在使用 libbpf 库时的入门难度,libbpf-bootstrap 框架应运而生。基于libbpf-bootstrap框架的编程方案是目前网络上看到的最主流编程方案。此外,网络上也偶见比较古老的仅依赖一个bpf_load.c文件的C语言编程方案,这个方案并不需要依赖libbpf库的支持。
主流的C语言实现的eBPF编程方案,大体上就是以下三种,笔者总共将其归纳为3代。
代际 | 方案指称 | 识别方法 | 备注 |
第1代 | bpf_load.c文件方案 | 代码中有bpf_load.c文件,还有load_bpf_file函数。 | Linux 4.x 系列早期内核版本的源码实例大多基于此文件,这个旧 API 方案已经在内核中被逐步废弃。 |
第2代 | 原生libbpf库方案 | 代码中有libbpf.c文件 | Linux 5.x版本内核的源码实例很多使用以libbpf.c为核心的原生libbpf库方案,是本文重点阐述的方案。 |
第3代 | libbpf-bootstrap骨架方案 | 代码中除了libbpf.c文件,还有libbpf-bootstrap、skeleton和*.skel.h关键词 | 最新版本内核的源码实例已经开始采用此方案。业界最新的eBPF介绍文章较多基于此方案。 |
除了经典的C语言编程方案,一些编程框架还选择使用Python语言,Go语言,或者Rust语言作为用户态加载的实现语言。
尽管libbpf-bootstrap骨架C语言方案、基于libbpfgo库的go语言方案等已经被大家广泛使用和接受。但笔者认为基于原生libbpf库的eBPF编程方案仍然具备很多独特的优势。以下是原生libbpf库eBPF编程方案的一些独特优势:
本文将由浅入深介绍第 2 代原生libbpf库的eBPF编程方案,并提出一种改进思路。
主流的linux发行版大多是基于rpm包或deb包的包管理系统。不同的包管理系统,初始化eBPF开发环境时所依赖的包,也略有差别。本文将分别进行介绍。
在RPM包发行版环境,需要安装一些编译过程的基础包、编译工具包、库依赖包和头文件依赖包等。我们推荐使用如下一些发行版及其兼容环境:Anolis 8.8、Kylin V10、CentOS 8.5、和 Fedora 39 等。
详细安装步骤如下:
- $ yum install git make # 基础包
- $ yum install kernel-headers-$(uname -r) # 头文件依赖包
- $ yum install clang llvm elfutils-libelf-devel # 编译工具和依赖库包
-
- ## 依次选择如下命令之一,安装bpftool工具
- $ yum install bpftool-$(uname -r)
- $ yum install bpftool
在 DEB 包发行版环境,需要安装一些编译过程的基础包、编译工具包、库依赖包和头文件依赖包等。推荐使用Ubuntu 22.04 或Debian 12 等发行版及其兼容环境。
详细安装步骤如下:
- $ apt-get update # 更新apt源信息
- $ apt install git make # 基础包
- $ apt install linux-libc-dev # 头文件依赖包
- $ apt install clang llvm libelf-dev # 编译工具和依赖库包
-
- ## 依次选择如下命令之一,安装bpftool工具
- $ apt install linux-tools-common linux-tools-$(uname -r)
- $ apt install linux-tools-common linux-tools-generic
- $ apt install linux-tools-$(uname -r) linux-cloud-tools-$(uname -r)
- $ apt install bpftool
本文的目的是向大家分享一个以第2代 ebpf 编程方案为基础的改进ebpf编译构建方案。本节先用一些篇幅内容,对第2代方案本身的构建编译过程做一些介绍。
libbpf库具有一定的向下兼容能力,可以选择使用截至目前最新的归档版本libbpf-1.3.0来搭建编程环境。以 libbpf-1.3.0版本libbpf库为基础,下文会提供若干实例代码,来剖析ebpf构建原理。完成了基础环境的初始化,就可以开始搭建我们的eBPF项目。所有的代码示例都可以通过如下git项目获取。为了后面访问方便,这里用一个shell变量NATIVE_LIBBPF用来存储工作目录。
- $ cd ~
- $ git clone https://github.com/alibaba/sreworks-ext.git
- $ NATIVE_LIBBPF=~/sreworks-ext/demos/native_libbpf_guide/
首先来看一个基于原生libbpf库的第2代eBPF构建实例。ebpf初学者,可以考虑选择跟踪 execve 系统调用产生的事件。
- $ cd $NATIVE_LIBBPF # 返回工作目录
- $ cd trace_execve_libbpf130 # 进入项目目录
- $ make
- $ sudo ./trace_execve
- trace_execve 15836221 5501 bash 1534 bash 0 /usr/bin/ls
- trace_execve 15914126 5502 bash 1534 bash 0 /usr/bin/ps
-
- $ make clean
执行trace_execve命令,对编译结果进行验证,完美验证通过。
介绍下trace_execve_libbpf130的目录结构。
trace_execve_libbpf130目录 | 说明 |
./ | 项目用户态代码和主Makefile |
./progs | 项目内核态bpf程序代码 |
./include | 项目的业务代码相关的头文件 |
./helpers | 非来自于libbpf库的一些helpler文件 |
./tools/lib/bpf/ | 来自于libbpf-1.3.0/src/ |
./tools/include/ | 来自于libbpf-1.3.0/include/ |
./tools/build/ | 项目构建时一些feature探测代码 |
./tools/scripts/ | 项目Makefile所依赖的一些功能函数 |
再介绍下本项目trace_execve_libbpf130和libbpf-1.3.0库的对应关系。下载libbpf-1.3.0库解压后,使用diff命令进行目录对比。
- $ cd ~
- $ wget http://github.com/libbpf/libbpf/archive/refs/tags/v1.3.0.tar.gz
- $ tar -zxvf v1.3.0.tar.gz
- $ diff -qr $NATIVE_LIBBPF/trace_execve_libbpf130/tools/lib/bpf/ ~/libbpf-1.3.0/src/
- Only in ~/libbpf-1.3.0/src/: .gitignore
- Files ~/native_libbpf_guide/trace_execve_libbpf130/tools/lib/bpf/Makefile and ~/libbpf-1.3.0/src/Makefile differ
-
- $ diff -qr $NATIVE_LIBBPF/trace_execve_libbpf130/tools/include/ ~/libbpf-1.3.0/include/
在这个项目中添加ebpf的代码,可以遵循这样的目录结构。用户态加载文件放到根目录下,内核态bpf文件放到progs目录下,用户态和内核态公共的头文件放到include目录下。
- $ cd $NATIVE_LIBBPF # 返回工作目录
- $ cd trace_execve_libbpf130 # 进入项目目录
- $ find . -name "trace_execve*"
- ./trace_execve.c
- ./progs/trace_execve.bpf.c
- ./include/trace_execve.h
- $ cd $NATIVE_LIBBPF # 返回工作目录
- $ cd trace_execve_libbpf130 # 进入项目目录
- $ find . -name Makefile
- ./Makefile
- ./progs/Makefile
- ./tools/lib/bpf/Makefile
- ./tools/build/feature/Makefile
trace_execve_libbpf130项目有4个Makefile,分别如下:
在项目空间的根目录运行make命令进行项目构建时,会首先执行Makefile文件。在Makefile文件中会通过make的-C选项间接触发progs/Makefile和tools/lib/bpf/Makefile的执行。
感兴趣的同学可以通过上一章节中提到的make --debug=v,m SHELL="bash -x" 命令逐步debug这些makefile的执行过程。
下文重点分析下编译过程的一些编译参数,让我们加深对eBPF构建过程的理解。
在开始分析eBPF程序的编译参数之前,先要简单说一下C语言编译器(gcc/clang)的目录搜索选项。C语言的头文件都需要按照目录搜索选项的指引,才能正确找到它所在位置。
除了日常我们熟知的-I选项,clang/gcc的目录搜索选项还有很多,它们优先级的顺序依次如下:
内核态bpf程序trace_execve.bpf.o文件,是由 bpf 文件trace_execve.bpf.c使用clang命令编译产生。trace_execve.bpf.c文件的头文件依赖如下。
- $ cat progs/trace_execve.bpf.c
- // SPDX-License-Identifier: GPL-2.0
- #include <vmlinux.h>
- #include <bpf/bpf_helpers.h>
- #include <bpf/bpf_tracing.h>
-
- #include "common.h"
- #include "trace_execve.h"
从前面项目构建过程中,可以提取出完整的内核态bpf程序的编译命令。
- $ clang -iquote ./../include/ -iquote ./../helpers -I./../tools/lib/ -I./../tools/include/uapi -idirafter /usr/lib64/clang/15.0.7/include \
- -idirafter /usr/include -idirafter /usr/include/x86_64-linux-gnu/ -DENABLE_ATOMICS_TESTS -D__KERNEL__ -D__BPF_TRACING__ \
- -D__TARGET_ARCH_x86 -g -Werror -O2 -mlittle-endian -target bpf -mcpu=v3 -c trace_execve.bpf.c -o trace_execve.bpf.o
下面对一些关键环节做一些解析:
用户态eBPF程序trace_execve文件,是由源文件trace_execve.c文件使用gcc命令编译。trace_execve.c文件的头文件依赖如下。
- $ cat trace_execve.c
- // from kernel-headers
- #include <errno.h>
- #include <limits.h>
- #include <stdio.h>
- #include <unistd.h>
- #include <linux/limits.h>
- #include <linux/perf_event.h>
- #include <sys/resource.h>
-
- // from libbpf
- #include <linux/ring_buffer.h>
- #include <bpf/libbpf.h>
- #include "common.h"
- #include "trace_execve.h"
从前面项目构建过程中,也可以提取出完整的用户态程序的编译命令。
gcc -iquote ./helpers/ -iquote ./include/ -I./tools/lib/ -I./tools/include/ -g -c -o trace_execve.o trace_execve.c
关于libbpf.a静态库的编译过程,上一篇文章已经有所介绍。这里仅再次强调下,在本项目中,我们完全实现了libbpf库的自主可控,可控源代码,可控编译构建过程。这至少给我们带来如下两方面好处:
在上文中,我们初步实现了基于libbpf库的第 2 代 eBPF项目的构建。但截止到目前,此方案还有一个明显的缺陷。让我们继续上一篇的案例来分析,在搭建完开发环境后执行如下步骤。
- $ cd $NATIVE_LIBBPF # 返回工作目录
- $ cd trace_execve_libbpf130 # 进入项目目录
- $ make clean
- $ make
- $ sudo ./trace_execve
- trace_execve 160646349 5503 sa1 1 systemd 0 /usr/lib64/sa/sa1
- trace_execve 160646371 5503 sa1 1 systemd 0 /usr/lib64/sa/sadc
-
- $ mv progs/trace_execve.bpf.o progs/trace_execve.bpf.o.bak
- $ sudo ./trace_execve
- libbpf: elf: failed to open progs/trace_execve.bpf.o: No such file or directory
- ERROR: failed to open prog: 'No such file or directory'
-
- $ mv progs/trace_execve.bpf.o.bak progs/trace_execve.bpf.o
- $ sudo ./trace_execve
- trace_execve 190767474 5566 crond 5565 crond 0 /bin/bash
- trace_execve 190767486 5566 bash 5565 crond 0 /bin/run-parts

从实验结果可以看出,当我们把bpf目标文件trace_execve.bpf.o改名为trace_execve.bpf.o.bak后,trace_execve程序执行会报错,提示读取trace_execve.bpf.o文件不存在。而当我们再次将备份后的bpf目标文件trace_execve.bpf.o.bak改回原名trace_execve.bpf.o后,重新执行trace_execve程序又一切正常了。这说明,当前方案构建后,需要将trace_execve程序和bpf目标文件trace_execve.bpf.o这一组文件一起进行分发,才能正常执行。这给我们在工程的实现上带来了很大的挑战。
为了解决上面提到的问题,第 3 代 ebpf 编程方案 libbpf-bootstrap框架发明了skeleton骨架,即使用*.skel.h头文件的方式,将bpf目标文件trace_execve.bpf.o的内容编译进trace_execve程序。这样后续只需分发trace_execve二进制程序文件即可。
如果不依赖libbpf-bootstrap编程框架,继续仅依赖 libbpf 库是否可以做到这一点呢?答案是可以的,本文独辟蹊径,给大家分享一个使用hexdump命令轻松实现*.skel.h头文件的方式。
简单归纳一下使用libbpf-bootstrap框架编程过程中的构建步骤。
步骤 | libbpf-bootstrap框架构建 | 可改进机会点 |
1 | bpftool btf dump file vmlinux format c > vmlinux.h | |
2 | clang -O2 -target bpf -c trace_execve.bpf.c -o trace_execve.bpf.o | |
3 | bpftool gen skeleton trace_execve.bpf.o > trace_execve.skel.h | 此步骤用hexdump替换bpftool |
4 | gcc -o trace_execve trace_execve.c -lbpf -lelf | 此步骤更改加载函数为libbpf标准函数 |
分析libbpf-bootstrap编程框架的实现原理,可以了解到。在第3步会依靠bpftool工具将trace_execve.bpf.o这个目标文件转换成十六进制格式的文本,并将这个文本内容作为trace_execve.skel.h头文件中的一个变量的值,最后还需要让trace_execve.c用户态加载文件包含这个trace_execve.skel.h头文件。这其中将bpf目标文件转换成十六进制文本并生成skel.h头文件的过程最为关键。
libbpf-bootstrap编程框架非常成熟,但方案使用中必须遵循他的一些规则,比如头文件trace_execve.skel.h的命令必须包含程序的关键词trace_execve,再比如加载函数trace_execve_bpf__load()也必须包含程序的关键词trace_execve。如何能不依赖这个规范,实现一个更加轻量级的编程方案呢?这让我们想到了hexdump命令,可以用它替换bpftool工具,并且生成符合自己期望的头文件。
$ hexdump -v -e '"\\\x" 1/1 "%02x"' trace_execve.bpf.o > trace_execve.hex
下面我们就尝试依靠hexdump命令实现一个单一可执行文件的解决方案。开始体验我们基于第 2 代编程方案改进的eBPF项目,进入项目代码。
- $ cd $NATIVE_LIBBPF # 返回工作目录
- $ cd hexdump_skel_libbpf130 # 进入项目目录
- $ make
- $ sudo ./trace_execve
- trace_execve bash su 74113 74112 0 /usr/bin/bash
- trace_execve bash su 74113 74112 0 /usr/bin/bash
-
- $ sudo ./probe_execve
- probe_execve 19076757 5572 0anacron 5570 0anacron 0
- probe_execve 19076758 5573 0anacron 5570 0anacron 0
分别执行trace_execve和probe_execve两个命令,对编译结果进行验证,均完美验证通过。这里我们在trace_execve实例基础上又增加了一个probe_execve实例,说明hexdump_skel_libbpf130项目是支持多实例编译的。
下面我们来验证下本文开头的情况,看看没有了bpf目标文件时的情形。
- $ cd $NATIVE_LIBBPF # 返回工作目录
- $ cd hexdump_skel_libbpf130 # 进入项目目录
- $ rm -fr progs/trace_execve.bpf.o progs/probe_execve.bpf.o
- $ sudo ./trace_execve
- trace_execve 19076759 5574 run-parts 5566 run-parts 0 /bin/basename
- trace_execve 19076760 5575 run-parts 5566 run-parts 0 /bin/logger
-
- $ sudo ./probe_execve
- probe_execve sh python 78841 78838 0
- probe_execve sh python 78841 78838 0
从运行结果看,虽然删除了两个bpf目标文件trace_execve.bpf.o和probe_execve.bpf.o,仅仅依靠trace_execve和probe_execve两个文件即可成功执行。可以再尝试将trace_execve 可执行文件拷贝到其他目录,结果依然可行。
hexdump_skel_libbpf130项目也是同样的4个Makefile,其中将bpf目标文件编译到用户态加载进程中的环节主要在项目的主Makefile中实现。还是老办法获取make构建的详细过程。
- $ cd $NATIVE_LIBBPF # 返回工作目录
- $ cd hexdump_skel_libbpf130 # 进入项目目录
- $ make clean
- $ make --debug=v,m SHELL="bash -x" > make.log 2>&1
对于构建日志的分析可以参考前面文章,我们把关键环节提取出来。
- $ cat make.log | grep -n "Considering target file"
- 14:Considering target file 'all'.
- 16: Considering target file 'tools/lib/bpf/libbpf.a'.
- 21: Considering target file 'helpers/uprobe_helper.o'.
- 23: Considering target file 'helpers/uprobe_helper.c'.
- 31: Considering target file 'probe_execve'.
- 33: Considering target file 'probe_execve.o'.
- 35: Considering target file 'probe_execve.c'.
- 38: Considering target file 'probe_execve.skel.h'.
- 40: Considering target file 'probe_execve.hex'.
- 42: Considering target file 'progs/probe_execve.bpf.o'.
- 44: Considering target file 'progs/probe_execve.bpf.c'.
- 145: Considering target file 'trace_execve'.
- 147: Considering target file 'trace_execve.o'.
- 149: Considering target file 'trace_execve.c'.
- 152: Considering target file 'trace_execve.skel.h'.
- 154: Considering target file 'trace_execve.hex'.
- 156: Considering target file 'progs/trace_execve.bpf.o'.
- 158: Considering target file 'progs/trace_execve.bpf.c'.

从关键构建步骤中,我们可以了解到:
- trace_execve
- ├── trace_execve.o
- │ ├── trace_execve.c
- │ ├── trace_execve.skel.h
- │ │ ├── trace_execve.hex
- │ │ │ ├──progs/trace_execve.bpf.o
- │ │ │ │ └── progs/trace_execve.bpf.c
再看一下主Makefile的源码,为了实现以上的目标依赖,我们连用了5个静态模式规则(Static Pattern Rules)。
- $(HELPER_OBJECTS): %.o:%.c
-
- $(BPF_OBJECT):./progs/%.bpf.o:./progs/%.bpf.c
-
- $(HEX_OBJECT):%.hex:./progs/%.bpf.o
-
- $(SKEL_OBJECT):%.skel.h:%.hex
-
- $(USER_OBJECT):%.o:%.c %.skel.h
-
- $(LOADER_OBJECT): %:%.o
其中任何一个静态模式规则的目标集合,都是通过项目根目录下*.c文件的集合,进行局部字符串替换获得。
- SOURCES := $(wildcard *.c)
- HELPER_OBJECTS := $(patsubst %.c,%.o,$(wildcard $(HELPERS_PATH)/*.c))
- LOADER_OBJECT := $(patsubst %.c,%,$(SOURCES))
- USER_OBJECT := $(patsubst %.c,%.o,$(SOURCES))
- SKEL_OBJECT := $(patsubst %.c,%.skel.h,$(SOURCES))
- HEX_OBJECT := $(patsubst %.c,%.hex,$(SOURCES))
- BPF_OBJECT := $(patsubst %.c,./progs/%.bpf.o,$(SOURCES))
本方案的主要逻辑是在主Makefile中实现,但也需要c代码中做一些调整。bpf文件trace_execve.bpf.c并不需要任何修改,只需要在用户态加载程序trace_execve.c做一些调整。
传统的读取bpf目标文件方式,相关代码如下:
- char filename[256] = "progs/trace_execve.bpf.o";
- struct bpf_object * bpf_obj = bpf_object__open_file(filename, NULL);
改进后的读取memory方式,相关代码如下:
- #include "skeleton.skel.h"
-
- struct bpf_object * bpf_obj = bpf_object__open_mem(obj_buf, obj_buf_sz, NULL);
很明显libbpf库提供了bpf_object__open_file(bpf_object__open)和bpf_object__open_mem两个函数用于读取elf格式的bpf目标文件trace_execve.bpf.o。区别是bpf_object__open_file是在trace_execve运行时,再去读取trace_execve.bpf.o文件内容,而bpf_object__open_mem是在编译时,已经把elf内容编译进trace_execve程序。至于bpf_object__open函数在libbpf库的libbpf.c文件中是对bpf_object__open_file函数的封装。
这两个libbpf库函数,最终都是调用elf标准库函数实现了相关功能,具体代码实现是在libbpf库的libbpf.c文件中的bpf_object__elf_init函数中,代码如下:
- static int bpf_object__elf_init(struct bpf_object *obj){
- ......
- if (obj->efile.obj_buf_sz > 0) {
- elf = elf_memory((char *)obj->efile.obj_buf, obj->efile.obj_buf_sz);
- } else {
- obj->efile.fd = open(obj->path, O_RDONLY | O_CLOEXEC);
- ......
- elf = elf_begin(obj->efile.fd, ELF_C_READ_MMAP, NULL);
- }
- ......
- }
可以看出,bpf_object__open_mem函数的最终实现是elf的elf_memory函数,bpf_object__open_file函数的最终实现是elf的elf_begin函数。
相比较第3代的 libbpf-bootstrap框架方案和第2代的传统libbpf库方案,使用hexdump命令的原生libbpf库第 2 代改进方案方案在实现方法上,有一些独特的优势。
这里将这三种方案的主要区别归纳总结如下:
比较项 | 传统libbpf库的2代方案 | libbpf-bootstrap的3代方案 | hexdump的libbpf库的2代改进方案 |
生成头文件 | 无 | bpftool gen skeleton | hexdump |
使用头文件 | 无 | 将程序名trace_execve添加到头文件名称中trace_execve.skel.h | 统一成一个固定的名称skeleton.skel.h |
加载函数 | 使用libbpf库标准加载函数bpf_object__open_file();bpf_object__load();bpf_program__attach(); | 将程序名添加到加载函数名称中trace_execve_bpf__open();trace_execve_bpf__load();trace_execve_bpf__attach(); | 使用libbpf库标准加载函数bpf_object__open_mem();bpf_object__load();bpf_program__attach(); |
这里补充下,trace_execve_bpf__open()函数的实现,也是间接通过libbpf库的bpf_object__open_skeleton()函数,最终也调用了bpf_object__open_mem()函数。
在ebpf用户态程序的加载过程中,有一个attach的步骤。细心的读者应该已经发现了,在trace_execve_libbpf130项目中,我们使用的是bpf_program__attach()函数实现的静态探针点的attach。而在hexdump_skel_libbpf130项目中,我们使用的却是bpf_program__attach_tracepoint()函数实现的静态探针点的attach。区别是bpf_program__attach_tracepoint函数的参数中会指定静态探针点的具体信息,而bpf_program__attach不用指定静态探针点的信息。进一步阅读bpf_program__attach函数的源代码可以了解到,它是依靠内核态的bpf目标文件中SEC的节名称信息来获取和确定静态探针点的信息的。总结这两种方法如下:
trace_execve.c中相关代码 | trace_execve.bpf.c中相关代码 | |
attach方案A | bpf_program__attach(bpf_prog) | SEC("tracepoint/syscalls/sys_enter_execve") |
attach方案B | bpf_program__attach_tracepoint(bpf_prog, "syscalls", "sys_enter_execve") | SEC("tracepoint") |
很明显,在trace_execve.c和trace_execve.bpf.c的代码中,只要有一处设置静态探针点即可。如果两处都设置,而且两处设置的静态探针点信息冲突的情况下,会以用户态的bpf_program__attach_tracepoint函数设置的信息为准。
libbpf库中的bpf_link__destroy()函数是负责对attach函数生成的link进行销毁的函数。attach和destroy的过程实际上就是对内核静态探针点开启和关闭的过程。
在这里特别推荐使用方案B中的bpf_program__attach_tracepoint替代方案A中的bpf_program__attach方法,这样方便我们在用户态代码中灵活的开关ebpf的采集。除了专门用于静态探针点的bpf_program__attach_tracepoint()函数,还有适用于其他类型的专用的attach函数,例如bpf_program__attach_kprobe()、bpf_program__attach_kprobe()、bpf_program__attach_uprobe()和bpf_program__attach_usdt()等。
在稍早一些libbpf库中提供2个函数用于获取bpf progam 类型数据,分别是bpf_object__find_program_by_name()函数和bpf_object__find_program_by_title()函数。以trace_execve_libbpf130项目的 bpf代码为例。
- SEC("tracepoint/syscalls/sys_enter_execve")
- int trace_execve_enter(struct syscalls_enter_execve_args *ctx){
- ......
- }
其中tracepoint/syscalls/sys_enter_execve这个字符串就称为title,trace_execve_enter这个函数名就称为name。结合上文的结论,后续推荐bpf内核态代码中都使用SEC("tracepoint")的语法格式,那么使用by_title函数将不再能做出区分。因此这里特别推荐大家今后使用by_name的函数替代by_titile的函数。而且,在最新版的libbpf库中,也彻底移除了bpf_object__find_program_by_title()函数。
基于hexdump命令的改进型原生libbpf库编程方案不但在内核态跟踪诊断上表现完美,在用户态应用进程的跟踪诊断上依然可以表现得非常出色。本节内容将在上文的基础上,继续分析如何使用原生libbpf库开发和构建USDT和Uprobe项目。
用户态应用程序的ebpf,还需要准备一个模拟程序。尤其是针对USDT类型,还需要在模拟程序中进行静态打点。本小节将提供一个如何打USDT跟踪点的实例。
- $ cd $NATIVE_LIBBPF # 返回工作目录
- $ cd mark_usdt_uprobe # 进入项目目录
- $ make
- $ sudo cp umark /usr/bin/
- $ sudo umark >/dev/null 2>/dev/null &
- $ make clean
执行完以上步骤,就启动了用户态模拟程序umark,后续即可通过USDT和Uprobe方式,追踪umark进程的运行情况。
下面初步对umark模拟程序的代码做一些介绍。
- $ ls
- Makefile README.md sdt.h umark.c
-
- $ cat umark.c
- #include <unistd.h>
- #include <stdio.h>
- //#include <sys/sdt.h>
- #include "sdt.h"
-
- unsigned long long int func_uprobe1(unsigned long long int x){
- return x + 1;
- }
- unsigned long long int func_uprobe2(unsigned long long int x, unsigned long long int y){
- return x + y;
- }
- int main(int argc, char const *argv[]) {
- unsigned long long int i;
- int var1 = 10, var2 = 20, var3 = 30;
- for (i = 0; i < 86400000; i++) {
- sleep(1);
- DTRACE_PROBE1(groupa, probe1, var1);
- DTRACE_PROBE2(groupb, probe2, var2, var3);
- printf("hit uprobe1 %llu\n", func_uprobe1(i));
- printf("hit uprobe2 %llu\n", func_uprobe2(i + 3, i + 8));
- }
- return 0;
- }

其中func_uprobe1和func_uprobe2是两个C语言函数用于下文的uprobe跟踪实例的追踪。DTRACE_PROBE1和DTRACE_PROBE2是两个宏函数,用于在umark.c程序中打USDT的静态跟踪点。最多支持传入12个跟踪点参数,即DTRACE_PROBE1、DTRACE_PROBE2,一直到DTRACE_PROBE12。probe1和probe2是这个静态跟踪点的name,groupa和groupb是跟踪点name的分组名,可以省略。
DTRACE_PROBE1宏函数定义在std.h头文件内,需要提前安装头文件所在包。
在rpm包环境,sdt.h头文件属于systemtap-sdt-devel这个rpm包。
- $ find /usr/include/ -name sdt.h
- /usr/include/sys/sdt.h
-
- $ rpm -qf /usr/include/sys/sdt.h
- systemtap-sdt-devel-4.8-2.0.2.al8.x86_64
在deb包环境,sdt.h头文件属于systemtap-sdt-dev这个deb包。
- $ find /usr/include/ -name sdt.h
- /usr/include/x86_64-linux-gnu/sys/sdt.h
-
- $ dpkg -S /usr/include/x86_64-linux-gnu/sys/sdt.h
- systemtap-sdt-dev:amd64: /usr/include/x86_64-linux-gnu/sys/sdt.h
令人欣慰的是,这个sdt.h头文件并无太多额外依赖,简单修改后,可以独立维护。于是,我们可以将其拷贝到本项目根目录。并将的头文件引用方式改为"sdt.h"。
下面我们就进一步介绍下使用第 2 代改进编程方案的ebpf跟踪用户态进程的解决方案。开始体验我们的eBPF项目trace_user_libbpf130,进入项目代码。
- $ cd $NATIVE_LIBBPF # 返回工作目录
- $ cd trace_user_libbpf130 # 进入项目目录
- $ make
- $ sudo ./uprobe_test
- func_uprobe1 2374242 4604 umark 1534 bash 0 23368 23373
- func_uprobe2 2374242 4604 umark 1534 bash 0 23371 23376
-
- $ sudo ./usdt_test
- func_usdt1 2375442 4604 umark 1534 bash 0 10 17
- func_usdt2 2375442 4604 umark 1534 bash 0 20 30
分别执行uprobe_test和usdt_test两个命令,对编译结果进行验证,均完美验证通过。
trace_user_libbpf130项目的构建和编译过程与前面项目hexdump_skel_libbpf130无太多差异,不再做过多赘述。下文将着重对本项目中USDT和Uprobe的相关C语言源码进行解析。
trace_user_libbpf130项目中的USDT部分,开启了2个usdt静态探针点的跟踪,这2个静态探针点分别是probe1和probe2。
第一个静态探针点实例,选择在attach时,通过bpf_program__attach_usdt函数的参数指定静态探针点的相关信息。包括跟踪的进程信息"/usr/bin/umark",usdt组名信息"groupa",usdt名称信息"probe1"等,代码如下:
bpf_program__attach_usdt(bpf_prog1, -1, "/usr/bin/umark", "groupa", "probe1", NULL);
第二个静态探针点实例,选择在bpf目标文件中,通过SEC宏的方式指定静态探针点的相关信息。包括跟踪的进程信息"/usr/bin/umark",usdt组名信息"groupb",usdt名称信息"probe2"等,代码如下:
SEC("usdt//usr/bin/umark:groupb:probe2")
目前主流的USDT类型的ebpf代码实例,在bpf目标文件中都使用BPF_USDT宏函数来定义ebpf的处理函数,例如本项目实例中。
int BPF_USDT(usdt_probe1, int x)
在这里,宏函数BPF_USDT的第1个参数"usdt_probe1"才是真正的函数名,也就是前文所述by_name的name信息。宏函数的第2个参数"int x"才是usdt_probe1函数的第一个参数,依次类推。
各种USDT类型的ebpf代码实例中,很少见到对这个宏函数BPF_USDT原理的分析。此处,我们借助第二个USDT静态探针点在bpf目标文件中的使用来解析它。代码实例的关键部分如下:
- int usdt_probe2(struct pt_regs *ctx);
-
- static inline __attribute__((always_inline)) typeof(usdt_probe2(0)) ____usdt_probe2(struct pt_regs *ctx, int x, int y);
-
- typeof(usdt_probe2(0)) usdt_probe2(struct pt_regs *ctx) {
- return ____usdt_probe2(ctx, ({ long _x; bpf_usdt_arg(ctx, 0, &_x); (void *)_x; }), ({ long _x; bpf_usdt_arg(ctx, 1, &_x); (void *)_x; }));
- }
-
- static inline __attribute__((always_inline)) typeof(usdt_probe2(0)) ____usdt_probe2(struct pt_regs *ctx, int x, int y)
- {
- ......
- }
这4行代码,前两行是函数声明,后两行是函数定义。usdt_probe2函数内部调用了____usdt_probe2函数。一些代码解读:
特别强调一下bpf_usdt_arg辅助函数来自于usdt.bpf.h头文件,但本项目有2个usdt.bpf.h头文件,其中一个在libbpf库中,另外一个在./helpers/目录下,helpers 目录下的是经过本项目改造过的。此示例中生效的是./helpers/目录下的。
- $ cd $NATIVE_LIBBPF # 返回工作目录
- $ cd trace_user_libbpf130 # 进入项目目录
- $ find . -name usdt.bpf.h
- ./tools/lib/bpf/usdt.bpf.h
- ./helpers/usdt.bpf.h6.5、Uprobe代码解析
trace_user_libbpf130项目中的Uprobe部分,开启了2个uprobe类型探针点的跟踪,这2个uprobe探针点分别是probe1和probe2。
第一个uprobe探针点实例,选择在attach时,通过bpf_program__attach_uprobe函数的参数指定uprobe探针点的相关信息。包括uprobe的类型(0表示函数进入时,1表示函数返回时),跟踪的进程信息"/usr/bin/umark",被跟踪的函数在进程中的偏移量 func_off1等。需要提前通过get_elf_func_offset()函数计算出这个偏移量,此函数定义在了helpers/uprobe_helper.c文件内。相关代码如下:
- func_off1 = get_elf_func_offset("/usr/bin/umark", "func_uprobe1");
- bpf_program__attach_uprobe(bpf_prog1, 0, -1, "/usr/bin/umark", func_off1);
第二个uprobe探针点实例,选择在bpf目标文件中,通过SEC宏的方式指定uprobe探针点的相关信息。包括跟踪的进程信息"/usr/bin/umark",被跟踪的应用进程中的函数"func_uprobe2"等。此种情况,libbpf库会自动计算这个偏移量。代码如下:
SEC("uprobe//usr/bin/umark:func_uprobe2")
目前主流的Uprobe类型的ebpf代码实例,在bpf目标文件中都使用BPF_KPROBE宏函数来定义ebpf的处理函数,例如本项目实例中。
int BPF_KPROBE(user_probe1, unsigned long long int x)
在这里,宏函数BPF_KPROBE的第1个参数"user_probe1"才是真正的函数名,也就是前文所述by_name的name信息。宏函数的第2个参数"unsigned long long int x"才是user_probe1函数的第一个参数,依次类推。
各种Uprobe类型的ebpf代码实例中,也同样很少见到对这个宏函数BPF_KPROBE原理的分析。此处,我们借助第二个Uprobe探针点在bpf目标文件中的使用来解析它。关键的代码实例如下:
- long user_probe2(struct pt_regs *ctx);
-
- inline typeof(user_probe2(0)) ____user_probe2(struct pt_regs *ctx, unsigned long long int x, unsigned long long int y);
-
- inline typeof(user_probe2(0)) ____user_probe2(struct pt_regs *ctx, unsigned long long int x, unsigned long long int y)
- {
- ......
- }
-
- typeof(user_probe2(0)) user_probe2(struct pt_regs *ctx) {
- return ____user_probe2(ctx, (unsigned long long int)PT_REGS_PARM1(ctx), (unsigned long long int)PT_REGS_PARM2(ctx));
- }
这4行代码,前两行是函数声明,后两行是函数定义。user_probe2函数内部调用了____user_probe2函数。一些代码解读:
如果对于以上的代码解读如果还有不明白的地方,可以尝试问问GPT。
作者:闻茂泉
本文为阿里云原创内容,未经允许不得转载。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。