【BPF入门系列-10】使用 tracepoint 跟踪文件 open 系统调用
1. 什么是 tracepoint
tracepoint
的介绍可以参见 Kernel 文档这里。从 Linux 内核 4.7 开始,eBPF 程序可以挂载到内核跟踪点 tracepoint
。在此之前,要完成内核中函数跟踪的工作,只能用 kprobes/kretprobe
等方式挂载到导出的内核函数(参见 /proc/kallsyms
),正如我们前几篇文章跟踪 open
系统调用方式那样。尽管 kprobes
可以达到跟踪的目的,但存在很多不足:
- 内核的内部 API 不稳定,如果内核版本变化导致声明修改,我们的跟踪程序就不能正常工作;
- 出于性能考虑,大部分网络相关的内层函数都是内联或者静态的,两者都不能使用
kprobes
方式探测; - 找出调用某个函数的所有地方是相当乏味的,有时所需的字段数据不全具备;
tracepoint
是由内核开发人员在代码中设置的静态 hook
点,具有稳定的 API
接口,不会随着内核版本的变化而变化,可以提高我们内核跟踪程序的可移植性。但是由于 tracepoint
是需要内核研发人员参数编写,因此在内核代码中的数量有限,并不是所有的内核函数中都具有类似的跟踪点,所以从灵活性上不如 kprobes
这种方式。在 3.10 内核中,kprobe
与 tracepoint
方式对比如下:
项目 | kprobes | tracepoint |
---|---|---|
跟踪类型 | 动态 | 静态 |
可跟踪数量 | 100000+ | 1200+ (perf list|wc -l) |
是否需要内核开发者维护 | 不需要 | 需要 |
禁止的开销 | 无 | 少许 (NOPs 和元数据) |
稳定的 API | 否 | 是 |
参考:《BPF Performace Tools》 2.9 Tracepoints,数据有更新。
在我们的内核跟踪程序中,如果存在 tracepoint
方式,我们应该优先使用,这使得跟踪程序具有良好的可移植性。
2. 使用 tracepoint 实现
open
系统调用具有两个 syscalls
类型的静态跟踪点,分别是 syscalls:sys_enter_open
和 syscalls:sys_exit_open
,前者是进入函数,后者是从函数返回,功能基本等同于 kprobe/kretprobe
。其中 syscalls
表示子系统模块, sys_enter_open
表示跟踪点名称。
tracepoint
的完整列表可以使用 perf
工具的 perf list
命令查看,当然如果知道 tracepoint
的子系统,也可以进行过滤,比如 perf list 'syscalls:*'
命令只用于显示 syscalls
相关的 tracepoints
。
|
|
为了在 eBPF 程序中使用,我们还需要知道 tracepoint
相关参数的格式,syscalls:sys_enter_open
格式定义在 /sys/kernel/debug/tracing/events/syscalls/sys_enter_open/format
文件中。
|
|
2.1 TRACEPOINT_PROBE 宏
对于 tracepoint
的跟踪,在 BCC
中可以使用 TRACEPOINT_PROBE
宏进行定义。宏的格式如下:
|
|
其中 category
就是子系统,event
代表事件名。对于 syscalls:sys_enter_open
则为:
TRACEPOINT_PROBE(syscalls,sys_enter_open)
注意子模块中的
syscalls
的名字最后包含s
。
tracepoint
中的所有参数都会包含在一个固定名称的 args
的结构体中。格式在上面我们已经进行了输出(/sys/kernel/debug/tracing/events/category/event/format
)。args
结构体还可以作为内核函数中传递 ctx
参数的替代,比如使用 perf_submit
的第一个参数。
对于 args
参数的详细定义,我们将在后续章节进行展开讨论。
2.2 tracepoint 版本
基础知识已经完成了铺垫,这里我们就将 perf_event 版本
代码进行少许调整,我们主要是将 BPF 程序中的 trace_syscall_open
函数进行替换即可。替换后的代码如下:
|
|
需要注意的是,TRACEPOINT_PROBE
定义的过程中未出现 args
相关的定义,但是我们可以直接使用,这是因为 BCC 协助我们完成了这步工作。另外 args
可以充当函数 ctx
也进行了展示,open_events.perf_submit((struct pt_regs *)args, &evt, sizeof(evt));
。
此外,由于 TRACEPOINT_PROBE
完成了 BPF 程序中主动注册的过程,因此原来版本中的 b.attach_kprobe(event=b.get_syscall_fnname("open"), fn_name="trace_syscall_open")
也不再需要。调整后的完整代码如下,在线版本参考这里:
|
|
2.3 args
参数揭秘
对于 TRACEPOINT_PROBE
中出现的 args
我们还是抱有一种好奇的心理,这到底是怎么样的一个结构体定义呢?
在 Python 代码中的 BPF
对象(b = BPF(text=prog)
) 中包含了一定的调试的功能,我们可以通过调试功能来一览 args
的面目。BPF
对象的完整语法如下,参见这里。:
|
|
创建 BPF 对象。它是定义 BPF 程序的主要对象,并与它的输出进行交互。
必须提供 text
或 src_file
中的一个(不是两个)。
cflags
指定要传递给编译器的额外参数,例如 -DMACRO_NAME=value
或 -I/include/path
。参数以数组形式传递,每个元素都是一个附加参数。注意,字符串不会被分割成空白,所以每个参数必须是数组中的不同元素,例如:["-include", "header.h"]
。
debug
标志控制调试输出,可以通过或操作进行组合。
DEBUG_LLVM_IR = 0x1
编译LLVM IRDEBUG_BPF = 0x2
加载BPF字节码和分支上的寄存器状态DEBUG_PREPROCESSOR = 0x4
预处理器结果DEBUG_SOURCE = 0x8
ASM指令嵌入了源码DEBUG_BPF_REGISTER_STATE = 0x10
除DEBUG_BPF
外,所有指令的寄存器状态DEBUG_BTF = 0x20
打印来自libbpf
库的信息。
这里我们为 debug
参数传入 DEBUG_PREPROCESSOR
则可以得到预处理后的完成 BPF 代码。
主要调整如下:
|
|
再次运行程序程序,则可以看到程序运行结果的首部打印出了预编译后的 BPF 程序,这里我们看到了这个神秘的 args
结构题变量的定义,类型为 struct tracepoint__syscalls__sys_enter_open
,其中第一个字段为 u64 __do_not_use__;
,该字段为 ctx 的保留位置,这也是 args
可以作为 ctx
替代参数的原因。完整预处理结果如下:
|
|
C 语言版本的 tracepoint 样例参见这里,可以参考上述代码自己定义 args
对应的 struct
结构体。
3. 参考
- 原文作者:DavidDi
- 原文链接:https://www.ebpf.top/post/open_tracepoint_trace/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 最后更新时间:2022-11-05 21:36:52.144938002 +0800 CST