libbpf-bootstrap
libbpf-bootstrap是一个由社区和libbpf核心开发者维护的eBPF开发示例工程,包含了大量的模板项目,系统地演示了如何用基于libbpf实现高效、可移植的eBPF应用。首先需要安装相关依赖软件:
1
2
|
sudo apt install make build-essential pkg-config git
sudo apt install clang libelf1 libelf-dev zlib1g-dev
|
随后获取libbpf-bootstrap源码,并且同时自动把它依赖的所有子模块也一起克隆下来:
1
2
|
cd ~
git clone --recurse-submodules https://github.com/libbpf/libbpf-bootstrap
|
运行eBPF程序
新建终端编译、运行eBPF程序,该程序会调用bpf_printk()辅助函数,将信息输出到内核的缓冲区。
1
2
3
|
cd libbpf-bootstrap/examples/c/
make minimal
sudo ./minimal
|

新建终端使用cat命令实时查看输出的信息。
1
|
sudo cat /sys/kernel/debug/tracing/trace_pipe
|

tracepoint
tracepoint是Linux内核开发者在内核里埋入的观测点(hook点),可以在文件系统里直接查看,每个tracepoint有一个format文件,记录了该tracepoint的参数和结构体字段:
1
2
3
|
# 格式为/sys/kernel/debug/tracing/events/<category>/<name>/
sudo ls /sys/kernel/debug/tracing/events/syscalls/sys_enter_write
sudo cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_write/format
|

当然也可以使用bpftrace工具查询字段结构:
1
|
sudo bpftrace -lv 'tracepoint:syscalls:sys_enter_write'
|

