ebpf 支持
- hardware
- iter
- kfunc
- kprobe
- software
- tracepoint
以及 uprobe 等几大类探针,我们从最常用的应用场景:探测内核和应用函数调用入手。
dynamic tracing
首先是动态 trace,ebpf 提供了
- 面向内核的 kprobe/kretprobe,k = kernel
- 面向应用的 uprobe/uretprobe,u = user land
分别用于探测函数入口处和函数返回 (ret) 处的信息。
kprobe/kretprobe 可以探测内核大部分函数,出于安全考虑,有部分内核函数不允许安装探针,另外也可以配合 offset 探测函数中任意位置的信息。
你可以这样查看 kprobe 的黑名单:
cat /sys/kernel/debug/kprobes/blacklist
uprobe/uretprobe 则可以为应用的任意函数安装探针。
动态 trace 技术依赖内核和应用的符号表,对于那些 inline 或者 static 函数则无法直接安装探针,需要自行通过 offset 实现。可以借助 nm
或者 strings
指令查看应用的符号表。
这两种动态 trace 技术的原理与 GDB 类似,当对某段代码安装探针,内核会将目标位置指令复制一份,并替换为 int3
中断, 执行流跳转到用户指定的探针 handler,再执行备份的指令,如果此时也指定了 ret 探针,也会被执行,最后再跳转回原来的指令序列。
kprobe 和 uprobe 可以通过 arg0
、arg1
... ... 访问所有参数;kretprobe 和 uretprobe 通过 retval
访问函数的返回值。除了基本类型:char、int 等,字符串需通过 str()
函数才能访问。
# bpftrace -e 'uprobe:/lib/x86_64-linux-gnu/libc-2.23.so:fopen { printf("fopen: %s\n", str(arg0)); }'
Attaching 1 probe...
fopen: /proc/filesystems
fopen: /usr/share/locale/locale.alias
fopen: /proc/self/mountinfo
复合参数
动态 trace 除了可以访问基本类型参数,当然也可以访问有高级结构的参数,前提是 bpftrace 代码中可以获得该结构的定义。
有 2 种方式描述这些参数的结构:
对于结构简单的参数,可以直接在 bpftrace 代码中按 C 语言风格定义复杂参数的结构。必须确保 bpftrace 可以获取需要访问成员的准确 offset 和实际大小,结构体前半部分其他成员可以准确定义,也定义可以替换为更简单的
char[N]
,只要 offset 与实际一致即可,关心的成员之后其他部分,可以不定义:/* 比如实际参数定义如下 */ struct arg_s{ struct arg_s1 mem1; struct arg_s1 mem2; int32_t x; int32_t y; char *name; .... // other members }; /* 当我们只关心 name,且知道结构体前 4 个成员总大小为 64,那么在 bpftrace 中可以这样写*/ # someprobe.bt struct arg_s{ /* name 前其他成员替换为 padding,只要 offset 一致*/ char padding[64]; char *name; /* name 之后成员无需定义 */ }; someprobe:funcx { printf("%s\n", str(((struct arg_s *)arg0)->name); }
某些情况下参数结构复杂,也可按照 C 风格
#include
头文件,让 bpftrace 知道参数的定义。# cat path.bt #include <linux/path.h> #include <linux/dcache.h> kprobe:vfs_open { printf("open path: %s\n", str(((struct path *)arg0)->dentry->d_name.name)); }
需要特别注意结构体可能发生的内存对齐,否则可能获得错误的成员 offset。
static tracing
另一种常用的探针是静态 trace,所谓 “静态” 是指探针的位置、名称都是在代码中硬编码的,编译时就确定了。静态 trace 的实现原理类似 callback,当被激活时执行,关闭时不执行,性能比动态 trace 高一些。
- 其中
tracepoint
是内核中的, - usdt = Userland Statically Defined Tracing,是应用中的。
静态 trace 已经在内核和应用中饱含了探针参数信息,可以直接通过 args->参数名
访问函数参数。tracepoint 的 参数 format 信息可以通过 bpftrace -v probe
查看:
# bpftrace -lv tracepoint:raw_syscalls:sys_exit
tracepoint:raw_syscalls:sys_exit
long id;
long ret;
或者访问 debugfs:
# cat /sys/kernel/debug/tracing/events/raw_syscalls/sys_exit/format
name: sys_exit
ID: 20
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:long id; offset:8; size:8; signed:1;
field:long ret; offset:16; size:8; signed:1;
print fmt: "NR %ld = %ld", REC->id, REC->ret
下面是一个官方示例,通过 args->filename
访问 sys_enter_openat
的 filename
参数:
# bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
Attaching 1 probe...
irqbalance /proc/interrupts
irqbalance /proc/stat
snmpd /proc/diskstats
snmpd /proc/stat
snmpd /proc/vmstat
snmpd /proc/net/dev
[...]