Table of contents
Open Table of contents
1. 编写程序
使用 libbpf + skeleton + vmlinux.h 的开发方式。
1.1 编译流程
工具包:
aarch64.libbpf
bpftool
llvm
pkg-config
必须的库:
hello
├── libbpf.so.1
│ └── libelf.so.1
│ ├── libz.so.1
│ ├── libzstd.so.1
│ ├── liblzma.so.5
│ └── libbz2.so.1
└── libc.so.6
生成 vmlinux.h:
bpftool btf dump \
file build/kernel/vmlinux \
format c > src/vmlinux.h
编写 hello.bpf.c:
// SPDX-License-Identifier: GPL-2.0
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
struct sys_enter_ctx {
__u64 common_fields;
long id;
};
SEC("tracepoint/raw_syscalls/sys_enter")
int hello_sys_enter(struct sys_enter_ctx *ctx)
{
__u32 pid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("syscall entered: pid=%u nr=%ld", pid, ctx->id);
return 0;
}
char LICENSE[] SEC("license") = "GPL";
编译:
clang \
-g -O2 -Wall -Werror \
-target bpf \
-D__TARGET_ARCH_arm64 \
-c hello.bpf.c \
-o hello.bpf.o
生成 hello.skel.h:
bpftool gen skeleton \
build/ebpf/hello.bpf.o \
> build/ebpf/hello.skel.h
编写 hello.c:
// SPDX-License-Identifier: GPL-2.0
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <bpf/libbpf.h>
#include "hello.skel.h"
static int libbpf_log(enum libbpf_print_level level, const char *format,
va_list args)
{
if (level == LIBBPF_DEBUG)
return 0;
return vfprintf(stderr, format, args);
}
int main(void)
{
struct hello_bpf *skel;
int err;
libbpf_set_print(libbpf_log);
skel = hello_bpf__open();
if (!skel) {
fprintf(stderr, "failed to open BPF skeleton\n");
return 1;
}
err = hello_bpf__load(skel);
if (err) {
fprintf(stderr, "failed to load BPF program: %s\n",
strerror(-err));
goto out;
}
err = hello_bpf__attach(skel);
if (err) {
fprintf(stderr, "failed to attach BPF program: %s\n",
strerror(-err));
goto out;
}
/* fork(2) triggers the raw sys_enter tracepoint while BPF is attached. */
pid_t child = fork();
if (child < 0) {
perror("fork");
err = -1;
goto out;
}
if (child == 0)
_exit(0);
if (waitpid(child, NULL, 0) < 0) {
perror("waitpid");
err = -1;
goto out;
}
printf("Triggered syscalls while creating child pid %ld.\n", (long)child);
printf("Read the eBPF message with:\n"
" cat /sys/kernel/tracing/trace\n");
out:
hello_bpf__destroy(skel);
return err != 0;
}
编译:
aarch64-unknown-linux-gnu-gcc \
-g -O2 \
-Ibuild/ebpf \
-I"$ARM64_LIBBPF_INCLUDE" \
src/hello.c \
-L"$ARM64_LIBBPF_LIB" \
-lbpf \
-o build/ebpf/hello
运行 hello 程序,hello_sys_enter 就会被挂载到 tracepoint/raw_syscalls/sys_enter 上了,而 bpf_printk 会输出到 tracefs 的 ring buffer 中,程序退出后该回调就被移除:
~ # hello
Triggered syscalls while creating child pid 64.
Read the eBPF message with:
cat /sys/kernel/tracing/trace
~ # cat /sys/kernel/tracing/trace
# tracer: nop
#
# entries-in-buffer/entries-written: 16/16 #P:4
#
# _-----=> irqs-off/BH-disabled
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / _-=> migrate-disable
# |||| / delay
# TASK-PID CPU# ||||| TIMESTAMP FUNCTION
# | | | ||||| | |
hello-62 [001] ...31 2.469110: bpf_trace_printk: syscall entered: pid=62 nr=29
hello-62 [001] ...31 2.470493: bpf_trace_printk: syscall entered: pid=62 nr=135
hello-62 [001] ...31 2.470731: bpf_trace_printk: syscall entered: pid=62 nr=220
hello-62 [001] ...31 2.472214: bpf_trace_printk: syscall entered: pid=62 nr=135
hello-62 [001] ...31 2.472370: bpf_trace_printk: syscall entered: pid=62 nr=260
hello-64 [002] ...31 2.472374: bpf_trace_printk: syscall entered: pid=64 nr=99
hello-64 [002] ...31 2.472623: bpf_trace_printk: syscall entered: pid=64 nr=135
hello-64 [002] ...31 2.472966: bpf_trace_printk: syscall entered: pid=64 nr=94
hello-62 [001] ...31 2.475065: bpf_trace_printk: syscall entered: pid=62 nr=80
hello-62 [001] ...31 2.475126: bpf_trace_printk: syscall entered: pid=62 nr=29
hello-62 [001] ...31 2.475639: bpf_trace_printk: syscall entered: pid=62 nr=64
hello-62 [001] ...31 2.476340: bpf_trace_printk: syscall entered: pid=62 nr=64
hello-62 [001] ...31 2.476572: bpf_trace_printk: syscall entered: pid=62 nr=64
hello-62 [001] ...31 2.476859: bpf_trace_printk: syscall entered: pid=62 nr=29
hello-62 [001] ...31 2.477417: bpf_trace_printk: syscall entered: pid=62 nr=57
hello-62 [001] ...31 2.477437: bpf_trace_printk: syscall entered: pid=62 nr=57
1.2 声明挂载点
首先要使用 SEC 宏将主函数声明为一个单独的段,段名就是挂载点:
SEC("tracepoint/raw_syscalls/sys_enter")
int hello_sys_enter(struct sys_enter_ctx *ctx)
{
__u32 pid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("syscall entered: pid=%u nr=%ld", pid, ctx->id);
return 0;
}
There are 13 section headers, starting at offset 0x618:
节头:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .strtab STRTAB 0000000000000000 000573 0000a1 00 0 0 1
[ 2] .text PROGBITS 0000000000000000 000040 000000 00 AX 0 0 4
[ 3] tracepoint/raw_syscalls/sys_enter PROGBITS 0000000000000000 000040 000058 00 AX 0 0 8
[ 4] .reltracepoint/raw_syscalls/sys_enter REL 0000000000000000 0004d0 000010 10 I 12 3 8
[ 5] .rodata PROGBITS 0000000000000000 000098 00001f 00 A 0 0 1
[ 6] license PROGBITS 0000000000000000 0000b7 000004 00 WA 0 0 1
[ 7] .BTF PROGBITS 0000000000000000 0000bc 0002e4 00 0 0 4
[ 8] .rel.BTF REL 0000000000000000 0004e0 000020 10 I 12 7 8
[ 9] .BTF.ext PROGBITS 0000000000000000 0003a0 0000a0 00 0 0 4
[10] .rel.BTF.ext REL 0000000000000000 000500 000070 10 I 12 9 8
[11] .llvm_addrsig LLVM_ADDRSIG 0000000000000000 000570 000003 00 E 0 0 1
[12] .symtab SYMTAB 0000000000000000 000440 000090 18 1 4 8
1.3 helper / kfunc
由于 BPF 程序不能直接调用内核函数,所以其需要靠内核提供的 helper / kfunc 来访问内核资源。
BPF helper 是由 Linux 内核实现并提供给 eBPF 程序调用的一系列函数。它们的 UAPI 编号和相关定义位于 include/uapi/linux/bpf.h,而供 BPF C 程序使用的函数原型由 libbpf 的 bpf_helper_defs.h提供,并通过 #include <bpf/bpf_helpers.h> 间接引入。一个 BPF 程序不能调用所有 helper:内核 verifier 会根据 BPF 程序类型、预期挂载类型、许可证以及内核配置等条件限制可调用的 helper 集合。
BPF kfunc 是由内核通过 BTF 暴露并显式注册、供 eBPF 程序调用的内核函数。与使用固定 BPF_FUNC_* 编号的 helper 不同,kfunc 依靠函数名和 BTF 类型信息进行解析,BPF 程序通常通过带有 __ksym 标记的 extern 声明调用。内核 verifier 会根据程序类型、参数类型和 KF_ACQUIRE、KF_RELEASE、KF_RET_NULL 等属性检查调用是否合法以及对象生命周期是否正确。并非所有内核函数都是 kfunc,只有明确注册给相应 BPF 程序类型的函数才能调用,而且其兼容性通常不如 helper 稳定。
例如:
extern struct task_struct *bpf_task_from_pid(s32 pid) __ksym;
extern void bpf_task_release(struct task_struct *task) __ksym;
1.4 Map
BPF map 就是 BPF 程序和用户态、以及多个 BPF 程序之间共享数据的容器。
BPF 侧定义:
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u64);
} syscall_count SEC(".maps");
用户态访问:
map_fd = bpf_map__fd(skel->maps.syscall_count);
if (bpf_map_lookup_elem(map_fd, &key, &count) == 0)
printf("syscall count: %llu\n",
(unsigned long long)count);
else
perror("bpf_map_lookup_elem");
常见类型:
-
BPF_MAP_TYPE_HASH:哈希表 -
BPF_MAP_TYPE_ARRAY:定长数组 -
BPF_MAP_TYPE_PERCPU_HASH/ARRAY:每 CPU 一份数据 -
BPF_MAP_TYPE_RINGBUF:BPF → 用户态传事件 -
BPF_MAP_TYPE_PERF_EVENT_ARRAY:老一些的事件输出方式 -
BPF_MAP_TYPE_LRU_HASH:带 LRU 淘汰 -
BPF_MAP_TYPE_PROG_ARRAY:用于 tail call
2. 加载流程
整体流程:
eBPF 源码
│
│ clang -target bpf
▼
┌─────────────────┐
│ BPF ELF (.o) │
│ │
│ .text / prog │
│ .maps │
│ .BTF │
│ .BTF.ext │
│ license │
└────────┬────────┘
│
│ libbpf / bpftool / skel
▼
┌──────────────────────┐
│ 用户态加载器 │
│ │
│ 1. 解析 ELF / BTF │
│ 2. 创建 BPF Map │
│ 3. CO-RE 重定位 │
│ 4. 处理 Map / kfunc等 │
└──────────┬───────────┘
│
│ bpf(BPF_MAP_CREATE)
│ bpf(BPF_PROG_LOAD)
▼
┌──────────────────────────────────────┐
│ Linux 内核 │
│ │
│ ┌──────────────────────────┐ │
│ │ BPF Verifier │ │
│ │ │ │
│ │ 指令合法性 │ │
│ │ CFG / 路径分析 │ │
│ │ 寄存器类型追踪 │ │
│ │ 内存边界检查 │ │
│ │ Map / Helper / kfunc检查 │ │
│ └────────────┬─────────────┘ │
│ │ │
│ 校验通过 │
│ ▼ │
│ ┌────────────────────┐ │
│ │ BPF JIT Compiler │ │
│ │ BPF → 本机指令 │ │
│ └──────────┬─────────┘ │
│ │ │
│ ▼ │
│ 已加载 BPF Program │
└──────────────────┬───────────────────┘
│
│ attach
│ bpf_link / perf_event
│ netlink / ioctl ...
▼
┌─────────────────────┐
│ Hook Point │
│ │
│ tracepoint │
│ kprobe / uprobe │
│ fentry / fexit │
│ XDP / TC │
│ cgroup │
│ socket │
└──────────┬──────────┘
│
事件发生时触发
▼
┌─────────────────────┐
│ 执行 BPF Program │
│ │
│ 访问 BPF Map │
│ 调用 Helper │
│ 调用 kfunc │
│ ringbuf 输出数据 │
└─────────────────────┘
2.1 用户态加载器
先来看下 skel 中生成的 hello_bpf 结构体,用户态程序首先会调用 hello_bpf__open 来将其填充:
struct hello_bpf {
/* 指向其他各个字段以及字节码数据 */
struct bpf_object_skeleton *skeleton;
/* 整个bpf obj */
struct bpf_object *obj;
/* map */
struct {
struct bpf_map *rodata;
} maps;
/* bpf 程序入口 */
struct {
struct bpf_program *hello_sys_enter;
} progs;
struct {
struct bpf_link *hello_sys_enter;
} links;
};
然后调用 hello_bpf__load,其会先调用到 libbpf 中的 bpf_object_prepare 对解析的 BPF 程序进行处理,核心工作有两步,首先会根据 .BTF 与 .BTF.ext 两个程序段对程序进行重定向,核心目标就是使得程序能够适配目标内核,并且将所有函数都拼接到主函数里:
bpf_object__relocate
│
┌───────────────┴───────────────┐
▼ │
CO-RE relocation │
类型/字段等目标内核适配 │
▼
call relocation
│
├─ 拼接 subprog
└─ 修正 call offset
│
▼
data relocation
│
├─ Map
├─ .rodata
├─ .data/.bss
└─ global var
│
▼
BTF.ext fixup
随后通过系统调用 bpf(BPF_MAP_CREATE) 在内核态创建 map 并返回 Map FD,通过系统调用 bpf(BPF_PROG_LOAD) 把一段已经完成重定位的 eBPF 指令提交给内核,经过 Verifier 检查后创建一个内核中的 bpf_prog 对象,返回 Program FD。
最后,用户态加载器调用 hello_bpf__attach,根据程序的 section 类型,将 Program 挂载到对应的内核 Hook 上。
2.2 附加到内核
libbpf 中会复用 perf events 来挂载 tp,这里会构造一个 erf_event_attr ,通过系统调用 perf_event_open 注册一个 CPU 域的 perf_event:
memset(&attr, 0, attr_sz);
attr.type = PERF_TYPE_TRACEPOINT;
attr.size = attr_sz;
attr.config = tp_id;
pfd = syscall(__NR_perf_event_open, &attr, -1 /* pid */, 0 /* cpu */,
-1 /* group_fd */, PERF_FLAG_FD_CLOEXEC);
其主要会做三件事:
- 分配并初始化
perf_event,根据tp_id查找已有的trace_event_call,通过tracepoint_probe_register建立底层订阅; - 将构造好的
perf_event挂载到perf_cpu_context->ctx上,用于管理; - 将构造好的
perf_event挂载到trace_event_call->perf_events[0]上,用于分发;
接下来会将 BPF 的字节码与 perf_event 建立联系,如果内核支持 FEAT_PERF_LINK (>=5.15),那么就会使用 bpf(BPF_LINK_CREATE),否则使用 ioctl(PERF_EVENT_IOC_SET_BPF)。
这里只讲一下前者的具体实现:
- 创建 link:返回给用户态一个 link fd,用于管理关联的生命周期;
struct bpf_perf_link
├─ link.prog ─────► struct bpf_prog
└─ perf_file ────► struct file
└─ private_data ─► struct perf_event
- 建立关联:让事件发生能执行 BPF 程序,将
bpf_prog挂载到perf_event->prog以及trace_event_call->prog_array[i];
struct trace_event_call
├─ perf_events
| └─ [0] -> perf_event
| └─ prog -> bpf_prog
└─ prog_array
└─ [0].prog -> bpf_prog
3. 执行 BPF
当用户态发起任意系统调用时,内核会首先检查线程标志 SYSCALL_TRACEPOINT,判断系统调用是否被 trace,如果是则进入调用 trace_sys_enter 交由 tracepoint 进行处理;
tracepoint 会调用 tracepoint->funcs 中注册的 perf 回调,来到 perf_trace_sys_enter,其会根据 trace_event_call 中的prog_array 找到 bpf_prog,然后开始执行 BPF 程序。一般情况下 BPF 程序返回 0,那么后续用于 perf 计数的 perf_tp_event 就不会被调用;
下面按本例的 ARM64 正常入口路径绘制,省略 ptrace/seccomp 提前退出等分支:
用户态发起系统调用:svc #0
|
v
ARM64 系统调用入口
|
v
syscall_trace_enter()
|
v
当前线程有 SYSCALL_TRACEPOINT 标志?
| 否 | 是
| v
| trace_sys_enter(regs, syscallno)
| |
| v
| 底层 tracepoint 分发已注册回调
| |
| | funcs 中的 perf 回调
| | data = trace_event_call *
| v
| perf_trace_sys_enter(data, regs, id)
| |
| v
| do_perf_trace_sys_enter()
| 构造事件记录 raw_data
| 填充 id、args[]
| |
| v
| perf_trace_run_bpf_submit()
| |
| v
| call->prog_array 中有 BPF?
| | 否 | 是
| | v
| | trace_call_bpf()
| | |
| | v
| | bpf_prog_run_array()
| | |
| | v
| | 执行 hello_sys_enter(ctx)
| | bpf_printk(...)
| | return 0
| | |
| | v
| | 汇总返回值为 0?
| | | 是 | 否
| | | v
| | | 当前 CPU 的
| | | perf_events 为空?
| | | | 是 | 否
| | | | |
| v | | |
| perf_tp_event()<----------------+
| | | |
| | | |
| 分发 perf 事件 | |
| 计数/按配置采样| |
| | | |
| +----------+------+
| |
| v
| 追踪路径返回
| |
+---------------------+
|
v
invoke_syscall()
|
v
执行真正的系统调用处理函数
4. 挂载点总结
4.1 借助 perf event
| 挂载点 | 回调注册位置 | 触发后如何执行 BPF | 使用场景 |
|---|---|---|---|
| 普通 tracepoint | TP.funcs[].func = perf_trace_<事件类名> | perf_trace_* → perf_trace_run_bpf_submit() → trace_call_bpf() → call->prog_array | 系统调用、调度、块 I/O 等已有事件;直接读取格式化字段 |
| kprobe | kprobe.pre_handler = kprobe_dispatcher | dispatcher → kprobe_perf_func() → trace_call_bpf() → call->prog_array | 追踪没有现成 tracepoint 的内核函数,或合法的函数内偏移 |
| kretprobe | kretprobe.handler = kretprobe_dispatcher | dispatcher → kretprobe_perf_func() → trace_call_bpf() → call->prog_array | 获取内核函数返回值;结合入口探针统计耗时 |
| uprobe | consumer.handler = uprobe_dispatcher | dispatcher → uprobe_perf_func() → __uprobe_perf_func() → call->prog_array | 观察用户程序、动态库函数参数或指定指令位置 |
| uretprobe | consumer.ret_handler = uretprobe_dispatcher | dispatcher → uretprobe_perf_func() → __uprobe_perf_func() → call->prog_array | 获取用户函数返回值、统计调用耗时 |
perf_event 性能采样 | 无独立回调赋值;设置 event->prog | __perf_event_overflow() → bpf_overflow_handler() → bpf_prog_run() | CPU 热点、调用栈采样;按时钟或硬件计数器周期分析性能 |
4.2 不借助 perf event
| 挂载点 | 回调注册位置 | 触发后如何执行 BPF | 使用场景 |
|---|---|---|---|
| raw tracepoint | TP.funcs[].func = __bpf_trace_<事件类名> | __bpf_trace_*() → bpf_trace_runN() → 执行 raw TP link 中的程序 | 读取 tracepoint 原始参数,省去部分事件记录构造开销 |
tp_btf | TP.funcs[].func = __bpf_trace_<事件类名> | 同 raw tracepoint 路径,参数类型由 BTF 校验 | 需要带类型信息的原始参数,访问内核对象 |
kprobe.multi | fprobe.entry_handler = kprobe_multi_link_handler | handler → kprobe_multi_link_prog_run() → bpf_prog_run() | 批量追踪多个内核函数入口,不支持任意函数内偏移 |
kretprobe.multi | fprobe.exit_handler = kprobe_multi_link_exit_handler | handler → kprobe_multi_link_prog_run() → bpf_prog_run() | 批量观察内核函数返回、统计多函数耗时 |
kprobe.session | fprobe.entry_handler = kprobe_multi_link_handler fprobe.exit_handler = kprobe_multi_link_exit_handler | 入口/返回 handler → kprobe_multi_link_prog_run() → 同一个 BPF 程序 | 配对追踪内核函数入口与返回,利用 session cookie 保存单次调用状态、统计耗时 |
uprobe.multi | consumer.handler = uprobe_multi_link_handler | handler → uprobe_prog_run() → bpf_prog_run() | 用一个 link 管理同一用户态文件中的多个探测位置 |
uretprobe.multi | consumer.ret_handler = uprobe_multi_link_ret_handler | handler → uprobe_prog_run() → bpf_prog_run() | 批量追踪同一用户态文件中的函数返回 |
uprobe.session | consumer.handler = uprobe_multi_link_handler consumer.ret_handler = uprobe_multi_link_ret_handler | 入口/返回 handler → uprobe_prog_run() → 同一个 BPF 程序 | 配对追踪用户函数入口与返回,保存单次调用状态、统计耗时 |
fentry | 目标函数入口插桩 → BPF trampoline | trampoline 在原函数入口执行 BPF | 目标支持 BTF/插桩时,低开销读取函数入参 |
fexit | 目标函数入口插桩 → BPF trampoline | trampoline 调用原函数,并在其返回后执行 BPF | 同时观察函数入参和返回值,分析执行结果 |
iter/task | seq_operations.show = task_seq_show | 读取 iterator FD → show 路径 → bpf_iter_run_prog() → bpf_prog_run() | 主动遍历任务等内核对象,按需输出状态 |
注:
- USDT:用于应用预埋的语义事件,如请求开始、GC 阶段;底层复用传统 uprobe 或 uprobe multi。
kprobe的挂载点不一定位于函数入口,所以参数寄存器可能在回调执行前就被覆盖;而kprobe.multi的挂载点可能位于多个不同的内核函数;所以二者都不能根据 BTF 提前整理函数参数。