4.libbpf

1. libbpf

1.1. 官方文档

1.2. 开发环境

推荐 Ubuntu 22.04 (因为内核版本比较高)

1.3. 开发脚手架 libbpf-bootstrap

libbpf-bootstrap 是一个基于 libbpf 库的 BPF 开发脚手架, libbpf-bootstrap 实现一次编译 重复使用的目的。基于 libbpf-bootstrap 的 BPF 程序对于源文件有一定的命名规则,用于生成内核态字节码的 bpf 文件以 .bpf.c 结尾,用户态加载字节码的文件以 .c 结尾,且这两个文件的前缀必须相同。

基于 libbpf-bootstrap 的 BPF 程序在编译时会先将 *.bpf.c 文件编译为对应的 .o 文件,然后根据此文件生成 skeleton 文件,即 *.skel.h ,这个文件会包含内核态中定义的一些 数据结构,以及用于装载内核态代码的关键函数。在用户态代码 include 此文件之后调用对应的装载函数即可将字节码装载到内核中。

 下面将使用 libbpf-boostrap 为 eBPF 编写原生的 libbpf 用户态代码,并建立完整的 libbpf 工程。

2. 监控进程的创建和退出的信息

2.1. 内核态 eBPF 代码

代码位于 bootstrap.bpf.c ,主要用于跟踪 exec()exit() 系统调用,通过 eBPF 程序捕获进程的创建和退出事件,并将相关信息发送到用户态程序进行处理。

  1. 引入所需的头文件并定义 eBPF 程序的许可证,
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "bootstrap.h"

char LICENSE[] SEC("license") = "Dual BSD/GPL";
  1. 定义一个哈希类型 BPF_MAP_TYPE_HASH 的 eBPF maps: exec_start ,用于存储进程开始执行时的时间戳,最多可以容纳 8192 个条目;定义一个环形缓冲区类型的 eBPF map: rb ,用于存储捕获的事件数据,并将其发送到用户态程序。
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 8192);
    __type(key, pid_t);
    __type(value, u64);
} exec_start SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} rb SEC(".maps");

const volatile unsigned long long min_duration_ns = 0;
  1. bootstrap.h 中定义了和用户态交互的数据结构 event ,包括进程的PID、父进程的PID、进程的退出码、进程的生命周期(持续时间)、存储进程名的字符数组、存储文件名的字符数组、表示是否是一个退出事件的布尔值。该数据结构会在下面两个 eBPF 程序中使用: struct event *e; ,保存内核态的进程信息进而与用户态交互进程信息。
#ifndef __BOOTSTRAP_H
#define __BOOTSTRAP_H

#define TASK_COMM_LEN 16
#define MAX_FILENAME_LEN 127

struct event {
    int pid;
    int ppid;
    unsigned exit_code;
    unsigned long long duration_ns;
    char comm[TASK_COMM_LEN];
    char filename[MAX_FILENAME_LEN];
    bool exit_event;
};

#endif /* __BOOTSTRAP_H */
  1. 定义了一个名为 handle_exec 的 eBPF 程序,它会在进程执行 exec() 系统调用时触发
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx){...eBPF程序具体实现...}
  1. 从当前进程中获取 PID,记录进程开始执行的时间戳,然后将其存储在 exec_start map 中
    pid = bpf_get_current_pid_tgid() >> 32;
    ts = bpf_ktime_get_ns();
    bpf_map_update_elem(&exec_start, &pid, &ts, BPF_ANY);
  1. 从环形缓冲区 map rb 中预留一个事件结构,并填充相关数据,如进程 ID、父进程 ID、进程名等,之后将这些数据发送到用户态程序进行处理
    /* 指定了昀小持续时间,就只关心进程exit,故这里则直接结束 */
    if (min_duration_ns)
        return 0;

    e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
    if (!e)
        return 0;

    task = (struct task_struct *)bpf_get_current_task();

    e->exit_event = false;
    e->pid = pid;
    e->ppid = BPF_CORE_READ(task, real_parent, tgid);
    bpf_get_current_comm(&e->comm, sizeof(e->comm));

    /* 从内核中读取一个以空字符结尾的字符串,存储到 e->filename 中 */
    fname_off = ctx->__data_loc_filename & 0xFFFF;
    bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx +fname_off);

    /* 成功将其提交到用户空间进行后期处理 */
    bpf_ringbuf_submit(e, 0);
    return 0;
  1. 定义了一个名为 handle_exit 的 eBPF 程序,它会在进程执行 exit() 系统调用时触发
