mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1633 字
5 分钟
CO-RE:一次编译到处运行
2026-03-03

你写了一个 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维护成本高,版本组合爆炸
flowchart TB subgraph BCC方案["BCC 方案(运行时编译)"] SRC["C 源码"] -->|"嵌入 Python"| BCC["BCC 运行时"] BCC -->|"目标机器上<br/>需要 clang + headers"| OBJ["编译为字节码"] OBJ --> LOAD["加载到内核"] end subgraph CORE方案["CO-RE 方案(一次编译)"] SRC2["C 源码"] -->|"开发机上<br/>clang 预编译"| OBJ2["字节码 + BTF 重定位信息"] OBJ2 -->|"目标机器上<br/>只需 libbpf"| REL["运行时重定位"] REL --> LOAD2["加载到内核"] end style BCC方案 fill:#ffcdd2,stroke:#c62828 style CORE方案 fill:#c8e6c9,stroke:#2e7d32

二、BTF:BPF 类型格式#

2.1 BTF 是什么#

BTF(BPF Type Format)是一种紧凑的类型信息编码格式,类似于 DWARF 调试信息但更轻量:

特性DWARFBTF
大小数十 MB数百 KB
编码方式LEB128 + 复杂结构简洁的 Type Entry
用途通用调试信息eBPF 专用类型信息
内核支持不需要5.2+ 可选,5.15+ 默认启用

2.2 BTF 的内容#

BTF 编码了以下类型信息:

类型BTF 编码示例
整数BTF_KIND_INTint, u32, unsigned long
指针BTF_KIND_PTRint *, void *
数组BTF_KIND_ARRAYchar[16], u32[10]
结构体BTF_KIND_STRUCTstruct task_struct
联合体BTF_KIND_UNIONunion sigval
枚举BTF_KIND_ENUMenum node_states
函数BTF_KIND_FUNCdo_sys_openat2
函数原型BTF_KIND_FUNC_PROTO函数签名
变量BTF_KIND_VAR全局变量
数据段BTF_KIND_DATASEC.bss, .data, .rodata

2.3 生成 BTF#

# 检查内核是否支持 BTF
ls /sys/kernel/btf/vmlinux
# 使用 pahole 从内核二进制生成 BTF
pahole -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.h

2.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;
}
Note

不要手动编辑 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 支持链式访问,可以穿越多级指针:

// 获取当前进程的 PID
u32 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 程序的打开、加载、附加、销毁等操作:

