如 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 手段是 fentry
和 fexit
。
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"
|
- 第一步,创建 kprobe 程序,其中包含了
bpf2bpf
函数调用。
- 第二步,给
fentry
和 fexit
程序指定 AttachTarget
和 AttachTo
。
- 其中,
AttachTarget
是 kprobe 程序,AttachTo
是 kprobe 程序中的函数名。
- 即,将
fentry
和 fexit
程序 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
函数调用,本质上是对 bpf 纯函数进行 trace。
回过头来复习 trampoline on x86【汇编慎入】,便能明白:
对 bpf 程序进行 trace 的底层实现是将 bpf 程序的第一条 nop
指令替换成 call
指令。