eBPF学习记录(四)使用libbpf开发eBPF程序

本文介绍了如何在不依赖LLVM和内核头文件的情况下,利用libbpf开发eBPF程序。方法包括生成内核数据结构头文件、开发eBPF程序、编译为字节码以及编写用户态加载程序。文章详细阐述了libbpf程序与BCC的不同之处,并提供了完整的eBPF程序和用户态程序示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上一节,我们讲解了使用BCC开发eBPF程序,不过,在我们想要分发这个程序到客户环境时,又会碰到一个新的难题:BCC 依赖于 LLVM 和内核头文件才可以动态编译和加载 eBPF 程序,然而,我们在客户环境中通常没有这些开发工具,甚至不允许安装这些开发工具。
解决办法有三种:
第一种方法就是是把 BCC 和开发工具都安装到容器中,容器本身不提供对外服务,这样可以降低安全风险。
第二种方法就是linux内核源代码的samples/bpf目录下中有eBPF示例,在那里开发一个匹配当前内核版本的 eBPF 程序,并编译为字节码,再分发到生产环境中。
第三种方法就是用 libbpf 来开发一个 eBPF 程序。只要内核已经支持了 BPF 类型格式,可以使用从内核源码中抽离出来的 libbpf 进行开发,这样可以借助 BTF 获得更好的移植性。

在 eBPF 程序中,由于内核已经支持了 BTF,你不再需要引入众多的内核头文件来获取内核数据结构的定义。取而代之的是一个通过 bpftool 生成的 vmlinux.h 头文件,其中包含了内核数据结构的定义。
使用 libbpf 开发 eBPF 程序也是需要内核态的 eBPF 程序和用户态的加载、挂载、映射读取以及输出程序的,可以通过以下步骤完成:

1.内核头文件生成
我们使用 bpftool 生成内核数据结构定义头文件。BTF 开启后,你可以在系统中找到 /sys/kernel/btf/vmlinux 这个文件,bpftool 正是从它生成了内核数据结构头文件。 生成命令如下:

sudo bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

如果运行失败,需要根据提示使用apt安装linux-tools-5.13.0-37-generic和linux-cloud-tools-5.13.0-37-generic包。

2.eBPF 程序开发
eBPF 程序,包括定义哈希映射、性能事件映射以及跟踪点的处理函数等,而对这些数据结构和跟踪函数的定义都可以通过 SEC() 宏定义来完成。在编译时,通过 SEC() 宏定义的数据结构和函数会放到特定的 ELF 段中,这样后续在加载 BPF 字节码时,就可以从这些段中获取所需的元数据。这种方法非常方便。
我们看看那eBPF程序代码:
execsnoop.bpf.c:

//execsnoop.bpf.c
// 包含头文件
#include "vmlinux.h"
#include "execsnoop.h"
#include <bpf/bpf_helpers.h>

static const struct event empty_event = { };

// 定义哈希映射
struct {
	__uint(type, BPF_MAP_TYPE_HASH);
	__uint(max_entries, 10240);
	__type(key, pid_t);
	__type(value, struct event);
} execs SEC(".maps");

// 定义性能事件映射
struct {
	__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
	__uint(key_size, sizeof(u32));
	__uint(value_size, sizeof(u32));
}events SEC(".maps");

// 定义sys_enter_execve跟踪点函数
SEC("tracepoint/syscalls/sys_enter_execve")
int tracepoint__syscalls__sys_enter_execve(struct trace_event_raw_sys_enter
					   *ctx)
{
	struct event *event;
	const char **args = (const char **)(ctx->args[1]);
	const char *argp;

	// 查询PID
	u64 id = bpf_get_current_pid_tgid();
	pid_t pid = (pid_t) id;

	// 保存一个空的event到哈希映射中
	if (bpf_map_update_elem(&execs, &pid, &empty_event, BPF_NOEXIST)) {
		return 0;
	}
	event = bpf_map_lookup_elem(&execs, &pid);
	if (!event) {
		return 0;
	}
	// 初始化event变量
	event->pid = pid;
	event->args_count = 0;
	event->args_size = 0;

	// 查询第一个参数
	unsigned int ret = bpf_probe_read_user_str(event->args, ARGSIZE,
						   (const char *)ctx->args[0]);
	if (ret <= ARGSIZE) {
		event->args_size += ret;
	} else {
		/* write an empty string */
		event->args[0] = '\0';
		event->args_size++;
	}

	// 查询其他参数,使用pragma unroll控制循环次数
	event->args_count++;
#pragma unroll
	for (int i = 1; i < TOTAL_MAX_ARGS; i++) {
		bpf_probe_read_user(&argp, sizeof(argp), &args[i]);
		if (!argp)
			return 0;

		if (event->args_size > LAST_ARG)
			return 0;

		ret =
		    bpf_probe_read_user_str(&event->args[event->args_size],
					    ARGSIZE, argp);
		if (ret > ARGSIZE)
			return 0;

		event->args_count++;
		event->args_size += ret;
	}

	// 再尝试一次,确认是否还有未读取的参数
	bpf_probe_read_user(&argp, sizeof(argp), &args[TOTAL_MAX_ARGS]);
	if (!argp)
		return 0;

	// 如果还有未读取参数,则增加参数数量(用于输出"...")
	event->args_count++;

	return 0;
}

