eBPF 开发实战:CO-RE 与 libbpf

eBPF 是 Linux 内核中最具影响力的可观测性与网络编程技术,libbpf 与 CO-RE 让它具备了跨内核版本的可移植性。本文剖析 BTF 类型信息与 CO-RE 重定位原理,详解 libbpf 工程骨架与 skeleton 生成流程,并给出验证器报错排查清单。

eBPF(extended Berkeley Packet Filter)早已不是字面上的"扩展包过滤器"。从 Linux 3.18 起,它演化为一个运行在内核态的通用虚拟机:程序先被验证器做静态分析,再经 JIT 编译成本地机器码,挂载到 kprobe、tracepoint、XDP、cgroup 等钩子上执行。可观测性、网络加速、安全策略、流量整形,几乎所有现代内核子系统的可编程化都建立在这一层之上。

本文从 BPF 指令集与程序类型出发,剖析 BTF 类型信息与 CO-RE(Compile Once - Run Everywhere)重定位原理,详解 libbpf 的工程骨架与 bpftool gen skeleton 生成流程,对比 ring buffer 与 perf buffer 的取舍,梳理 kprobe、tracepoint、fentry、XDP 等挂载点与 SEC() 段名约定,最后给出生产部署要点与验证器报错排查清单。


一、eBPF 基础回顾

1.1 从 cBPF 到 eBPF 的演进

最初的 BPF(后称 cBPF)诞生于 1992 年,用于 tcpdump 在内核态过滤数据包,只有两个 32 位寄存器 A 与 X。2014 年 Linux 3.18 引入 eBPF,寄存器扩展为 11 个 64 位通用寄存器,并配套了验证器、JIT 与 map 机制:

时间点内核版本关键能力
20143.18eBPF 指令集、bpf() 系统调用、JIT
20164.4kprobe 挂载、perf event 输出
20184.18BTF 类型信息,CO-RE 的前提
20195.3有界循环、BPF_PROG_TYPE_TRACING
20205.8ring buffer、CAP_BPF 与 CAP_PERFMON
20215.11map 内存计入 memcg,摆脱 memlock

1.2 指令集与寄存器约定

eBPF 指令定长 8 字节,格式为 opcode:8 | dst_reg:4 | src_reg:4 | off:16 | imm:32。寄存器约定是编写程序时必须牢记的契约:

  • r0:函数返回值,也是程序退出时的返回码
  • r1 ~ r5:函数调用参数,跨调用不保证保留
  • r6 ~ r9:被调用者保存,可跨 helper 调用存活
  • r10:只读栈帧指针,指向 512 字节栈顶,不可写

栈空间上限是 512 字节,这是验证器最常触发的限制之一,任何超过该尺寸的局部数组都会在加载阶段被拒绝。

1.3 程序类型一览

不同挂载点对应不同的程序类型,验证器会据此施加不同的能力约束,包括可调用的 helper 集合、可访问的内存区域与返回值语义:

程序类型典型用途典型挂载点
BPF_PROG_TYPE_KPROBE内核函数探针kprobe/、kretprobe/
BPF_PROG_TYPE_TRACEPOINT静态追踪点tracepoint/
BPF_PROG_TYPE_TRACINGfentry/fexit/tp_btffentry/、fexit/
BPF_PROG_TYPE_XDP网卡驱动层收包XDP hook
BPF_PROG_TYPE_SCHED_CLStc 分类器tc ingress/egress
BPF_PROG_TYPE_CGROUP_SKB容器网络策略cgroup v2
BPF_PROG_TYPE_PERF_EVENT定时采样perf event

1.4 加载流程与 JIT

所有用户态库最终都落到同一个系统调用上,加载失败时 log_buf 中会写入验证器的人类可读日志:

union bpf_attr attr = {
    .prog_type = BPF_PROG_TYPE_KPROBE,
    .insns     = (unsigned long)insns,
    .insn_cnt  = insn_cnt,
    .license   = (unsigned long)"GPL",
    .log_buf   = (unsigned long)log_buf,
    .log_size  = log_size,
    .log_level = 1,
};
int fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr));