SEC("tp/sched/sched_process_exit")
int handle_exit(struct trace_event_raw_sched_process_template* ctx){...eBPF程序具体实现...}
  1. 从当前进程中获取 PID 和 TID(线程 ID),如果 PID 和 TID 不相等,说明这是一个线程退出,将忽略此事件
    id = bpf_get_current_pid_tgid();
    pid = id >> 32;
    tid = (u32)id;

    if (pid != tid)
        return 0;
  1. 查找之前存储在 exec_start map 中的进程开始执行的时间戳;如果找到了时间戳,将计算进程的生命周期(持续时间),然后从 exec_start map 中删除该记录;如果未找到时间戳且指定了昀小持续时间,则直接返回
    start_ts = bpf_map_lookup_elem(&exec_start, &pid);
    if (start_ts)
        duration_ns = bpf_ktime_get_ns() - *start_ts;
    else if (min_duration_ns)
        return 0;
    bpf_map_delete_elem(&exec_start, &pid);

    /* 如果进程运行时间没有设置的昀小持续时间长,则提前返回,不记录该进程信息 */
    if (min_duration_ns && duration_ns < min_duration_ns)
        return 0;
  1. 从环形缓冲区 map rb 中预留一个事件结构,并填充相关数据,如进程 ID、父进程 ID、进程名、进程持续时间等。昀后将这些数据发送到用户态程序进行处理。
    e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
    if (!e)
        return 0;

    task = (struct task_struct *)bpf_get_current_task();

    e->exit_event = true;
    e->duration_ns = duration_ns;
    e->pid = pid;
    e->ppid = BPF_CORE_READ(task, real_parent, tgid);
    e->exit_code = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff;
    bpf_get_current_comm(&e->comm, sizeof(e->comm));

    bpf_ringbuf_submit(e, 0);
    return 0;
  1. 通过上面两个 eBPF 程序 SEC("tp/sched/sched_process_exec")SEC("tp/sched/sched_process_exit") ,当系统中进程执行 exec()exit() 系统调用时,我们的 eBPF 程序会捕获相应的事件,并将详细信息发送到用户态程序进行后续处理。这使得可以轻松地监控进程的创建和退出,并获取有关进程的详细信息

2.2. 用户态代码

代码位于 bootstrap.c ,用户态程序主要用于加载、验证、附加 eBPF 程序,以及接收 eBPF 程序收集的事件数据,并将其打印出来。

  1. 定义一个 env 结构,用于存储命令行参数,包括是否输出详细的调试信息、指定昀小进程持续时间
static struct env {
    bool verbose;
    long min_duration_ms;
} env;
  1. 使用 argp 库来解析命令行参数
static const struct argp_option opts[] = {
    { "verbose", 'v', NULL, 0, "Verbose debug output" },
    { "duration", 'd', "DURATION-MS", 0, "Minimum process duration (ms) to report" },
    {},
};

static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
    switch (key) {
    case 'v':
        env.verbose = true;
        break;
    case 'd':
        errno = 0;
        env.min_duration_ms = strtol(arg, NULL, 10);
        ...
        break;
    ......
    }
    return 0;
}
...
...
  1. main() 函数中,调用 bootstrap_bpf__open 函数打开一个 eBPF 对象,并将返回的指针存储在 skel 变量中;将昀小持续时间参数传递给 eBPF 对象;调用 bootstrap_bpf__load 函数加载 eBPF 对象到内核空间,并返回错误码;调用 bootstrap_bpf__attach 函数将加载的 eBPF 程序附加到相应的系统调用点上,使 eBPF 程序生效;创建一个新的环形缓冲区(ring buffer),用于接收 eBPF 程序发送的事件数据,设置回调函数 handle_event ,用于处理从环形缓冲区读取到的数据,根据事件类型(进程执行或退出),提取并打印事件信息,如时间戳、进程名、进程 ID、父进程 ID、文件名或退出代码等;while 循环中轮询环形缓冲区,处理收到的事件数据;当程序收到 SIGINT 或 SIGTERM 信号时,会退出轮询并完成清理、退出操作,关闭和卸载 eBPF 程序。
int main(int argc, char **argv)
{
    ...
    ...
    skel = bootstrap_bpf__open();
    ...
    ...

    skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL;

    err = bootstrap_bpf__load(skel);
    ...
    ...

    /* 附加跟踪点 */
    err = bootstrap_bpf__attach(skel);
    ...
    ...

    /* 设置环形缓冲区 */
    rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
    ...
    ...

    while (!exiting) {
        err = ring_buffer__poll(rb, 100 /* timeout, ms */);
        ...
        ...
    }

...
...
}

2.3. 编译运行

cd eBPFSecureDev/libbpf-bootstrap/ && make

不指定最小进程持续时间,输出系统中所有进程的创建和退出的信息,
指定最小进程持续时间为10ms,,输出系统中进程运行持续时间大于10ms的,