前言¶
记录一些编写 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_fchmodat 和 sys_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 事件的 dfd 、 filename 以及 mode 信息, 这里就包含了前面所说的文件名称以及权限 mode 信息。
确定事件处理函数的参数¶
第四步,确定函数的参数类型。在知道了事件本身可以提供的信息后,我们还需要知道如何在 ebpf 程序中读取这些信息。 这里就涉及到如何确认 ebpf 事件处理函数的参数是啥,这样我们才能从函数的入参中获取到事件本身包含的信息。
基于 vmlinux.h¶
一种方法是,在 vmlinux.h 文件中进行查找, 一般 sys_enter_xx 对应 trace_event_raw_sys_enter , sys_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]);
// ...
}
使用该方法的完整示例程序详见:
- tracepoint/syscalls/sys_enter_fchmodat: 07-tracepoint-args
- tracepoint/sched/sched_switch: 14-tracepoint-args-sched_switch
手动构造参数结构体¶
除了使用 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 之前的所有字段, 然后我们再一一定义了我们的程序想要获取的 dfd 、 filename 、 mode 字段, 之所以使用 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;
// ...
}
使用该方法的完整示例程序详见:
- tracepoint/syscalls/sys_enter_fchmodat: 35-tracepoint-args-use-custom-struct
- tracepoint/sched/sched_switch: 36-tracepoint-args-sched_switch-use-custom-struct
Comments