加载的完整链路是编译、验证、JIT、挂载。验证器会做控制流图分析、寄存器状态追踪、内存边界检查与有界循环证明;JIT 则把 eBPF 指令翻译为宿主机指令,x86-64 上寄存器几乎一对一映射,执行开销接近原生代码。


二、CO-RE 与 BTF 原理

2.1 为什么需要 CO-RE

早期 BCC 工具链的痛点是运行时编译:程序在目标机器上现场调用 clang 编译,依赖完整的内核头文件包,编译耗时可观,且无法在容器中轻松分发。更根本的问题是内核结构体布局随版本漂移,struct task_struct 的字段偏移在不同内核上并不相同。

CO-RE 的核心思想是编译一次,到处运行。程序在编译期记录下我要访问哪个类型的哪个字段这类意图,加载期由 libbpf 根据目标内核的真实 BTF 信息,把意图重定位成实际的字节偏移。

2.2 BTF 类型信息

BTF(BPF Type Format)是一种紧凑的类型元数据格式,从 Linux 4.18 起内建,描述了内核中所有结构体、联合体、枚举与函数原型的布局信息:

ls -l /sys/kernel/btf/vmlinux                     # 需要 CONFIG_DEBUG_INFO_BTF=y
grep CONFIG_DEBUG_INFO_BTF /boot/config-$(uname -r)
# 若内核未内建 BTF,可从 https://github.com/aquasecurity/btfhub 下载归档补齐

2.3 vmlinux.h 生成

有了 BTF,就能反向生成一份只包含类型声明、不含任何函数体的头文件,用它替代庞大的内核头文件依赖:

# 从运行中内核导出,约 12 万行,仅类型定义,无 include
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 也可从 BTFHub 归档导出,用于离线场景
bpftool btf dump file ./5.15.0-91-generic.btf format c > vmlinux.h

vmlinux.h 顶部包含 #ifndef __VMLINUX_H__ 保护宏,可直接被 .bpf.c 引入,配合 libbpf 的 bpf_helpers.h 使用。

2.4 preserve_access_index 与重定位

CO-RE 的实现依赖两件事:编译期标记与加载期重定位。

/* vmlinux.h 中每个结构体都被标注了 preserve_access_index,
   编译器会为每次字段访问额外生成一条重定位记录 */
struct task_struct {
    int __state;
    pid_t pid;
} __attribute__((preserve_access_index));
pid_t pid = task->pid;   /* 直接访问即可,编译器自动埋点 */

加载时 libbpf 遍历这些记录,按类型名与字段名在当前内核 BTF 中查找真实偏移并改写指令。重定位共有三类:

重定位类型触发来源失败后果
field-based结构体字段访问字段不存在则加载失败
type-basedbpf_core_type_id_* 等宏类型不匹配则加载失败
enum-based枚举值引用枚举项缺失则加载失败

对多级解引用,推荐使用 BPF_CORE_READ 宏,它会自动处理中间指针的逐级读取与重定位:

#include <bpf/bpf_core_read.h>
pid_t ppid = BPF_CORE_READ(task, real_parent, pid);
/* 需要写入局部指针变量时改用 bpf_core_read */
struct task_struct *parent;
bpf_core_read(&parent, sizeof(parent), &task->real_parent);

三、libbpf 工程骨架

3.1 目录结构

libbpf-bootstrap 定义了事实上的标准布局,每个工具由三个核心文件与一个 Makefile 组成:

myprobe/
├── myprobe.bpf.c      # 内核态:BPF 程序
├── myprobe.c          # 用户态:加载器与事件消费
├── myprobe.h          # 共享的事件结构体定义
├── vmlinux.h          # 由 bpftool 生成
├── Makefile
└── .output/
    ├── myprobe.skel.h # 由 bpftool gen skeleton 生成
    └── myprobe.bpf.o

3.2 内核态程序

内核态文件以 SEC() 段名声明挂载点,程序体是一个普通 C 函数:

// myprobe.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "myprobe.h"
char LICENSE[] SEC("license") = "GPL";
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");
SEC("kprobe/do_unlinkat")
int BPF_KPROBE(handle_unlink, int dfd, struct filename *name)
{
    struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e)
        return 0;
    e->pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&e->comm, sizeof(e->comm));
    bpf_probe_read_kernel_str(&e->fname, sizeof(e->fname),
                              BPF_CORE_READ(name, name));
    bpf_ringbuf_submit(e, 0);
    return 0;
}

3.3 用户态加载器

用户态通过 skeleton 暴露的 __open、__load、__attach 三步完成加载,并用 ring_buffer__poll 消费事件:

// myprobe.c
#include <stdio.h>
#include <bpf/libbpf.h>
#include "myprobe.skel.h"
static int on_event(void *ctx, void *data, size_t len)
{
    const struct event *e = data;
    (void)ctx; (void)len;
    printf("%-16s %-8d %s\n", e->comm, e->pid, e->fname);
    return 0;
}
int main(void)
{
    struct myprobe_bpf *skel = myprobe_bpf__open_and_load();
    struct ring_buffer *rb;
    if (!skel)
        return 1;
    if (myprobe_bpf__attach(skel))
        return 1;
    rb = ring_buffer__new(bpf_map__fd(skel->maps.events), on_event, NULL, NULL);
    while (ring_buffer__poll(rb, 100) >= 0)
        ;
    myprobe_bpf__destroy(skel);
    return 0;
}

3.4 skeleton 生成与 Makefile

bpftool gen skeleton 把编译好的 .bpf.o 转成一个自包含的 C 头文件,内嵌字节码与所有 map、程序、全局变量的元数据:

BPF_CFLAGS := -g -O2 -target bpf -D__TARGET_ARCH_x86 -I.output
.output/myprobe.bpf.o: myprobe.bpf.c myprobe.h vmlinux.h
	@mkdir -p .output
	clang $(BPF_CFLAGS) -c $< -o $@
.output/myprobe.skel.h: .output/myprobe.bpf.o
	bpftool gen skeleton $< > $@
myprobe: myprobe.c .output/myprobe.skel.h
	cc -g -O2 -I.output $< -o $@ -lbpf -lelf -lz

关键点:-target bpf 让 clang 输出 BPF 字节码而非本机码;-g 让 BTF 被写入 .bpf.o;skeleton 头文件使运行时无需任何外部文件,天然适合打包进容器镜像。


四、Map 类型与 ring buffer

4.1 常用 Map 类型

Map 是内核态与用户态共享数据的唯一官方通道,选型直接影响性能与语义:

Map 类型语义适用场景
BPF_MAP_TYPE_HASH哈希表,自动扩容通用键值统计
BPF_MAP_TYPE_ARRAY定长数组,索引即键按 CPU 索引的计数器
BPF_MAP_TYPE_PERCPU_HASH每 CPU 一份副本高频写入,避免锁竞争
BPF_MAP_TYPE_LRU_HASH容量满时淘汰最久未用跟踪活跃连接
BPF_MAP_TYPE_RINGBUF无锁环形缓冲事件流输出
BPF_MAP_TYPE_PERF_EVENT_ARRAYperf 事件数组旧式事件输出
BPF_MAP_TYPE_PROG_ARRAY程序跳转表tail call 分发

PERCPU_* 的关键价值在于免锁:每个 CPU 写自己的副本,用户态读取时再求和,适合每秒百万级更新的计数器。

4.2 ring buffer 原理

ring buffer 是内核 5.8 引入的单生产者单消费者共享内存环,内核态通过三步 API 使用:

/* 1. 预留空间:环满时返回 NULL,绝不阻塞 */
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e)
    return 0;
/* 2. 填充数据 */
e->pid = bpf_get_current_pid_tgid() >> 32;
/* 3. 提交使其对用户态可见,也可用 bpf_ringbuf_discard 撤销 */
bpf_ringbuf_submit(e, 0);