eBPF程序:minimal.bpf.c
该eBPF程序监听write()系统调用,当指定进程调用write()时,会在内核日志中输出一条消息:
- 定义eBPF程序的许可证:Linux内核为了保护GPL授权的核心代码,只允许符合GPL许可证的软件访问某些特定功能和内核辅助函数。为此,内核在加载eBPF程序时会检查声明的许可证类型,并根据许可证决定该eBPF程序能否调用特定的内核辅助函数或访问内核符号。这样可以有效防止闭源模块随意使用受GPL保护的核心功能。本程序声明了“Dual BSD/GPL”许可证,从而既满足GPL要求,又保持对BSD许可的兼容。
- 定义全局变量my_pid:用于存放需要监控的进程PID,可通过用户态程序写入、设置该变量。
- 附加到tracepoint:使用SEC()宏将handle_tp()函数进行标记,表示该eBPF程序附加到内核tracepoint——sys_enter_write(在调用write()系统调用时触发,进而触发handle_tp()函数)。函数的参数ctx是上下文指针,用于接收tracepoint的上下文信息。
- 根据PID过滤:如果当前进程的PID不等于预设的my_pid,则直接返回、不进行任何处理,因此只对指定进程的write()调用进行监控。
- 打印调试信息:使用内核辅助函数bpf_printk()输出调试信息,消息会输出到内核的trace_pipe文件中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
// 必须定义eBPF程序的许可证
char LICENSE[] SEC("license") = "Dual BSD/GPL";
// 定义全局变量指示需要监控的进程PID
int my_pid = 0;
// 表示该eBPF程序附加到sys_enter_write上
// tp表示这是一个tracepoint类型的eBPF程序
// syscall表示该tracepoint所属的category
// sys_enter_write是tracepoint的具体名字
SEC("tp/syscalls/sys_enter_write")
int handle_tp(void *ctx)
{
// bpf_get_current_pid_tgid()函数是eBPF程序中的Helper函数,用于获取当前进程的PID和TGID
// 函数返回一个64位的整数,其中高32位是线程组ID (TGID)、低32位是进程ID (PID)
// 因此需要右移32位获得进程ID
int pid = bpf_get_current_pid_tgid() >> 32;
// 检查当前进程的PID是否与预设的my_pid一致
if (pid != my_pid)
return 0;
// 输出调试信息到内核的trace_pipe文件
bpf_printk("BPF triggered from PID %d", pid);
return 0;
}
|
用户态程序:minimal.c
用户态进程加载eBPF程序:
- 引入头文件:minimal.bpf.c是一个eBPF程序源码文件,其在make的过程中,先由Clang/LLVM编译生成eBPF字节码目标文件minimal.bpf.o,随后bpftool工具会基于minimal.bpf.o文件自动生成一个skeleton头文件minimal.skel.h。该skeleton文件内部会把编译好的BPF对象代码及其结构(程序、映射、全局变量等)封装到一个易用的C接口中,在用户态程序只需包含该头文件,就能方便地加载、配置和挂载eBPF程序,而不必编写复杂的libbpf调用逻辑。
- 加载eBPF程序:在用户态程序中,首先调用minimal_bpf__open()函数打开并初始化skeleton,随后使用minimal_bpf__load()函数将eBPF字节码真正加载到内核,同时由内核verifier对程序进行合法性与安全性检查。
- 设置跟踪的进程PID:eBPF程序中定义了全局变量my_pid,在用户态中可以通过skeleton提供的.bss段指针,直接给这个变量赋值。
- 附加到内核tracepoint:使用minimal_bpf__attach()函数将eBPF程序附加到内核的特定tracepoint(如sys_enter_write),当内核执行到对应tracepoint时,就会自动触发eBPF程序的执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h> // 引入libbpf库
#include "minimal.skel.h" // 引入minimal.bpf.c
// libbpf的日志回调函数
// 让libbpf把内部调试信息、加载错误信息(例如加载、附加失败时的详细错误信息)通过该回调函数打印
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
return vfprintf(stderr, format, args);
}
int main(int argc, char **argv)
{
// skeleton结构体指针
// 用于接收随后自动生成的skeleton实例
struct minimal_bpf *skel;
int err;
// 注册libbpf日志回调函数
libbpf_set_print(libbpf_print_fn);
// 加载eBPF对象文件到内存:解析ELF格式、建立与内核交互的内部数据结构
// 此时只是在用户态准备,还没加载进内核
// 返回的skel是自动生成的skeleton结构体,方便操作eBPF程序、map、变量等
skel = minimal_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
// skeleton自动生成了对eBPF程序中.bss段的访问指针bss
// my_pid是eBPF程序里的一个全局变量,用来保存需要跟踪的进程号
// 设置为当前进程PID,让eBPF 程序只跟踪本进程触发的事件
skel->bss->my_pid = getpid();
// 真正调用libbpf API把eBPF字节码加载到内核
err = minimal_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton\n");
goto cleanup;
}
// 根据eBPF程序里的SEC()宏自动attach到对应的tracepoint
// attach成功后,每次write()内核调用就会触发eBPF程序
err = minimal_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
"to see output of the BPF programs.\n");
// 死循环保证进程不退出,并且每秒打印一个点
for (;;) {
fprintf(stderr, ".");
sleep(1);
}
cleanup:
// 释放eBPF对象和skeleton占用的资源
minimal_bpf__destroy(skel);
return -err;
}
|
补充说明:vfprintf函数
vfprintf是C标准库函数,用来根据给定的格式字符串和va_list参数,将格式化后的输出写入指定的文件流(如stderr)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#include <stdio.h>
#include <stdarg.h>
void my_logger(const char *format, ...)
{
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
}
int main()
{
my_logger("Hello, %s! Number: %d\n", "World", 42);
my_logger("Another message: %.2f\n", 3.14159);
return 0;
}
|
输出效果为:
1
2
|
Hello, World! Number: 42
Another message: 3.14
|
补充说明:minimal.skel.h

minimal.skel.h是由bpftool工具基于eBPF对象文件minimal.bpf.o自动生成的skeleton头文件,它的主要作用是封装eBPF程序和相关资源,简化用户态程序对 eBPF 程序的加载、管理和附加过程。里面主要包含:
- eBPF程序(progs);
- eBPF映射(maps);
- .bss和.data这类全局变量的内存映射指针:定义指针可以让用户态直接访问eBPF程序中的全局变量,比如上述代码中访问的skel->bss->my_pid;
- 方便访问和管理所有eBPF相关资源的成员:自动生成的一组以minimal_bpf__开头的函数,如:
- minimal_bpf__open():打开并初始化eBPF对象,准备加载;
- minimal_bpf__load():加载eBPF程序到内核并进行验证;
- minimal_bpf__attach():附加eBPF程序到指定内核钩子(如tracepoint、kprobe);
- minimal_bpf__destroy():释放资源,关闭文件描述符。
补充说明:.bss和.data
.bss和.data是程序中存放全局变量的两个不同内存区段。.data段用于存放已初始化的全局变量和静态变量,这些变量在编译时就有明确的初始值,比如int x = 42。.bss段用于存放未初始化的全局变量和静态变量,比如int y与static int z。