本系列是 x86 架构平台上 trampoline 的实现,从原理和实现上进行了详细的介绍。


类似 freplacetrampoline 是对 prologue 进行 poke 的另一个应用。

TL;DR 类似 freplacetrampoline 是将 prologue 里的第一条 nop 指令替换成 call 指令;call 后可以回去/也可以不回去继续执行原来的函数。


以前了解到 trampoline 的时候,只知道基于 trampolinefentry/fexitkprobe/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 特性有 fentryfexitfmod_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 的实现原理:

  1. call __bpf_prog_enter
  2. call 目标函数入口偏移 5 个字节的地址。
  3. call fentry bpf progs。
  4. call 目标函数。
  5. call fmod_ret bpf progs。
  6. call fexit bpf progs。
  7. call __bpf_prog_exit
  8. 按需跳过目标函数的执行。

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() 函数时,便可知道上面代码片段的主要处理逻辑:

  1. 直接取出 bpf prog 里已准备好的目标 trampoline 对象。
  2. 调用 arch_prepare_bpf_trampoline() 生成一段 bpf trampoline 程序。
  3. 调用 bpf_arch_text_poke() 将目标 trampoline 入口的第一条 nop 指令 live patch 成 call 指令,call 到新生成的 trampoline 的入口地址。

总结

只要掌握了 bpf trampoline 的实现原理,便能轻松掌握 fentryfexitfmod_ret 等特性。

不过,好奇的是:使用 fexit 的时候,为什么在 ret 前增加 add $0x8,%rsp 指令就能跳过执行原来的函数?