用户态消费接口是 ring_buffer__new、ring_buffer__poll 与 ring_buffer__consume,回调在 poll 返回时同步触发。ring buffer 支持多生产者语义,多个 CPU 可同时 reserve,内核用原子操作协调位置。

4.3 ring buffer 与 perf buffer 对比

维度perf bufferring buffer
引入版本4.45.8
内存模型每 CPU 独立缓冲全局共享环
事件顺序跨 CPU 无全局顺序全局有序
内存开销CPU 数乘以缓冲大小单份缓冲
数据拷贝两次一次,走共享内存
通知机制每事件一次唤醒批量唤醒,可延迟
推荐度仅兼容旧内核新项目首选

五、挂载点与 SEC 段名

5.1 kprobe 与 kretprobe

kprobe 可挂到任意内核函数入口,是兼容性最好的探针,但属于不稳定 ABI,函数可能被内联或改名:

SEC("kprobe/do_unlinkat")
int BPF_KPROBE(handle_enter, int dfd, struct filename *name)
{
    return 0;
}
/* 返回探针:用 PT_REGS_RC 宏提取返回值 */
SEC("kretprobe/do_unlinkat")
int BPF_KRETPROBE(handle_exit, int ret)
{
    bpf_printk("do_unlinkat returned %d\n", ret);
    return 0;
}

BPF_KPROBE 宏依赖 -D__TARGET_ARCH_x86 正确传入,否则寄存器到参数的映射会错位,这是参数全是乱码最常见的根因。

5.2 tracepoint 与 tp_btf

tracepoint 是内核开发者显式埋设的稳定接口,格式由 tracefs 定义,字段名固定不漂移。传统 tracepoint 的字段需要从 ctx 手动读取,而 tp_btf 直接使用内核函数原型,参数即字段:

SEC("tracepoint/syscalls/sys_enter_openat")
int handle_openat(struct trace_event_raw_sys_enter *ctx)
{
    const char *fname = (const char *)ctx->args[1];
    bpf_printk("openat: %s\n", fname);
    return 0;
}
SEC("tp_btf/sched_switch")
int BPF_PROG(handle_switch, bool preempt,
             struct task_struct *prev, struct task_struct *next)
{
    bpf_printk("switch %d -> %d\n", prev->pid, next->pid);
    return 0;
}

可在目标机器上枚举可用 tracepoint 与其字段格式:

cat /sys/kernel/tracing/available_events | grep '^syscalls:sys_enter_open'
cat /sys/kernel/tracing/events/sched/sched_switch/format

5.3 fentry 与 fexit

fentry/fexit 基于 BPF trampoline,从 5.5 起稳定,是 BPF 的原生挂载方式:无需修改内核指令、可同时拿到入参与返回值、性能优于 kprobe:

SEC("fentry/do_unlinkat")
int BPF_PROG(fentry_unlink, int dfd, struct filename *name)
{
    bpf_printk("fentry: dfd=%d\n", dfd);
    return 0;
}
/* fexit:最后一个参数是返回值 */
SEC("fexit/do_unlinkat")
int BPF_PROG(fexit_unlink, int dfd, struct filename *name, int ret)
{
    bpf_printk("fexit: ret=%d\n", ret);
    return 0;
}

fentry 要求目标内核导出 BTF,且函数不能是 static 或被完全内联。若加载报 -ENOTSUPP,通常意味着该函数没有 BTF 原型,退回 kprobe 即可。

5.4 XDP 与 tc

XDP 在网卡驱动收包最早阶段执行,返回码决定包的命运:

返回码语义
XDP_PASS交给协议栈正常处理
XDP_DROP立即丢弃
XDP_TX从同一网卡原路发回
XDP_REDIRECT重定向到其他网卡或 CPU
XDP_ABORTED出错路径,等价于丢弃
SEC("xdp")
int xdp_drop_icmp(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;
    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;
    return XDP_PASS;
}