// 定义sys_exit_execve跟踪点函数
SEC("tracepoint/syscalls/sys_exit_execve")
int tracepoint__syscalls__sys_exit_execve(struct trace_event_raw_sys_exit *ctx)
{
	u64 id;
	pid_t pid;
	int ret;
	struct event *event;

	// 从哈希映射中查询进程基本信息
	id = bpf_get_current_pid_tgid();
	pid = (pid_t) id;
	event = bpf_map_lookup_elem(&execs, &pid);
	if (!event)
		return 0;

	// 更新返回值和进程名称
	ret = ctx->ret;
	event->retval = ret;
	bpf_get_current_comm(&event->comm, sizeof(event->comm));

	// 提交性能事件
	size_t len = EVENT_SIZE(event);
	if (len <= sizeof(*event))
		bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, event,
				      len);

	// 清理哈希映射
	bpf_map_delete_elem(&execs, &pid);
	return 0;
}
// 定义许可证(前述的BCC默认使用GPL)
char LICENSE[] SEC("license") = "GPL";

execsnoop.h:

#ifndef __EXECSNOOP_H
#define __EXECSNOOP_H

#define ARGSIZE  128
#define TASK_COMM_LEN 16
#define TOTAL_MAX_ARGS 60
#define FULL_MAX_ARGS_ARR (TOTAL_MAX_ARGS * ARGSIZE)
#define BASE_EVENT_SIZE (size_t)(&((struct event*)0)->args)
#define EVENT_SIZE(e) (BASE_EVENT_SIZE + e->args_size)
#define LAST_ARG (FULL_MAX_ARGS_ARR - ARGSIZE)

struct event {
	char comm[TASK_COMM_LEN];
	pid_t pid;
	int retval;
	int args_count;
	unsigned int args_size;
	char args[FULL_MAX_ARGS_ARR];
};

#endif				/* __EXECSNOOP_H */

代码分析:

  • 头文件 vmlinux.h 是第一步生成的,他包含了内核数据结构;execsnoop.h是自己编写的,我们不可以把这部分直接写到c文件中,因为这些结构体和宏定义我们在后面的用户态程序中需要用到;bpf/bpf_helpers.h 包含了BPF 辅助函数,包括SEC、BPF_MAP_TYPE_HASH、BPF_MAP_TYPE_PERF_EVENT_ARRAY等宏定义和bpf_get_current_pid_tgid等函数;
  • struct event 定义了进程基本信息数据结构,它会用在后面的哈希映射中;
  • SEC(“.maps”) 定义了哈希映射和性能事件映射;会把相应的变量放入ELF的数据段中;
  • SEC(“tracepoint/<跟踪点名称>”) 定义了跟踪点处理函数,系统调用跟踪点函数的格式是 “tracepoint_syscalls_<系统调用名称>”。简单来说就是把"/“换成”__"。ruguo 需要定义内核插桩和用户插桩,也是以类似的格式定义;
  • SEC(“license”) 定义了 eBPF 程序的许可证。在之前的 BCC eBPF 程序中,我们并没有定义许可证,这是因为 BCC 自动使用了 GPL 许可。

从这些代码中你可以看到,它的处理逻辑跟上述的 BCC 程序基本上是相同的。不过,详细对比一下,你会发现它们之间还是有不同的,不同点主要在两个方面:第一、函数名的定义格式不同。BCC 程序使用的是 TRACEPOINT_PROBE 宏,而 libbpf 程序用的则是 SEC 宏。第二、映射的访问方法不同。BCC 封装了很多更易用的映射访问函数(如 tasks.lookup()),而 libbpf 程序则需要调用BPF 辅助函数(比如查询要使用 bpf_map_lookup_elem())。

3.编译 eBPF 程序为字节码
有了 eBPF 程序,就可以使用 clang 和 bpftool 将其编译成 BPF 字节码,然后再生成其头文件 execsnoop.skel.h :

clang -g -O2 -target bpf -D__TARGET_ARCH_x86_64 -I/usr/include/x86_64-linux-gnu -I. -c execsnoop.bpf.c -o execsnoop.bpf.o
bpftool gen skeleton execsnoop.bpf.o > execsnoop.skel.h

4.开发用户态程序
同 BCC 的 Python 前端程序类似,libbpf 用户态程序也需要 eBPF 程序加载、挂载到跟踪点,以及通过 BPF 映射获取和打印执行结果等几个步骤。虽然 C 语言听起来可能比 Python 语言麻烦一些,但实际上,这几个步骤都可以通过字节码头文件中自动生成的函数来完成。
execsnoop.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include <time.h>
#include "execsnoop.h"
#include "execsnoop.skel.h"

