4. eBPF C 程序基础:编写第一个 eBPF 程序(Hello World)

本章是 eBPF 开发的起点。我们将从零编写一个经典的 “Hello World” eBPF 程序,理解其加载、挂载到内核事件的过程,并实现用户态与内核态之间的数据交互。这是后续构建复杂资源模型与流量可视化工具的基础。

4.1 eBPF 程序的生命周期与核心组件

一个完整的 eBPF 程序通常由两部分组成:内核态 eBPF 字节码(C 语言编写,由 LLVM/Clang 编译)和用户态加载器(通常使用 libbpf 库或 bpftool)。其生命周期包括:编写、编译、加载、验证、挂载、运行与卸载。

核心概念: eBPF 程序本身是一个事件驱动的沙箱程序,它不会主动运行,而是挂载到指定的内核事件(如系统调用、网络包到达、函数入口/出口)上,当事件发生时被触发执行。
eBPF 程序生命周期与数据交互流程 eBPF 程序生命周期与数据交互流程 用户态 (User Space) 内核态 (Kernel Space) 1. 编写 eBPF C 源码 2. LLVM/Clang 编译为 BPF 字节码 3. 用户态加载器 (libbpf) 加载 5. 读取 BPF Map 数据 4. 内核验证器检查 & JIT 编译 6. 挂载到事件 (kprobe/tracepoint) 7. 事件触发,执行 eBPF 程序 8. 写入数据到 BPF Map BPF Map 是用户态与内核态共享数据的桥梁,是实现数据交互的核心机制。

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);
BPF Map 数据交互架构 BPF Map 数据交互架构 用户态程序 bpf_map_lookup_elem bpf_map_update_elem 内核空间 BPF Map (键值对存储) eBPF 程序读写 Map Map 是用户态与内核态共享内存的唯一安全通道,支持多种数据类型。

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 的适用场景,为后续章节构建租户隔离的资源监控模型打下基础。