eBPF CO-RE 与 BTF Portable Tracing 实战:跨内核版本一次编译到处运行
摘要
传统的 eBPF 开发依赖目标机器的内核头文件,导致每次内核升级都需要重新编译。eBPF CO-RE (Compile Once – Run Everywhere) 技术结合内核自带的 BTF (BPF Type Format) 元数据,实现了 eBPF 程序的"编写一次、到处运行"——只需在构建时编译一次,即可在任意支持 BTF 的内核版本上零配置加载执行。本篇文章将深入剖析 BTF 的底层数据布局、CO-RE 的重定位机制、libbpf 的骨架加载流程,并最终构建一个完整的、跨内核版本可运行的进程 tracer。
1. 为什么需要 CO-RE
1.1 传统 eBPF 开发之痛
在 CO-RE 出现之前,编写一个访问内核结构的 eBPF 程序至少需要:
- 目标机器安装完整内核头文件包
- 使用
bpf_probe_read()手动读取每一个字段 - 为每个内核版本字段偏移量的变化编写条件编译
- 每次目标内核升级后重新交叉编译
在云原生环境下,Kubernetes 集群可能运行着数十个不同的内核版本(4.18 / 5.4 / 5.15 / 6.1 / 6.6 ...),为每个版本维护独立的 eBPF 二进制文件是不可承受的运维负担。
1.2 三个核心问题的解法
CO-RE 解决的三个根本性障碍:
- 内核结构定义来源:不需要内核头文件,直接从目标内核的 BTF 信息获取
- 内核结构在不同版本中的差异:libbpf 的字段重定位机制自动处理偏移量、存在性、类型变更
- 内核头文件不可用:依赖 Linux 5.2+ 内核普遍开启的
CONFIG_DEBUG_INFO_BTF=y
2. BPF Type Format (BTF) 底层解析
2.1 BTF 是什么
BTF 是一种紧凑的类型元数据格式,定义了整型、结构体、联合、枚举、函数签名等类型信息。Linux 内核编译时通过 pahole 工具从 DWARF 调试信息转换为 BTF,存储在 /sys/kernel/btf/vmlinux 中。
BTF 的精妙之处在于它只保存类型定义而不保存调试信息、代码位置、变量地址等冗余数据,整个 BTF 段通常只有几 MB:
# ls -lh /sys/kernel/btf/vmlinux
-rw-r--r-- 1 root root 5.5M Oct 10 14:22 /sys/kernel/btf/vmlinux
bpftool btf dump file /sys/kernel/btf/vmlinux format raw | wc -l
82341
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
2.2 BTF 数据布局
BTF 数据由两部分组成:BTF 类型表和字符串表。类型表是一个紧凑的字节序列,每个类型条目通过 struct btf_type 描述,包含以下关键字段:
struct btf_type {
__u32 name_off; // 类型名称在字符串表中的偏移
__u32 info; // 类型种类(vlen/Kind/kind_flag)+ vlen 的组合位字段
union {
__u32 size; // 对 STRUCT/UNION/ENUM 表示结构体大小
__u32 type; // 对 PTR/TYPEDEF/CONST/VOLATILE 表示指向的类型 ID
}
};
通过 bpftool 可以浏览完整的类型图:
# 查看所有任务的 task_struct BTF 信息
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c | grep -A 50 "struct task_struct"
查看特定类型 ID 的详细信息
$ bpftool btf dump id 12345 format raw
2.3 vmlinux.h — 一切的基石
CO-RE 开发的第一步是从目标环境的 vmlinux BTF C 头文件:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
这个文件包含了内核中所有公共和内部类型定义,包括我们需要的 task_struct、mm_struct、file_operations、tcp_sock 等全部内部结构。eBPF 程序可以直接 #include "vmlinux.h",因此不再需要内核头文件——libbpf 会通过 BTF 重定位机制来"知道"该结构体在目标内核中的实际大小和字段偏移量。
2.4 BPF Type Tags 与 BTF_KIND_TYPE_TAG
BTF 还支持 Type Tag(从 Linux 5.16 开始),使得程序能够区分同一结构体类型在不同上下文中的语义差异:
// 标记需要特定访问权限的字段
struct task_struct {
struct thread_info __ptrauth *thread_info;
...
};
CO-RE 扩展还引入了 preserve_access_index 编译属性,使 BPF 后端保留重定位编码信息,让 libbpf 在加载时执行精确的重定位操作。
3. CO-RE 重定位机制深度解析
3.1 重定位的基本原理
CO-RE 的核心是一种编译时记录重定位信息、加载时自动修补的机制。它包含以下步骤:
- 编译阶段:Clang 在 eBPF C 代码中,对每个访问内核结构字段的表达式(如
task->mm),生成一条 BPF 重定位记录,记录"需要跳转到 struct task_struct 类型中 mm 字段偏移量"的语义 - 编码为重定位记录:每条重定位记录编码在 ELF 的
.BTF.ext段中,包含"源类型 ID"和"源字段名" - 加载阶段重定位:libbpf 在加载 ELF 对象时读取目标内核的 BTF,解析各类型的字段偏移量,修补 BPF 指令中的立即数偏移量
3.2 重定位的三种操作
CO-RE 在加载时对 BPF 指令执行三类修补操作:
类型重定位 (Type Relocation):处理字节大小差异,确保 BPF 程序不会读取超出目标内核结构范围的字节:
// 访问 task_struct.comm 字段 (TASK_COMM_LEN=16)
bpf_probe_read_kernel(&task.comm, TASK_COMM_LEN, &task->comm);
// → 如果目标内核 TASK_COMM_LEN 变成 32,auto-zero 高 16 字节
字段重定位 (Field Relocation):处理字段偏移量的变化:
// task->mm (偏移量从 v5.10 的 1096 变为 v5.15 的 1032)
// CO-RE 自动修补为正确的偏移量
存在性重定位 (Existence Relocation):处理字段在内核中被移除或重命名的情况,支持可选记录字段提供默认值:
// BPF_CORE_READ_INTO — 宏展开后若字段不存在则设置默认值
BPF_CORE_READ_INTO(&pid, task, tgid);
3.3 BPF_CORE_READ 宏家族解析
CO-RE 提供了一套按访问模式分层的宏家族,每个宏都会生成正确的重定位记录:
// 1. 直接取值 (返回值或NULL/0)
struct mm_struct *mm = BPF_CORE_READ(task, mm);
// 2. 带错误码的读取
BPF_CORE_READ_RET_INTO(&mm_ptr, task, mm);
// 3. 内联读取到变量
BPF_CORE_READ_INTO(&pid, task, tgid);
// 4. 读取用户空间指针
BPF_CORE_READ_USER_INTO(&pid, task, tgid);
// 5. 全局/静态字段绝对地址读取 (用于常量偏移量)
unsigned long addr = BPF_CORE_READ(task, mm, total_vm);
// 6. 字段的位字段读取
unsigned int flags = BPF_CORE_READ_BITFIELD(task, flags);
这些宏通过 Clang 编译时生成的 __builtin_preserve_access_index 属性携带重定位信息。
3.4 Type Compatibility 机制
CO-RE 通过 BTF 的 Type Index 匹配实现了结构体之间的"type compatibility"验证:
// 允许结构体 B 作为结构体 A 的参考——只要字段名和类型在 B 中都在 A 中存在
struct my_task_struct {
int pid;
unsigned long rss;
};
// CO-RE 编译到 my_task_struct 的 BPF 代码可以通过重定位来读取 target task_struct
// 只要这些字段名在两个类型中都存在且字节宽度兼容
这让我们可以只声明我们关心的子集字段,而无需关心目标 ev_struct 的完整性偏移量。
4. 实战:构建一个可移植进程追踪器
我们将构建一个完整的 CO-RE 追踪器,它能够在任意 BTF-enabled 内核上记录进程的 fork、exec、exit 事件,包括进程名、父进程信息、PID、运行时间等,编译一次就可以在内部数千台机器上运行。
4.1 项目结构
trace-process/
├── Makefile # 使用 clang + libbpf 编译
├── vmlinux.h # 从 /sys/kernel/btf/vmlinux 生成
├── process_tracer.bpf.c # BPF 内核程序
├── process_tracer.h # 用户空间/BPF 共享头文件
└── process_tracer.c # 用户空间加载器
4.2 BPF 核心程序 (process_tracer.bpf.c)
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "process_tracer.h"
/* BPF Maps: 用于内核到用户空间的数据传递 */
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(".maps");
/* 可选: 用于过滤跟踪特定进程组 ID */
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10240);
__type(key, u32);
__type(value, u32);
} filter_pids SEC(".maps");
char LICENSE[] SEC("license") = "GPL";
/**
- 通用的事件采集例程 - 利用 CO-RE 从 task_struct 安全读取字段
*
- 通过 BPF_CORE_READ_INTO 宏自动执行重定位:
- 1. 如果在编译时 vmlinux.h 中的 task_struct 有
start_time 字段
- 2. 目标内核的 BTF 中 task_struct 也有该字段
- 3. libbpf 自动修补偏移量和字节数
*
- 如果目标内核中某个字段不存在,宏将优雅地设置默认值为 0
*/
static __always_inline int
__attribute__((always_inline))
collect_task_info(struct event *e, struct task_struct *task)
{
/* CO-RE: 即使 RHS 中的字段偏移量在不同内核版本中不一样,重定位会自动修补 */
BPF_CORE_READ_INTO(&e->pid, task, tgid);
BPF_CORE_READ_INTO(&e->ppid, task, real_parent, tgid);
/* 使用 BPF_CORE_READ_STR_INTO 读取字符串字段 */
BPF_CORE_READ_STR_INTO(&e->comm, task, comm);
/* 读取内存和调度信息 */
BPF_CORE_READ_INTO(&e->start_time_boot, task, start_boottime);
/* 通过 BPF_PROBE_READ_USER 替代直接指针解引用 — 防止 RCU 失效 */
const struct cred *cred = BPF_CORE_READ(task, cred);
if (cred)
BPF_CORE_READ_INTO(&e->uid, cred, uid.val);
return 0;
}
/**
- Tracepoint: 进程 fork
- 跟踪 sched_process_fork — 也捕获 vfork/clone
*/
SEC("tp/sched/sched_process_fork")
int tracepoint__sched_process_fork(struct trace_event_raw_sched_process_fork *ctx)
{
struct event e = {};
struct task_struct *parent = (struct task_struct *)bpf_get_current_task();
struct task_struct *child;
if (!parent)
return 0;
child = (struct task_struct *)ctx->child_pid;
e.type = EVENT_FORK;
collect_task_info(&e, child);
/* perf buffer 提交到用户空间 — 单次上下文切换 */
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
&e, sizeof(e));
return 0;
}
/**
- Tracepoint: 进程 exec
- 跟踪 sched_process_exec
*/
SEC("tp/sched/sched_process_exec")
int tracepoint__sched_process_exec(struct trace_event_raw_sched_process_exec *ctx)
{
struct event e = {};
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
e.type = EVENT_EXEC;
e.exec_ret = ctx->ret;
collect_task_info(&e, task);
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
&e, sizeof(e));
return 0;
}
/**
- Tracepoint: 进程退出
- 跟踪 sched_process_exit
*/
SEC("tp/sched/sched_process_exit")
int tracepoint__sched_process_exit(struct trace_event_raw_sched_process_exit *ctx)
{
struct event e = {};
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
e.type = EVENT_EXIT;
/* 注意: exit_code 在 sched_process_exit 中的数据 */
e.exit_code = BPF_CORE_READ(task, exit_code) >> 8;
collect_task_info(&e, task);
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
&e, sizeof(e));
return 0;
}
/**
- Tracepoint: 高精度进程创建
- 跟踪 task_newtask — 捕获所有 fork/clone/vfork 事件(包括线程)
*/
SEC("tp/task/task_newtask")
int tracepoint__task_newtask(struct trace_event_raw_task_newtask *ctx)
{
struct event e = {};
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
struct task_struct *new_task = (struct task_struct *)ctx->pid;
u32 pid = bpf_get_current_pid_tgid() >> 32;
/* PID 过滤 — 可选 */
if (bpf_map_lookup_elem(&filter_pids, &pid))
return 0;
e.type = EVENT_NEWTASK;
collect_task_info(&e, new_task);
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
&e, sizeof(e));
return 0;
}
4.3 共享头文件 (process_tracer.h)
#pragma once
#define TASK_COMM_LEN 16
#define MAX_FILENAME_LEN 256
/* 截 EVENT_TYPE 枚举 */
enum event_type {
EVENT_FORK = 1,
EVENT_EXEC = 2,
EVENT_EXIT = 3,
EVENT_NEWTASK = 4,
};
/* 内核-用户空间共享的事件结构 — 两边必须一致 */
struct event {
u32 type; // 事件类型 (fork/exec/exit)
u32 pid; // TGID (进程级 PID)
u32 ppid; // 父进程 PID
u32 uid; // Effective UID
u64 timestamp; // ktime 或 bpf_ktime_get_ns()
u64 start_time_boot; // 启动时间(用于 delta 计算)
s32 exec_ret; // exec 的返回码或 0
u32 exit_code; // 退出码
char comm[TASK_COMM_LEN]; // 进程名
char parent_comm[TASK_COMM_LEN]; // 父进程名
/* 填充字段以达到 8 字节对齐要求 — perf buffer 提交需要 8 字节对齐 */
u32 _pad;
};
4.4 用户空间加载器 (process_tracer.c)
#include <stdio.h>#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <linux/perf_event.h>
#include <linux/hw_breakpoint.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "process_tracer.skel.h" /* 由 bpftool gen skeleton 自动生成 */
#include "process_tracer.h"
static volatile sig_atomic_t exiting = 0;
static void sig_handler(int sig)
{
exiting = 1;
}
/**
- Libbpf 的 trampoline 级别调试回调
*
- CO-RE 加载期间的错误信息会通过这里输出,提示你为什么被拒绝加载
- 常见错误:
- - "field 'xxx' not found in target kernel" → 字段在当前内核不存在
- - "failed to find BTF info" → 目标内核未启用 BTF,需升级内核
*/
static int libbpf_print_fn(enum libbpf_print_level level,
const char *format, va_list args)
{
if (level == LIBBPF_DEBUG)
return 0;
return vfprintf(stderr, format, args);
}
static void handle_event(void *ctx, int cpu, void *data, __u32 data_sz)
{
struct event *e = data;
u64 ts = e->timestamp ?: bpf_ktime_get_ns();
switch (e->type) {
case EVENT_FORK:
printf("%-12llu FORK pid=%-8u ppid=%-8u uid=%-6u comm=%-16s parent=%-16s\n",
ts, e->pid, e->ppid, e->uid, e->comm, e->parent_comm);
break;
case EVENT_EXEC:
printf("%-12llu EXEC pid=%-8u ret=%-3d uid=%-6u comm=%-16s\n",
ts, e->pid, e->exec_ret, e->uid, e->comm);
break;
case EVENT_EXIT:
printf("%-12llu EXIT pid=%-8u code=%-3u uid=%-6u comm=%-16s\n",
ts, e->pid, e->exit_code, e->uid, e->comm);
break;
default:
break;
}
}
static void handle_lost_events(void *ctx, int cpu, __u64 lost_cnt)
{
fprintf(stderr, "Lost %llu events on CPU %d (buffer overrun)\n",
lost_cnt, cpu);
}
int main(int argc, char **argv)
{
struct process_tracer_bpf *skel;
struct perf_buffer *pb = NULL;
int err;
/* 设置 libbpf 调试输出 — 查看 CO-RE 重定位细节 */
libbpf_set_print(libbpf_print_fn);
/* 优雅关闭信号 */
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
/* 打开 BPF 对象 — 触发 CO-RE 重定位机制 */
skel = process_tracer_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
err = -ENOMEM;
goto cleanup;
}
/* --------
- CO-RE 重定位在这里自动发生
- libbpf 读取 /sys/kernel/btf/vmlinux 中的实际偏移量
- 修补 BPF 指令中的立即数值
*
- 如果目标内核中某些字段不存在,你会在控制台中看到:
- "libbpf: CO-RE: field 'xxx' not found in target kernel"
- 并且相关指令将被改写为 MOV_IMM(0) 或跳过
- -------- */
err = process_tracer_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton: %d\n", err);
goto cleanup;
}
/* 附加 tp 程序到 syscalls */
err = process_tracer_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton: %d\n", err);
goto cleanup;
}
/* 设置 perf buffer */
pb = perf_buffer__new(bpf_map__fd(skel->maps.events), 128,
handle_event, handle_lost_events, NULL, NULL);
if (!pb) {
err = -errno;
fprintf(stderr, "Failed to create perf buffer: %s\n", strerror(errno));
goto cleanup;
}
printf("Successfully started! Press Ctrl+C to exit.\n");
while (!exiting) {
err = perf_buffer__poll(pb, 100 /* timeout_ms */);
if (err < 0 && err != -EINTR) {
fprintf(stderr, "Error polling perf buffer: %s\n", strerror(-err));
goto cleanup;
}
}
cleanup:
perf_buffer__free(pb);
process_tracer_bpf__destroy(skel);
return err != 0;
}
4.5 Makefile (使用 libbpf 骨架工作流)
# Makefile for CO-RE eBPF process tracer
ARCH ?= $(shell uname -m | sed 's/x86_64/x86/' | sed 's/aarch64/arm64/')
CLANG ?= clang
LLVM_STRIP ?= llvm-strip
BPFTOOL ?= bpftool
CC ?= gcc
libbpf 安装路径 — 通过 apt install libbpf-dev 或编译安装
LIBBPF_SRC := $(abspath ../libbpf/src)
LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a)
BPF_CFLAGS := -target bpf -D__TARGET_ARCH_$(ARCH) -I$(OUTPUT) \
-I../libbpf/include/uapi -g -O2 -Wall
CONFIGURE: 用户空间的 C 编译
APP_CFLAGS := -g -O2 -Wall
APP_LDFLAGS := $(LIBBPF_OBJ) -lelf -lz
OUTPUT := .output
APPS := process_tracer
.PHONY: all clean
all: $(APPS)
Step 1: 编译 BPF 内核代码
$(OUTPUT)/%.bpf.o: %.bpf.c $(wildcard %.h) vmlinux.h
$(CLANG) $(BPF_CFLAGS) -c $< -o $@
$(LLVM_STRIP) -g $@ # 移除 DWARF 以减小体积但保留 BTF
Step 2: 生成 libbpf 骨架头文件
$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o
$(BPFTOOL) gen skeleton $< > $@
Step 3: 编译用户空间加载器
$(OUTPUT)/%.o: %.c $(OUTPUT)/%.skel.h
$(CC) $(APP_CFLAGS) -c $< -o $@
Step 4: 链接生成可执行文件
$(OUTPUT)/%: $(OUTPUT)/%.o libbpf
$(CC) $^ $(APP_LDFLAGS) -o $@
libbpf 构建
$(LIBBPF_OBJ):
$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \
OBJDIR=$(dir $@) libbpf.a
.PHONY: vmlinux
vmlinux: $(OUTPUT)/vmlinux.h
$(OUTPUT)/vmlinux.h:
$(BPFTOOL) btf dump file /sys/kernel/btf/vmlinux format c > $@
clean:
rm -rf $(OUTPUT) $(APPS)
5. 进阶技巧:处理真正的内核差异
5.1 BPF_KPROBE 与 CO-RE 联合使用动态函数探测
Tracepoint 是稳定的 ABI,但 kprobe 允许跟踪任意函数。CO-RE + kprobe 的组合让我们在五的内核版本上探测相同的函数:
SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(trace_tcp_sendmsg, struct sock *sk, struct msghdr *msg, size_t size)
{
// 直接获得函数签名的参数,无需解析寄存器
// 自动适应不同 ABI 的参数传递约定
u16 family = BPF_CORE_READ(sk, __sk_common.skc_family);
if (family == AF_INET) {
// IPv4 处理
BPF_CORE_READ_INTO(&e->daddr, sk, __sk_common.skc_daddr);
BPF_CORE_READ_INTO(&e->saddr, sk, __sk_common.skc_rcv_saddr);
} else if (family == AF_INET6) {
// IPv6 处理
BPF_CORE_READ_INTO(&e->daddr_v6, sk, __sk_common.skc_v6_daddr.in6_u.u6_addr32);
}
return 0;
}
BPF_KPROBE 宏自动提供当前内核中函数签名的形参,并通过 PT_REGS_PARAM 宏从寄存器中正确提取。
5.2 处理内核字段重命名与移除
假设目标内核中 task_struct 的以下字段在某些版本中不存在或重命名:
/* 在 Linux 5.14 之前,UID 通过 taskcred_* 宏访问,之后变为 task_struct->cred
- 且 cred 结构体本身在 5.8 前后有字段布局变化 */
#if __has_include("vmlinux.h")
/* CO-RE 推荐方式:使用条件读取 */
struct event {
u32 pid;
u64 start_time;
};
/* 在 newer 内核中 */
BPF_CORE_READ_INTO(&event.start_time, task, start_boottime);
/* 退化方案:使用 bpf_core_field_exists 检查字段 */
if (bpf_core_field_exists(task_struct, start_boottime)) {
BPF_CORE_READ_INTO(&e->start_time, task, start_boottime);
} else {
// 退化到旧的 start_time (使用 INVALID_HZ 但功能相同)
BPF_CORE_READ_INTO(&e->start_time, task, start_time);
}
#endif
/* 使用 bpf_core_enum_value_exists 处理枚举变更 */
int val = bpf_core_enum_value(task_states, __TASK_STOPPED);
5.3 BPF Ring Buffer vs Perf Buffer 的选择
CO-RE 中两种主要的内核→用户空间数据传输方式对比:
| 特征 | Perf Buffer | Ring Buffer (Linux 5.8+) |
|---|---|---|
| 最大项数 | N CPU × PAGE_SIZE | 固定字节(通常 256KB–8MB) |
| 并发语义 | 按 CPU 非重叠环形缓冲区 | 全局单一环形缓冲区 |
| 丢失通知 | 有 | 有(但语义更清晰) |
| 延迟 | 略高(perf_event_open 开销) | 更低(纯 MPSC 队列) |
| API 复杂度 | perf_buffer__poll | ring_buffer__poll |
| 丢失事件上报 | 全局丢失计数 | 全局丢失计数 + 实时通知 |
对于大多数追踪场景,我们推荐 Ring Buffer,因为它具有以下优势:
- 全局一致性的事件顺序(跨 CPU)
- 无需按 CPU 轮询
- 更高的吞吐量和更低的延迟
- 与 BPF timers 配合更好的超时语义
5.4 文件描述符 CO-RE 追踪 (kprobe + fentry)
通过跟踪 do_sys_openat2 来捕捉文件打开事件:
// CO-RE 处理 open/openat 不同版本差异
SEC("fentry/do_sys_openat2")
int BPF_PROG(trace_do_sys_openat2, int dfd, struct filename *name,
struct open_flags *how, umode_t mode)
{
// BPF_PROG 不依赖 struct pt_regs — 用 fentry 直接获取函数参数
const char *fname = BPF_CORE_READ(name, name);
// bpf_probe_read_str 安全读取用户空间指针
char buf[256];
long ret = bpf_probe_read_user_str(buf, sizeof(buf), fname);
if (ret > 0) {
// 过滤 uid
u32 uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;
if (uid != 0) { // 只追踪 root 操作
struct event e = {};
e.type = EVENT_OPEN;
e.uid = uid;
bpf_probe_read_kernel_str(e.open.path, sizeof(e.open.path), buf);
bpf_ringbuf_submit(&e, 0);
}
}
return 0;
}
这里的 fentry 替代 kprobe 是因为 fentry 完全消除了函数入口的额外 tracepoint 开销,并且对热路径性能损耗极小。
6. 内核验证器交互与优化
6.1 CO-RE 重定位与验证器的协同
BPF 验证器在加载时执行静态分析,确保程序的安全性。CO-RE 重定位在验证之前完成,这意味着:
- 验证器看到的是已经修补完偏移量的 BPF 指令
- 验证器会检查修补后的访问是否违反安全边界
- 如果某些字段因差异导致越界访问,验证器会拒绝加载(此时 libbpf 会 output 清晰的错误信息)
6.2 BPF 循环与 CO-RE 的协同
传统 BPF 禁止循环(防止无限执行),但 Linux 5.3+ 引入了有界循环支持。结合 CO-RE 可以编写复杂的数据处理逻辑:
// 遍历进程的每个 VMA 查找匹配的文件
struct vm_area_struct *vma = BPF_CORE_READ(task, mm, mmap);
struct vm_area_struct *cur = vma;
struct file *vm_file = NULL;
for (int i = 0; i < 64; i++) {
if (!cur)
break;
BPF_CORE_READ_INTO(&vm_file, cur, vm_file);
if (vm_file) {
// 读取文件路径,匹配目标文件路径
struct path f_path = BPF_CORE_READ(vm_file, f_path);
...
}
BPF_CORE_READ_INTO(&cur, cur, vm_next);
}
6.3 BPF 尾调用与 MAP 转移式状态机
CO-RE 处理重定位时,尾调用段定位表的偏移量是固定的(编译时确定),因此尾调用在不同内核版本中保持稳定。这让我们可以构建事件过滤状态机:
// BPF 尾调用跳转表
struct {
__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
__uint(max_entries, 8);
__type(key, u32);
__type(value, u32);
} progs SEC(".maps");
// 在 tp 中通过 tail call 进入下一个处理阶段
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
u32 key = EXEC_FILTER_STAGE;
bpf_tail_call(ctx, &progs, key);
return 0;
}
7. 性能调优与生产部署
7.1 关闭不必要的 libbpf 调试输出
生产环境中重定位信息不应该出现在日志中,通过修改静态链接或运行时设置抑制:
// 生产模式静默输出
libbpf_set_print(NULL); // 完全静默
// 或只保留错误级别
static int silent_print(enum libbpf_print_level level,
const char *format, va_list args)
{
if (level == LIBBPF_WARN)
return vfprintf(stderr, format, args);
return 0;
}
7.2 CO-RE BPF 程序大小优化
通过 BPF skeleton 直接嵌入二进制对象,避免磁盘 I/O 和维护多个文件:
// 编译时内联 BPF 二进制
$ bpftool gen object process_tracer.bpf.o process_tracer.bpf.o
或直接 GCC 链接: gcc ... process_tracer.bpf.o -lelf -lz
// 在骨架流程中直接引用
skel = process_tracer_bpf__open_and_load(); // 一次性完成
7.3 LSM 安全集成
CO-RE 可以与 Linux Security Modules 集成,实现安全策略强制执行:
// LSM hook 使用 BPF_PROG 宏 — 自动处理返回值和函数签名
SEC("lsm/file_receive")
int BPF_PROG(file_receive, struct file *file)
{
// 通过 CO-RE 读取文件的 inode UID
u32 uid = BPF_CORE_READ(file, f_inode, i_uid.val);
// 安全策略检查逻辑...
return 0;
}
7.4 Systemd 服务集成
作为系统级追踪器,通过 systemd 管理:
[Unit]
Description=eBPF Process Tracer (CO-RE)
After=network.target
ConditionKernelCommandLine=|btf
[Service]
Type=simple
ExecStart=/usr/local/bin/process_tracer
Restart=on-failure
RestartSec=5
MemoryMax=32M
CPUQuota=5%
PrivateTmp=true
ProcSubset=pid
ProtectHome=true
ProtectSystem=strict
NoNewPrivileges=true
AmbientCapabilities=CAP_BPF CAP_PERFMON CAP_NET_ADMIN CAP_SYS_ADMIN
[Install]
WantedBy=multi-user.target
8. 跨内核测试策略
8.1 如何在不接触物理机器的情况下测试跨版本兼容性
编写 __s32 ver 的条件宏:
// 在 BPF 程序中通过 bpf_core_kernel_version 判断
#if __has_builtin(__builtin_preserve_type_info)
if (bpf_core_kernel_version < KERNEL_VERSION(5, 15, 0)) {
// 使用旧字段名
BPF_CORE_READ_INTO(&pid, task, tgid);
} else {
// 使用新字段名
BPF_CORE_READ_INTO(&pid, task, thread_pid);
}
#endif
更优雅的方式 — 使用 libbpf 升级宏:
// vmlinux.h 可以按内核版本生成不同的 BPFTOOL_TAG 进行预定义
// 让我们可以使用 bpf_core_field_exists、 bpf_core_field_size 等宏
if (bpf_core_field_exists(task_struct, thread_pid)) {
// 5.15+ 内核,用 thread_pid 替代 tgid
BPF_CORE_READ_INTO(&pid, task, thread_pid);
} else if (bpf_core_field_exists(task_struct, tgid)) {
BPF_CORE_READ_INTO(&pod, task, tgid);
}
8.2 实际跨版本测试 — QEMU + 自定义 vm 方案
# 创建包含不同内核版本的 virtiofs/9pfs 共享目录,通过 CI 测试:
- linux-5.4 (Alpine 3.15/16)
- linux-5.10 (Ubuntu 22.04)
- linux-5.15 (Debian 12)
- linux-6.1 (Ubuntu 24.04)
- linux-6.6 (Fedora 39)
复用同一份 eBPF 二进制在所有 VM 上加载测试
$ ./process_tracer && echo "PASS on $(uname -r)"
8.3 Deployer 模式:一次构建、多次部署
CO-RE 二进制具有全局唯一性,因此在 CI/CD 流程中:
- 在 build 容器(任意内核版本,只要 > 5.2 且支持 BTF)中编译
- 生成
.bpf.o和.skel.h,嵌入最终二进制 - 分发同一份二进制文件到所有异构机器
- 每台机器上的 libbpf 加载时独立完成重定位和验证
9. 完整可运行代码清单与示例输出
9.1 运行输出示例
$ sudo ./process_tracer
Successfully started! Press Ctrl+C to exit.
1234567890123 FORK pid=4521 ppid=4510 uid=1000 comm=bash parent=sshd
1234567890124 EXEC pid=4521 ret=0 uid=1000 comm=bash
1234567890456 FORK pid=4522 ppid=4521 uid=1000 comm=nginx parent=bash
1234567890789 EXEC pid=4522 ret=0 uid=1000 comm=nginx
1234567891234 EXIT pid=4521 code=0 uid=1000 comm=bash
1234567891567 FORK pid=4523 ppid=1 uid=0 comm=kworker/u4:2 parent=systemd
...
[Press Ctrl+C]
$ # 退出时总结
$ sudo ./process_tracer --stats
Processed: 147830 events
Lost events: 12 (0.008%) — triggered by short-lived processes during ringbuffer saturation
Runtime: 142.3s
Avg rate: 1041.7 events/sec
9.2 使用 BPF 尾调用构建分级过滤器
一个优雅的方案是:主事件路由 BPF 程序使用尾调用进入分级过滤器,每个过滤器 BPF 程序只针对一种事件类型,实现热点分离和代码模块化:
// progs map:
// KEY 0 → 处理 FORK 事件
// KEY 1 → 处理 EXEC 事件
// KEY 2 → 处理 EXIT 事件
// KEY 3 → 处理所有日志事件
SEC("tp/sched/sched_process_fork")
int handle_fork(struct trace_event_raw_sched_process_fork *ctx)
{
u32 key = 0;
bpf_tail_call(ctx, &progs, key);
return 0;
}
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
u32 key = 1;
bpf_tail_call(ctx, &progs, key);
return 0;
}
10. 总结与未来方向
CO-RE 和 BTF 是近年来 eBPF 生态中最关键的基础设施改进之一。它们带来的核心价值包括:
- 一次构建到处运行:消除内核头文件和交叉编译依赖
- 类型安全保证:加载时验证目标内核的重定位合法性
- 抽象层简化:隐藏内核内部结构变更的复杂度
- 性能无损:编译时编码重定位,运行时零成本的补丁修补
CO-RE 目前已经成为了业界事实标准,几乎所有主流 eBPF 项目都已迁移:
- BCC:提供向 libbpf/CO-RE 迁移的编译时推荐
- bpftrace:通过
--info输出提示用户升级到 CO-RE - Falco:已实现完整的 CO-RE 支持,内核模块不再是必须
- Pixie:通过 CO-RE 在 GKE/EKS 等云 Kubernetes 上无缝部署
- Tetragon:Cilium 的 eBPF 安全观测引擎,100% 依赖 CO-RE
Kernel 的未来发展方向也在进一步优化 CO-RE 的体验:
- BPF Type Format 的扩展:支持更多类型种类(如位字段、异常追踪、多维数组)
- 强化的重定位缓存:对频繁加载相同 eBPF 程序时加速重定位
- 用户空间 CO-RE (USCORE):目标应用进程与 UPROBE 追踪的结构体访问也可享受类似机制
- BPF CO-RE uprobe 追踪的跨进程 ABI 兼容性保证
推荐资源
- BPF CO-RE Reference Guide — Andrii Nakryiko 的权威指南
- libbpf bootstrap — 官方推荐的 CO-RE 项目模板
- docs.ebpf.io — eBPF 技术文档集大成者
- eBPF 相关 man pages — BPF 系统调用和辅助函数的权威参考
- libbpf-rs — Rust 绑定支持 CO-RE 完整开发

发表评论 取消回复