4. eBPF C 程序基础:编写第一个 eBPF 程序(Hello World)
本章是 eBPF 开发的起点。我们将从零编写一个经典的 “Hello World” eBPF 程序,理解其加载、挂载到内核事件的过程,并实现用户态与内核态之间的数据交互。这是后续构建复杂资源模型与流量可视化工具的基础。
4.1 eBPF 程序的生命周期与核心组件
一个完整的 eBPF 程序通常由两部分组成:内核态 eBPF 字节码(C 语言编写,由 LLVM/Clang 编译)和用户态加载器(通常使用 libbpf 库或 bpftool)。其生命周期包括:编写、编译、加载、验证、挂载、运行与卸载。
核心概念: eBPF 程序本身是一个事件驱动的沙箱程序,它不会主动运行,而是挂载到指定的内核事件(如系统调用、网络包到达、函数入口/出口)上,当事件发生时被触发执行。
4.2 编写第一个 eBPF 程序:Hello World
我们的第一个程序将挂载到 execve 系统调用上,每当有新的程序被执行时,打印一条日志。由于 eBPF 程序不能直接使用 printk,我们使用 bpf_trace_printk 辅助函数将消息写入内核的 trace pipe。
4.2.1 内核态 eBPF 代码 (hello.bpf.c)
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "GPL";
SEC("kprobe/sys_execve")
int hello_execve(void *ctx) {
bpf_trace_printk("Hello World from eBPF!\\n");
return 0;
}
| 代码元素 | 说明 |
|---|---|
#include <bpf/bpf_helpers.h> |
引入 eBPF 辅助函数和宏定义。 |
SEC("license") |
指定许可证为 GPL,这是使用某些 eBPF 辅助函数的必要条件。 |
SEC("kprobe/sys_execve") |
定义程序段 (Section),指定挂载点为 sys_execve 内核函数的 kprobe。 |
bpf_trace_printk |
用于调试的打印函数,输出到 /sys/kernel/debug/tracing/trace_pipe。 |
4.2.2 编译 eBPF 程序
使用 Clang 编译器将 C 源码编译为 eBPF 目标文件 (.o)。
clang -O2 -target bpf -c hello.bpf.c -o hello.bpf.o
-O2:开启优化,生成更高效的字节码。-target bpf:指定目标架构为 BPF。-c:只编译不链接。
4.3 加载与挂载 eBPF 程序
我们可以使用 bpftool 或编写用户态 C 程序(基于 libbpf)来加载和挂载。这里演示使用 bpftool 快速验证。
4.3.1 使用 bpftool 加载与挂载
# 加载 eBPF 程序到内核,并获取程序 ID
sudo bpftool prog load hello.bpf.o /sys/fs/bpf/hello
# 查看已加载的程序
sudo bpftool prog list
# 将程序挂载到 sys_execve 的 kprobe 事件上
sudo bpftool prog attach pinned /sys/fs/bpf/hello kprobe sys_execve
提示:
bpftool prog attach 命令会自动创建 kprobe 事件并关联 eBPF 程序。挂载后,每次执行新程序(如 ls、cat),eBPF 程序都会被触发。
4.3.2 验证程序运行
查看 eBPF 程序的输出:
sudo cat /sys/kernel/debug/tracing/trace_pipe
在另一个终端执行任意命令(如 ls),你将在 trace_pipe 中看到类似输出:
<...>-12345 [001] .... 123456.789012: 0: Hello World from eBPF!
4.4 用户态与内核态数据交互:BPF Map
bpf_trace_printk 仅用于调试。生产环境中,eBPF 程序通过 BPF Map 与用户态交换数据。BPF Map 是内核中的键值对存储,用户态程序可以通过文件描述符访问。
4.4.1 使用 BPF Map 的 eBPF 程序示例
以下程序统计执行 execve 的次数,并将计数存储在 Map 中。
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "GPL";
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u64);
} execve_count SEC(".maps");
SEC("kprobe/sys_execve")
int count_execve(void *ctx) {
__u32 key = 0;
__u64 *count = bpf_map_lookup_elem(&execve_count, &key);
if (count) {
__sync_fetch_and_add(count, 1);
}
return 0;
}
| Map 定义元素 | 说明 |
|---|---|
BPF_MAP_TYPE_ARRAY |
指定 Map 类型为数组,索引为键。 |
max_entries |
数组大小,这里为 1。 |
__sync_fetch_and_add |
原子自增操作,保证并发安全。 |
4.4.2 用户态读取 Map 数据
用户态程序(C 或 Python)通过 libbpf 提供的 API 读取 Map 中的值。
// 伪代码示例 (C 语言)
int map_fd = bpf_object__find_map_fd_by_name(obj, "execve_count");
__u32 key = 0;
__u64 value;
bpf_map_lookup_elem(map_fd, &key, &value);
printf("Total execve calls: %llu\\n", value);
4.5 关键概念对比与常见问题
| 概念 | 说明 | 注意事项 |
|---|---|---|
| kprobe | 动态内核探针,可挂载到任意内核函数入口/出口。 | 依赖内核符号表,函数名可能因内核版本变化。 |
| tracepoint | 静态内核跟踪点,由内核开发者预定义,参数稳定。 | 比 kprobe 更安全、更稳定,推荐优先使用。 |
| BPF Map | 内核中的数据结构,用于 eBPF 程序间或与用户态通信。 | 需要指定类型(如 Array、Hash、Per-CPU Array 等)。 |
| 验证器 | 确保 eBPF 程序安全:无死循环、无非法内存访问。 | 程序复杂度受限(最大指令数 4096,5.2 内核后放宽)。 |
常见错误: 如果加载 eBPF 程序时遇到
Permission denied,请确保以 root 权限运行,并且内核已启用 BPF 相关配置(CONFIG_BPF、CONFIG_BPF_SYSCALL、CONFIG_DEBUG_FS)。使用 bpftool feature list 可查看当前内核支持的 eBPF 特性。
4.6 本章小结
- 掌握了 eBPF 程序从编写、编译到加载、挂载的完整流程。
- 实现了第一个 “Hello World” eBPF 程序,并通过
bpf_trace_printk验证其运行。 - 理解了 BPF Map 作为用户态与内核态数据交互的核心机制,并编写了基于 Map 的计数程序。
- 对比了 kprobe 与 tracepoint 的适用场景,为后续章节构建租户隔离的资源监控模型打下基础。