上一次,我们尝试了用户空间自定义函数探测,里边会用到进程ID参数,那么实际使用场景下,我们可能需要能检测到进程的拉起和退出,今天就一起来实现下这个小功能。
一、概述
eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具,它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。
本文主要尝试通过探测内核的sys_enter_execve、sched_process_exit事件,捕获进程的拉起和退出事件。并通过ring buffer输出到用户空间程序中。
事实上我们还可以捕获一些关于新进程的有趣信息,例如二进制文件的文件名、测量进程的生命周期、消耗的资源量等。这是深入了解内核内部并观察事物如何运作的良好起点。
二、探测实现
1.定义事件信息结构体
我们新建一个exec.h用于定义存储事件的结构体,以方便内和空间和用户空间都可以方便的使用它。
#ifndef __EXEC_H
#define __EXEC_H
#define EXEC_CMD_LEN 128
struct event {
int pid;
int ppid;
int uid;
int retval;
bool is_exit;
char cmd[EXEC_CMD_LEN];
unsigned long long ns;
};
#endif /* __EXEC_H */
2.ebpf探测程序实现
定义ebpf程序exec.bpf.c,用于探测进程的启动和退出事件。
/**
* 捕获 Linux 内核中进程执行的事件
*/
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include "exec.h"
// 定义ring buffer Map
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024); // 256 KB
} rb SEC(".maps");
// 捕获进程执行事件,使用 ring buffer 向用户态打印输出
SEC("tracepoint/syscalls/sys_enter_execve")
int snoop_process_start(struct trace_event_raw_sys_enter* ctx)
{
u64 id;
pid_t pid;
struct event *e;
struct task_struct *task;
// 获取当前进程的用户ID
uid_t uid = (u32)bpf_get_current_uid_gid();
// 获取当前进程ID
id = bpf_get_current_pid_tgid();
pid = id >> 32;
// 获取当前进程的task_struct结构体
task = (struct task_struct*)bpf_get_current_task();
// 读取进程名称
char *cmd = (char *) BPF_CORE_READ(ctx, args[0]);
// 预订一个ringbuf样本空间
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e)
return 0;
// 设置数据
e->pid = pid;
e->uid = uid;
e->ppid = BPF_CORE_READ(task, real_parent, pid);
bpf_probe_read_str(&e->cmd, EXEC_CMD_LEN, cmd);
e->ns = bpf_ktime_get_ns();
// 提交到ringbuf用户空间进行后处理
bpf_ringbuf_submit(e, 0);
// 使用bpf_printk函数在内核日志中打印 PID 和文件名
// bpf_printk("TRACEPOINT EXEC pid = %d, uid = %d, cmd = %s\n", pid, uid, e->cmd);
return 0;
}
// 监控进程退出事件,使用 ring buffer 向用户态打印输出
SEC("tp/sched/sched_process_exit")
int snoop_process_exit(struct trace_event_raw_sched_process_template* ctx)
{
struct task_struct *task;
struct event *e;
pid_t pid, tid;
u64 id, ts, *start_ts, start_time = 0;
// 获取当前进程的用户ID
uid_t uid = (u32)bpf_get_current_uid_gid();
// 获取当前进程/线程ID
id = bpf_get_current_pid_tgid();
pid = id >> 32;
tid = (u32)id;
// 获取当前进程的task_struct结构体
task = (struct task_struct *)bpf_get_current_task();
start_time = BPF_CORE_READ(task, start_time);
/* ignore thread exits */
if (pid != tid)
return 0;
// 预订一个ringbuf样本空间
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e)
return 0;
// 设置数据
e->ns = bpf_ktime_get_ns() - start_time;
e->pid = pid;
e->uid = uid;
e->ppid = BPF_CORE_READ(task, real_parent, tgid);
e->is_exit = true;
e->retval = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff;
bpf_get_current_comm(&e->cmd, sizeof(e->cmd));
// 提交到ringbuf用户空间进行后处理
bpf_ringbuf_submit(e, 0);
// 使用bpf_printk函数在内核日志中打印 PID 和文件名
// bpf_printk("TRACEPOINT EXIT pid = %d, uid = %d, cmd = %s\n", pid, uid, e->cmd);
return 0;
}
char LICENSE[] SEC("license") = "GPL";
3.用户空间程序实现
编写用户空间程序exec.cc,加载ebpf程序到内核并读取ringbuffer中的探测数据。
/**
* ebpf 用户空间程序(loader、read perf buffer)
*/
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "exec.skel.h"
#include "exec.h"
int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
/* Ignore debug-level libbpf logs */
if (level > LIBBPF_INFO)
return 0;
return vfprintf(stderr, format, args);
}
// Control-C process
static volatile bool exiting = false;
static void sig_handler(int sig)
{
exiting = true;
}
// ring buffer data process
static int handle_event(void *ctx, void *data, size_t data_sz)
{
const struct event *e = reinterpret_cast<struct event *>(data);
struct tm *tm;
char ts[32];
time_t t;
time(&t);
tm = localtime(&t);
strftime(ts, sizeof(ts), "%H:%M:%S", tm);
if (e->is_exit) {
printf("%s %-5s %d %d %s %d %llums\n", ts, "EXIT", e->pid, e->uid, e->cmd, e->retval, e->ns / 1000000);
} else {
printf("%s %-5s %d %d %s\n", ts, "EXEC", e->pid, e->uid, e->cmd);
}
return 0;
}
int main(int argc, char **argv)
{
struct exec_bpf *skel;
int err;
struct ring_buffer *rb = NULL;
/* 设置libbpf错误和调试信息回调 */
libbpf_set_print(libbpf_print_fn);
/* Control-C 停止信号 */
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
/* 加载并验证 exec.bpf.c 应用程序 */
skel = exec_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
/* 附加 exec.bpf.c 程序到跟踪点 */
err = exec_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
exec_bpf__destroy(skel);
return -err;
}
// printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` to see output of the BPF programs.\n");
/* 设置环形缓冲区轮询 */
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
if (!rb) {
err = -1;
fprintf(stderr, "Failed to create ring buffer\n");
exec_bpf__destroy(skel);
return -err;
}
/* 处理收到的内核数据 */
printf("%-8s %-8s %-7s %-7s %-16s %-8s %-8s\n", "TIME", "TYPE", "PID", "UID", "CMD", "RET", "DURATION");
while (!exiting) {
// 轮询内核数据
err = ring_buffer__poll(rb, 100 /* timeout, ms */);
if (err == -EINTR) { /* Ctrl-C will cause -EINTR */
err = 0;
break;
}
if (err < 0) {
printf("Error polling perf buffer: %d\n", err);
break;
}
}
}
4.编译测试
cd build
cmake ..
make
sudo ./exec
TIME TYPE PID UID CMD RET DURATION
10:37:29 EXEC 459980 1000 /usr/bin/ls
10:37:29 EXIT 459980 1000 ls 0 3ms
三、总结
本文主要尝试通过探测内核的sys_enter_execve、sched_process_exit事件,捕获进程的拉起和退出事件,并通过ring buffer输出到用户空间程序中供使用或分析。
好了,本文相对比较简单,大家有对哪些内容感兴趣希望我介绍的,可以留言告诉我~
yan 23.1.5