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

前言

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

eBPF 程序类型

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

tracepoint 可以监控哪些事件

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

    <category>:<name>
    

    比如:

    syscalls:sys_enter_execve
    
  • 也可以使用 bpftrace 工具查询:

    $ sudo bpftrace -l tracepoint:* | grep 'sys_enter_execve'
    tracepoint:syscalls:sys_enter_execve
    tracepoint:syscalls:sys_enter_execveat
    

SEC 内容的格式

tracepoint 事件对应的 SEC 格式为:

SEC("tracepoint/<category>/<name>")

// 比如:
// SEC("tracepoint/syscalls/sys_enter_openat")

或:

SEC("tp/<category>/<name>")

// 比如:
// SEC("tp/syscalls/sys_enter_openat")

<category><name> 的值均取值前面 available_events 文件中列出的内容。

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

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

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

确定需要追踪的 tracepoint 事件

第一步,先确定 chmod 所使用的系统调用,这个比较简单,有很多种方法可以做到,比如通过 strace 命令:

$ strace chmod 600 a.txt
...
fchmodat(AT_FDCWD, "a.txt", 0600)       = 0
...

第二步,找到针对这个系统调用可以使用的 tracepoint 事件:

$ sudo cat /sys/kernel/debug/tracing/available_events |grep fchmodat
syscalls:sys_exit_fchmodat
syscalls:sys_enter_fchmodat

可以看到,有 sys_enter_fchmodatsys_exit_fchmodat 这两个事件。这里选择 sys_enter_fchmodat 这个事件进行后续的说明。

确定事件包含的信息

第三步,确定事件本身可以获取到哪些信息,虽然我们知道 fchmodat 系统调用需要提供文件名称和 mode 信息, 但是,我们不确定是否可以在 ebpf 程序中获取到这些信息。

  • 可以通过查看 /sys/kernel/debug/tracing/events/<category>/<name>/format 文件获取到我们可以获取哪些信息。

    比如 sys_enter_fchmodat 这个事件的 /sys/kernel/debug/tracing/events/syscalls/sys_enter_fchmodat/format 的内容如下:

    $ sudo cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_fchmodat/format
    name: sys_enter_fchmodat
    ID: 647
    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:int __syscall_nr; offset:8;       size:4; signed:1;
            field:int dfd;  offset:16;      size:8; signed:0;
            field:const char * filename;    offset:24;      size:8; signed:0;
            field:umode_t mode;     offset:32;      size:8; signed:0;
    
    print fmt: "dfd: 0x%08lx, filename: 0x%08lx, mode: 0x%08lx", ((unsigned long)(REC->dfd)), ((unsigned long)(REC->filename)), ((unsigned long)(REC->mode))
    

    format 列出的字段中,前8个字节对应的字段普通的 ebpf 程序都不能直接访问(部分 bpf helpers 辅助函数可以访问) [1] , 其他的字段一般都可以访问,具体以 print fmt 中引用的字段为准。 fmt 这里引用的这些字段都是我们可以在 ebpf 程序中获取的信息。

  • 也可以使用 bpftrace 工具查询:

    $ sudo bpftrace  -l tracepoint:syscalls:sys_enter_fchmodat -v
    tracepoint:syscalls:sys_enter_fchmodat
        int __syscall_nr
        int dfd
        const char * filename
        umode_t mode
    

从上面可以看到,我们可以获取 sys_enter_fchmodat 事件的 dfdfilename 以及 mode 信息, 这里就包含了前面所说的文件名称以及权限 mode 信息。

确定事件处理函数的参数

第四步,确定函数的参数类型。在知道了事件本身可以提供的信息后,我们还需要知道如何在 ebpf 程序中读取这些信息。 这里就涉及到如何确认 ebpf 事件处理函数的参数是啥,这样我们才能从函数的入参中获取到事件本身包含的信息。

基于 vmlinux.h

一种方法是,在 vmlinux.h 文件中进行查找, 一般 sys_enter_xx 对应 trace_event_raw_sys_entersys_exit_xx 对应 trace_event_raw_sys_exit , 其他的一般对应 trace_event_raw_<name> ,如果没找到的话,可以参考 trace_event_raw_sys_enter 的例子找它相近的 struct。

对于 sys_enter_fchmodat ,我们使用 trace_event_raw_sys_enter 这个 struct:

struct trace_event_raw_sys_enter {
    struct trace_entry ent;
    long int id;
    long unsigned int args[6];
    char __data[0];
};

其中 args 中就存储了事件相关的我们可以获取的信息,即第三步中 format 文件的 fmt 那里包含的字段。 因此,我们可以通过 args[0] 获取 dfd , args[1] 获取 filename 以此类推。

信息都确定好了,就可以写程序了。比如上面 sys_enter_fchmodat 事件的示例 ebpf 程序如下:

SEC("tracepoint/syscalls/sys_enter_fchmodat")
int tracepoint__syscalls__sys_enter_fchmodat(struct trace_event_raw_sys_enter *ctx)
{
        // ...

        char *filename_ptr = (char *) BPF_CORE_READ(ctx, args[1]);
        bpf_core_read_user_str(&event->filename, sizeof(event->filename), filename_ptr);
        event->mode = BPF_CORE_READ(ctx, args[2]);

        // ...
}

使用该方法的完整示例程序详见:

手动构造参数结构体

除了使用 vmlinux.h 中预定义的结构体外,我们还可以基于第三步中 format 文件的内容自定义一个结构体来作为eBPF程序的参数。 比如 sys_enter_fchmodat 这个事件的 /sys/kernel/debug/tracing/events/syscalls/sys_enter_fchmodat/format 的内容如下:

$ sudo cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_fchmodat/format
name: sys_enter_fchmodat
ID: 647
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:int __syscall_nr; offset:8;       size:4; signed:1;
        field:int dfd;  offset:16;      size:8; signed:0;
        field:const char * filename;    offset:24;      size:8; signed:0;
        field:umode_t mode;     offset:32;      size:8; signed:0;

print fmt: "dfd: 0x%08lx, filename: 0x%08lx, mode: 0x%08lx", ((unsigned long)(REC->dfd)), ((unsigned long)(REC->filename)), ((unsigned long)(REC->mode))

基于此信息我们可以定义如下的结构体作为 eBPF 事件处理函数的参数:

struct sys_enter_fchmodat_args {
    char _[16];
    long dfd;
    long filename_ptr;
    long mode;
};

在这个结构体中,我们首先通过 char _[16] 表示了前16个字节的内容,对应的是 format 文件中 dfd 之前的所有字段, 然后我们再一一定义了我们的程序想要获取的 dfdfilenamemode 字段, 之所以使用 long 类型是为了确保每个成员的大小是 format 中标明的8个字节 (每个事件中字段成员的大小不尽相同,需要根据实际的事件format文件的内容进行调整), 你也可以使用其他类型,不过需要保证每个字段成员的偏移量跟 format 中的说明是一致的。

前面 sys_enter_fchmodat 事件的使用手动构造的自定义结构体作为参数的示例 ebpf 程序如下:

SEC("tracepoint/syscalls/sys_enter_fchmodat")
int tracepoint__syscalls__sys_enter_fchmodat(struct sys_enter_fchmodat_args *ctx) {
    // ...

    char *filename_ptr = (char *)ctx->filename_ptr;
    bpf_core_read_user_str(&event->filename, sizeof(event->filename), filename_ptr);
    event->mode = (u32)ctx->mode;

    // ...
}

使用该方法的完整示例程序详见:


Comments