用 kprobe 追踪 tcp_rcv_established() ——每条
TCP 包到达都触发一次 int3
断点、进入异常处理、切换到 BPF 程序、再 iret
回来。内核文档
Documentation/bpf/bpf_devel_QA.rst 和社区
benchmark 给出的固定开销量级约为 100–300ns(不含 BPF
程序本身);在高吞吐服务器上,每秒数百万次调用的函数被
kprobe 挂载后,CPU
占用可能显著上升。这不是理论上的”可能有影响”——在生产环境中,不恰当的
kprobe
挂载曾导致丢包和延迟抖动。(以下性能数字均引用该文档及社区测试,非本站实测。)
fentry/fexit(Linux 5.5+)通过 BPF
蹦床(trampoline)机制,把 “中断→上下文切换→BPF→恢复” 替换为
“call→蹦床→BPF→ret”。同一来源给出的
fentry 固定开销量级约为 10–20ns,相对 kprobe
约一个数量级(~10x)。本文将 fentry/fexit 的 “零开销”
完整拆开:bpf_trampoline
的内核数据结构、arch_prepare_bpf_trampoline()
如何生成架构相关的栈帧代码、struct_ops
如何复用蹦床机制、以及什么情况下蹦床的开销并非为零。
一、kprobe 的开销来自哪里
kprobe
的机制决定了它必须走异常处理路径。在函数入口处,kprobe
将目标指令的第一个字节替换为 int3(x86
的断点指令,单字节 0xCC):
/* Linux 6.6: arch/x86/kernel/kprobes/core.c */
/* kprobe 使用 text_poke 将目标地址的首字节替换为 0xCC */
int __kprobes arch_arm_kprobe(struct kprobe *p)
{
text_poke(p->addr, ((unsigned char []){INT3_INSN_OPCODE}), 1);
...
}当 CPU 执行流到达被替换的 int3
时,触发完整的异常处理链路:目标函数入口 ->
int3 陷阱(trap gate)->
do_int3()
(arch/x86/kernel/traps.c) ->
kprobe_int3_handler() ->
pre_handler(BPF kprobe 程序)->
post_handler(可选)-> iret
返回原函数继续。 每一步都有不可忽略的成本:
- trap gate 进入:CPU 压栈 SS/RSP/RFLAGS/CS/RIP(40 字节),切换内核栈,加载 IDT 条目。~20–40 个 CPU 周期。
- 异常处理分发:
do_int3()→kprobe_int3_handler()的函数调用链,涉及多个条件判断和 RCU 锁定。~30–50 个周期。 - BPF
程序执行:
bpf_prog_run()本身的开销(取决于程序复杂度)。~5–500+ 个周期。 - iret 返回:恢复上下文并跳回原指令。~20–40 个周期。
合计:不包含 BPF 程序本身的执行,kprobe 的固定开销约
80–130 个周期(bpf_devel_QA.rst 给出的量级约
100–300ns @
3GHz,非本站实测)。如果挂钩点是每条包路径都要经过的函数(如
__netif_receive_skb_core()),这足够将软中断处理时间撑大一个数量级。
kprobe 的另一个问题是它修改的指令必须保存并模拟执行。对于
PC 相对寻址指令(如 x86 的
jmp、call),kprobe
需要模拟其行为——模拟错误会导致内核崩溃。
二、fentry 的机制:nop sled 替换
fentry 的机制完全不同。它不靠异常路径,而是直接利用内核编译时预留的 nop sled。
2.1 编译期的 nop sled 预留
当内核编译时启用了 CONFIG_FTRACE 和
CONFIG_FUNCTION_TRACER,编译器为每个内核函数生成入口
nop 指令。这通过 GCC/Clang 的
-fpatchable-function-entry=N 参数实现:
/* 编译参数(x86-64):
* -fpatchable-function-entry=5,5
* 效果:在函数入口放置 5 个 nop(每个 1 字节),
* 后面紧跟一条 5 字节 nop(作为原始入口标记)
*/编译后,每个内核函数的开始是这样的(x86-64):
/* 假设内核函数 kfree() 的编译后代码 */
<kfree>:
nop /* fentry trampoline 的 call 目标 */
nop
nop
nop
nop
/* --- 原始函数入口 --- */
endbr64 /* CET 标记 */
push %rbp /* 正常函数体开始 */
mov %rsp,%rbp
...nop 序列替代了传统的
mcount/fentry 调用点。Linux 5.5+ 使用这 5 字节
nop 作为 BPF fentry 的挂载点。
2.2 挂载过程:从 nop 到 call
fentry/fexit 程序类型为
BPF_PROG_TYPE_TRACING,通过
bpf_link(如 BPF_TRACE_FENTRY /
BPF_TRACE_FEXIT)或 libbpf 的
bpf_program__attach_fentry()
挂载。挂载时内核调用
bpf_trampoline_link_prog()(BPF_RAW_TRACEPOINT_OPEN
用于 raw tracepoint,不用于 fentry):
/* Linux 6.6: kernel/bpf/trampoline.c */
int bpf_trampoline_link_prog(struct bpf_tramp_link *link,
struct bpf_trampoline *tr)
{
enum bpf_tramp_prog_type kind;
int err = 0;
...
/* 分配 trampoline 镜像内存 */
if (!tr->cur_image) {
err = bpf_trampoline_update(tr, true);
...
}
...
}bpf_trampoline_update()
是核心。它将生成的蹦床代码通过
arch_prepare_bpf_trampoline() 写入 trampoline
镜像,然后通过 text_poke_bp() 将原始函数的 nop
替换为 call <trampoline>。
text_poke_bp 是 x86
的安全代码修改机制——它用一个三步流程确保多核安全:
- 在目标地址写入一条
int3(一个字节,原子操作),停止其他 CPU 在此执行 - 修改
int3后面的字节(剩余的 4 字节地址) - 将
int3替换为call的首字节(0xE8)
/* Linux 6.6: kernel/bpf/trampoline.c */
static int bpf_trampoline_update(struct bpf_trampoline *tr, bool lock_direct_mutex)
{
void *trampoline = NULL;
int err;
...
/* 生成蹦床代码 */
err = arch_prepare_bpf_trampoline(im, im->image, im->image + PAGE_SIZE,
&tr->func.model, tr->fops,
fentry_cnt, fexit_cnt,
&tr->func.addr);
...
/* 通过 text_poke_bp 替换 nop -> call */
err = bpf_arch_text_poke(tr->func.addr, BPF_MOD_CALL,
NULL, im->image + image_off);
...
}替换完成后,内核函数的执行流变为:
sequenceDiagram
participant K as 内核函数
participant T as 蹦床 (Trampoline)
participant B as BPF 程序
Note over K: 原始函数入口
K->>T: call <trampoline><br/>(原 nop 被替换)
T->>T: 保存 caller-saved 寄存器<br/>(rdi,rsi,rdx,rcx,r8,r9,rax)
T->>T: 分配 BPF 栈帧
T->>T: 构造 ctx (参数结构体)
T->>B: call BPF fentry 程序
B-->>T: return
T->>T: 恢复寄存器
T->>T: 恢复栈帧
T-->>K: ret<br/>返回原始函数体
Note over K: 原始函数体继续执行
与 kprobe 的对比——kprobe 的执行路径:
sequenceDiagram
participant K as 内核函数
participant CPU as CPU 异常处理
participant KP as kprobe handler
participant B as BPF 程序
Note over K: 原始函数入口
K->>CPU: int3 断点触发
CPU->>CPU: 压栈 SS/RSP/RFLAGS/CS/RIP<br/>(40 字节)
CPU->>CPU: 加载 IDT 条目
CPU->>CPU: 切换内核栈
CPU->>KP: do_int3() → kprobe_int3_handler()
KP->>KP: RCU 锁定
KP->>B: call BPF kprobe 程序
B-->>KP: return
KP->>KP: RCU 解锁
KP->>CPU: iret
CPU->>CPU: 恢复上下文
CPU-->>K: 跳回原始指令
Note over K: 原始函数体继续执行
fentry 的执行流用文字表示:原始函数入口 ->
call <trampoline> -> 蹦床保存寄存器
-> call BPF fentry 程序 -> 蹦床恢复寄存器
-> ret -> 原始函数体继续执行。
对于 fexit,蹦床在函数返回前执行。内核在函数的
ret
指令位置生成额外的蹦床代码:原始函数体执行完毕 ->
ret 之前,蹦床在栈帧中插入出口代码 ->
蹦床保存寄存器并捕获返回值 -> call BPF fexit
程序 -> 蹦床恢复寄存器 -> ret
返回原始调用者。
三、bpf_trampoline 数据结构
蹦床的核心数据结构定义在
include/linux/bpf.h:
/* Linux 6.6: include/linux/bpf.h */
struct bpf_trampoline {
struct hlist_node hlist; /* 按目标函数地址 hash */
struct ftrace_ops *fops; /* ftrace ops(共享 ftrace 基础设施)*/
struct mutex mutex;
spinlock_t lock;
refcount_t refcnt;
u32 key; /* hash key(目标函数地址)*/
struct bpf_tramp_image *cur_image;
struct bpf_tramp_links *progs_hlist[BPF_TRAMP_MAX];
/* prog 按类型组织:
* [BPF_TRAMP_FENTRY] 链表
* [BPF_TRAMP_FEXIT] 链表
* [BPF_TRAMP_MODIFY_RETURN] 链表
*/
struct bpf_func_model func;
struct module *mod;
...
};3.1 关键字段解析
progs_hlist:每个蹦床可以挂载多个
BPF 程序,按类型分组。BPF_TRAMP_FENTRY(值为
0)和 BPF_TRAMP_FEXIT(值为
1)分别存储入口和出口程序的链表:
/* Linux 6.6: include/linux/bpf.h */
enum bpf_tramp_prog_type {
BPF_TRAMP_FENTRY,
BPF_TRAMP_FEXIT,
BPF_TRAMP_MODIFY_RETURN,
BPF_TRAMP_MAX,
};bpf_func_model:描述目标函数的签名(参数个数、类型、是否有返回值),蹦床生成器用它来决定保存/恢复哪些寄存器和如何传递参数:
/* Linux 6.6: include/linux/bpf.h */
struct bpf_func_model {
u8 ret_type; /* 返回值类型 */
u8 nr_args; /* 参数个数(最多 6 个) */
u8 arg_size[6]; /* 每个参数是否为 64-bit */
u8 ret_size; /* 返回值大小 */
};bpf_tramp_image:蹦床的可执行代码镜像。每个蹦床有一块独立的内存页,包含生成的
trampoline 指令:
/* Linux 6.6: include/linux/bpf.h */
struct bpf_tramp_image {
void *image; /* 可执行镜像地址 */
struct bpf_ksym ksym; /* kallsyms 项(/proc/kallsyms 可见)*/
struct percpu_ref pcref;
void *ip_after_call; /* 返回地址(fexit 使用)*/
void *ip_epilogue; /* epilogue 地址 */
...
};3.2 蹦床的生命周期
蹦床的生命周期通过以下函数调用链管理:
bpf_trampoline_get(key)获取或创建蹦床(按目标函数 hash):调用bpf_trampoline_lookup(key)在全局 hash 表中查找,不存在则bpf_trampoline_alloc()分配新的bpf_trampolinebpf_trampoline_link_prog(link, tr)将 BPF 程序挂载到蹦床:调用bpf_trampoline_update()生成或重新生成蹦床代码bpf_trampoline_unlink_prog(link, tr)卸载 BPF 程序:调用bpf_trampoline_update()重新生成(移除该程序)bpf_trampoline_put(tr)释放引用,最后一个引用释放时清理
所有活跃的蹦床存储在一个全局 hash 表
trampoline_table 中,以目标函数地址为 key:
/* Linux 6.6: kernel/bpf/trampoline.c */
static struct hlist_head trampoline_table[TRAMPOLINE_TABLE_SIZE];四、arch_prepare_bpf_trampoline():x86-64 栈帧构造
arch_prepare_bpf_trampoline()
是蹦床的最核心实现——它生成实际的 x86
机器码序列。这个函数位于架构相关代码中:
/* Linux 6.6: arch/x86/kernel/bpf_jit.c(其他架构见 arch/*/kernel/bpf_jit.c)*/
int arch_prepare_bpf_trampoline(struct bpf_tramp_image *im, void *image,
void *image_end,
const struct bpf_func_model *model, u32 flags,
struct bpf_tramp_links *tlinks,
void *orig_call)4.1 蹦床的栈帧布局
蹦床在目标函数栈帧的 “上方” 分配自己的栈空间。完整布局从高地址到低地址依次为:
- 调用者的栈帧
- 返回地址(原始
call压栈) - 保存的 callee-saved
寄存器(
rbx、rbp、r12--r15)——蹦床保存 - 保存的 caller-saved
寄存器(
rdi、rsi、rdx、rcx、r8、r9、rax)——蹦床保存(fentry) - BPF 程序栈(512 字节 BPF 运行时栈)——
RSP指向这里
4.2 生成的 x86 指令序列
arch_prepare_bpf_trampoline()
生成的代码大致如下(简化表示):
/* === 蹦床入口(fentry) === */
/* 第 1 步:为蹦床分配栈空间 */
push %rbp
mov %rsp, %rbp
sub $STACK_SIZE, %rsp /* 分配蹦床栈帧 */
/* 第 2 步:保存 caller-saved 寄存器 */
/* 将传入目标函数的参数保存到蹦床栈上 */
mov %rdi, SAVE_RDI(%rsp)
mov %rsi, SAVE_RSI(%rsp)
mov %rdx, SAVE_RDX(%rsp)
mov %rcx, SAVE_RCX(%rsp)
mov %r8, SAVE_R8(%rsp)
mov %r9, SAVE_R9(%rsp)
/* 第 3 步:构造 BPF 程序上下文(ctx)*/
/* RDI = ctx 指针(指向包含参数的结构体) */
lea CTX_SLOT(%rsp), %rdi
/* 第 4 步:调用 BPF fentry 程序 */
call <bpf_prog_fentry_addr>
/* 第 5 步:恢复寄存器 */
mov SAVE_RDI(%rsp), %rdi
mov SAVE_RSI(%rsp), %rsi
mov SAVE_RDX(%rsp), %rdx
mov SAVE_RCX(%rsp), %rcx
mov SAVE_R8(%rsp), %r8
mov SAVE_R9(%rsp), %r9
/* 第 6 步:恢复栈帧并返回 */
leave
ret /* 回到原始函数体 */对于 fexit,蹦床的入口代码更复杂——它需要修改原始函数的返回路径。fexit 蹦床通过修改函数栈帧来插入出口钩子:
/* === 蹦床入口(fexit 部分)=== */
/* 保存原始返回地址 */
mov 8(%rbp), %rax /* 读取原始返回地址 */
mov %rax, SAVED_RET_ADDR(%rsp)
/* 将返回地址替换为 fexit 蹦床的地址 */
lea FEXIT_TRAMPOLINE(%rip), %rax
mov %rax, 8(%rbp) /* 覆盖返回地址 */
/* 继续执行原始函数(函数的 ret 将跳到 FEXIT_TRAMPOLINE)*/
/* === FEXIT_TRAMPOLINE(函数 ret 后到达这里)=== */
/* 第 1 步:保存返回值(在 %rax 中)*/
mov %rax, SAVED_RAX(%rsp)
/* 第 2 步:保存 caller-saved 寄存器 */
mov %rdi, SAVE_RDI(%rsp)
...
/* 第 3 步:构造 ctx(包含返回值和参数)*/
lea CTX_SLOT(%rsp), %rdi
/* 第 4 步:调用 BPF fexit 程序 */
call <bpf_prog_fexit_addr>
/* 第 5 步:恢复寄存器和返回值 */
mov SAVED_RAX(%rsp), %rax
mov SAVE_RDI(%rsp), %rdi
...
/* 第 6 步:跳转到真正的返回地址 */
jmp *SAVED_RET_ADDR(%rsp)4.3 多程序执行
单个蹦床可以挂载多个 BPF 程序——多个 fentry 程序和多个
fexit 程序。arch_prepare_bpf_trampoline()
为每个挂载的程序生成一个单独的 call 指令:
/* 有 3 个 fentry 程序时的生成代码 */
/* 保存寄存器... */
/* 第 1 个 fentry 程序 */
lea CTX_SLOT(%rsp), %rdi
call <bpf_prog_1>
/* 第 2 个 fentry 程序 */
lea CTX_SLOT(%rsp), %rdi
call <bpf_prog_2>
/* 第 3 个 fentry 程序 */
lea CTX_SLOT(%rsp), %rdi
call <bpf_prog_3>
/* 恢复寄存器... */
ret程序按挂载顺序依序执行。每个 fentry 程序可以看到相同的上下文(原始函数参数)。
五、性能特征与开销分析
5.1 fentry vs kprobe 的性能对比
下表对比了 fentry 和 kprobe 在 x86-64
上的单次调用固定开销(不含 BPF 程序本身的执行时间;数据引用
Documentation/bpf/bpf_devel_QA.rst 及社区
benchmark,非本站实测):
| 指标 | kprobe (optimized) | fentry | 倍率 |
|---|---|---|---|
| CPU 周期(固定开销) | ~300–500 | ~30–50 | ~10x |
| 时间(ns,固定开销) | ~100–170ns | ~10–17ns | ~10x |
| 上下文切换 | 异常处理 + iret | 无(直接 call) | N/A |
| 中断禁用影响 | 在中断上下文中可拒绝 probe | 无影响 | N/A |
| 挂载/卸载耗时 | ~μs 级(text_poke 修改 1 字节) | ~μs 级(text_poke 修改 5 字节) | 相近 |
实测数据(引用自内核源码
Documentation/bpf/bpf_devel_QA.rst 讨论和社区
benchmark):
- 追踪空函数(body 为
return):fentry ~4ns, kprobe ~100ns - 追踪
tcp_connect():fentry ~12ns, kprobe ~130ns - 追踪
__schedule():fentry ~15ns, kprobe ~160ns
5.2 开销”并不为零”的情况
蹦床的零开销是相对 kprobe 而言的。以下情况中,fentry 的开销仍然不可忽略:
高频率调用: 如果目标函数每秒被调用 10M 次,每次 15ns 的蹦床开销 = 150ms CPU 时间/秒 = 15% CPU 占用。在这种情况下,即使是 fentry 也可能需要采样或节流。
多程序串行执行: 每个额外的 fentry/fexit
程序都会增加一次 call 指令 +
上下文构造开销(~5–10ns 每个)。挂载 10 个 fentry
程序后,总开销为 ~50–100ns,接近 kprobe 的开销范围。
fexit 的附加开销: fexit 需要修改栈帧中的返回地址,在函数返回时触发蹦床。这涉及两次栈帧操作(saved return address + restore),额外开销约 ~10–15ns。
cache 效应: 蹦床的代码镜像通常不在 CPU L1 指令缓存中。蹦床调用导致 L1-I cache miss 的成本(~20–30 cycles)可能超过蹦床代码本身的执行时间。在频繁调用的热点函数上,蹦床代码通常会被缓存在 L1 中;但刚挂载或长时间未调用的蹦床可能完全从 cache 中掉出。
六、struct_ops 与蹦床的协作
struct_ops
复用蹦床机制来重定向内核函数指针到 BPF 程序。两者共享相同的
arch_prepare_bpf_trampoline() 基础设施。
6.1 struct_ops 的蹦床模型
struct_ops
的目标不是追踪(tracing),而是替换——BPF
程序完全替代原有的内核函数。例如,tcp_congestion_ops->cong_avoid
被一个 BPF 程序替代后,每次拥塞避免逻辑都执行 BPF
程序而不是内核函数。
/* Linux 6.6: kernel/bpf/bpf_struct_ops.c */
static int bpf_struct_ops_link_update(struct bpf_link *link, ...)
{
struct bpf_struct_ops_link *st_link;
...
/* 为每个 struct_ops 成员(函数指针)创建蹦床 */
for (i = 0; i < st_ops->cfi_stubs_size; i++) {
...
err = bpf_trampoline_link_prog(&st_link->tramp_links[i], ...);
}
...
}struct_ops 蹦床的关键差异:
- 入口上下文:struct_ops 蹦床接收的是
原始调用者的参数(如
struct sock *sk),而不是追踪上下文。传递方式是 “透传”——蹦床不构造 ctx 结构体,而是直接映射寄存器。 - 返回值处理:struct_ops 的返回值直接返回给原始调用者,而不是丢弃。调用者依赖返回值来做正确性决策。
- 无 fexit 语义:struct_ops 只替换入口,没有 “调用后钩子”。
6.2 BTF 驱动的类型验证
struct_ops 的蹦床生成依赖 BTF 类型信息来验证函数签名匹配:
/* Linux 6.6: kernel/bpf/bpf_struct_ops.c */
static int bpf_struct_ops_map_update_elem(struct bpf_map *map, void *key,
void *value, u64 flags)
{
...
/* 对每个 struct_ops 成员,走 verifier 验证 BPF 程序是否符合签名 */
for_each_member(i, t) {
...
prog = *(struct bpf_prog **)(udata + moff);
if (prog) {
err = bpf_struct_ops_prepare_trampoline(st_map, prog, ...);
}
}
...
}七、fentry 的可观测性
7.1 查看活跃的蹦床
# 查看所有蹦床(通过 kallsyms)
grep bpf_trampoline /proc/kallsyms | head -20
# bpftool 查看 fentry/fexit 挂载的程序
bpftool prog list | grep -A2 fentry
bpftool prog list | grep -A2 fexit7.2 bpftrace 使用 fentry
# fentry: 在 tcp_connect 入口执行
bpftrace -e 'fentry:tcp_connect { printf("pid=%d\n", pid); }'
# 对比 kprobe 版本
bpftrace -e 'kprobe:tcp_connect { printf("pid=%d\n", pid); }'
# 比较两者的调用次数和开销
bpftrace -e '
fentry:tcp_connect { @fentry[tid] = nsecs; }
kprobe:tcp_connect { @kprobe[tid] = nsecs; }
'7.3 蹦床是否活跃
# 检查某个函数是否有蹦床挂载
bpftool btf dump file /sys/kernel/btf/vmlinux | grep -A5 'FUNC.*tcp_connect'
# 查看蹦床的 JIT 镜像
bpftool prog dump xlated id <BPF_PROG_ID> | head -50八、限制与适用边界
8.1 fentry 的限制
必须依赖 BTF: fentry/fexit 只能挂载到 BTF 描述的函数上。内核中所有启用了 ftrace 的函数都有 BTF 信息,但内核模块中的函数、动态生成的代码、以及某些用汇编编写的函数可能没有 BTF 覆盖。
不能修改参数: 与 kprobe / fmod_ret
程序可通过 bpf_override_return()
修改返回值不同,fentry
程序不能修改传入的寄存器值。bpf_override_return()
用于 kprobe 和
BPF_MODIFY_RETURN(fmod_ret)挂钩点,与 map
操作无关。fentry 的限制来自蹦床在调用 BPF
程序后恢复所有寄存器到原始状态。
不支持函数返回值篡改(一般情况下):
fentry 在函数入口执行,看不到返回值。fexit
可以观察返回值但不能修改它(除非使用
BPF_MODIFY_RETURN,这是 5.8+ 的独立特性,需要
BPF_TRAMP_MODIFY_RETURN 标志)。
8.2 与 kprobe 的可选对比
| 场景 | 推荐 | 原因 |
|---|---|---|
| 高频函数追踪(>100K calls/s) | fentry | 相对 kprobe 约 10x 更低开销(引用
bpf_devel_QA.rst) |
| 需要修改参数 | kprobe | fentry 不支持参数修改 |
| 追踪非 BTF 函数 | kprobe | fentry 需要 BTF |
| 追踪函数返回值 | fexit | 比 kretprobe 开销低 5–10x |
| 追踪内核模块函数 | 取决于是否导出 BTF | 编译时 BTF 决定 |
| perf_event 驱动的采样 | kprobe(perf-based) | fentry 暂不支持 perf-based 触发 |
8.3 内核版本兼容性
| 特性 | 最低内核版本 | 说明 |
|---|---|---|
| fentry/fexit hook | 5.5 | BPF_PROG_TYPE_TRACING |
| BPF_MODIFY_RETURN | 5.8 | 允许 fmod_ret 修改返回值 |
| multi-fentry (单蹦床多程序) | 5.10 | 一个蹦床执行多个 BPF 程序 |
| BTF-based trampoline | 5.5 | 替代 ftrace-based 实现 |
| ARM64 trampoline 支持 | 5.11 | ARM64 架构的蹦床实现 |
九、fentry/fexit 程序示例
9.1 最小 fentry 程序
/* SPDX-License-Identifier: GPL-2.0 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
SEC("fentry/tcp_connect")
int BPF_PROG(tcp_connect_entry, struct sock *sk)
{
/* 可以访问 sk 的所有字段 */
bpf_printk("tcp_connect called\n");
return 0;
}
char LICENSE[] SEC("license") = "GPL";9.2 fexit 捕获返回值
SEC("fexit/tcp_connect")
int BPF_PROG(tcp_connect_exit, struct sock *sk, int ret)
{
/* ret 是 tcp_connect() 的返回值 */
if (ret != 0)
bpf_printk("tcp_connect failed: %d\n", ret);
return 0;
}
char LICENSE[] SEC("license") = "GPL";9.3 加载并挂载
clang -O2 -target bpf -c fentry_example.bpf.c -o fentry_example.bpf.o
bpftool gen skeleton fentry_example.bpf.o > fentry_example.skel.h
# 在 C 程序中使用 skeleton 加载和挂载十、总结
fentry/fexit 的蹦床机制是 eBPF
追踪基础设施的一次重要跃迁。它的核心思想——在目标函数的 nop
sled 上直接放入 call
指令,绕过整个异常处理链路——将追踪的固定开销从 kprobe 的
~100ns 量级降至 fentry 的 ~10ns 量级(引用
bpf_devel_QA.rst,非本站实测)。
理解蹦床的关键是三层抽象:
bpf_trampoline数据结构:管理蹦床的生命周期和程序挂载arch_prepare_bpf_trampoline():生成架构相关的汇编代码——保存/恢复寄存器、分配栈帧、调用 BPF 程序text_poke_bp():安全地将 nop 替换为 call,在多核系统上不中断执行
蹦床并非真正的零开销——在高频调用场景下,15ns 的累积仍然可观。但相对于 kprobe,它是一个数量级的改进,且避免了异常处理路径的不可预测性(中断禁用、IDT 缓存等)。
在 eBPF 系列中,本文与 第 16
篇(BPF 并发模型) 直接相关——蹦床的执行上下文决定了 BPF
程序在什么样的并发环境中运行。与 第 18
篇(sched_ext) 也有交集——sched_ext 通过
struct_ops 复用蹦床机制来将调度回调重定向到
BPF。
参考资料
- Linux 内核源码
kernel/bpf/trampoline.c:蹦床的通用实现 - Linux 内核源码
arch/x86/kernel/bpf_jit.c(及arch/*/kernel/bpf_jit.c):arch_prepare_bpf_trampoline()的架构相关实现 - Linux 内核源码
include/linux/bpf.h:struct bpf_trampoline和相关类型定义 - Linux 内核源码
arch/x86/kernel/kprobes/core.c:kprobe 的 x86 实现(用于对比) - Linux 内核文档
Documentation/bpf/bpf_devel_QA.rst:BPF 开发 Q&A - Linux 内核
tools/testing/selftests/bpf/:fentry/fexit 的自测用例(prog_tests/fentry_test.c)
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【eBPF 内核实现深度拆解】从验证器到 JIT,从 BTF 到调度器
eBPF 内核虚拟机内部实现系统讲解:BPF 指令集与寄存器机器、验证器的抽象解释与状态裁剪、JIT 编译器后端、Map 各类型的并发与内存模型、helper 函数注册与类型检查、BTF 格式规范与 CO-RE 重定位引擎、libbpf 加载器工程、fentry/fexit 蹦床机制、sched_ext 调度器内核接口。面向想读懂 eBPF 内核源码、写生产级 BPF 程序的系统工程师。
【eBPF 内核实现深度拆解】sched_ext 深度:用 BPF 写内核调度器
从 struct sched_ext_ops 的 10+ 回调语义出发,拆解 select_cpu/enqueue/dispatch/tick 等核心回调、scx_bpf_dispatch/scx_bpf_kick_cpu 等 kfunc 的内核实现、ext 调度类与 CFS/EEVDF 的共存策略(SCX_OPS_SWITCH_PARTIAL),以及 scx_layered 和 scx_rustland 的用户态调度器参考实现。
【eBPF 内核实现深度拆解】BPF 指令集解码:寄存器机器、调用约定与指令编码
从 eBPF 虚拟机的 11 个 64-bit 寄存器和 struct bpf_insn 出发,逐条拆解 ALU64/ALU32、跳转、加载存储、call 四类指令的字段语义与编码格式,建立后续 verifier 和 JIT 讨论的精确基础。
【eBPF 内核实现深度拆解】验证器框架:从 BPF_PROG_LOAD 到 do_check()
跟踪 BPF_PROG_LOAD 系统调用的内核执行路径,逐层拆解 bpf_prog_load()→bpf_check()→do_check_main() 的调用链,建立 verifier 执行全景——这是理解 verifier 安全保证的入口。