flowchart LR BPF["hello.bpf.c"] -->|"clang 编译"| OBJ["hello.bpf.o"] OBJ -->|"bpftool gen skeleton"| SKEL["hello.skel.h"] SKEL -->|"include"| APP["hello.c<br/>用户态程序"] APP -->|"gcc 编译"| BIN["hello<br/>可执行文件"] style SKEL fill:#c8e6c9,stroke:#2e7d32

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 program
APP = execsnoop
BPF_SRC = $(APP).bpf.c
SKEL_H = $(APP).skel.h
CLANG ?= clang
ARCH := $(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 程序时:

sequenceDiagram participant APP as 用户态程序 participant LIBBPF as libbpf participant BTF_OBJ as eBPF 对象 BTF participant BTF_KERN as 内核 BTF APP->>LIBBPF: bpf_object__open() LIBBPF->>BTF_OBJ: 解析对象文件中的 BTF LIBBPF->>BTF_OBJ: 收集重定位记录 APP->>LIBBPF: bpf_object__load() LIBBPF->>BTF_KERN: 读取内核 BTF loop 每条重定位记录 LIBBPF->>BTF_KERN: 查找目标类型/字段 LIBBPF->>BTF_KERN: 计算实际偏移/大小 LIBBPF->>LIBBPF: 修补字节码中的立即数 end LIBBPF->>APP: 加载完成

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_parent

5.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 最佳实践#

  1. 始终使用 BPF_CORE_READ 访问内核数据结构
  2. 使用 bpf_core_field_exists 检查字段是否存在
  3. 使用 vmlinux.h 而非手动定义内核结构体
  4. 使用 libbpf 骨架 管理程序生命周期
  5. 避免硬编码偏移量 和常量值
  6. 使用 SEC(“license”) 声明 GPL 许可证
Warning

如果目标内核没有启用 BTF(CONFIG_DEBUG_INFO_BTF),CO-RE 程序将无法加载。Ubuntu 22.04+ 和大多数现代发行版默认启用 BTF。对于旧内核,需要手动安装 linux-modules-extra 并使用 pahole 生成 BTF。

七、动手实践#

7.1 生成 vmlinux.h 并查看类型信息#

# 生成 vmlinux.h
bpftool 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 -l

7.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 ./execsnoop

7.3 验证 CO-RE 重定位#

# 查看对象文件中的重定位信息
llvm-readelf -r execsnoop.bpf.o
# 查看 BTF 信息
bpftool btf dump file execsnoop.bpf.o
# 使用 libbpf 调试模式查看重定位过程
export LIBBPF_LOG_LEVEL=4
sudo ./execsnoop 2>&1 | grep -i reloc
flowchart LR SRC["BPF C 源码"] --> CLANG["clang -target bpf<br/>编译为 .o"] --> BTF["内核 BTF<br/>类型信息"] BTF --> RELOC["libbpf CO-RE<br/>重定位修正"] --> LOAD["加载到内核<br/>JIT 编译"] style RELOC fill:#fff9c4,stroke:#f9a825 style LOAD fill:#c8e6c9,stroke:#2e7d32

八、本章小结#

上一章深入探讨了eBPF Hook 点与追踪机制。 本章详解了 CO-RE 的完整技术栈:

主题核心要点关键词
可移植性问题内核数据结构在不同版本间变化,硬编码偏移不可移植可移植性问题
BTF 类型信息紧凑的类型编码,记录内核数据结构的布局BTF 类型信息
vmlinux.h从内核 BTF 自动生成的头文件,包含所有内核类型定义vmlinux.h
BPF_CORE_READ运行时重定位的字段访问宏,自动计算正确偏移BPF_CORE_READ
libbpf 骨架自动生成的管理代码,封装打开/加载/附加/销毁libbpf 骨架
重定位原理编译时记录重定位信息,加载时根据目标内核 BTF 修补字节码重定位原理

CO-RE 是 eBPF 从”实验性技术”走向”生产级方案”的——它让 eBPF 程序像普通应用程序一样,一次编译即可在不同内核版本上运行。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

CO-RE:一次编译到处运行
https://blog.souloss.com/posts/ebpf/co-re/
作者
Souloss
发布于
2026-03-03
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
eBPF Hook 点:kprobe/tracepoint/uprobe
eBPF eBPF 程序的价值在于它能挂载到内核的各种检查点——Hook 点。本章详解三大 Hook 机制——kprobe(动态内核函数追踪)、tracepoint(静态追踪点)、uprobe(用户态函数追踪),以及 USDT 静态用户态追踪点,并通过实战代码展示每种 Hook 的使用方式与适用场景。
2
eBPF 可观测性
eBPF eBPF 最大的应用场景是可观测性——零侵入、低开销、内核级的全链路追踪。本章详解三大可观测性工具链——bpftrace(一行命令追踪内核)、BCC(Python 前端 + 丰富工具集)、Beyla(零侵入应用性能监控),并通过实战展示性能分析、分布式追踪、应用性能监控的完整工作流。
3
TC:流量控制与 eBPF
eBPF TC(Traffic Control)是 Linux 内核的流量控制子系统,通过 cls_bpf 分类器可以在 TC 层挂载 eBPF 程序,实现灵活的数据包分类、修改和重定向。本章详解 TC eBPF 的架构、ingress/egress 双向处理、direct action 模式、sk_buff 操作,以及 TC 与 XDP 的选择策略。
4
eBPF 与 WebAssembly
eBPF eBPF 提供内核可编程能力,WebAssembly 提供跨平台可移植性——两者的融合会带来什么?本章详解 Wasm-eBPF 项目、用户态 eBPF 运行时、eBPF 程序的 Wasm 封装,以及 eBPF + Wasm 在边缘计算、插件系统、跨平台可观测性中的应用前景。
5
eBPF 生产部署
eBPF eBPF 生产部署实践——性能开销分析、调试技巧、版本兼容、内核版本要求与升级策略。