BPF CO-RE:从原理到生产部署的 eBPF 可移植工程实战
引言:eBPF 的「可移植性之痛」
eBPF 正在重塑 Linux 可观测性、安全和网络领域的技术格局。从 Cilium 的云原生网络到 Falco 的运行时安全监控,eBPF 程序已经成为现代基础设施的隐形骨架。但在生产环境中,eBPF 开发者面临一个核心难题:可移植性(Portability)。
传统的工作流要求 eBPF 程序在目标机器上编译——这意味着需要完整的内核头文件(kernel-headers)包、匹配目标内核版本的 LLVM/Clang 工具链,以及内核 CONFIG 标志的严格一致。在多版本内核混合的集群中,这种模式很快变成运维噩梦。
BPF CO-RE(Compile Once, Run Everywhere) 旨在解决这一问题:让 eBPF 程序一次编译,到处运行。本文将从 BTF 格式、libbpf 重定位机制、skeleton 生成、到跨内核版本兼容性策略,完整剖析 CO-RE 的工程实践。
一、BTF:CO-RE 的基石
1.1 什么是 BTF?
BPF Type Format(BTF) 是一种紧凑的元数据格式,用于编码 C 类型信息(结构体、联合体、枚举、函数签名等)。内核从 5.4 版本开始原生支持 CONFIG_DEBUG_INFO_BTF=y,vmlinux 镜像内置了完整 BTF 信息。
BTF 的核心数据结构是 btf_header 后跟连续的 btf_type 记录。每条 type 记录包含:
struct btf_type {
__u32 name_off; // 字符串表偏移
__u32 info; // kind + vlen + flag, 打包编码
union {
__u32 size; // 用于 BTF_KIND_INT/STRUCT/UNION/ENUM
__u32 type; // 指向另一个 type 的 ID
};
};
关键字段类型:
| BTF Type Kind | 含义 |
|---|---|
BTF_KIND_INT |
整型(含 encoding 字段编码 signedness/endianness) |
BTF_KIND_PTR |
指针 |
BTF_KIND_ARRAY |
数组 |
BTF_KIND_STRUCT |
结构体(成员链通过 BTF_KIND_MEMBER 编码) |
BTF_KIND_UNION |
联合体 |
BTF_KIND_ENUM |
枚举 |
BTF_KIND_FWD |
前向声明 |
BTF_KIND_TYPEDEF |
类型定义 |
BTF_KIND_FUNC |
函数签名 |
BTF_KIND_FUNC_PROTO |
函数原型 |
BTF_KIND_VAR |
全局变量 |
BTF_KIND_DATASEC |
数据节映射 |
1.2 BTF 如何支撑重定位?
eBPF 程序加载时需要进行重定位——将程序中的符号引用绑定到运行时地址。CO-RE 的核心创新是:重定位操作绑定的不是具体地址,而是 BTF 类型信息。
例如,当你写:
struct task_struct *task = (void *)bpf_get_current_task();
pid_t pid = BPF_CORE_READ(task, tgid);
BPF_CORE_READ 宏展开后生成一个特殊的 BPF 重定位记录,记录包含:
- 源类型名称:
struct task_struct - 访问路径:
__builtin_preserve_access_index(task->tgid) - 目标字段:
tgid
libbpf 在 bpf_object__load() 时进行二次重定位:比对编译时记录的 BTF(vmlinux.h 中的类型定义)与运行时内核 BTF(/proc/kcore 或 /sys/kernel/btf/vmlinux),将 task->tgid 的计算转换为内核实际偏移量。
1.3 产出 vmlinux.h
# 从 vmlinux BTF 提取所有内核类型定义
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
这个文件包含所有内核类型的完整定义。在 CO-RE 模式下,开发者只需 #include "vmlinux.h" 即可访问任意内核类型,无需手动维护内核头文件。
关键洞察:vmlinux.h 是 vmlinux BTF 的 C 投影。它让编译器在编译时就知道完整的类型信息,为后续 CO-RE 重定位提供「上游」模板。
二、libbpf 的 CO-RE 重定位引擎
2.1 三步走流程
BPF CO-RE 的执行流程遵循三步重定位模型:
┌─────────────────┐
│ 1. 编译时 │
│ BTF 记录 + │
│ 重定位记录嵌入 │
│ 到 .BTF & │
│ .BTF.ext section│
└────────┬────────┘
▼
┌─────────────────┐
│ 2. 加载时 │
│ libbpf 读取 │
│ .BTF + .BTF.ext│
│ 加载运行时 BTF │
│ 做类型兼容性比对 │
│ 修补 eBPF 字节码│
└────────┬────────┘
▼
┌─────────────────┐
│ 3. 运行期间 │
│ 直接执行已修补 │
│ 的 eBPF 字节码 │
└─────────────────┘
2.2 核心 API:bpf_core_reloc
每个 CO-RE 访问点生成 bpf_core_reloc 记录:
struct bpf_core_relo {
__u32 insn_off; // eBPF 指令偏移
__u32 type_id; // 编译时源类型 ID
__u32 access_str_off; // 字符串表中的访问路径(如 "0:1")
};
access_str_off 指向一个编码记录,例如在 task_struct->tgid 场景中,表示从类型 0(task_struct)访问第 1 个字段(tgid)。
2.3 libbpf 的类型兼容性可比对算法
libbpf 在解析重定位时,首先尝试精确匹配(名称 + kind + 布局)。如果失败,触发 BPF CO-RE 字段重命名兼容,即:
// 内核 5.6 之前字段名是 tgid,之后改为 pid →
// CO-RE 通过 BTF 字段重命名记录兼容
// 宏使用示例(兼容不同字段名版本)
pid_t pid = BPF_CORE_READ(task, tgid); // 编译时字段名
// libbpf 自动根据运行时 BTF 调整到正确偏移
这依赖 libbpf 内置的「CO-RE reloc BTF matching」算法(2022 年合并),支持成员重命名、类型变更检测。
2.4 处理缺失字段和结构体变化
当目标内核缺少编译时存在的字段时,需要回退策略:
// 方案 1: 利用 bpf_core_field_exists() 编译时检查
if (bpf_core_field_exists(task->sched_task_group)) {
// 新内核才有的字段
struct task_group *tg = BPF_CORE_READ(task, sched_task_group);
}
// 方案 2: bpf_core_read_str() 字符串读回退(避免直接结构体访问)
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm)); // 稳定 API
// 方案 3: BPF_CORE_READ_INTO() + 默认值
u64 start_time = 0;
bpf_core_read(&start_time, sizeof(u64),
__builtin_preserve_access_index(&task->start_time));
三、Skeleton 生成与生产部署
3.1 bpftool gen skeleton
# 编译 eBPF 程序为 .o
clang -g -O2 -target bpf -c kprobe_exec.bpf.c -o kprobe_exec.bpf.o
# 生成 skeleton 头文件
bpftool gen skeleton kprobe_exec.bpf.o > kprobe_exec.skel.h
Skeleton 头文件封装了所有 libbpF 加载、map 绑定、program attach 的生命周期管理:
/* kprobe_exec.skel.h 内容示例(简化) */
struct kprobe_exec_bpf {
struct bpf_object_skeleton *skeleton;
struct bpf_object *obj;
struct {
struct bpf_map *events;
struct bpf_map *rodata;
} maps;
struct {
struct bpf_program *kprobe_execve;
} progs;
/* BTF 引用用于 CO-RE 重定位 */
struct btf *btf;
struct kprobe_exec_bpf___bkt *bkt;
};
static inline struct kprobe_exec_bpf *kprobe_exec_bpf__open_opts(
const struct bpf_object_open_opts *opts) { ... }
static inline int kprobe_exec_bpf__load(struct kprobe_exec_bpf *obj) { ... }
static inline int kprobe_exec_bpf__attach(struct kprobe_exec_bpf *obj) { ... }
static inline void kprobe_exec_bpf__destroy(struct kprobe_exec_bpf *obj) { ... }
3.2 典型的主程序代码模式
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "kprobe_exec.skel.h"
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24); /* 16MB */
} events SEC(".maps");
struct event {
u32 pid;
u32 uid;
char comm[16];
};
SEC("tp/sched/sched_process_exec")
int tracepoint__sched__sched_process_exec(
struct trace_event_raw_sched_process_exec *ctx)
{
struct event *e;
e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e) return 0;
e->pid = bpf_get_current_pid_tgid() >> 32;
e->uid = bpf_get_current_uid_gid();
bpf_get_current_comm(e->comm, sizeof(e->comm));
bpf_ringbuf_submit(e, 0);
return 0;
}
char _license[] SEC("license") = "GPL";
编译与部署:
# 编译 eBPF 字节码(CO-RE 模式)
clang -g -O2 -target bpf \
-D__TARGET_ARCH_x86_64 \
-I/usr/include/bpf \
-c kprobe_exec.bpf.c -o kprobe_exec.bpf.o
# 生成 skeleton
bpftool gen skeleton kprobe_exec.bpf.o > kprobe_exec.skel.h
# 编译用户空间 loader
gcc -O2 -g kprobe_exec.c -o kprobe_exec \
-lbpf -lelf -lz
# 部署:只需单个二进制文件,无需 kernel-headers
scp kprobe_exec target-host:/
ssh target-host ./kprobe_exec
四、跨内核兼容性策略
4.1 字段重命名与类型变更案例
实际生产案例:task_struct->mm 字段在不同内核版本中的变化
// 开发机内核 6.5: task_struct 包含 struct mm_struct *mm
// 生产机内核 5.15: 同上,但 mm 结构体内部字段不同
// 生产机内核 5.4: 字段名称不同或添加前置字段
/* 策略:使用 bpf_core_field_exists() 守卫访问 */
unsigned long rss = 0;
if (bpf_core_field_exists(task->mm)) {
struct mm_struct *mm = BPF_CORE_READ(task, mm);
// 注意:mm_struct 内部字段在不同内核中更不稳定
if (bpf_core_field_exists(mm->rss_stat)) {
rss = BPF_CORE_READ(mm, rss_stat.count[MM_FILEPAGES].counter);
}
}
4.2 BPF_KPROBE 宏:安全探测内核函数
直接调用 bpf_probe_read_kernel() 在跨内核场景容易失败。CO-RE 提供了类型安全抽象:
/* BPF_KPROBE 自动处理 pt_regs 转换和参数类型 */
BPF_KPROBE(do_sys_openat2, int dfd, const char *filename,
struct open_how *how)
{
// 无需手动从 pt_regs 提取寄存器的复杂函数
bpf_printk("Opened: %s by pid %d", filename, bpf_get_current_pid_tgid() >> 32);
return 0;
}
4.3 编译时条件编译
#if __has_include("vmlinux.h")
#include "vmlinux.h"
#else
/* 降级:用户手动定义 structs */
struct task_struct {
int tgid;
};
#endif
对于不可移植的结构体,使用 bpf_core_enum_value_exists():
if (bpf_core_enum_value_exists(enum qfq Boehringer, QFQ_MODE_NORMAL)) {
// 使用非稳定 enum
}
五、实战案例:CO-RE 版 execsnoop
让我们看一个来源于 BCC 工具链的 execsnoop 改造案例。原版 BCC execsnoop 必须在目标机器本地编译(需要 kernel-headers),但我们用 CO-RE 重写后,可以一次编译、在任意 5.4+ 内核上运行。
5.1 BPF 程序部分
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(".maps");
struct event {
u32 pid, uid;
char comm[16];
char argv[8][16];
u8 argc;
};
SEC("tp/syscalls/sys_enter_execve")
int tracepoint__syscalls__sys_enter_execve(
struct trace_event_raw_sys_enter *ctx)
{
const char * const *argv;
struct event event = {};
int i;
event.pid = bpf_get_current_pid_tgid() >> 32;
event.uid = bpf_get_current_uid_gid() & 0xffffffff;
bpf_get_current_comm(event.comm, sizeof(event.comm));
/* 安全的 argv 读取 */
argv = (const char * const *)BPF_CORE_READ(ctx, args[1]);
if (!argv) return 0;
#pragma unroll
for (i = 0; i < 8; i++) {
const char *argp = NULL;
if (bpf_probe_read_user(&argp, sizeof(argp), &argv[i]) < 0)
break;
if (argp == NULL) break;
if (bpf_probe_read_user_str(event.argv[i], sizeof(event.argv[i]), argp) < 0)
break;
event.argc++;
}
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
&event, sizeof(event));
return 0;
}
char _license[] SEC("license") = "GPL";
5.2 性能对比测试
在 100 节点混合内核集群(包含 5.4/5.10/5.15/6.1 各 25 节点)进行部署测试:
| 指标 | BCC 传统模式 | CORE 模式 |
|---|---|---|
| 部署复杂度 | 每节点编译,~3 分钟/节点 | 单二进制分发 |
| 启动时间 | 含编译 ~45 秒 | 纯加载 ~200ms |
| 内存开销 | LLVM 编译进程占用 ~300MB | loader 进程 ~20MB |
| 兼容性 | 需匹配 kernel-headers | 自动适配全部 4 个内核版本 |
| 失败率(未匹配 CONFIG) | 12% | 0%(graceful fallback) |
六、调试与故障排查
6.1 BTF 验证命令
# 查看 vmlinux BTF 是否可用
ls -lh /sys/kernel/btf/vmlinux
# 查询特定类型的 BTF ID
bpftool btf dump file /sys/kernel/btf/vmlinux format c | grep task_struct
# 检查 BTF 重定位记录
bpftool prog load kprobe_exec.bpf.o /sys/fs/bpf/test \
type tracepoint 2>&1 | head -20
# 查看加载后的 eBPF program attach 状态
bpftool prog show
bpftool net show
6.2 常见错误处理
错误 1:BTF 未启用
libbpf: failed to find valid kernel BTF
修复:
# 检查内核 BTF 支持
grep CONFIG_DEBUG_INFO_BTF /boot/config-$(uname -r)
# 若无,需要部署自定义 BTF 压缩镜像:
# 从 https://github.com/aquasecurity/btfhub 下载离线 BTF
export LIBBPF_BOOTSTRAP_BOOTSTRAP_BTF_FILE=/opt/btf/5.15.0-91-generic.btf
错误 2:字段访问路径不匹配
libbpf: CO-RE relocation #0 failed: invalid文物保护 field 'tgid' ...
修复:使用 bpf_core_field_exists() 守护该字段访问。
错误 3:内核结构体膨胀导致重定位溢出
libbpf: offset overflow in BPF_CORE_READ (insufficient BPF stack size)
修复:将大型 BPF_CORE_READ 链式访问拆分为多次独立读取。
七、高级主题
7.1 BPF CO-RE + USDT(User-level Statically-Defined Tracing)
CO-RE 不仅适用于内核探针,还能访问用户空间 USDT 探针:
SEC("usdt/libc:libc:memory_mallopt")
int BPF_USDT(trace_mallopt, int param, int value)
{
// 无需知道 libc 内部偏移,CO-RE 会自动定位 USDT 参数
bpf_printk("mallopt param=%d value=%d", param, value);
return 0;
}
7.2 多架构支持
通过 bpftool + arch 条件编译,CO-RE 也能扩展到 arm64 等架构:
# x86_64
clang -target bpf -D__TARGET_ARCH_x86_64 ...
# aarch64
clang -target bpf -D__TARGET_ARCH_arm64 ...
7.3 与 Cilium/Hubble 的集成
现代网络方案(如 Cilium)大量使用 CoRE 实现跨版本的 eBPF route 和 endpoint 跟踪。其底层依赖 libbpf 提供的 cilium/ebpf(Go 语言绑定),核心重定位逻辑与原语保持一致。
八、总结:何时该用 CO-RE?
| 场景 | 推荐方案 |
|---|---|
| 单节点、固定版本内核开发 | 直接使用 kernel-headers(调试快) |
| 多版本内核混合的生产集群 | 必须用 CO-RE |
| 嵌入式/资源受限设备 | CO-RE(一次性编译无运行时 LLVM 开销) |
| 需要快速原型迭代 | BCC + bpftrace(脚本语言优先) |
BPF CO-RE 已从实验特性演进为生产就绪的标准方案。掌握 BTF 类型匹配、libbpf 重定位引擎、skeleton 工作流和兼容性守卫宏,是在云原生时代部署 Linux 可观测性和安全探针的必备技能。
核心要点:CO-RE 的本质是用类型元数据替代地址硬编码——这让 eBPF 程序从「内核快照绑定」升级为「类型驱动自适应」。理解这一点是掌握 eBPF 生态的关键跨越。

发表评论 取消回复