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类:静态插桩点、动态插桩点、内核软件插桩点、硬件插桩点、时间事件插桩点、特殊事件插桩点。

probes

这个图把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种:

  1. 内置变量
  2. 临时变量,语法是$name,比如BEGIN { $x=1; printf("%d",$x) }
  3. 映射表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程序不难,难的是知道什么情况下去追踪哪些函数、获得的数据又意味着什么,这就要求对操作系统、网络等基础知识以及目标程序比较熟悉了。

参考链接