挂载用 ip link set dev eth0 xdp obj prog.o sec xdp,或用 libbpf 的 bpf_xdp_attach。tc 程序段名为 SEC("tc"),返回 TC_ACT_OK 或 TC_ACT_SHOT。


六、用户态与内核态通信

6.1 全局变量与只读配置

libbpf 允许把全局变量直接暴露成 map,用户态写入后内核态立即可见:

const volatile int target_pid = 0;   /* 只读配置,用户态可覆写 */
int counter = 0;                     /* 可写状态,运行时共享 */

用户态通过 skeleton 直接赋值,且必须在 load 之前:

skel = myprobe_bpf__open();
skel->rodata->target_pid = getpid();
myprobe_bpf__load(skel);

.rodata 会被内核映射为只读页,验证器可对其做常量折叠,比 map 查找快得多;.bss 与 .data 则可读写。

6.2 map 查找与更新

内核态操作 map 的 helper 语义需要留意:bpf_map_lookup_elem 返回指向 map 内部的指针,就地修改即生效,无需再 update:

/* counts 为 BPF_MAP_TYPE_HASH,键是 pid,值是计数 */
SEC("kprobe/vfs_read")
int count_reads(void *ctx)
{
    __u32 pid = bpf_get_current_pid_tgid() >> 32;
    __u64 *val = bpf_map_lookup_elem(&counts, &pid);
    if (val) {
        __sync_fetch_and_add(val, 1);   /* 原子自增 */
    } else {
        __u64 one = 1;
        bpf_map_update_elem(&counts, &pid, &one, BPF_NOEXIST);
    }
    return 0;
}

用户态用 bpf_map__fd 拿到 fd 后,可用 bpf_map_lookup_elem、bpf_map_update_elem 与 bpf_map_delete_elem 三个 libbpf 封装操作,也可用 bpf_map_get_next_key 遍历。

6.3 常用 helper

Helper作用注意点
bpf_get_current_pid_tgid返回 pid 与 tgid 打包值需右移 32 位取 pid
bpf_get_current_comm写进程名到缓冲上限 16 字节
bpf_ktime_get_ns单调时钟纳秒适合算差值,不适合绝对时间
bpf_probe_read_kernel_str安全读内核字符串越界返回负值,不崩溃
bpf_probe_read_user_str安全读用户态字符串需先确认指针来自用户
bpf_get_prandom_u32伪随机数用于采样与哈希扰动
bpf_printk写 tracefs 调试输出仅调试用,生产禁用

bpf_printk 的输出在 /sys/kernel/tracing/trace_pipe,格式限定为 3 个参数,且格式化在用户态完成,开销不小。


七、性能采样与生产部署

7.1 采样策略

高频事件全量记录会压垮 ring buffer 并显著抬升开销,标准做法是按比例采样:

/* 1/64 采样:掩码必须是 2 的幂减一 */
if ((bpf_get_prandom_u32() & 63) != 0)
    return 0;
/* 也可只在特定 CPU 上采样,降低缓存争用 */
if (bpf_get_smp_processor_id() != 3)
    return 0;

7.2 bpf_perf_event_output

对于 BPF_PROG_TYPE_PERF_EVENT 类型,例如 CPU 周期采样,输出走 perf 通道而非 ring buffer:

/* perf_events 为 BPF_MAP_TYPE_PERF_EVENT_ARRAY 类型的 map */
SEC("perf_event")
int on_sample(struct bpf_perf_event_data *ctx)
{
    struct sample s = {};
    s.pid = bpf_get_current_pid_tgid() >> 32;
    s.ip  = PT_REGS_IP(&ctx->regs);
    bpf_perf_event_output(ctx, &perf_events, BPF_F_CURRENT_CPU,
                          &s, sizeof(s));
    return 0;
}

用户态用 perf_buffer__new 与 perf_buffer__poll 消费,接口与 ring buffer 类似,但失去了全局有序性。

7.3 CO-RE 跨内核版本

CO-RE 的价值在异构集群中最明显,同一个二进制可覆盖多个内核版本。但仍有边界需要注意:

