前言¶
记录一些编写 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_enter 和 sys_exit 事件来监控所有系统调用事件。
如何确定 raw tracepoint 事件处理函数的参数类型,获取对应的内核调用参数¶
假设,我们想通过 raw tracepoint 监控 chmod 这个命令涉及的 fchmodat 系统调用, 那么,如何确定ebpf 中事件处理函数的参数类型,以及如何获取到对应的 fchmodat 这个系统调用涉及的参数的内容, 比如拿到操作文件名称以及操作的权限 mode 的值。
第一步,找到针对这个系统调用可以使用的 raw tracepoint 事件。前面说了,可以用 sys_enter 和 sys_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_args 的 args 拿到的信息。 id 是系统调用的 id, regs 中包含了对应的系统调用的参数。 可以通过 id 过滤只处理 fchmodat 的系统调用事件(通过命令 ausyscall fchmodat 找到对应的系统调用 id)
然后在继续获取对应的系统调用参数。
fchmodat 这个系统调用的函数定义如下:
int fchmodat(int dirfd, const char *pathname, mode_t mode, int flags);
因为 regs 是 pt_regs 类型,所以我们可以通过 PT_REGS_PARM1_CORE(regs) 获取第一个参数的值, PT_REGS_PARM2_CORE(regs) 获取第二个参数的值, PT_REGS_PARM3_CORE(regs) 获取第三个参数的值,以此类推, 可以通过 PT_REGS_PARM4_CORE 和 PT_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
参考资料¶
- Using the TRACE_EVENT() macro (Part 1) [LWN.net]
- Using the TRACE_EVENT() macro (Part 2) [LWN.net]
- Using the TRACE_EVENT() macro (Part 3) [LWN.net]
- bpf, tracing: introduce bpf raw tracepoints [LWN.net]
- BPF CO-RE reference guide
- bcc/reference_guide.md at master · iovisor/bcc
- libbpf/libbpf.c at 12e932ac0e18546dd7247e66ea1b4aa236d2ef38 · libbpf/libbpf
- kernel/git/torvalds/linux.git - Linux kernel source tree
- BCC to libbpf conversion guide
- The art of writing eBPF programs: a primer. –Sysdig
- Where do you find the syscall table for Linux? - Unix & Linux Stack Exchange
- ausyscall(8) - Linux man page
Comments