如 trace XDP/tc-bpf 程序,bpf2bpf 函数调用也是可以被 trace 的。

trace bpf2bpf 函数调用的 demo

demo 效果如下:

1
2
3
4
5
6
7
8
9
# ./fentry_fexit-bpf2bpf
2023/07/16 23:27:35 Attached fentry(bpf2bpf)
2023/07/16 23:27:35 Attached fexit(bpf2bpf)
2023/07/16 23:27:35 Attached kprobe(tcp_connect)
2023/07/16 23:27:35 Attached kprobe(inet_csk_complete_hashdance)
2023/07/16 23:27:35 Listening events...
2023/07/16 23:27:38 new tcp connection: 10.0.2.15:36532 -> 172.253.118.101:80 (fentry)
2023/07/16 23:27:38 new tcp connection: 10.0.2.15:36532 -> 172.253.118.101:80 (kprobe)
2023/07/16 23:27:38 new tcp connection: 10.0.2.15:36532 -> 172.253.118.101:80 (fexit: 192)

其中使用的 trace 手段是 fentryfexit

demo 使用的 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
enum probing_type {
    PROBE_TYPE_DEFAULT = 0,
    PROBE_TYPE_FENTRY,
    PROBE_TYPE_FEXIT,
    PROBE_TYPE_FREPLACE,
};

typedef struct event {
    __be32 saddr, daddr;
    __be16 sport, dport;
    __u8 probe_type;
    __u8 retval;
    __u16 pad;
} __attribute__((packed)) event_t;

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

static __always_inline void
__handle_new_connection(void *ctx, struct sock *sk, enum probing_type type, int retval)
{
    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));
    ev.probe_type = (__u8)type;
    ev.retval = (__u8)retval;

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

SEC("fentry/bpf2bpf")
int BPF_PROG(fentry_bpf2bpf, struct pt_regs *regs, struct sock *sk)
{
    bpf_printk("tcpconn, fentry_bpf2bpf\n");

    __handle_new_connection(ctx, sk, PROBE_TYPE_FENTRY, 0);

    return 0;
}

SEC("fexit/bpf2bpf")
int BPF_PROG(fexit_bpf2bpf, struct pt_regs *regs, struct sock *sk, int retval)
{
    bpf_printk("tcpconn, fexit_bpf2bpf\n");

    __handle_new_connection(ctx, sk, PROBE_TYPE_FEXIT, retval);

    return 0;
}

对下面的 bpf2bpf 函数调用进行 trace 的时候,

 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
static __noinline void
handle_new_connection(void *ctx, struct sock *sk)
{
    __handle_new_connection(ctx, sk, PROBE_TYPE_DEFAULT, 0);
}

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 0;
}

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 0;
}

用户态的 Go 代码需要做的事情是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    var obj tcpconnObjects
    if err := loadTcpconnObjects(&obj, nil); err != nil {
        log.Fatalf("Failed to load bpf obj: %v", err)
    }
    defer obj.Close()

    spec, err := loadFentryFexit()
    if err != nil {
        log.Printf("Failed to load bpf obj: %v", err)
        return
    }

    bpf2bpfFentry := spec.Programs["fentry_bpf2bpf"]
    bpf2bpfFentry.AttachTarget = obj.tcpconnPrograms.K_tcpConnect
    bpf2bpfFentry.AttachTo = "handle_new_connection"
    bpf2bpfFexit := spec.Programs["fexit_bpf2bpf"]
    bpf2bpfFexit.AttachTarget = obj.tcpconnPrograms.K_tcpConnect
    bpf2bpfFexit.AttachTo = "handle_new_connection"
  1. 第一步,创建 kprobe 程序,其中包含了 bpf2bpf 函数调用。
  2. 第二步,给 fentryfexit 程序指定 AttachTargetAttachTo
  3. 其中,AttachTarget 是 kprobe 程序,AttachTo 是 kprobe 程序中的函数名。
  4. 即,将 fentryfexit 程序 attach 到 kprobe 程序的 handle_new_connection 函数上。

P.S. demo 源代码:GitHub Asphaltt/learn-by-example/ebpf/fentry_fexit-bpf2bpf

fentry/fexit 的函数参数

仔细对比上面 fentry/fexit 的函数定义和 bpf2bpf 函数调用的函数定义:

1
2
3
4
5
6
7
8
SEC("fentry/bpf2bpf")
int BPF_PROG(fentry_bpf2bpf, struct pt_regs *regs, struct sock *sk);

SEC("fexit/bpf2bpf")
int BPF_PROG(fexit_bpf2bpf, struct pt_regs *regs, struct sock *sk, int retval);

static __noinline void
handle_new_connection(void *ctx, struct sock *sk);

fentry 的函数定义中,第一个参数是 unsigned long long *ctx,其余参数对应 bpf2bpf 函数调用的参数。

fexit 的函数定义中,第一个参数是 unsigned long long *ctx,其余参数对应 bpf2bpf 函数调用的参数,最后一个参数是 bpf2bpf 函数调用的返回值。

