我们可以采用以下手段来trace内核调用,只说缺点:
- 侵入式插入ko,危险系数高
- 需要编写内核代码,难度系数大
- 需要编写stp代码,步骤较多
- 需要高版本内核支持
- 需要编写两处代码,步骤较多
- 配置步骤繁琐,从配置到看出效果,至少要经历五个以上的步骤
- 功能受限,对知识点要求较高
后来我发现了greg 写的一个kprobe 封装工具:https://github.com/brendangregg/perf-tools/blob/master/kernel/kprobe,它可以把繁杂的ftrace 一个 kprobe event 缩略为一个命令,极大拓展了我对ftrace的了解。然而这个工具使用起来仍有以下困难:
- 只能追踪一个kprobe点,我往往需要追踪多个kprobe点;
- 深入追踪困难:比如我们要在__netif_receive_skb_core 函数中打出skb参数中ip头里面的protocol成员,对应的表达式是 proto=+0x9(+0xf0(%di)):s8,光推导这个表达式的过程或许要耗费我们10分钟左右的时间。而且这个表达式并非固定不变,在不同的内核上还需要重新计算;
上述两点成为了我使用ftrace的拦路虎,一直想对它改造优化,但受限于自己的蹩脚的bash能力,进展比较慢。于是换了一个思路,改用python。
在后面的使用中,会用到两类表达式,一种是程序员可以直观通过结构体定义理解的,比如:
p __netif_receive_skb_core proto=@(struct iphdr *)l3%0->protocol ip_src=@(struct iphdr *)l3%0->saddr ip_dst=@(struct iphdr *)l3%0->daddr data=@(struct iphdr *)l3%0->sdata[1] f:proto==1&&ip_src==127.0.0.1
称为结构化表达式
这类表达式不能被ftrace识别,需要在surftrace中进行转换,转换后的
p __netif_receive_skb_core proto=+0x9(+0xf0(%di)):x8 ip_src=+0xc(+0xf0(%di)):x32 ip_dst=+0x10(+0xf0(%di)):x32 type=+0x14(+0xf0(%di)):x8 seq=+0x1c(+0xf0(%di)):s16 f:common_pid==0&&proto==1&&ip_src==0x100007f
称为ftrace表达式
如果你想使用surftrace的完整功能,至少需要以下条件:
- 内核支持ftrace、已经mount了debugfs、root权限
- python2.7或更高,推荐3 以上 下面的条件三选一即可
- 1、公开发行版内核,可以访问 pylcc.openanolis.cn
- 2、公开发行版内核,已经从 http://pylcc.openanolis.cn/db/ 下载了 对应内核的db文件
- 3.1、环境上安装了gdb 版本大于 9,如果是x86 平台,可以直接从 http://pylcc.openanolis.cn/gdb/ 下载
- 3.2、安装了对应内核的 vmlinux (结构化表达式依赖,非必须)
usage: surftrace.py [-h] [-v VMLINUX] [-m MODE] [-r RIP] [-f FILE] [-g GDB]
[-F FUNC] [-o OUTPUT] [-l LINE] [-a ARCH] [-s] [-S]
[traces [traces ...]]
Trace ftrace kprobe events.
positional arguments:
trace set trace args.
optional arguments:
-h, --help show this help message and exit
-v VMLINUX, --vmlinux VMLINUX
set vmlinux path.
-m MODE, --mode MODE set arg parser, fro
-r RIP, --rip RIP set remote server ip, remote mode only.
-f FILE, --file FILE set input args path.
-g GDB, --gdb GDB set gdb exe file path.
-F FUNC, --func FUNC disasassemble function.
-o OUTPUT, --output OUTPUT
set output bash file
-l LINE, --line LINE get file disasemble info
-a ARCH, --arch ARCH set architecture.
-s, --stack show call stacks.
-S, --show only show expressions.
-f: 从文件中读取表达式,适合大量配置的场景
-o: 将执行过程导出到脚本中
-a:指定cpu架构,涉及到寄存器转换,目前只支持x86_64/aarch64,不指定的话,会根据lscpu获取
-s:打印probe点调用栈,这是个全局开关
-S:只生成ftrace表达式,不下发到ftrace。该模式适合交叉调试场景,比如我们要想在树莓派上去probe 钩子,但是树莓派的资源空间有限,不可能去安装gdb和vmlinux。因此我们可以在宿主机上将结构化表达式转成ftrace表达式。然后在树莓派下发即可。
我们以open anolis为例,将surftrace.py取下来
sudo sh -c su
chmod +x surftrace.py
按Ctrl + C 停止
#./surftrace.py 'p _do_fork' 'r _do_fork'
echo 'p:f0 _do_fork ' >> /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/f0/enable
echo 'r:f1 _do_fork ' >> /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/f1/enable
echo 0 > /sys/kernel/debug/tracing/options/stacktrace
echo 1 > /sys/kernel/debug/tracing/tracing_on
<...>-1637241 [000] d... 7462686.335257: f0: (_do_fork+0x0/0x3a0)
<...>-1637241 [000] d... 7462686.335323: f1: (SyS_clone+0x36/0x40 <- _do_fork)
systemd-1 [004] d... 7462686.854375: f0: (_do_fork+0x0/0x3a0)
systemd-1 [004] d... 7462686.854446: f1: (SyS_clone+0x36/0x40 <- _do_fork)
……
systemd-1 [004] d... 7462688.104383: f0: (_do_fork+0x0/0x3a0)
systemd-1 [004] d... 7462688.104464: f1: (SyS_clone+0x36/0x40 <- _do_fork)
^Cecho 0 > /sys/kernel/debug/tracing/events/kprobes/f0/enable
<...>-1637241 [000] d... 7462688.134451: f0: (_do_fork+0x0/0x3a0)
<...>-1637241 [000] d... 7462688.135278: f1: (SyS_clone+0x36/0x40 <- _do_fork)
echo 0 > /sys/kernel/debug/tracing/events/kprobes/f1/enable
<...>-1637241 [000] d... 7462688.155188: f1: (SyS_clone+0x36/0x40 <- _do_fork)
echo > /sys/kernel/debug/tracing/kprobe_events
echo 0 > /sys/kernel/debug/tracing/tracing_on
可以看到 surftrace支持多个probe,所有表达式要用单引号括起来,表达式中,第一段字母p 表示probe函数入口,r表示probe函数返回位置,第二段为函数符号,该符号必须要在 tracing/available_filter_functions 中可以查找到的
还是以_do_fork为例,我们可以查找到它的入参是:
#ifdef CONFIG_FORK2
long _do_fork(struct task_struct *parent,
struct task_struct *source,
unsigned long clone_flags,
#else
long _do_fork(unsigned long clone_flags,
#endif
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls)
我们可以确认它的第一个入参类型是 struct task_struct,如果要获取任务名,即common,可以采用以下方法:
#./surftrace.py 'p _do_fork comm=%0->comm'
echo 'p:f0 _do_fork comm=+0xafc(%di):string ' >> /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/f0/enable
echo 0 > /sys/kernel/debug/tracing/options/stacktrace
echo 1 > /sys/kernel/debug/tracing/tracing_on
<...>-1642046 [001] d... 7463503.175187: f0: (_do_fork+0x0/0x3a0) comm="surftrace.py"
systemd-1 [002] d... 7463503.606161: f0: (_do_fork+0x0/0x3a0) comm="systemd"
python-16819 [003] d... 7463504.383400: f0: (_do_fork+0x0/0x3a0) comm="python"
systemd-1 [002] d... 7463504.856166: f0: (_do_fork+0x0/0x3a0) comm="systemd"
<...>-1642087 [002] d... 7463506.031046: f0: (_do_fork+0x0/0x3a0) comm="sh"
<...>-1642087 [002] d... 7463506.031363: f0: (_do_fork+0x0/0x3a0) comm="sh"
systemd-1 [004] d... 7463506.106159: f0: (_do_fork+0x0/0x3a0) comm="systemd"
^Cecho 0 > /sys/kernel/debug/tracing/events/kprobes/f0/enable
<...>-1642046 [001] d... 7463506.356102: f0: (_do_fork+0x0/0x3a0) comm="surftrace.py"
echo > /sys/kernel/debug/tracing/kprobe_events
echo 0 > /sys/kernel/debug/tracing/tracing_on
参数表达式中,第一个 comm是变量名,可以自己定义,%0 表示第一个入参,%1 表示第二入参,以此类推。~连接符号表示的是后面会紧跟结构化成员,surftrace会根据解析结果得到comm成员类型是string并显示出来。
仍以 _do_fork 和 struct task_struct为例,在一个结构化表达式中,uesrs=%0->mm->mm_users,入参编号%0与连接符~中间增加了一个S字母来指定整数显示格式,共有SUX三种类型,分别对应有符号十进制、无符号十进制和十六进制。如果不指定,默认是X,16进制
#级联指针和指定整数数据格式
./surftrace.py 'p _do_fork comm=%0->comm uesrs=S%0->mm->mm_users'
echo 'p:f0 _do_fork comm=+0xafc(%di):string uesrs=+0x48(+0x858(%di)):s32 ' >> /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/f0/enable
echo 0 > /sys/kernel/debug/tracing/options/stacktrace
echo 1 > /sys/kernel/debug/tracing/tracing_on
<...>-1650321 [005] d... 7464948.730210: f0: (_do_fork+0x0/0x3a0) comm="surftrace.py" uesrs=1
systemd-1 [000] d... 7464949.359231: f0: (_do_fork+0x0/0x3a0) comm="systemd" uesrs=1
<...>-1650361 [005] d... 7464949.424381: f0: (_do_fork+0x0/0x3a0) comm="sh" uesrs=1
<...>-1650361 [005] d... 7464949.424606: f0: (_do_fork+0x0/0x3a0) comm="sh" uesrs=1
python-16819 [004] d... 7464950.235552: f0: (_do_fork+0x0/0x3a0) comm="python" uesrs=4
\....
#级联结构体成员
./surftrace.py 'p _do_fork comm=%0->comm node=%0->pids[1].node.next'
echo 'p:f0 _do_fork comm=+0xafc(%di):string node=+0x988(%di):x64 ' >> /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/f0/enable
echo 0 > /sys/kernel/debug/tracing/options/stacktrace
echo 1 > /sys/kernel/debug/tracing/tracing_on
<...>-1652761 [000] d... 7465380.899543: f0: (_do_fork+0x0/0x3a0) comm="surftrace.py" node=0x0
systemd-1 [000] d... 7465381.610144: f0: (_do_fork+0x0/0x3a0) comm="systemd" node=0x0
python-16819 [001] d... 7465382.017634: f0: (_do_fork+0x0/0x3a0) comm="python" node=0xffff88062231be08
过滤器需要放在表达式最后,以f:开头,可以使用括号和&& ||逻辑表达式进行组合,具体写法可以参考ftrace文档说明
./surftrace.py 'p _do_fork comm=%0->comm uesrs=S%0->mm->mm_users f:comm==systemd'
echo 'p:f0 _do_fork comm=+0xafc(%di):string uesrs=+0x48(+0x858(%di)):s32' >> /sys/kernel/debug/tracing/kprobe_events
echo 'comm==systemd' > /sys/kernel/debug/tracing/instances/surftrace/events/kprobes/f0/filter
echo 1 > /sys/kernel/debug/tracing/instances/surftrace/events/kprobes/f0/enable
echo 0 > /sys/kernel/debug/tracing/instances/surftrace/options/stacktrace
echo 1 > /sys/kernel/debug/tracing/instances/surftrace/tracing_on
systemd-1 [002] d... 4817737.060026: f0: (_do_fork+0x0/0x3a0) comm="systemd" uesrs=1
systemd-1 [002] d... 4817738.310035: f0: (_do_fork+0x0/0x3a0) comm="systemd" uesrs=1
systemd-1 [002] d... 4817739.560046: f0: (_do_fork+0x0/0x3a0) comm="systemd" uesrs=1
systemd-1 [001] d... 4817740.607892: f0: (_do_fork+0x0/0x3a0) comm="systemd" uesrs=1
systemd-1 [001] d... 4817741.810041: f0: (_do_fork+0x0/0x3a0) comm="systemd" uesrs=1
#./surftrace.py 'p _do_fork comm=%0->comm users=S%0->mm->mm_users f:comm==python||users<4'
echo 'p:f0 _do_fork comm=+0xafc(%di):string users=+0x48(+0x858(%di)):s32 ' >> /sys/kernel/debug/tracing/kprobe_events
echo 'comm==python||users<4' > /sys/kernel/debug/tracing/events/kprobes/f0/filter
echo 1 > /sys/kernel/debug/tracing/events/kprobes/f0/enable
echo 0 > /sys/kernel/debug/tracing/options/stacktrace
echo 1 > /sys/kernel/debug/tracing/tracing_on
<...>-1655729 [004] d... 7465872.793430: f0: (_do_fork+0x0/0x3a0) comm="surftrace.py" users=1
systemd-1 [000] d... 7465872.861176: f0: (_do_fork+0x0/0x3a0) comm="systemd" users=1
systemd-1 [000] d... 7465874.111185: f0: (_do_fork+0x0/0x3a0) comm="systemd" users=1
<...>-1655773 [003] d... 7465874.123909: f0: (_do_fork+0x0/0x3a0) comm="sh" users=1
<...>-1655773 [003] d... 7465874.124134: f0: (_do_fork+0x0/0x3a0) comm="sh" users=2
python-16819 [002] d... 7465874.909735: f0: (_do_fork+0x0/0x3a0) comm="python" users=4
systemd-1 [000] d... 7465875.361189: f0: (_do_fork+0x0/0x3a0) comm="systemd" users=1
我们还会常用common_pid作为current tid进行过滤,该变量由系统提供,无需定义
#./surftrace.py 'p _do_fork comm=%0->comm users=S%0->mm->mm_users f:common_pid==1&&(comm==python||users<4)'
echo 'p:f0 _do_fork comm=+0xafc(%di):string users=+0x48(+0x858(%di)):s32 ' >> /sys/kernel/debug/tracing/kprobe_events
echo 'common_pid==1&&(comm==python||users<4)' > /sys/kernel/debug/tracing/events/kprobes/f0/filter
echo 1 > /sys/kernel/debug/tracing/events/kprobes/f0/enable
echo 0 > /sys/kernel/debug/tracing/options/stacktrace
echo 1 > /sys/kernel/debug/tracing/tracing_on
systemd-1 [004] d... 7466113.361698: f0: (_do_fork+0x0/0x3a0) comm="systemd" users=1
systemd-1 [004] d... 7466114.611729: f0: (_do_fork+0x0/0x3a0) comm="systemd" users=1
systemd-1 [004] d... 7466115.861707: f0: (_do_fork+0x0/0x3a0) comm="systemd" users=1
systemd-1 [004] d... 7466117.111716: f0: (_do_fork+0x0/0x3a0) comm="systemd" users=1
以一下汇编代码为例,要获取偏移21位置时的%r12值
disas _do_fork
Dump of assembler code for function _do_fork:
0xffffffff8108a560 <+0>: callq 0xffffffff8174db10 <__fentry__>
0xffffffff8108a565 <+5>: push %rbp
0xffffffff8108a566 <+6>: mov %rsp,%rbp
0xffffffff8108a569 <+9>: push %r15
0xffffffff8108a56b <+11>: push %r14
0xffffffff8108a56d <+13>: push %r13
0xffffffff8108a56f <+15>: push %r12
0xffffffff8108a571 <+17>: xor %r14d,%r14d
0xffffffff8108a574 <+20>: push %rbx
0xffffffff8108a575 <+21>: mov %rdx,%r12
#./surftrace.py 'p _do_fork+21 r=%r12'
echo 'p:f0 _do_fork+21 r=%r12 ' >> /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/f0/enable
echo 0 > /sys/kernel/debug/tracing/options/stacktrace
echo 1 > /sys/kernel/debug/tracing/tracing_on
<...>-1659986 [000] d... 7466637.258116: f0: (_do_fork+0x15/0x3a0) r=0x38
python-16819 [004] d... 7466637.577201: f0: (_do_fork+0x15/0x3a0) r=0x38
<...>-497835 [004] d... 7466638.002734: f0: (_do_fork+0x15/0x3a0) r=0x3a
<...>-497835 [004] d... 7466638.003674: f0: (_do_fork+0x15/0x3a0) r=0x38
同样的,也可以对寄存器对应的指针进行解析和过滤,这里就不在详细展开了
这个和原kprobe方法一致,返回值用$retval 表示
#./surftrace.py 'r _do_fork r=$retval'
echo 'r:f0 _do_fork r=$retval ' >> /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/f0/enable
echo 0 > /sys/kernel/debug/tracing/options/stacktrace
echo 1 > /sys/kernel/debug/tracing/tracing_on
<...>-1661938 [000] d... 7466977.685020: f0: (SyS_clone+0x36/0x40 <- _do_fork) r=0x195bfe
systemd-1 [004] d... 7466977.863627: f0: (SyS_clone+0x36/0x40 <- _do_fork) r=0x195bff
systemd-1 [004] d... 7466979.113626: f0: (SyS_clone+0x36/0x40 <- _do_fork) r=0x195c00
<...>-497835 [000] d... 7466979.731526: f0: (sys_vfork+0x3c/0x40 <- _do_fork) r=0x195c01
sk_buff 是linux网络协议栈重要的结构体,但是通过上面的方法,并不能直接解析到我们关注的报文内容,需要进行特殊处理。以追踪icmp接收ping报文为例,我们在__netif_receive_skb_core 函数中进行probe和过滤
#./surftrace.py 'p __netif_receive_skb_core proto=@(struct iphdr *)l3%0->protocol ip_src=@(struct iphdr *)%0->saddr ip_dst=@(struct iphdr *)l3%0->daddr data=X@(struct iphdr *)l3%0->sdata[1] f:proto==1&&ip_src==127.0.0.1'
echo 'p:f0 __netif_receive_skb_core proto=+0x9(+0xf0(%di)):u8 ip_src=+0xc(+0xf0(%di)):u32 ip_dst=+0x10(+0xf0(%di)):u32 data=+0x16(+0xf0(%di)):x16' >> /sys/kernel/debug/tracing/kprobe_events
echo 'proto==1&&ip_src==0x100007f' > /sys/kernel/debug/tracing/instances/surftrace/events/kprobes/f0/filter
echo 1 > /sys/kernel/debug/tracing/instances/surftrace/events/kprobes/f0/enable
echo 0 > /sys/kernel/debug/tracing/instances/surftrace/options/stacktrace
echo 1 > /sys/kernel/debug/tracing/instances/surftrace/tracing_on
<...>-2076163 [000] d.s1 4818041.743856: f0: (__netif_receive_skb_core+0x0/0xa80) proto=1 ip_src=127.0.0.1 ip_dst=127.0.0.1 data=0x4dec
<...>-2076163 [000] d.s1 4818041.743905: f0: (__netif_receive_skb_core+0x0/0xa80) proto=1 ip_src=127.0.0.1 ip_dst=127.0.0.1 data=0x4df4
<...>-2076163 [000] d.s1 4818042.767865: f0: (__netif_receive_skb_core+0x0/0xa80) proto=1 ip_src=127.0.0.1 ip_dst=127.0.0.1 data=0xef26
<...>-2076163 [000] d.s1 4818042.767914: f0: (__netif_receive_skb_core+0x0/0xa80) proto=1 ip_src=127.0.0.1 ip_dst=127.0.0.1 data=0xef2e
<...>-2076163 [000] d.s1 4818043.791858: f0: (__netif_receive_skb_core+0x0/0xa80) proto=1 ip_src=127.0.0.1 ip_dst=127.0.0.1 data=0x9069
<...>-2076163 [000] d.s1 4818043.791905: f0: (__netif_receive_skb_core+0x0/0xa80) proto=1 ip_src=127.0.0.1 ip_dst=127.0.0.1 data=0x9071
<...>-2076163 [000] d.s1 4818044.815861: f0: (__netif_receive_skb_core+0x0/0xa80) proto=1 ip_src=127.0.0.1 ip_dst=127.0.0.1 data=0x31a6
<...>-2076163 [000] d.s1 4818044.815911: f0: (__netif_receive_skb_core+0x0/0xa80) proto=1 ip_src=127.0.0.1 ip_dst=127.0.0.1 data=0x31ae
协议的获取表达式为 @(struct iphdr *)l3%0->protocol,和之前不一样的是,寄存器的结构体名左括号加了@符号进行特殊标记,表示需要用该结构体来解析skb->data指针数据,结构体名和右括号后加了l3标记(命名为右标记),表示当前skb->data指向了TCP/IP 层3位置。
右标记有l2、l3、l4三个选项,也可以不标记,默认为l3,如 ip_src=@(struct iphdr *)%0->saddr,没有右标记。
报文结构体有 'struct ethhdr', 'struct iphdr', 'struct icmphdr', 'struct tcphdr', 'struct udphdr'五类,如果协议栈层级和报文结构体对应不上,解析器会报参数错误,如右标记为l3,但是报文结构体是 struct ethhdr类型;
'struct icmphdr', 'struct tcphdr', 'struct udphdr'这三个4层结构体增加了xdata成员,用于获取协议对应报文内容。xdata有 cdata. sdata, ldata, qdata 四类场景,位宽对应 1 2 4 8. 数组下标是按照位宽进行对齐的,如实例表达式中的 data=%0~$(struct icmphdr)l3->sdata[1],sdata[1]表示要提取icmp报文中的2~3字节内容
surftrace 会对以 ip_xx开头的变量进行ipv4<->u32 ,如 ip_src=@(struct iphdr *)%0->saddr,会转成对应的IP格式。对B16_、B32_、B64_、b16_、b32_、b64_开头的变量也会进行大小端转换,B开头按照16进制输出,b以10进制输出。
trace event 信息参考 /sys/kernel/debug/tracing/events目录下的事件 描述,以追踪wakeup等待超过10ms任务为例,
#./surftrace.py 'e sched/sched_stat_wait f:delay>1000000'
echo 'delay>1000000' > /sys/kernel/debug/tracing/instances/surftrace/events/sched/sched_stat_wait/filter
echo 1 > /sys/kernel/debug/tracing/instances/surftrace/events/sched/sched_stat_wait/enable
echo 0 > /sys/kernel/debug/tracing/instances/surftrace/options/stacktrace
echo 1 > /sys/kernel/debug/tracing/instances/surftrace/tracing_on
<idle>-0 [001] dN.. 11868700.419049: sched_stat_wait: comm=h2o pid=3046552 delay=87023763 [ns]
<idle>-0 [005] dN.. 11868700.419049: sched_stat_wait: comm=h2o pid=3046617 delay=87360020 [ns]