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文件直接加载。

BCC vs libbpf 框架架构对比 BCC 框架 Python/C++ 前端(用户态) Clang/LLVM 运行时编译 BPF 字节码 + 校验器 内核 eBPF 运行时 libbpf 框架 C 语言 BPF 源码(.bpf.c) Clang 预编译(开发机) BPF .o 文件 + BTF 信息 libbpf 加载器 + 校验器 内核 eBPF 运行时 关键差异:BCC 在目标机器上编译;libbpf 在开发机编译,通过 CO-RE 适配不同内核

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程序中解析网络包,提取五元组、协议、应用层信息

3.5 本章小结