分享两条 bpfsnoop(emm 从 btrace 改名而来)使用 tp_btf 支持动态追踪 tracepoint 的经验。

  1. 如何确定 tracepoint 对应的参数?
  2. 如何绕过 verifier 校验 tp_btf prog 的 BUG?

bpfsnoop 里的解法请参考:PR tp: Trace tracepoints

tp_btf 简介

tp_btfbpf 子系统在 v5.5 内核引入的新特性,方便在 tp_btf prog 里直接使用 tracepoint 的参数。

tp_btfraw_tracepoint 的 BTF 版本;直接讲解 tp_btf 的资料较少,可以参考 raw_tracepoint 的资料:

如何确定 tracepoint 对应的参数?

tp_btf prog 里使用的参数指 /sys/kernel/debug/tracing/events 下的 tracepoint 参数吗?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ cat /sys/kernel/debug/tracing/events/sched/sched_process_fork/format
name: sched_process_fork
ID: 324
format:
    field:unsigned short common_type;   offset:0;   size:2; signed:0;
    field:unsigned char common_flags;   offset:2;   size:1; signed:0;
    field:unsigned char common_preempt_count;   offset:3;   size:1; signed:0;
    field:int common_pid;   offset:4;   size:4; signed:1;

    field:char parent_comm[16]; offset:8;   size:16;    signed:0;
    field:pid_t parent_pid; offset:24;  size:4; signed:1;
    field:char child_comm[16];  offset:28;  size:16;    signed:0;
    field:pid_t child_pid;  offset:44;  size:4; signed:1;

print fmt: "comm=%s pid=%d child_comm=%s child_pid=%d", REC->parent_comm, REC->parent_pid, REC->child_comm, REC->child_pid

不是的,完全不是这么回事。

先来看下正确的 tracepoint 参数:

1
2
3
$ sudo ./bpfsnoop -t sched_process_fork --show-func-proto
Kernel tracepoints: (total 1)
void sched_process_fork(struct task_struct *parent, struct task_struct *child);

看起来,完全两码事。

先看下 bpf verifier 是怎么获取 tracepoint 参数的:

 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
// kernel/bpf/verifier.c

int bpf_check_attach_target(struct bpf_verifier_log *log,
                            const struct bpf_prog *prog,
                            const struct bpf_prog *tgt_prog,
                            u32 btf_id,
                            struct bpf_attach_target_info *tgt_info)
{
    // ...

        /* The func_proto of "btf_trace_##tname" is generated from typedef without argument
         * names. Thus using bpf_raw_event_map to get argument names.
         */
        btp = bpf_get_raw_tracepoint(tname);
        if (!btp)
            return -EINVAL;
        fname = kallsyms_lookup((unsigned long)btp->bpf_func, NULL, NULL, NULL,
                                trace_symbol);
        bpf_put_raw_tracepoint(btp);

        if (fname)
            ret = btf_find_by_name_kind(btf, fname, BTF_KIND_FUNC);

    // ...
}

啊哈,原来是:

  1. 根据 tracepoint 的名字查找到内部信息;
  2. 查找 bpf_func 字段对应的符号信息;
  3. 根据符号信息查找到 BTF 信息。

以下代码片段就是根据 tracepoint 的名字查找到内部信息的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// kernel/trace/bpf_trace.c

extern struct bpf_raw_event_map __start__bpf_raw_tp[];
extern struct bpf_raw_event_map __stop__bpf_raw_tp[];

struct bpf_raw_event_map *bpf_get_raw_tracepoint(const char *name)
{
    struct bpf_raw_event_map *btp = __start__bpf_raw_tp;

    for (; btp < __stop__bpf_raw_tp; btp++) {
        if (!strcmp(btp->tp->name, name))
            return btp;
    }

    return bpf_get_raw_tracepoint_module(name);
}

bpfsnoop 里,如法炮制,使用同样的方式读取到所有 tracepoint 的信息:

 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
static __noinline void
probe_tp_info(struct bpf_raw_event_map *btp, int i)
{
    struct tp_info *tp = &tps[i];
    const char *str;

    str = BPF_CORE_READ(btp, tp, name);
    bpf_probe_read_kernel_str(tp->name, sizeof(tp->name), str);
    BPF_CORE_READ_INTO(&tp->func_proto_symbol, btp, bpf_func);
    BPF_CORE_READ_INTO(&tp->num_args, btp, num_args);
}

SEC("fentry/__x64_sys_nanosleep")
int probe(struct pt_regs *regs)
{
    struct bpf_raw_event_map *btp = (typeof(btp)) __start;

    if (run)
        return BPF_OK;
    run = true;

    for (int i = 0; i < TP_MAX; i++) {
        if (i >= nr_tps)
            break;

        probe_tp_info(btp, i);
        btp++;
    }

    return BPF_OK;
}

接着,在 Go 里:

  1. /proc/kallsyms 里查找 func_proto_symbol 对应的符号信息;
  2. 使用符号名称到内核 BTF 里查找到对应的 BTF 信息,即 *btf.Func
  3. 根据 *btf.Func 里的 *btf.FuncProto 信息打印出参数信息。

需要使用 tp_btf 的时候,欢迎使用 bpfsnoop -t <tracepoint> --show-func-proto 查看参数信息。

