ebpf/libbpf 程序使用 raw tracepoint 的常见问题

前言

记录一些编写 ebpf/libbpf 程序(比如编写类型为 BPF_PROG_TYPE_RAW_TRACEPOINT 的 ebpf 程序)时 涉及到的 raw tracepoint 相关的常见问题。

eBPF 程序类型

本文涉及的 eBPF 程序类型为 BPF_PROG_TYPE_RAW_TRACEPOINT

raw tracepoint 可以监控哪些事件

可以通过查看 /sys/kernel/debug/tracing/available_events 文件的内容找到 raw tracepoint 可监控的事件。 文件中每行内容的格式是:

<category>:<name>

比如:

sched:sched_switch

不过,raw tracepoint 用到的是 <name> 的值,而不是整个 <category>:<name> , 详见下方介绍。

SEC 内容的格式

raw tracepoint 事件对应的 SEC 格式为:

SEC("raw_tracepoint/<name>")

// 比如:
// SEC("raw_tracepoint/sched_switch")

或:

SEC("raw_tp/<name>")

// 比如:
// SEC("raw_tp/sched_switch")

<name> 值为前面面 available_events 文件中列出的那些 <name>

SEC("raw_tp/xx")SEC("raw_tracepoint/xx") 其实是等效的,看个人喜好随便用哪种都行。

有两个特殊情况,那就是:

  • 统一用 sys_enter 表示 syscalls 分类下的 sys_enter_xxx 事件: SEC("raw_tracepoint/sys_enter")
  • 统一用 sys_exit 表示 syscalls 分类下的 sys_exit_xxx 事件: SEC("raw_tracepoint/sys_exit")

即,可以用 sys_entersys_exit 事件来监控所有系统调用事件。

如何确定 raw tracepoint 事件处理函数的参数类型,获取对应的内核调用参数

假设,我们想通过 raw tracepoint 监控 chmod 这个命令涉及的 fchmodat 系统调用, 那么,如何确定ebpf 中事件处理函数的参数类型,以及如何获取到对应的 fchmodat 这个系统调用涉及的参数的内容, 比如拿到操作文件名称以及操作的权限 mode 的值。

第一步,找到针对这个系统调用可以使用的 raw tracepoint 事件。前面说了,可以用 sys_entersys_exit 事件来监控所有系统调用事件。

第二步,确定函数的参数类型。raw tracepoint 统一使用 bpf_raw_tracepoint_args 这个结构体

struct bpf_raw_tracepoint_args {
    __u64 args[0];
};

其中 args 中就存储了事件相关的我们可以获取的信息,至于里面包含了哪些信息就是第三步需要确定的信息。

第三步,确定事件本身可以获取到哪些信息。这里以 sys_enter 为例(内容取自 include/trace/events/syscalls.h , 大部分事件主要集中在 include/trace/events/ 目录下) 。

TRACE_EVENT_FN(sys_enter,
    TP_PROTO(struct pt_regs *regs, long id),
    TP_ARGS(regs, id),
    TP_STRUCT__entry(
        __field(    long,           id              )
        __array(    unsigned long,  args,   6       )
    ),
    TP_fast_assign(
        __entry->id = id;
        syscall_get_arguments(current, regs, __entry->args);
    ),
    TP_printk("NR %ld (%lx, %lx, %lx, %lx, %lx, %lx)",
          __entry->id,
          __entry->args[0], __entry->args[1], __entry->args[2],
          __entry->args[3], __entry->args[4], __entry->args[5]),
    syscall_regfunc, syscall_unregfunc
);

其中

  • TP_PROTO(struct pt_regs *regs, long id) 定义了可以通过 bpf_raw_tracepoint_argsargs 拿到的信息。 id 是系统调用的 id, regs 中包含了对应的系统调用的参数。 可以通过 id 过滤只处理 fchmodat 的系统调用事件(通过命令 ausyscall fchmodat 找到对应的系统调用 id)

然后在继续获取对应的系统调用参数。

fchmodat 这个系统调用的函数定义如下:

int fchmodat(int dirfd, const char *pathname, mode_t mode, int flags);

因为 regspt_regs 类型,所以我们可以通过 PT_REGS_PARM1_CORE(regs) 获取第一个参数的值, PT_REGS_PARM2_CORE(regs) 获取第二个参数的值, PT_REGS_PARM3_CORE(regs) 获取第三个参数的值,以此类推, 可以通过 PT_REGS_PARM4_COREPT_REGS_PARM5_CORE 分别获取 regs 中第四个和第五个参数的值。

信息都确定好了,就可以写程序了。比如上面通过 sys_enter 事件处理 fchmodat 系统调用的示例 ebpf 程序如下:

SEC("raw_tracepoint/sys_enter")
int raw_tracepoint__sys_enter(struct bpf_raw_tracepoint_args *ctx)
{
    unsigned long syscall_id = ctx->args[1];
    if(syscall_id != 268)    // 过滤系统调用 id,只处理 fchmodat 系统调用
        return 0;

    struct pt_regs *regs;
    regs = (struct pt_regs *) ctx->args[0];

    char pathname[256];
    u32 mode;

    // 读取第二个参数的值
    char *pathname_ptr = (char *) PT_REGS_PARM2_CORE(regs);
    bpf_core_read_user_str(&pathname, sizeof(pathname), pathname_ptr);

    // 读取第三个参数的值
    mode = (u32) PT_REGS_PARM3_CORE(regs);

    char fmt[] = "fchmodat %s %d\n";
    bpf_trace_printk(fmt, sizeof(fmt), &pathname, mode);
    return 0;
}

完整的示例程序详见:

raw tracepoint 和 tracepoint 的区别

主要区别是,raw tracepoint 不会像 tracepoint 一样在传递上下文给 ebpf 程序时 预先处理好事件的参数(构造好相应的参数字段), raw tracepoint ebpf 程序中访问的都是事件的原始参数。

因此,raw tracepoint 相比 tracepoint 性能通常会更好一点 (数据来自 https://lwn.net/Articles/750569/ )

samples/bpf/test_overhead performance on 1 cpu:

tracepoint    base  kprobe+bpf tracepoint+bpf raw_tracepoint+bpf
task_rename   1.1M   769K        947K            1.0M
urandom_read  789K   697K        750K            755K

Comments