// libbpf错误和调试信息回调的处理程序。
static int libbpf_print_fn(enum libbpf_print_level level, const char *format,
			   va_list args)
{
#ifdef DEBUGBPF
	return vfprintf(stderr, format, args);
#else
	return 0;
#endif
}

// 丢失事件的处理程序。
void handle_lost_events(void *ctx, int cpu, __u64 lost_cnt)
{
	fprintf(stderr, "Lost %llu events on CPU #%d!\n", lost_cnt, cpu);
}

// 打印参数(替换'\0'为空格)
static void print_args(const struct event *e)
{
	int args_counter = 0;

	for (int i = 0; i < e->args_size && args_counter < e->args_count; i++) {
		char c = e->args[i];
		if (c == '\0') {
			args_counter++;
			putchar(' ');
		} else {
			putchar(c);
		}
	}
	if (e->args_count > TOTAL_MAX_ARGS) {
		fputs(" ...", stdout);
	}
}

// 性能事件回调函数(向终端中打印进程名、PID、返回值以及参数)
void handle_event(void *ctx, int cpu, void *data, __u32 data_sz)
{
	const struct event *e = data;
	printf("%-16s %-6d %3d ", e->comm, e->pid, e->retval);
	print_args(e);
	putchar('\n');
}

// Bump RLIMIT_MEMLOCK,允许BPF子系统做任何它需要的事情。  
static void bump_memlock_rlimit(void)
{
	struct rlimit rlim_new = {
		.rlim_cur = RLIM_INFINITY,
		.rlim_max = RLIM_INFINITY,
	};

	if (setrlimit(RLIMIT_MEMLOCK, &rlim_new)) {
		fprintf(stderr, "Failed to increase RLIMIT_MEMLOCK limit!\n");
		exit(1);
	}
}

int main(int argc, char **argv)
{
	struct execsnoop_bpf *skel;
	struct perf_buffer_opts pb_opts;
	struct perf_buffer *pb = NULL;
	int err;

	// 1. 设置调试输出函数,libbpf发生错误会回调libbpf_print_fn
	libbpf_set_print(libbpf_print_fn);

	// 2. 增大进程限制的内存,默认值通常太小,不足以存入BPF映射的内容
	bump_memlock_rlimit();

	// 3. 打开BPF程序
	skel = execsnoop_bpf__open();
	if (!skel) {
		fprintf(stderr, "Failed to open BPF skeleton\n");
		return 1;
	}

	// 4. 加载BPF字节码
	err = execsnoop_bpf__load(skel);
	if (err) {
		fprintf(stderr, "Failed to load and verify BPF skeleton\n");
		goto cleanup;
	}

	// 5. 挂载BPF字节码到跟踪点
	err = execsnoop_bpf__attach(skel);
	if (err) {
		fprintf(stderr, "Failed to attach BPF skeleton\n");
		goto cleanup;
	}

	// 6. 配置性能事件回调函数
	pb_opts.sample_cb = handle_event;
	pb_opts.lost_cb = handle_lost_events;
	pb = perf_buffer__new(bpf_map__fd(skel->maps.events), 64, &pb_opts);
	err = libbpf_get_error(pb);
	if (err) {
		pb = NULL;
		fprintf(stderr, "failed to open perf buffer: %d\n", err);
		goto cleanup;
	}

	printf("%-16s %-6s %3s %s\n", "COMM", "PID", "RET", "ARGS");

	// 7. 从缓冲区中循环读取数据
	while ((err = perf_buffer__poll(pb, 100)) >= 0) ;
	printf("Error polling perf buffer: %d\n", err);

 cleanup:
	perf_buffer__free(pb);
	execsnoop_bpf__destroy(skel);
	return err != 0;
}

代码分析:

  • 头文件execsnoop.h记录着struct event结构体信息;头文件execsnoop.skel.h是上一步生成的,包含了 BPF 字节码和相关的管理函数。
  • 第 1 步设置调试输出函数,可以调用 printf() 把调试信息输出到终端中。
  • 第 2 步增大进程锁定内存限制,因为系统默认的锁定内存通常过小,无法满足 BPF 映射的需要。
  • 第 3~5 步直接调用execsnoop.skel.h文件中的函数,加载 BPF 字节码并挂载到跟踪点。
  • 第 6~7 步设置性能事件的回调函数,回调函数会从缓冲区中循环读取数据。

最后使用下面命令编译用户态程序,得到execsnoop应用程序:

clang -g -O2 -Wall -I . -c execsnoop.c -o execsnoop.o
clang -Wall -O2 -g execsnoop.o -static -lbpf -lelf -lz -o execsnoop

可以直接把这个execsnoop应用程序复制到开启了 BTF 的其他机器中,无需安装额外的 LLVM 开发工具和内核头文件,也可以直接执行。如果命令失败,并且你看到如下的错误,这说明当前机器没有开启 BTF,需要重新编译内核开启 BTF 才可以运行:

Failed to load and verify BPF skeleton
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小坚学Linux

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值