如何绕过 verifier 校验 tp_btf prog 的 BUG?

如果直接使用上述方法找到的参数,可能会遇到如下 BUG:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 func 'xdp_redirect_err' arg0 has btf_id 6973 type STRUCT 'net_device'
 14: R0_w=trusted_ptr_net_device() R1=ctx()
 14: (7b) *(u64 *)(r10 -96) = r0       ; R0_w=trusted_ptr_net_device() R10=fp0 fp-96_w=trusted_ptr_net_device()
 15: (79) r0 = *(u64 *)(r1 +8)
 func 'xdp_redirect_err' arg1 has btf_id 6845 type STRUCT 'bpf_prog'
 16: R0_w=trusted_ptr_bpf_prog() R1=ctx()
 16: (7b) *(u64 *)(r10 -88) = r0       ; R0_w=trusted_ptr_bpf_prog() R10=fp0 fp-88_w=trusted_ptr_bpf_prog()
 17: (79) r0 = *(u64 *)(r1 +16)
 func 'xdp_redirect_err' arg2 type UNKNOWN is not a struct
 invalid bpf_context access off=16 size=8

这是在动态追踪 xdp_redirect_err tracepoint 时遇到的问题。

如下是 xdp_redirect_err tracepoint 的参数信息:

1
2
3
$ sudo ./bpfsnoop -t xdp_redirect_err --show-func-proto
Kernel tracepoints: (total 1)
void xdp_redirect_err(const struct net_device *dev, const struct bpf_prog *xdp, const void *tgt, int err, enum bpf_map_type map_type, u32 map_id, u32 index);

第 2 个参数是 const void *tgt,为什么 verifier 报错了呢?看看 verifier 源代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// kernel/bpf/btf.c

bool btf_ctx_access(int off, int size, enum bpf_access_type type,
                    const struct bpf_prog *prog,
                    struct bpf_insn_access_aux *info)
{
    // ...

    /* skip modifiers */
    while (btf_type_is_modifier(t)) {
        info->btf_id = t->type;
        t = btf_type_by_id(btf, t->type);
    }
    if (!btf_type_is_struct(t)) {
        bpf_log(log,
            "func '%s' arg%d type %s is not a struct\n",
            tname, arg, btf_type_str(t));
        return false;
    }

    // ...
}

笔者认为这是 verifier 的 BUG,得修。

但不能因为 verifier 有 BUG 而放弃使用 tp_btfbpfsnoop 里的解决方案是:

  • 使用 bpf_probe_read_kernel() helper 读取参数。

这方式可行吗?如果 tracepoint 有很多参数,比如 12 个参数,这样的方式可行吗?

好问题,直接看看内核 bpf_trace_run12() 的源代码:

 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
// kernel/trace/bpf_trace.c

static __always_inline
void __bpf_trace_run(struct bpf_raw_tp_link *link, u64 *args)
{
    struct bpf_prog *prog = link->link.prog;
    struct bpf_run_ctx *old_run_ctx;
    struct bpf_trace_run_ctx run_ctx;

    cant_sleep();
    if (unlikely(this_cpu_inc_return(*(prog->active)) != 1)) {
        bpf_prog_inc_misses_counter(prog);
        goto out;
    }

    run_ctx.bpf_cookie = link->cookie;
    old_run_ctx = bpf_set_run_ctx(&run_ctx.run_ctx);

    rcu_read_lock();
    (void) bpf_prog_run(prog, args);
    rcu_read_unlock();

    bpf_reset_run_ctx(old_run_ctx);
out:
    this_cpu_dec(*(prog->active));
}

// ...

#define BPF_TRACE_DEFN_x(x)                                 \
    void bpf_trace_run##x(struct bpf_raw_tp_link *link,     \
                  REPEAT(x, SARG, __DL_COM, __SEQ_0_11))    \
    {                                                       \
        u64 args[x];                                        \
        REPEAT(x, COPY, __DL_SEM, __SEQ_0_11);              \
        __bpf_trace_run(link, args);                        \
    }                                                       \
    EXPORT_SYMBOL_GPL(bpf_trace_run##x)
BPF_TRACE_DEFN_x(1);
BPF_TRACE_DEFN_x(2);
BPF_TRACE_DEFN_x(3);
BPF_TRACE_DEFN_x(4);
BPF_TRACE_DEFN_x(5);
BPF_TRACE_DEFN_x(6);
BPF_TRACE_DEFN_x(7);
BPF_TRACE_DEFN_x(8);
BPF_TRACE_DEFN_x(9);
BPF_TRACE_DEFN_x(10);
BPF_TRACE_DEFN_x(11);
BPF_TRACE_DEFN_x(12);

这是将所有参数都保存到一个 u64 数组里,然后通过 ctx 传递给 tp_btf prog。

看源代码是这样,bpfsnoop 跑起来也没问题,所以这是可行的。

总结

tp_btf 值得推荐!

在使用的时候,先用 bpfsnoop -t <tracepoint> --show-func-proto 查看参数信息,然后根据参数信息编写 tp_btf prog。

如果遇到 verifier 报错,可以使用 bpf_probe_read_kernel() helper 读取参数。