freplace 技术便是对 prologue 进行 poke 的简单应用。

TL;DR 究竟有多简单呢?就是将 prologue 里的第一条 nop 指令替换成 jmp 指令;jmp 后不再回来继续执行原来的函数,是谓 freplace

freplace 例子

demo 先行。源代码链接:ebpf-freplace

 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
45
46
47
48
49
50
51
52
53
// tcp-connecting.c

__noinline int
stub_handler()
{
    bpf_printk("freplace, stub handler\n");

    return 0;
}

typedef struct event {
    __be32 saddr, daddr;
    __be16 sport, dport;
} __attribute__((packed)) event_t;

struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
} events SEC(".maps");

static __noinline
void handle_new_connection(struct pt_regs *ctx, struct sock *sk)
{
    event_t ev = {};

    ev.saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
    ev.daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
    ev.sport = BPF_CORE_READ(sk, __sk_common.skc_num);
    ev.dport = bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport));

    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &ev, sizeof(ev));
}

SEC("kprobe/tcp_connect")
int k_tcp_connect(struct pt_regs *ctx)
{
    struct sock *sk;
    sk = (typeof(sk))PT_REGS_PARM1(ctx);

    handle_new_connection(ctx, sk);

    return stub_handler();
}

SEC("kprobe/inet_csk_complete_hashdance")
int k_icsk_complete_hashdance(struct pt_regs *ctx)
{
    struct sock *sk;
    sk = (typeof(sk))PT_REGS_PARM2(ctx);

    handle_new_connection(ctx, sk);

    return stub_handler();
}

没有进行 freplace 时:

1
2
# cat /sys/kernel/debug/tracing/trace_pipe
            curl-5875    [002] dN.31  3444.754757: bpf_trace_printk: freplace, stub handler
1
2
3
4
5
6
7
8
9
// freplace.c

SEC("freplace/stub_handler")
int freplace_handler()
{
    bpf_printk("freplace, replaced handler\n");

    return 0;
}

进行 freplace 后:

1
2
# cat /sys/kernel/debug/tracing/trace_pipe
          <idle>-0       [001] d.s51  2714.385269: bpf_trace_printk: freplace, replaced handler

freplace 实现原理

先看看 Go 代码是怎么调用 bpf() 系统调用的。

 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
45
46
47
48
49
50
51
52
53
54
55
56
// ${cilium/ebpf}/link/tracing.go

func AttachFreplace(targetProg *ebpf.Program, name string, prog *ebpf.Program) (Link, error) {
    // ...

    link, err := AttachRawLink(RawLinkOptions{
        Target:  target,
        Program: prog,
        Attach:  ebpf.AttachNone,
        BTF:     typeID,
    })
    // ...

    return &tracing{*link}, nil
}

// ${cilium/ebpf}/link/link.go

func AttachRawLink(opts RawLinkOptions) (*RawLink, error) {
    if err := haveBPFLink(); err != nil {
        return nil, err
    }

    if opts.Target < 0 {
        return nil, fmt.Errorf("invalid target: %s", sys.ErrClosedFd)
    }

    progFd := opts.Program.FD()
    if progFd < 0 {
        return nil, fmt.Errorf("invalid program: %s", sys.ErrClosedFd)
    }

    attr := sys.LinkCreateAttr{
        TargetFd:    uint32(opts.Target),
        ProgFd:      uint32(progFd),
        AttachType:  sys.AttachType(opts.Attach),
        TargetBtfId: uint32(opts.BTF),
        Flags:       opts.Flags,
    }
    fd, err := sys.LinkCreate(&attr)
    if err != nil {
        return nil, fmt.Errorf("create link: %w", err)
    }

    return &RawLink{fd, ""}, nil
}

// ${cilium/ebpf}/internal/sys/types.go

