3. eBPF编程入门:BCC与libbpf框架对比,编写第一个eBPF程序(Hello World)
在掌握了eBPF的基本原理与数据路径之后,本章将进入实战环节。我们将通过编写经典的“Hello World”程序,对比当前最主流的两个eBPF开发框架——BCC(BPF Compiler Collection)与libbpf。理解两者的设计哲学与适用场景,是构建高效、可维护的eBPF网络观测工具的基础。
3.1 两大框架的定位与演进
BCC与libbpf代表了eBPF程序开发的两种不同范式。BCC更注重快速原型开发与动态编译,而libbpf则强调静态链接、可移植性与生产环境部署。
| 对比维度 | BCC | libbpf |
|---|---|---|
| 编译模式 | 运行时动态编译(Clang/LLVM) | 预编译(CO-RE + BPF字节码) |
| 依赖要求 | 目标机器需安装LLVM、Clang、内核头文件 | 仅需libbpf库,无需运行时编译器 |
| 可移植性 | 较差,依赖内核版本与配置 | 优秀,通过BTF与CO-RE实现一次编译到处运行 |
| 开发效率 | 高,Python/C++前端快速迭代 | 中等,需手动管理生命周期与加载逻辑 |
| 生产环境 | 不推荐,运行时编译增加延迟与安全风险 | 推荐,轻量、稳定、适合长期运行 |
| 典型场景 | 调试、快速验证、教学演示 | 生产级Agent、网络观测、安全监控 |
选型建议: 在《eBPF网络观测与OTel标签体系》课程中,我们推荐使用 libbpf + CO-RE 作为生产级网络观测Agent的基础。BCC则适合在开发阶段快速验证Hook点与数据格式。
3.2 框架架构对比:从源码到内核
下图展示了BCC与libbpf在程序生命周期中的核心差异。BCC在用户态完成编译,而libbpf则通过预编译的.o文件直接加载。
3.3 编写第一个eBPF程序:Hello World
我们将分别使用BCC和libbpf实现同一个目标:在每次系统调用execve发生时,打印一条“Hello World”消息。这个程序虽然简单,但完整展示了eBPF的Hook、映射(可选)与用户态交互流程。
3.3.1 使用BCC实现
BCC的Python前端允许我们以内联方式编写C代码,极大降低了入门门槛。
#!/usr/bin/python3
from bcc import BPF
# BPF C 程序源码
bpf_text = """
int hello_world(void *ctx) {
bpf_trace_printk("Hello World from BCC!\\n");
return 0;
}
"""
# 加载 BPF 程序
b = BPF(text=bpf_text)
# 附加到 sys_enter_execve tracepoint
b.attach_tracepoint(tp="syscalls:sys_enter_execve", fn_name="hello_world")
print("Tracing execve... Ctrl+C to exit.")
# 读取 trace_pipe 输出
b.trace_print()
运行方式: 保存为
bcc_hello.py,以 root 权限执行 python3 bcc_hello.py。每当有进程执行新程序时,终端会打印内核trace输出。
3.3.2 使用libbpf实现
libbpf需要分离内核态(BPF程序)与用户态(加载器)代码。我们使用CO-RE(Compile Once – Run Everywhere)模式。
步骤1:内核态BPF程序(hello.bpf.c)
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "GPL";
SEC("tracepoint/syscalls/sys_enter_execve")
int hello_world(void *ctx) {
bpf_printk("Hello World from libbpf!\\n");
return 0;
}
步骤2:用户态加载器(hello.c)
#include <stdio.h>
#include <unistd.h>
#include <bpf/libbpf.h>
#include "hello.skel.h"
int main() {
struct hello_bpf *skel;
// 打开、加载并附加 BPF 骨架
skel = hello_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\\n");
return 1;
}
hello_bpf__attach(skel);
printf("Tracing execve... Ctrl+C to exit.\\n");
// 保持程序运行
while (1) sleep(1);
// 清理
hello_bpf__destroy(skel);
return 0;
}
步骤3:编译与运行
# 使用 bpftool 生成骨架头文件
bpftool gen skeleton hello.bpf.o > hello.skel.h
# 编译用户态程序
gcc -o hello hello.c -lbpf -lelf -lz
# 运行(需要 root 权限)
sudo ./hello
注意事项: libbpf 程序需要内核支持 BTF(BPF Type Format)。在较老的内核(< 5.4)上,可能需要手动禁用 CO-RE 或使用 BCC 作为替代方案。
3.4 从Hello World到网络观测:关键差异
虽然上述示例仅打印字符串,但它揭示了eBPF网络观测编程的核心模式:
- Hook点选择: 网络观测通常使用
XDP、TC、kprobe/tracepoint在协议栈关键路径上挂载。 - 数据传递: 从
bpf_printk(调试用)升级为BPF_MAP_TYPE_PERF_EVENT_ARRAY或BPF_MAP_TYPE_RINGBUF,实现高性能数据导出。 - 上下文结构: 网络程序使用
struct __sk_buff或struct xdp_md,而非简单的void *ctx。
| 组件 | Hello World 示例 | 网络观测场景 |
|---|---|---|
| Hook点 | tracepoint:syscalls/sys_enter_execve |
XDP / TC ingress/egress / kprobe:tcp_v4_connect |
| 数据输出 | bpf_printk(内核trace) |
BPF_RINGBUF 或 PERF_EVENT_ARRAY |
| 用户态交互 | 读取 /sys/kernel/debug/tracing/trace_pipe |
libbpf 回调函数 + 环形缓冲区轮询 |
| 标签注入 | 无 | 在BPF程序中解析网络包,提取五元组、协议、应用层信息 |