依赖项最低版本说明
BTF 内建5.2+ 发行版内核需 CONFIG_DEBUG_INFO_BTF=y
fentry/fexit5.5需 BTF 函数原型
ring buffer5.8低于此版本只能用 perf buffer
CAP_BPF5.8更细粒度的权限模型
map 计入 memcg5.11摆脱 RLIMIT_MEMLOCK 限制

对老内核(如 4.19 LTS),可借助 BTFHub 提供的外部 BTF 归档补齐类型信息,但 5.8 以下的 ring buffer 与 fentry 无法回填,需在代码中做运行时探测降级。

7.4 权限与资源限制

生产部署前必须核对的清单:

  • RLIMIT_MEMLOCK:5.11 之前 map 内存受此限制,需 ulimit -l unlimited 或调用 setrlimit,否则加载报 -EPERM
  • CAP_BPF 与 CAP_PERFMON:5.8 起可将 CAP_SYS_ADMIN 拆分为更小的权限集,容器中优先授予这两个
  • kernel.unprivileged_bpf_disabled:多数发行版默认设为 2,非特权用户完全无法加载
  • 容器中的 bpffs 挂载:bpftool 需要 /sys/fs/bpf 可写才能 pin 对象
  • /sys/kernel/btf/vmlinux 可读:缺失则该机器无法运行 CO-RE 程序

八、验证器限制排查

8.1 获取验证器日志

验证器拒绝程序时,唯一有效的信息来源是 log_buf,libbpf 提供了更友好的封装:

#include <bpf/libbpf.h>
static int log_cb(enum libbpf_print_level lvl, const char *fmt, va_list ap)
{
    return lvl == LIBBPF_DEBUG ? 0 : vfprintf(stderr, fmt, ap);
}
/* 注册后任何 open 或 load 失败都会打印完整验证器日志 */
libbpf_set_print(log_cb);

若使用原生 bpf() 系统调用,则显式提供缓冲并设置 log_level 为 1,设为 2 会输出逐指令的寄存器状态,信息量巨大但极慢:

bpftool prog load myprobe.bpf.o /sys/fs/bpf/myprobe \
  type kprobe 2>&1 | head -50

8.2 常见错误码与对策

错误码含义典型对策
-EPERM权限不足或 memlock 超限检查 CAP_BPF 与 ulimit -l
-EACCES验证器拒绝查看 log_buf,定位具体指令
-EINVAL参数非法或 helper 不适用核对 helper 白名单与 attr 字段
-ENOENT挂载目标不存在kprobe 函数名拼写错误或被内联
-ENOTSUPP内核不支持该特性降级到 kprobe 或检查内核版本
-EBUSY同一探针已被占用清理残留的 pin 对象

8.3 验证器常见报错与对策

报错片段根因修复方式
invalid stack off栈使用超 512 字节缩小局部数组或改用 map
back-edge from insn无界循环改成有界循环或用 #pragma unroll
R1 type=scalar expected=fp指针类型丢失用 bpf_probe_read 系列安全读取
A call to built-in function not supported调用了非 inline 函数改用 __always_inline

8.4 排查清单

遇到加载失败时,按以下顺序逐项排查:

  1. 先看 log_buf 最后一行,验证器日志是倒序的,末尾才是根因
  2. 确认 SEC() 段名与程序类型匹配,fentry/ 段配 kprobe 类型必然失败
  3. 检查栈用量,单个局部结构体超过 512 字节是头号杀手
  4. 确认循环有界,上界必须是编译期常量或已被验证
  5. 核对 helper 与程序类型的兼容性,bpf_probe_read_user_str 在 XDP 程序中不可用
  6. 确认 BTF 可用,ls /sys/kernel/btf/vmlinux,缺失则 fentry 与 CO-RE 全部失效
  7. 检查 memlock 与 capability,用 ulimit -l 与 capsh --print 核对,并用 bpftool prog tracelog 区分加载失败与加载成功但不触发

