你写了一个 eBPF 程序,在开发机(内核 5.15)上运行良好,部署到生产服务器(内核 5.10)却加载失败——因为 task_struct 结构体在两个内核版本中的字段偏移不同。这是 eBPF 开发者最常遇到的痛点:内核数据结构在不同版本之间会变化。
CO-RE(Compile Once – Run Everywhere)正是为解决这一问题而生。它通过 BTF(BPF Type Format)类型信息和 libbpf 的运行时重定位,让 eBPF 程序在编译时记录对内核数据结构的访问方式,在加载时根据目标内核的实际布局自动调整偏移量。
一、可移植性问题
1.1 问题根源
Linux 内核的数据结构不属于稳定的 ABI——开发者可以自由修改结构体的字段顺序、添加新字段、删除旧字段:
// 内核 5.10 中的 task_struct(简化)struct task_struct { unsigned int __state; // offset 0 void *stack; // offset 8 int prio; // offset 16 // ...};
// 内核 5.15 中的 task_struct(简化)struct task_struct { unsigned int __state; // offset 0 unsigned int flags; // offset 4 ← 新增字段 void *stack; // offset 8 → 偏移变了! int prio; // offset 16 → 偏移变了! // ...};如果 eBPF 程序硬编码了 stack 的偏移量为 8,在 5.15 内核上就会读到错误的值。
1.2 传统解决方案
在 CO-RE 出现之前,解决可移植性有两种方式:
| 方案 | 做法 | 缺点 |
|---|---|---|
| BCC 运行时编译 | 在目标机器上用 clang 重新编译 | 需要安装 clang 和内核头文件,启动慢 |
| 条件编译 | 针对每个内核版本编译不同的 .o | 维护成本高,版本组合爆炸 |
二、BTF:BPF 类型格式
2.1 BTF 是什么
BTF(BPF Type Format)是一种紧凑的类型信息编码格式,类似于 DWARF 调试信息但更轻量:
| 特性 | DWARF | BTF |
|---|---|---|
| 大小 | 数十 MB | 数百 KB |
| 编码方式 | LEB128 + 复杂结构 | 简洁的 Type Entry |
| 用途 | 通用调试信息 | eBPF 专用类型信息 |
| 内核支持 | 不需要 | 5.2+ 可选,5.15+ 默认启用 |
2.2 BTF 的内容
BTF 编码了以下类型信息:
| 类型 | BTF 编码 | 示例 |
|---|---|---|
| 整数 | BTF_KIND_INT | int, u32, unsigned long |
| 指针 | BTF_KIND_PTR | int *, void * |
| 数组 | BTF_KIND_ARRAY | char[16], u32[10] |
| 结构体 | BTF_KIND_STRUCT | struct task_struct |
| 联合体 | BTF_KIND_UNION | union sigval |
| 枚举 | BTF_KIND_ENUM | enum node_states |
| 函数 | BTF_KIND_FUNC | do_sys_openat2 |
| 函数原型 | BTF_KIND_FUNC_PROTO | 函数签名 |
| 变量 | BTF_KIND_VAR | 全局变量 |
| 数据段 | BTF_KIND_DATASEC | .bss, .data, .rodata |
2.3 生成 BTF
# 检查内核是否支持 BTFls /sys/kernel/btf/vmlinux
# 使用 pahole 从内核二进制生成 BTFpahole -J /sys/kernel/btf/vmlinux
# 从 eBPF 对象文件查看 BTF 信息bpftool btf dump file hello.bpf.o
# 从内核查看 BTF 类型信息bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h2.4 vmlinux.h
vmlinux.h 是从内核 BTF 信息自动生成的头文件,包含所有内核数据结构的定义:
// vmlinux.h 中的 task_struct(自动生成)struct task_struct { unsigned int __state; unsigned int flags; void *stack; int prio; int static_prio; int normal_prio; const struct sched_class *sched_class; // ... 数百个字段};
// 使用 vmlinux.h 的 eBPF 程序#include "vmlinux.h"#include <bpf/bpf_helpers.h>
SEC("kprobe/do_sys_openat2")int trace_open(struct pt_regs *ctx){ // 可以直接引用内核结构体 struct task_struct *task = (struct task_struct *)bpf_get_current_task(); // CO-RE 会自动处理字段偏移 int prio = BPF_CORE_READ(task, prio); return 0;}不要手动编辑 vmlinux.h——它是由 bpftool btf dump 自动生成的。每次目标内核更新后,应重新生成。
三、BPF_CORE_READ 宏
3.1 逐字段访问
BPF_CORE_READ 是 CO-RE 的核心宏,它实现了字段访问的运行时重定位:
// 不使用 CO-RE(硬编码偏移,不可移植)struct task_struct *task = (struct task_struct *)bpf_get_current_task();int prio = *(int *)((void *)task + 16); // 假设 prio 在偏移 16
// 使用 CO-RE(自动重定位,可移植)struct task_struct *task = (struct task_struct *)bpf_get_current_task();int prio = BPF_CORE_READ(task, prio); // 运行时计算正确偏移3.2 链式访问
BPF_CORE_READ 支持链式访问,可以穿越多级指针:
// 获取当前进程的 PIDu32 pid = BPF_CORE_READ(current, pid);
// 获取当前进程的父进程的 PID(两级指针解引用)u32 ppid = BPF_CORE_READ(current, real_parent, pid);
// 获取当前进程的文件系统的根目录的 inode 号(多级解引用)u64 ino = BPF_CORE_READ(current, fs, root, dentry, d_inode, i_ino);3.3 BPF_CORE_READ 宏族
| 宏 | 用途 | 示例 |
|---|---|---|
| BPF_CORE_READ(src, field) | 读取结构体字段 | BPF_CORE_READ(task, pid) |
| BPF_CORE_READ_INTO(dst, src, field) | 读取到指定变量 | BPF_CORE_READ_INTO(&pid, task, pid) |
| BPF_CORE_READ_STR_INTO(dst, src, field) | 读取字符串 | BPF_CORE_READ_STR_INTO(buf, task, comm) |
| BPF_CORE_READ_BITFIELD | 读取位域 | BPF_CORE_READ_BITFIELD(skb, l4_hash) |
| bpf_core_field_offset | 获取字段偏移 | bpf_core_field_offset(task, prio) |
| bpf_core_field_size | 获取字段大小 | bpf_core_field_size(task, prio) |
| bpf_core_type_size | 获取类型大小 | bpf_core_type_size(struct task_struct) |
| bpf_core_type_exists | 检查类型是否存在 | bpf_core_type_exists(struct rq) |
| bpf_core_field_exists | 检查字段是否存在 | bpf_core_field_exists(task, flags) |
3.4 条件编译与字段存在性检查
SEC("kprobe/do_sys_openat2")int trace_open(struct pt_regs *ctx){ struct task_struct *task = (struct task_struct *)bpf_get_current_task();
// 检查字段是否存在(不同内核版本可能不同) if (bpf_core_field_exists(task, flags)) { unsigned int flags = BPF_CORE_READ(task, flags); // 使用 flags... }
// 检查类型是否存在 if (bpf_core_type_exists(struct rq)) { // 使用 struct rq... }
return 0;}四、libbpf 骨架(Skeleton)
4.1 骨架的作用
libbpf 骨架是自动生成的 C 代码,封装了 eBPF 程序的打开、加载、附加、销毁等操作:
4.2 骨架的生成与使用
# 生成骨架头文件bpftool gen skeleton hello.bpf.o > hello.skel.h生成的骨架头文件包含以下关键函数:
| 函数 | 说明 |
|---|---|
hello_bpf__open() | 打开 eBPF 对象,解析 BTF |
hello_bpf__load() | 加载程序到内核,执行 CO-RE 重定位 |
hello_bpf__attach() | 附加程序到 Hook 点 |
hello_bpf__destroy() | 销毁对象,释放资源 |
hello_bpf__open_and_load() | 打开 + 加载(合并) |
hello_bpf__open_opts() | 带选项打开 |
4.3 完整的 CO-RE 程序示例
eBPF 程序(execsnoop.bpf.c):
#include "vmlinux.h"#include <bpf/bpf_helpers.h>#include <bpf/bpf_tracing.h>
struct event { pid_t pid; pid_t ppid; char comm[16]; char filename[256];};
struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024);} events SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_execve")int trace_execve(struct trace_event_raw_sys_enter *ctx){ struct event *e;
e = bpf_ringbuf_reserve(&events, sizeof(*e), 0); if (!e) return 0;
e->pid = bpf_get_current_pid_tgid() >> 32;
// CO-RE 方式获取父进程 PID struct task_struct *task = (struct task_struct *)bpf_get_current_task(); e->ppid = BPF_CORE_READ(task, real_parent, pid);
bpf_get_current_comm(&e->comm, sizeof(e->comm));
const char *filename = (const char *)ctx->args[0]; bpf_probe_read_user_str(&e->filename, sizeof(e->filename), filename);
bpf_ringbuf_submit(e, 0); return 0;}
char LICENSE[] SEC("license") = "GPL";用户态程序(execsnoop.c):
#include <stdio.h>#include <signal.h>#include <bpf/libbpf.h>#include <bpf/bpf.h>#include "execsnoop.skel.h"
static volatile bool exiting = false;static void sig_handler(int sig) { exiting = true; }
static int handle_event(void *ctx, void *data, size_t len){ struct event { int pid, ppid; char comm[16], filename[256]; } *e = data;
printf("%-8d %-8d %-16s %s\n", e->pid, e->ppid, e->comm, e->filename); return 0;}
int main(int argc, char **argv){ struct execsnoop_bpf *skel; struct ring_buffer *rb;
signal(SIGINT, sig_handler);
/* 1. 打开并加载(CO-RE 重定位在此发生) */ skel = execsnoop_bpf__open_and_load(); if (!skel) { fprintf(stderr, "Failed to open/load BPF skeleton\n"); return 1; }
/* 2. 附加到 tracepoint */ if (execsnoop_bpf__attach(skel)) { fprintf(stderr, "Failed to attach BPF program\n"); goto cleanup; }
/* 3. 设置 Ring Buffer */ rb = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event, NULL, NULL); if (!rb) goto cleanup;
printf("%-8s %-8s %-16s %s\n", "PID", "PPID", "COMM", "FILENAME");
/* 4. 事件循环 */ while (!exiting) ring_buffer__poll(rb, 100);
ring_buffer__free(rb);
cleanup: execsnoop_bpf__destroy(skel); return 0;}Makefile:
# Makefile for CO-RE eBPF programAPP = execsnoopBPF_SRC = $(APP).bpf.cSKEL_H = $(APP).skel.h
CLANG ?= clangARCH := $(shell uname -m | sed 's/x86_64/x86/' | sed 's/aarch64/arm64/')
.PHONY: all clean
all: $(APP)
# 生成 vmlinux.h(如果不存在)vmlinux.h: bpftool btf dump file /sys/kernel/btf/vmlinux format c > $@
# 编译 eBPF 程序$(APP).bpf.o: $(BPF_SRC) vmlinux.h $(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \ -I/usr/include/$(ARCH)-linux-gnu \ -c $< -o $@
# 生成骨架$(SKEL_H): $(APP).bpf.o bpftool gen skeleton $< > $@
# 编译用户态程序$(APP): $(APP).c $(SKEL_H) gcc -g -O2 -o $@ $< -lbpf -lelf -lz
clean: rm -f $(APP) $(APP).bpf.o $(SKEL_H) vmlinux.h五、CO-RE 重定位原理
5.1 重定位过程
CO-RE 重定位发生在 libbpf 加载 eBPF 程序时:
5.2 重定位记录
编译时,clang 在 eBPF 对象文件中生成重定位记录:
# 查看重定位记录llvm-readelf -r hello.bpf.o
# 输出示例:# Relocation section '.rel.tracepoint/syscalls/sys_enter_execve'# Offset Info Type Symbol's Value Symbol's Name# 0x00000012 00000001 R_BPF_64_64 00000000 task_struct.prio# 0x00000028 00000001 R_BPF_64_64 00000000 task_struct.real_parent5.3 重定位类型
| 重定位类型 | 说明 | 示例 |
|---|---|---|
| 字段偏移 | 字段在结构体中的偏移量 | task->prio 的偏移 |
| 字段大小 | 字段的大小 | task->prio 的大小(4 字节) |
| 字段存在性 | 字段是否存在 | task->flags 在 5.10 不存在 |
| 类型大小 | 类型的大小 | sizeof(struct task_struct) |
| 枚举值 | 枚举常量的值 | BPF_F_CURRENT_CPU |
| 类型存在性 | 类型是否存在 | struct rq 是否定义 |
六、CO-RE 的限制与最佳实践
6.1 限制
| 限制 | 说明 |
|---|---|
| 内核需要 BTF | 目标内核必须启用 CONFIG_DEBUG_INFO_BTF |
| 不支持宏重定位 | 内核宏的值无法通过 BTF 获取 |
| 不支持函数内联 | 内联函数的偏移无法重定位 |
| 不支持全局变量 | 内核全局变量的地址无法重定位 |
6.2 最佳实践
- 始终使用 BPF_CORE_READ 访问内核数据结构
- 使用 bpf_core_field_exists 检查字段是否存在
- 使用 vmlinux.h 而非手动定义内核结构体
- 使用 libbpf 骨架 管理程序生命周期
- 避免硬编码偏移量 和常量值
- 使用 SEC(“license”) 声明 GPL 许可证
如果目标内核没有启用 BTF(CONFIG_DEBUG_INFO_BTF),CO-RE 程序将无法加载。Ubuntu 22.04+ 和大多数现代发行版默认启用 BTF。对于旧内核,需要手动安装 linux-modules-extra 并使用 pahole 生成 BTF。
七、动手实践
7.1 生成 vmlinux.h 并查看类型信息
# 生成 vmlinux.hbpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 查看文件大小ls -lh vmlinux.h# -rw-r--r-- 1 root root 15M vmlinux.h
# 搜索特定结构体grep "struct task_struct {" vmlinux.h -A 50
# 查看内核 BTF 中的类型数量bpftool btf dump file /sys/kernel/btf/vmlinux | wc -l7.2 编译并运行 CO-RE 程序
# 编译 eBPF 程序clang -g -O2 -target bpf -D__TARGET_ARCH_x86 \ -c execsnoop.bpf.c -o execsnoop.bpf.o
# 生成骨架bpftool gen skeleton execsnoop.bpf.o > execsnoop.skel.h
# 编译用户态程序gcc -g -O2 -o execsnoop execsnoop.c -lbpf -lelf -lz
# 运行sudo ./execsnoop7.3 验证 CO-RE 重定位
# 查看对象文件中的重定位信息llvm-readelf -r execsnoop.bpf.o
# 查看 BTF 信息bpftool btf dump file execsnoop.bpf.o
# 使用 libbpf 调试模式查看重定位过程export LIBBPF_LOG_LEVEL=4sudo ./execsnoop 2>&1 | grep -i reloc八、本章小结
上一章深入探讨了eBPF Hook 点与追踪机制。 本章详解了 CO-RE 的完整技术栈:
| 主题 | 核心要点 | 关键词 |
|---|---|---|
| 可移植性问题 | 内核数据结构在不同版本间变化,硬编码偏移不可移植 | 可移植性问题 |
| BTF 类型信息 | 紧凑的类型编码,记录内核数据结构的布局 | BTF 类型信息 |
| vmlinux.h | 从内核 BTF 自动生成的头文件,包含所有内核类型定义 | vmlinux.h |
| BPF_CORE_READ | 运行时重定位的字段访问宏,自动计算正确偏移 | BPF_CORE_READ |
| libbpf 骨架 | 自动生成的管理代码,封装打开/加载/附加/销毁 | libbpf 骨架 |
| 重定位原理 | 编译时记录重定位信息,加载时根据目标内核 BTF 修补字节码 | 重定位原理 |
CO-RE 是 eBPF 从”实验性技术”走向”生产级方案”的——它让 eBPF 程序像普通应用程序一样,一次编译即可在不同内核版本上运行。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






