此处指的是 trace kprobe bpf 程序,而不是 trace kprobe 事件。

trace kprobe 程序的 demo

demo 效果如下:

1
2
3
4
5
6
7
8
9
# ./fentry_fexit-kprobe
2023/07/22 06:45:15 Attached fentry(tcp_connect)
2023/07/22 06:45:15 Attached fexit(tcp_connect)
2023/07/22 06:45:15 Attached kprobe(tcp_connect)
2023/07/22 06:45:15 Attached kprobe(inet_csk_complete_hashdance)
2023/07/22 06:45:15 Listening events...
2023/07/22 06:45:16 new tcp connection: 192.168.1.106:40296 -> 172.217.194.102:443 (fentry)
2023/07/22 06:45:16 new tcp connection: 192.168.1.106:40296 -> 172.217.194.102:443 (kprobe)
2023/07/22 06:45:16 new tcp connection: 192.168.1.106:40296 -> 172.217.194.102:443 (fexit: 0)

其中使用的 trace 手段是 fentryfexit

demo 中使用的 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
25
#include "lib_kprobe.h"

SEC("fentry/tcp_connect")
int BPF_PROG(fentry_tcp_connect, struct pt_regs *regs)
{
    bpf_printk("tcpconn, fentry_tcp_connect\n");

    struct sock *sk;
    sk = (typeof(sk))PT_REGS_PARM1(regs);
    __handle_new_connection(ctx, sk, PROBE_TYPE_FENTRY, 0);

    return 0;
}

SEC("fexit/tcp_connect")
int BPF_PROG(fexit_tcp_connect, struct pt_regs *regs, int retval)
{
    bpf_printk("tcpconn, fexit_tcp_connect\n");

    struct sock *sk;
    sk = (typeof(sk))PT_REGS_PARM1(regs);
    __handle_new_connection(ctx, sk, PROBE_TYPE_FEXIT, retval);

    return 0;
}

demo 中使用的 kprobe 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
#include "lib_kprobe.h"

struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __uint(key_size, 4);
    __uint(value_size, 4);
    __uint(max_entries, 1);
} progs SEC(".maps");

SEC("kprobe/hanle_new_connection")
int handle_new_connection(struct pt_regs *ctx)
{
    bpf_printk("tcpconn, handle_new_connection\n");

    struct sock *sk;
    sk = (typeof(sk))PT_REGS_PARM1(ctx);
    __handle_new_connection(ctx, sk, PROBE_TYPE_DEFAULT, 0);

    return 0;
}

SEC("kprobe/tcp_connect")
int k_tcp_connect(struct pt_regs *ctx)
{
    bpf_tail_call_static(ctx, &progs, 0);

    return 0;
}

SEC("kprobe/inet_csk_complete_hashdance")
int k_icsk_complete_hashdance(struct pt_regs *ctx)
{
    bpf_tail_call_static(ctx, &progs, 0);

    return 0;
}

对其中的 k_tcp_connect kprobe 进行 fenrty/fexit

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    spec, err := loadFentryFexit()
    if err != nil {
        log.Printf("Failed to load bpf obj: %v", err)
        return
    }

    kprobeFentry := spec.Programs["fentry_tcp_connect"]
    kprobeFentry.AttachTarget = obj.tcpconnPrograms.K_tcpConnect
    kprobeFentry.AttachTo = "k_tcp_connect"
    kprobeFexit := spec.Programs["fexit_tcp_connect"]
    kprobeFexit.AttachTarget = obj.tcpconnPrograms.K_tcpConnect
    kprobeFexit.AttachTo = "k_tcp_connect"
  1. 第一步,创建 kprobe 程序。
  2. 第二步,给 fentryfexit 程序指定 AttachTargetAttachTo
  3. 其中,AttachTargetkprobe 程序,AttachTokprobe 程序中的函数名。
  4. 即,将 fentryfexit 程序 attach 到 kprobe 程序的 k_tcp_connect 函数上。

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

fentry/fexit 的函数参数

仔细对比上面 fentry/fexit 的函数定义和 kprobe 程序的函数定义:

1
2
3
4
5
6
7
8
SEC("fentry/kprobe")
int BPF_PROG(fentry_kprobe, struct pt_regs *regs);

SEC("fexit/kprobe")
int BPF_PROG(fexit_kprobe, struct pt_regs *regs, int retval);

SEC("kprobe/tcp_connect")
int k_tcp_connect(struct pt_regs *ctx);

因为 kprobe 程序只有一个参数:struct pt_regs *ctx,所以 fentry/fexit 的函数参数就有一个对应的参数:struct pt_regs *regs;但参数名不能叫 ctx

fentry/fexit 的函数参数里不能再使用 ctx

这是因为 BPF_PROG() 宏里已默认提供了 ctx 参数,所以不能再使用 ctx 参数名了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#define BPF_PROG(name, args...)                                     \
name(unsigned long long *ctx);                                      \
static __always_inline typeof(name(0))                              \
____##name(unsigned long long *ctx, ##args);                        \
typeof(name(0)) name(unsigned long long *ctx)                       \
{                                                                   \
    _Pragma("GCC diagnostic push")                                  \
    _Pragma("GCC diagnostic ignored \"-Wint-conversion\"")          \
    return ____##name(___bpf_ctx_cast(args));                       \
    _Pragma("GCC diagnostic pop")                                   \
}                                                                   \
static __always_inline typeof(name(0))                              \
____##name(unsigned long long *ctx, ##args)

小结

不像 XDP 和 tc-bpf,kprobe 程序的参数就是运行的时候内核提供的实际参数,不需要在 verifier 阶段做 ctx 属性访问的转换。