本系列是 x86 架构平台上 trampoline 的实现,从原理和实现上进行了详细的介绍。
类似 freplace,trampoline 是对 prologue 进行 poke 的另一个应用。
TL;DR 类似 freplace,trampoline 是将 prologue 里的第一条 nop 指令替换成 call 指令;call 后可以回去/也可以不回去继续执行原来的函数。
以前了解到 trampoline 的时候,只知道基于 trampoline 的 fentry/fexit 比 kprobe/kretprobe 好;但却不知为什么好。
general trampoline
trampoline 是一个常用的指令操作技术,并不局限于 Linux 系统,同时适用于 Windows、macOS 等系统。
一个基于 jmp 实现的 trampoline 示意图如下:

trampoline 是许多高级工具的底层技术,比如调试器、动态追踪、动态注入等。
trampoline 学习资料:
bpf trampoline on x86
得益于 Linux kernel 为每个可 trace 的函数预留了位于最前方的 5 个字节大小的 nop 指令,bpf trampoline 的实现并没有 general trampoline 的那么复杂;直接省去了 function_gate 部分。
目前,基于 trampoline 技术的 bpf 特性有 fentry、fexit 和 fmod_ret。
在 kernel 里,bpf trampoline on x86 的实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
// ${KERNEL}/arch/x86/net/bpf_jit_comp.c
/* Example:
* ...
*
* The assembly code when eth_type_trans is called from trampoline:
*
* push rbp
* mov rbp, rsp
* sub rsp, 24 // space for skb, dev, return value
* push rbx // temp regs to pass start time
* mov qword ptr [rbp - 24], rdi // save skb pointer to stack
* mov qword ptr [rbp - 16], rsi // save dev pointer to stack
* call __bpf_prog_enter // rcu_read_lock and preempt_disable
* mov rbx, rax // remember start time if bpf stats are enabled
* lea rdi, [rbp - 24] // R1==ctx of bpf prog
* call addr_of_jited_FENTRY_prog // bpf prog can access skb and dev
* movabsq rdi, 64bit_addr_of_struct_bpf_prog // unused if bpf stats are off
* mov rsi, rbx // prog start time
* call __bpf_prog_exit // rcu_read_unlock, preempt_enable and stats math
* mov rdi, qword ptr [rbp - 24] // restore skb pointer from stack
* mov rsi, qword ptr [rbp - 16] // restore dev pointer from stack
* call eth_type_trans+5 // execute body of eth_type_trans
* mov qword ptr [rbp - 8], rax // save return value
* call __bpf_prog_enter // rcu_read_lock and preempt_disable
* mov rbx, rax // remember start time in bpf stats are enabled
* lea rdi, [rbp - 24] // R1==ctx of bpf prog
* call addr_of_jited_FEXIT_prog // bpf prog can access skb, dev, return value
* movabsq rdi, 64bit_addr_of_struct_bpf_prog // unused if bpf stats are off
* mov rsi, rbx // prog start time
* call __bpf_prog_exit // rcu_read_unlock, preempt_enable and stats math
* mov rax, qword ptr [rbp - 8] // restore eth_type_trans's return value
* pop rbx
* leave
* add rsp, 8 // skip eth_type_trans's frame
* ret // return to its caller
*/
int arch_prepare_bpf_trampoline(struct bpf_tramp_image *im, void *image, void *image_end,
const struct btf_func_model *m, u32 flags,
struct bpf_tramp_links *tlinks,
void *func_addr)
{
// ...
}
|
注释里的 example 已说明了 bpf trampoline 的实现原理:
call __bpf_prog_enter。
call 目标函数入口偏移 5 个字节的地址。
call fentry bpf progs。
call 目标函数。
call fmod_ret bpf progs。
call fexit bpf progs。
call __bpf_prog_exit。
- 按需跳过目标函数的执行。
fentry 的实现原理如下:

fexit 的实现原理如下:

上面是 bpf trampoline 指令级别的学习,下面看下 bpf trampoline 是什么时候使用 poke 的。
demo 源代码:fentry_fexit。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
// ${cilium/ebpf}/link/tracing.go
func AttachTracing(opts TracingOptions) (Link, error) {
// ...
return attachBTFID(opts.Program)
}
func attachBTFID(program *ebpf.Program) (Link, error) {
// ...
fd, err := sys.RawTracepointOpen(&sys.RawTracepointOpenAttr{
ProgFd: uint32(program.FD()),
})
// ...
return &tracing{RawLink: RawLink{fd: fd}}, nil
}
// ${cilium/ebpf}/internal/sys/types.go
func RawTracepointOpen(attr *RawTracepointOpenAttr) (*FD, error) {
fd, err := BPF(BPF_RAW_TRACEPOINT_OPEN, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
if err != nil {
return nil, err
}
return NewFD(int(fd))
}
|
由上面的代码片段可知,调用的是 bpf() 系统调用中的 BPF_RAW_TRACEPOINT_OPEN 命令。
下面看下 kernel 里发生了什么:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size) // ${KERNEL}/kernel/bpf/syscall.c
|-->__sys_bpf()
|-->bpf_raw_tracepoint_open()
|-->bpf_raw_tp_link_attach()
|-->bpf_tracing_prog_attach()
|-->bpf_link_init()
|-->tr = prog->aux->dst_trampoline;
|-->bpf_link_prime()
|-->bpf_trampoline_link_prog() // ${KERNEL}/kernel/bpf/trampoline.c
| |-->__bpf_trampoline_link_prog()
| |-->bpf_trampoline_update()
| |-->arch_prepare_bpf_trampoline() // ${KERNEL}/arch/x86/net/bpf_jit_comp.c
| |-->register_fentry()
| |-->void *ip = tr->func.addr;
| |-->bpf_arch_text_poke(ip, BPF_MOD_CALL, NULL, new_addr);
|-->bpf_link_settle()
|
看到 bpf_arch_text_poke() 函数时,便可知道上面代码片段的主要处理逻辑:
- 直接取出 bpf prog 里已准备好的目标
trampoline 对象。
- 调用
arch_prepare_bpf_trampoline() 生成一段 bpf trampoline 程序。
- 调用
bpf_arch_text_poke() 将目标 trampoline 入口的第一条 nop 指令 live patch 成 call 指令,call 到新生成的 trampoline 的入口地址。
总结
只要掌握了 bpf trampoline 的实现原理,便能轻松掌握 fentry、fexit 和 fmod_ret 等特性。
不过,好奇的是:使用 fexit 的时候,为什么在 ret 前增加 add $0x8,%rsp 指令就能跳过执行原来的函数?