eBPF学习笔记3
这篇文章主要来说说bpftrace,这个语法十分的简单而且功能还在不停的完善中,后续变化一定以官网文档为准。
基本语法
bpftrace程序由3个部分组成:
探针probes /过滤器filters/ {动作actions}
既可以把程序保存为.bt
文件后(建议而不强制)使用btftrace xxx.bt
来执行,也可以通过单行指令bpftrace -e 'probes /filters/ { actions }'
来执行。
其中,过滤器部分可以省略掉,不省略的话只有符合过滤器条件时才会执行动作。
支持?::
三元操作符、if{...}else{...}
语句、unroll (count) {statements}
这种有界的循环、while (condition) {...}
(5.3+内核版本添加的实验性支持)、[]
数组、(,)
元组、以及C语言中的常见运算符。
probes
探针可以分为6类:静态插桩点、动态插桩点、内核软件插桩点、硬件插桩点、时间事件插桩点、特殊事件插桩点。
这个图把usdt放到uprobe下面主要想表达这个插桩点属于用户态,而usdt是属于静态插桩点的。
常用的探针如下:
名称 | 缩写 | 描述 | 语法 |
---|---|---|---|
tracepoint | t | 内核态 静态 插桩点 | tracepoint:name |
usdt | U | 用户态 静态 插装点 | usdt:binary_path:probe_name 或者 usdt:binary_path:[probe_namespace]:probe_name 或者 usdt:library_path:probe_name 或者 usdt:library_path:[probe_namespace]:probe_name |
kprobe | k | 内核态 动态函数 插桩 | kprobe:function_name[+offset] |
kretprobe | kr | 内核态 动态函数 返回值 插桩 | kretprobe:function_name |
uprobe | u | 用户态 动态函数 插桩 | uprobe:library_name:function_name[+offset] 或者 uprobe:library_name:address |
uretprobe | ur | 用户态 动态函数返回值 插桩 | uretprobe:library_name:function_name |
software | s | 内核软件事件 | software:event_name:count |
hardware | h | 基于硬件计数器插桩 | hardware:event_name:count |
profile | p | 对全部CPU进行采样 | profile:[hz、s、ms、us]:rate |
interval | i | 周期性报告(从一个CPU上) | interval:[hz、s、ms、us]:rate |
BEGIN | bpftrace启动执行 | BEGIN | |
END | bpftrace结束执行 | END |
那么接下来,如何知道哪些函数可以用来插桩呢?第一种方式可以使用sudo perf list [hw|sw|cache|tracepoint|pmu|sdt|metric|metricgroup]
来查找,但更建议使用bpftrace -l
命令来查找,这个命令还支持过滤,比如:
root@iZj6c18dsejt417tcv4rv2Z:~# bpftrace -l "*fdb*"
tracepoint:bridge:br_fdb_add
tracepoint:bridge:br_fdb_external_learn_add
tracepoint:bridge:fdb_delete
tracepoint:bridge:br_fdb_update
kprobe:ndo_dflt_fdb_add
kprobe:ndo_dflt_fdb_del
kprobe:valid_fdb_dump_strict.constprop.0
kprobe:valid_fdb_get_strict.constprop.0
kprobe:rtnl_fdb_get
kprobe:valid_fdb_dump_legacy.constprop.0
kprobe:nlmsg_populate_fdb_fill.constprop.0
kprobe:rtnl_fdb_notify
kprobe:rtnl_fdb_add
kprobe:rtnl_fdb_del
kprobe:nlmsg_populate_fdb
kprobe:ndo_dflt_fdb_dump
...
还可以添加-v
参数查看具体参数细节,比如:
# bpftrace -lv tracepoint:syscalls:sys_enter_open
tracepoint:syscalls:sys_enter_open
int __syscall_nr;
const char * filename;
int flags;
umode_t mode;
如果使用man 2 open
查看这个系统函数,会发现bpftrace -lv
命令返回的参数多了一个int __syscall_nr
,这个参数代表了系统调用号。另外的建议就是在kprobe和tracepoint两者都可用的情况下,应该选择tracepoint,来保证程序的可移植性。
变量
bpftrace中变量有3种:
- 内置变量
- 临时变量,语法是
$name
,比如BEGIN { $x=1; printf("%d",$x) }
- 映射表map,可以当作全局变量来用,语法是
@name
或者@name[xxx,xxx]
,后者可以类比为python中的dict。
内置变量有下面几种:
- pid - 进程ID(内核的tgid)
- tid - 线程ID (内核的pid)
- uid - 用户ID
- gid - 用户组ID
- nsecs - 时间戳,单位纳秒
- elapsed - 时间差,单位纳秒,从程序启动开始计时
- cpu - 处理器ID
- comm - 进程名
- kstack - 内核调用栈信息
- ustack - 用户态调用栈信息
- arg0, arg1, …, argN. - 某些探针类型的参数
- args - 某些探针类型的参数
- retval - 某些探针类型的返回值
- func - 被跟踪的函数名称
- probe - 探针全名
- curtask - 内核tast_struct地址,类型为64位无符号整数
- rand - 随机数,类型为u32
- cgroup - 当前进程cgroup ID
- cpid - 子进程id,仅当使用
-c
参数时有效 - $1, $2, …, $N, $#. - bpftrace程序自身的位置参数
内置函数
这里不打算介绍了,目前bpftrace提供了大概28个内置函数以及9个操作映射表的函数,具体的用法见下面的reference_guide链接。
目前bpftrace还不支持自定义函数,所以复杂一些的功能还是建议使用BCC或者libbpf。
总结
相信有编程基础的人大概30分钟就能学会如何使用bpftrace编写程序,bpftrace程序通常用在快速排查和定位系统上。
而且bpftrace的功能有限,不支持特别复杂的eBPF程序,依赖于BCC和LLVM动态编译执行。
写一个bpftrace程序不难,难的是知道什么情况下去追踪哪些函数、获得的数据又意味着什么,这就要求对操作系统、网络等基础知识以及目标程序比较熟悉了。
参考链接
- bpftrace(8) Manual Page
- tutorial_one_liners
- reference_guide,建议没事上去看一看,最新的语法、特性都会更新在这里。