Featured image of post eBPF例程——基本框架示例

eBPF例程——基本框架示例

基于libbpf编写eBPF程序的基本框架minimal,该示例加载并附加一个tracepoint,只跟踪当前进程的write()调用,并用bpf_printk打印内核日志。

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()时,会在内核日志中输出一条消息:

  1. 定义eBPF程序的许可证:Linux内核为了保护GPL授权的核心代码,只允许符合GPL许可证的软件访问某些特定功能和内核辅助函数。为此,内核在加载eBPF程序时会检查声明的许可证类型,并根据许可证决定该eBPF程序能否调用特定的内核辅助函数或访问内核符号。这样可以有效防止闭源模块随意使用受GPL保护的核心功能。本程序声明了“Dual BSD/GPL”许可证,从而既满足GPL要求,又保持对BSD许可的兼容。
  2. 定义全局变量my_pid:用于存放需要监控的进程PID,可通过用户态程序写入、设置该变量。
  3. 附加到tracepoint:使用SEC()宏将handle_tp()函数进行标记,表示该eBPF程序附加到内核tracepoint——sys_enter_write(在调用write()系统调用时触发,进而触发handle_tp()函数)。函数的参数ctx是上下文指针,用于接收tracepoint的上下文信息。
  4. 根据PID过滤:如果当前进程的PID不等于预设的my_pid,则直接返回、不进行任何处理,因此只对指定进程的write()调用进行监控。
  5. 打印调试信息:使用内核辅助函数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程序:

  1. 引入头文件:minimal.bpf.c是一个eBPF程序源码文件,其在make的过程中,先由Clang/LLVM编译生成eBPF字节码目标文件minimal.bpf.o,随后bpftool工具会基于minimal.bpf.o文件自动生成一个skeleton头文件minimal.skel.h。该skeleton文件内部会把编译好的BPF对象代码及其结构(程序、映射、全局变量等)封装到一个易用的C接口中,在用户态程序只需包含该头文件,就能方便地加载、配置和挂载eBPF程序,而不必编写复杂的libbpf调用逻辑。
  2. 加载eBPF程序:在用户态程序中,首先调用minimal_bpf__open()函数打开并初始化skeleton,随后使用minimal_bpf__load()函数将eBPF字节码真正加载到内核,同时由内核verifier对程序进行合法性与安全性检查。
  3. 设置跟踪的进程PID:eBPF程序中定义了全局变量my_pid,在用户态中可以通过skeleton提供的.bss段指针,直接给这个变量赋值。
  4. 附加到内核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 程序的加载、管理和附加过程。里面主要包含:

  1. eBPF程序(progs);
  2. eBPF映射(maps);
  3. .bss和.data这类全局变量的内存映射指针:定义指针可以让用户态直接访问eBPF程序中的全局变量,比如上述代码中访问的skel->bss->my_pid;
  4. 方便访问和管理所有eBPF相关资源的成员:自动生成的一组以minimal_bpf__开头的函数,如:
    1. minimal_bpf__open():打开并初始化eBPF对象,准备加载;
    2. minimal_bpf__load():加载eBPF程序到内核并进行验证;
    3. minimal_bpf__attach():附加eBPF程序到指定内核钩子(如tracepoint、kprobe);
    4. minimal_bpf__destroy():释放资源,关闭文件描述符。

补充说明:.bss和.data

.bss和.data是程序中存放全局变量的两个不同内存区段。.data段用于存放已初始化的全局变量和静态变量,这些变量在编译时就有明确的初始值,比如int x = 42。.bss段用于存放未初始化的全局变量和静态变量,比如int y与static int z。

Licensed under CC BY-NC-SA 4.0
皖ICP备2025083746号-1
公安备案 陕公网安备61019002003315号



使用 Hugo 构建
主题 StackJimmy 设计