推荐 Ubuntu 22.04 (因为内核版本比较高)
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 工程。
代码位于 bootstrap.bpf.c
,主要用于跟踪 exec()
和 exit()
系统调用,通过 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";
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;
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 */
handle_exec
的 eBPF 程序,它会在进程执行 exec()
系统调用时触发SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx){...eBPF程序具体实现...}
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);
/* 指定了昀小持续时间,就只关心进程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;
handle_exit
的 eBPF 程序,它会在进程执行 exit() 系统调用时触发SEC("tp/sched/sched_process_exit")
int handle_exit(struct trace_event_raw_sched_process_template* ctx){...eBPF程序具体实现...}
id = bpf_get_current_pid_tgid();
pid = id >> 32;
tid = (u32)id;
if (pid != tid)
return 0;
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;
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;
SEC("tp/sched/sched_process_exec")SEC("tp/sched/sched_process_exit")
,当系统中进程执行 exec()
或 exit()
系统调用时,我们的 eBPF 程序会捕获相应的事件,并将详细信息发送到用户态程序进行后续处理。这使得可以轻松地监控进程的创建和退出,并获取有关进程的详细信息代码位于 bootstrap.c
,用户态程序主要用于加载、验证、附加 eBPF 程序,以及接收 eBPF 程序收集的事件数据,并将其打印出来。
static struct env {
bool verbose;
long min_duration_ms;
} env;
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;
}
...
...
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 */);
...
...
}
...
...
}
cd eBPFSecureDev/libbpf-bootstrap/ && make
不指定最小进程持续时间,输出系统中所有进程的创建和退出的信息,
指定最小进程持续时间为10ms,,输出系统中进程运行持续时间大于10ms的,