相关阅读

  • https://plumephp.com/os-ebpf-observability/ —— eBPF 在内核可观测性体系中的整体定位与典型工具
  • https://plumephp.com/os-kernel-tracing-ftrace/ —— ftrace 追踪框架,理解 tracepoint 与 kprobe 的内核侧基础
  • https://plumephp.com/os-performance-tools/ —— perf 与系统性能分析工具链,eBPF 工具的搭档

延伸阅读

  1. Linux Kernel Documentation: Documentation/bpf/,含 btf.rst 与 ringbuf 说明
  2. BPF CO-RE 参考文档:Documentation/bpf/llvm_reloc.rst 与 libbpf 仓库 docs/ 目录
  3. libbpf-bootstrap 项目模板:https://github.com/libbpf/libbpf-bootstrap
  4. Brendan Gregg,《BPF Performance Tools》,生产环境 BPF 工具的权威参考
  5. Andrii Nakryiko 的 BPF CO-RE Reference Guide 与 libbpf overview 系列文章
  6. bpf(2) 与 bpf-helpers(7) man page

#!/usr/bin/env bash
# 完整可运行示例:从零构建一个 CO-RE 可移植的 eBPF 工具
# 依赖 clang、libbpf-dev、bpftool;内核要求 >= 5.8
set -euo pipefail
mkdir -p demo && cd demo
# 1. 生成 vmlinux.h,需要内核开启 CONFIG_DEBUG_INFO_BTF=y
[ -r /sys/kernel/btf/vmlinux ] || { echo "内核未导出 BTF,无法使用 CO-RE" >&2; exit 1; }
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 2. 共享事件结构体
echo 'struct event { __u32 pid; char comm[16]; char fname[128]; };' > demo.h
# 3. 内核态程序
cat > demo.bpf.c <<'EOF'
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "demo.h"
char LICENSE[] SEC("license") = "GPL";
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");
SEC("kprobe/do_unlinkat")
int BPF_KPROBE(handle_unlink, int dfd, struct filename *name)
{
    struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e)
        return 0;
    e->pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&e->comm, sizeof(e->comm));
    bpf_probe_read_kernel_str(&e->fname, sizeof(e->fname),
                              BPF_CORE_READ(name, name));
    bpf_ringbuf_submit(e, 0);
    return 0;
}
EOF
# 4. 编译内核态并生成 skeleton
mkdir -p .output
clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -I. -c demo.bpf.c -o .output/demo.bpf.o
bpftool gen skeleton .output/demo.bpf.o > .output/demo.skel.h
# 5. 用户态加载器
cat > demo.c <<'EOF'
#include <stdio.h>
#include <bpf/libbpf.h>
#include "demo.skel.h"
static int on_event(void *c, void *d, size_t l) {
    const struct event *e = d;
    (void)c; (void)l;
    printf("pid=%u comm=%s file=%s\n", e->pid, e->comm, e->fname);
    return 0;
}
int main(void) {
    struct demo_bpf *skel = demo_bpf__open();
    struct ring_buffer *rb;
    if (!skel)
        return 1;
    if (demo_bpf__load(skel) || demo_bpf__attach(skel))
        return 1;
    rb = ring_buffer__new(bpf_map__fd(skel->maps.events), on_event, NULL, NULL);
    while (ring_buffer__poll(rb, 100) >= 0)
        ;
    demo_bpf__destroy(skel);
    return 0;
}
EOF
# 6. 编译并运行,需要 root 或 CAP_BPF 与 CAP_PERFMON
cc -g -O2 -I.output demo.c -o demo -lbpf -lelf -lz
ulimit -l unlimited 2>/dev/null || true
sudo ./demo &
sleep 1; touch /tmp/ebpf-demo && rm -f /tmp/ebpf-demo; sleep 1
sudo pkill -f './demo' || true
echo "=== 完成:上方应输出 unlink 事件 ==="

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「os」更多文章

  1. ARM64 体系结构与内核实现
  2. 内核网络栈:sk_buff、NAPI 与 XDP
  3. 内存回收机制:LRU、水位与 OOM