不过,fexit 拿到的返回值并不正确?!

不像 trace XDP/tc-bpf 程序,trace bpf2bpf 函数调用时,实质上是对纯函数进行 trace;因此,fentry/fexit 的函数参数和 bpf2bpf 函数调用的参数是一一对应的。

trace bpf2bpf 函数调用的本质

bpf2bpf 特性揭秘 中已讲解 bpf2bpf 的本质是函数调用。

那么,对 bpf2bpf 函数调用进行 trace,就是对 bpf2bpf 的目标函数进行 trace。

然而,bpf2bpf 的目标函数本质上就是一个纯粹的 bpf 函数;所以,对 bpf2bpf 函数调用进行 trace,就是对 bpf 纯函数进行 trace。

查看相关的函数入口地址:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# tail /proc/kallsyms
ffffffffc009155c t bpf_prog_6a56026d5e01160d_k_icsk_complete_hashdance  [bpf]
ffffffffc0091588 t bpf_prog_787ff09f346cd7c8_handle_new_connection      [bpf]
ffffffffc009155c t bpf_prog_8c2d1927fc72da2a_k_icsk_complete_hashdance  [bpf]
ffffffffc0094388 t bpf_prog_bcf1a2069976db8c_k_tcp_connect      [bpf]
ffffffffc00943c8 t bpf_prog_787ff09f346cd7c8_handle_new_connection      [bpf]
ffffffffc0094388 t bpf_prog_30478f0b8dc8d6fc_k_tcp_connect      [bpf]
ffffffffc00944e0 t bpf_prog_083bb574de6b3e92_fentry_bpf2bpf     [bpf]
ffffffffc0094628 t bpf_prog_5ce545a673cb4fa2_fexit_bpf2bpf      [bpf]
ffffffffc02a0000 t bpf_trampoline_103079215106_1        [bpf]

handle_new_connection() 函数进行 trace 后的 x86 汇编指令如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# gdb -q -c /proc/kcore -ex 'x/150i 0xffffffffc00943c8' -ex 'quit'
   0xffffffffc00943c8:  call   0xffffffffc02a0000
   0xffffffffc00943cd:  xchg   %ax,%ax
   0xffffffffc00943cf:  push   %rbp
   0xffffffffc00943d0:  mov    %rsp,%rbp
   0xffffffffc00943d3:  sub    $0x18,%rsp
   0xffffffffc00943da:  push   %rbx
   0xffffffffc00943db:  push   %r13
   0xffffffffc00943dd:  push   %r14
   0xffffffffc00943df:  push   %r15
   //...
   0xffffffffc00944a6:  pop    %r15
   0xffffffffc00944a8:  pop    %r14
   0xffffffffc00944aa:  pop    %r13
   0xffffffffc00944ac:  pop    %rbx

可以看到,第一条 nop 指令已被替换成 call 指令;而 call 指令的目标地址是 0xffffffffc02a0000,即 bpf_trampoline_103079215106_1,这是一个 bpf trampoline,里面会依次调用 fentry/fexit 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
# gdb -q -c /proc/kcore -ex 'x/150i 0xffffffffc02a0000' -ex 'quit'
   0xffffffffc02a0000:  push   %rbp
   0xffffffffc02a0001:  mov    %rsp,%rbp
   0xffffffffc02a0004:  sub    $0x48,%rsp
   0xffffffffc02a0008:  push   %rbx
   0xffffffffc02a0009:  mov    $0x5,%eax
   0xffffffffc02a000e:  mov    %rax,-0x38(%rbp)
   0xffffffffc02a0012:  mov    %rdi,-0x30(%rbp)
   0xffffffffc02a0016:  mov    %rsi,-0x28(%rbp)
   0xffffffffc02a001a:  mov    %rdx,-0x20(%rbp)
   0xffffffffc02a001e:  mov    %rcx,-0x18(%rbp)
   0xffffffffc02a0022:  mov    %r8,-0x10(%rbp)
   // ...
   0xffffffffc02a005a:  call   0xffffffffc00944e0           // fentry_bpf2bpf
   0xffffffffc02a005f:  movabs $0xffffba7fc0863000,%rdi
   // ...
   0xffffffffc02a00bc:  call   0xffffffffc0094628           // fexit_bpf2bpf
   0xffffffffc02a00c1:  movabs $0xffffba7fc08ad000,%rdi
   // ...
   0xffffffffc02a00e6:  mov    -0x8(%rbp),%rax
   0xffffffffc02a00ea:  pop    %rbx
   0xffffffffc02a00eb:  leave
   0xffffffffc02a00ec:  add    $0x8,%rsp
   0xffffffffc02a00f0:  ret

trace bpf2bpf

小结

  • trace bpf2bpf 函数调用,本质上是对 bpf 纯函数进行 trace。

回过头来复习 trampoline on x86【汇编慎入】,便能明白:

对 bpf 程序进行 trace 的底层实现是将 bpf 程序的第一条 nop 指令替换成 call 指令。