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 生态的关键跨越。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部