func LinkCreate(attr *LinkCreateAttr) (*FD, error) {
    fd, err := BPF(BPF_LINK_CREATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
    if err != nil {
        return nil, err
    }
    return NewFD(int(fd))
}

由上面的代码片段可知,调用的是 bpf() 系统调用中的 BPF_LINK_CREATE 命令。

 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()
    |-->link_create()
        |-->bpf_tracing_prog_attach()
            |-->struct bpf_attach_target_info tgt_info = {};
            |-->bpf_check_attach_target(NULL, prog, tgt_prog, btf_id, &tgt_info);   // ${KERNEL}/kernel/bpf/verifier.c
            |   |-->addr = (long) tgt_prog->bpf_func;
            |   |-->tgt_info->tgt_addr = addr;
            |-->tr = bpf_trampoline_get(key, &tgt_info); // lookup old trampoline by key, or create a new one
            |   |-->tr->func.addr = (void *)tgt_info->tgt_addr;
            |-->bpf_link_init()
            |-->bpf_link_prime()
            |-->bpf_trampoline_link_prog()                                          // ${KERNEL}/kernel/bpf/trampoline.c
            |   |-->__bpf_trampoline_link_prog()
            |       |-->bpf_arch_text_poke(tr->func.addr, BPF_MOD_JUMP, NULL, link->link.prog->bpf_func);
            |-->bpf_link_settle()

看到 bpf_arch_text_poke() 函数时,便可知道上面代码片段的主要处理逻辑:

  1. 查询目标 bpf prog 的信息,特别是 bpf prog 的入口地址。
  2. 生成一个 trampoline 对象。
  3. 调用 bpf_arch_text_poke() 将目标 bpf prog 的 prologue 的第一条 nop 指令 live patch 成 jmp 指令,jmp 到当前的 freplace bpf prog 的入口地址。

复习一下 eBPF Talk: perilogue on x86【汇编慎入】 中的 prologue of bpf2bpf

bpf2bpf 函数的第一条指令是 5 个字节大小的 nop 指令。

复习一下 eBPF Talk: poke on x86【汇编慎入】 中的 JIT 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
// ${KERNEL}/arch/x86/net/bpf_jit_comp.c

bpf_int_jit_compile()
|-->do_jit()
    |-->emit_prologue()
        static void emit_prologue(u8 **pprog, u32 stack_depth, bool ebpf_from_cbpf,
                    bool tail_call_reachable, bool is_subprog)
        {
            u8 *prog = *pprog;

            /* BPF trampoline can be made to work without these nops,
            * but let's waste 5 bytes for now and optimize later
            */
            EMIT_ENDBR();
            memcpy(prog, x86_nops[5], X86_PATCH_SIZE);  // NOTE:第一条指令,5 个字节大小的 nop
            prog += X86_PATCH_SIZE;
            if (!ebpf_from_cbpf) {
                if (tail_call_reachable && !is_subprog)
                    EMIT2(0x31, 0xC0); /* xor eax, eax */
                else
                    EMIT2(0x66, 0x90); /* nop2 */       // NOTE: 因为没有 tailcall,所以第二条指令是 1 个字节大小的 nop
            }
            EMIT1(0x55);             /* push rbp */
            EMIT3(0x48, 0x89, 0xE5); /* mov rbp, rsp */

            /* X86_TAIL_CALL_OFFSET is here */
            EMIT_ENDBR();

            /* sub rsp, rounded_stack_depth */
            if (stack_depth)
                EMIT3_off32(0x48, 0x81, 0xEC, round_up(stack_depth, 8));
            if (tail_call_reachable)
                EMIT1(0x50);         /* push rax */
            *pprog = prog;
        }

每个 bpf prog 的 prologue 都包含有 5 个字节大小的 nop 指令。据观察,每个可 trace 的内核函数的第一条指令也是如此。

总结

freplace 技术便是对 prologue 进行 poke 的简单应用,将 prologue 里的第一条 nop 指令替换成 jmp 指令。

不过,想要真正掌握 freplace,需要理解一些前置知识: