在『eBPF Talk 读者群』里讨论起 kfuncskprobefentry/fexit 等,聊到 kprobe.multifprobe;我立刻学习了一下 kprobe.multifprobe

TL;DR kprobe.multi 的底层是 fprobefprobe 的底层是 ftrace

P.S. kprobe.multi 最低要求 5.18 内核,起始于 bpf: Add multi kprobe link

kprobe.multi 例子:pwru

pwru 是基于 kprobe 实现的 Linux 内核网络包跟踪工具,目前默认使用 kprobe.multi 进行 kprobe

使用了 kprobe.multi 后,pwru 可以一次性对多个内核函数进行 “kprobe” 并运行同一个 bpf prog。

相关代码如下:

 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
// ${PWRU}/bpf/kprobe_pwru.c

#ifdef HAS_KPROBE_MULTI
#define PWRU_KPROBE_TYPE "kprobe.multi"
#define PWRU_HAS_GET_FUNC_IP true
#else
#define PWRU_KPROBE_TYPE "kprobe"
#define PWRU_HAS_GET_FUNC_IP false
#endif /* HAS_KPROBE_MULTI */

#define PWRU_ADD_KPROBE(X)                                                     \
  SEC(PWRU_KPROBE_TYPE "/skb-" #X)                                             \
  int kprobe_skb_##X(struct pt_regs *ctx) {                                    \
    struct sk_buff *skb = (struct sk_buff *) PT_REGS_PARM##X(ctx);             \
    return handle_everything(skb, ctx, PWRU_HAS_GET_FUNC_IP);                  \
  }

PWRU_ADD_KPROBE(1)
PWRU_ADD_KPROBE(2)
PWRU_ADD_KPROBE(3)
PWRU_ADD_KPROBE(4)
PWRU_ADD_KPROBE(5)

#undef PWRU_KPROBE
#undef PWRU_HAS_GET_FUNC_IP
#undef PWRU_KPROBE_TYPE

// ${PWRU}/main.go

            opts := link.KprobeMultiOptions{Symbols: funcsByPos[pos]} // 一次性提供进行 "kprobe" 的多个内核函数的函数名称
            kp, err := link.KprobeMulti(fn, opts)

kprobe.multi 源码分析

cilium/ebpf Go 库看下 kprobe.multi 会使用 BPF 系统调用的哪个子命令。

1
2
3
4
KprobeMulti()                                               // ${EBPF}/link/kprobe_multi.go
|-->kprobeMulti()
    |-->sys.LinkCreateKprobeMulti()                         // ${EBPF}/internal/sys/types.go
        |-->BPF(BPF_LINK_CREATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr))

接着,看下 BPF 系统调用的 BPF_LINK_CREATE 的源代码。

 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
57
58
59
60
61
62
63
64
65
66
67
68
69
// ${KERNEL}/kernel/bpf/syscall.c

static int __sys_bpf(int cmd, bpfptr_t uattr, unsigned int size)
{
    // ...
    switch (cmd) {
    case BPF_LINK_CREATE:
        err = link_create(&attr, uattr);
        break;
    }

    return err;
}

static int link_create(union bpf_attr *attr, bpfptr_t uattr)
{
    // ...
    switch (prog->type) {
    // ...
    case BPF_PROG_TYPE_KPROBE:
        if (attr->link_create.attach_type == BPF_PERF_EVENT)
            ret = bpf_perf_link_attach(attr, prog);
        else
            ret = bpf_kprobe_multi_link_attach(attr, prog);
        break;
    default:
        ret = -EINVAL;
    }

out:
    if (ret < 0)
        bpf_prog_put(prog);
    return ret;
}

// ${KERNEL}/kernel/trace/bpf_trace.c

int bpf_kprobe_multi_link_attach(const union bpf_attr *attr, struct bpf_prog *prog)
{
    // ...

    if (uaddrs) {
        if (copy_from_user(addrs, uaddrs, size)) {
            // ...
        }
    } else {
        // ...

        err = ftrace_lookup_symbols(us.syms, cnt, addrs);
        // ...
    }

    if (flags & BPF_F_KPROBE_MULTI_RETURN)
        link->fp.exit_handler = kprobe_multi_link_handler;
    else
        link->fp.entry_handler = kprobe_multi_link_handler;

    link->addrs = addrs;
    link->cookies = cookies;
    link->cnt = cnt;

    // ...

    err = register_fprobe_ips(&link->fp, addrs, cnt);

    // ...

    return err;
}

以上代码片段的主要处理逻辑如下:

  1. 将提供的内核函数名称转成函数地址。
  2. 设置 fprobe entry/exit 所需的回调函数。
  3. 注册 fprobe

fprobe 源码分析

接着,直接看下 register_fprobe_ips() 的源码吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
register_fprobe_ips()                                   // ${KERNEL}/kernel/trace/fprobe.c
|-->fprobe_init() {
|   if (fprobe_shared_with_kprobes(fp))
|       fp->ops.func = fprobe_kprobe_handler;
|   else
|       fp->ops.func = fprobe_handler;
|   }
|-->ftrace_set_filter_ips(&fp->ops, addrs, num, 0, 0);  // ${KERNEL}/kernel/trace/ftrace.c
|-->fprobe_init_rethook(fp, num);
|-->register_ftrace_function(&fp->ops);

以上代码片段的主要处理逻辑如下:

  1. 设置 fprobe 的回调函数。
  2. 将需要 trace 的函数地址加入到 ftrace 中。
  3. 向指定函数地址进行 ftrace

运行 bpf prog

kprobe.multifprobe 都设置了它们的回调函数。继续看下回调函数里做了哪些处理吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ${KERNEL}/kernel/trace/fprobe.c

static void fprobe_handler(unsigned long ip, unsigned long parent_ip,
               struct ftrace_ops *ops, struct ftrace_regs *fregs)
{
    // ...

    if (fp->entry_handler)
        fp->entry_handler(fp, ip, ftrace_get_regs(fregs));

    if (fp->exit_handler) {
        rh = rethook_try_get(fp->rethook);
        if (!rh) {
            fp->nmissed++;
            goto out;
        }
        fpr = container_of(rh, struct fprobe_rethook_node, node);
        fpr->entry_ip = ip;
        rethook_hook(rh, ftrace_get_regs(fregs), true);
    }

out:
    ftrace_test_recursion_unlock(bit);
}
  1. 如果有 entry_handler,就调用 entry_handler 函数。
  2. 如果有 exit_handler,就调用 exit_handler 函数。

kprobe.multi 设置的 fprobeentry_handlerexit_handler 都是 kprobe_multi_link_handler

 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
// ${KERNEL}/kernel/trace/bpf_trace.c

static void
kprobe_multi_link_handler(struct fprobe *fp, unsigned long fentry_ip,
              struct pt_regs *regs)
{
    struct bpf_kprobe_multi_link *link;

    link = container_of(fp, struct bpf_kprobe_multi_link, fp);
    kprobe_multi_link_prog_run(link, get_entry_ip(fentry_ip), regs);
}

static int
kprobe_multi_link_prog_run(struct bpf_kprobe_multi_link *link,
               unsigned long entry_ip, struct pt_regs *regs)
{
    struct bpf_kprobe_multi_run_ctx run_ctx = {
        .link = link,
        .entry_ip = entry_ip,
    };
    struct bpf_run_ctx *old_run_ctx;
    int err;

    if (unlikely(__this_cpu_inc_return(bpf_prog_active) != 1)) {
        err = 0;
        goto out;
    }

    migrate_disable();
    rcu_read_lock();
    old_run_ctx = bpf_set_run_ctx(&run_ctx.run_ctx);
    err = bpf_prog_run(link->link.prog, regs);
    bpf_reset_run_ctx(old_run_ctx);
    rcu_read_unlock();
    migrate_enable();

 out:
    __this_cpu_dec(bpf_prog_active);
    return err;
}

以上代码片段比较简单,就是将 fprobe 回调函数传递过来的 regs 传给 bpf_kprobe_multi_link 上的 bpf prog。

小结

从 5.18 内核版本起,需要进行 kprobe 的场景都可以使用 kprobe.multi 替换。

因为 kprobe.multi 底层使用的 fprobe on top of ftrace 进行 trace 的性能消耗比较低了,跟 fentry/fexit 的性能消耗相差不多。这是因为 ftracefentry/fexit 的底层都是 trampoline 技术。