Featured image of post 内核 bpf 程序的开发

内核 bpf 程序的开发

使用 bpftrace 编写 eBPF 程序

bpftrace -e 'kprobe:do_nanosleep { printf("PID %d sleeping...\n", pid); }'

使用libbpf库来开发eBPF程序

前置条件:

  • 内核编译选项支持 CONFIG_DEBUG_INFO_BTF=y

  • 依赖安装: sudo apt install clang llvm bpftool build-essential libbpf1 libbpf-dev

在没有 BTF 信息的情况下,eBPF 程序需要通过字节偏移和硬编码的方式来解析内核数据结构,这样做既繁琐又容易出错。BTF 让 eBPF 程序能够自动地根据类型信息对数据进行正确的解析,减少错误并提高开发效率。比如,内核中的 struct task_struct 或 struct net_device 等复杂数据结构,在没有 BTF 的支持下,eBPF 程序只能通过地址和偏移来手动处理。而有了 BTF 数据,eBPF 程序能够直接理解这些结构体的字段和内存布局。

许多 eBPF 功能,如 BPF Type-Safe Maps 和 BPF Type-Safe Helper 需要 BTF 支持才能正确工作。BTF 让这些特性能够获取内核类型信息,从而增强类型安全,避免类型不匹配等问题。

说白了,BTF 就是提高写和调试 BPF 程序的一种方法。

  • 确认内核支持btf: ls -la /sys/kernel/btf/vmlinux

  • 生成vmlinux.h : bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

命令生成的 vmlinux.h 文件是一个包含内核数据结构和类型信息的头文件,它对于开发、调试和优化 eBPF 程序至关重要。通过这个文件,开发者可以更容易地与内核数据进行交互,编写更加高效、安全的 BPF 程序,并减少因类型不匹配而引发的错误。

用户空间编写 eBPF 程序

hello.bpf.c

/**
 * 通过使用 kprobe(内核探针)在do_nanosleep函数的入口处放置钩子,实现对该系统调用的跟踪
*/
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

// 定义一个名为do_nanosleep的 kprobe,当进入do_nanosleep时,它会被触发
SEC("kprobe/do_nanosleep")
int BPF_KPROBE(do_nanosleep)
{
    pid_t pid;

    // 获取当前进程的 PID(进程标识符)
    pid = bpf_get_current_pid_tgid() >> 32;
    // 使用bpf_printk函数在内核日志中打印 PID
    bpf_printk("hello: pid = %d\n", pid);
    return 0;
}
// 定义许可证,以允许程序在内核中运行
char LICENSE[] SEC("license") = "Dual BSD/GPL";
  • 生成 ebpf 字节码 clang -g -Wall -D__x86_64__ -O2 -target bpf -I ./vmlinux.h -c hello.bpf.c -o hello.bpf.o

  • 生成BPF 脚手架(skeleton)文件: bpftool gen skeleton hello.bpf.o > hello.skel.h

命令用于生成 eBPF 程序的 C 语言脚手架代码,极大地简化了 BPF 程序的开发过程。通过生成的 hello.skel.h 文件,开发者可以快速获得一个与内核交互的基础框架,专注于 BPF 程序的核心逻辑,而不必去编写繁琐的底层交互代码。BPF maps 是 eBPF 程序中的核心概念之一,脚手架代码会为你自动生成与 BPF map 相关的定义和操作代码。这样,你可以通过脚手架文件提供的接口轻松管理 map 数据结构,而不必手动进行 map 的创建、更新和读取操作。

  • 用户侧编写用户空间程序: 用于加载ebpf程序到内核

hello.c

/**
 * ebpf 用户空间程序(loader、read ringbuffer)
*/
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "hello.skel.h"

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
        return vfprintf(stderr, format, args);
}

static volatile sig_atomic_t stop;
static void sig_int(int signo)
{
        stop = 1;
}

int main(int argc, char **argv)
{
        struct hello_bpf *skel;
        int err;

        /* 设置libbpf错误和调试信息回调 */
        libbpf_set_print(libbpf_print_fn);

        /* 加载并验证 hello.bpf.c 应用程序 */
        skel = hello_bpf__open_and_load();
        if (!skel) {
                fprintf(stderr, "Failed to open BPF skeleton\n");
                return 1;
        }

        /* 附加 hello.bpf.c 程序到跟踪点 */
        err = hello_bpf__attach(skel);
        if (err) {
                fprintf(stderr, "Failed to attach BPF skeleton\n");
                goto cleanup;
        }

        /* Control-C 停止信号 */
        if (signal(SIGINT, sig_int) == SIG_ERR) {
                fprintf(stderr, "can't set signal handler: %s\n", strerror(errno));
                goto cleanup;
        }

        printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
               "to see output of the BPF programs.\n");

        while (!stop) {
                fprintf(stderr, ".");
                sleep(1);
        }

cleanup:
        /* 销毁挂载的ebpf程序 */
        hello_bpf__destroy(skel);
        return -err;
}
  • 程序的加载及运行

gcc hello.c -lbpf -o hello

所以运行的时候直接 ./hello 就行了吗?是的,上面的过程会把之前写的 hello.bpf.o 加载进去。

大佬的分享