# coolKprobe **Repository Path**: chuyansz/cool-kprobe ## Basic Information - **Project Name**: coolKprobe - **Description**: coolKprobe,基于ftrace的内核符号probe工具 - **Primary Language**: Python - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 6 - **Forks**: 1 - **Created**: 2021-07-08 - **Last Updated**: 2023-07-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 1、产生背景 ​ 我们可以采用以下手段来trace内核函数,只说缺点: ## 1.1、kprobe/jprobe/kretprobe - 侵入式插入ko,危险系数高 - 需要编写内核代码,难度系数大 ## 1.2、systemtap - 需要编写stp代码,步骤较多 ## 1.3、bpf(含bcc和libebpf) - 需要高版本内核支持 - 需要编写两处代码,步骤较多 ## 1.4、ftrace-kprobe - 配置步骤繁琐,从配置到看出效果,至少要经历五个以上的步骤 - 功能受限,对知识点要求较高 ## 1.5、perf-tools kprobe 后来我发现了brendan gregg 写的一个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。 # 2、coolKprobe 准备工作 ## 2.1、命名约定 在后面的使用中,会用到两类表达式,一种是程序员可以直观通过结构体定义理解的,比如: ```bash p __netif_receive_skb_core proto=%0~$(struct iphdr)l3->protocol ip_src=%0~$(struct iphdr)l3->saddr ip_dst=%0~$(struct iphdr)l3->daddr type=%0~$(struct icmphdr)l3->type seq=%0S~$(struct icmphdr)l3->sdata[0] f:common_pid==0&&proto==1&&ip_src==127.0.0.1 ``` 称为**结构化表达式** 这类表达式不能被ftrace识别,需要在coolKprobe中进行转换,转换后的 ```bash 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表达式** # 2.2、依赖条件 如果你想使用coolKprobe的完整功能,至少需要以下条件: - 内核支持ftrace、已经mount了debugfs、root权限(show模式不需要这些) - python2.7或更高,我的代码只在2.7上验证过,3.x版本未验证过。 - 环境上安装了gdb(结构化表达式依赖,非必须) - 安装了对应内核的 vmlinux (结构化表达式依赖,非必须) ## 2.3、参数说明 ```bash usage: coolKprobe.py [-h] [-v VMLINUX] [-f FILE] [-a ARCH] [-s] [-S] trace [trace ...] 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. -f FILE, --file FILE set input args path. -a ARCH, --arch ARCH set architecture. -s, --stack show call stacks. -S, --show only show expressions. ``` -v: 指定vmlinux 路径,如果是centos,默认路径是是在 /usr/lib/debug/usr/lib/modules/$(uname -r)/vmlinux路径下 -f: 从文件中读取表达式,适合大量配置的场景 -a:指定cpu架构,涉及到寄存器转换,目前只支持x86/arm/aarch64,不指定的话,会从lscpu获取 -s:打印probe点调用栈,这是个全局开关 -S:只生成ftrace表达式,不下发到ftrace。该模式适合交叉调试场景,比如我们要想在树莓派上去probe 钩子,但是树莓派的资源空间有限,不可能去安装gdb和vmlinux。因此我们可以在宿主机上将结构化表达式转成ftrace表达式。然后在树莓派下发即可。 # 3、实战 我们以centos7为例,已经安装gdb和vmlinux在默认路径下。将coolKprobe.py取下来 ``` sudo sh -c su chmod +x coolKprobe.py ``` ## 3.1、追踪函数入口和返回位置 按Ctrl + C 停止 ```bash #./coolKprobe.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 ``` 可以看到 coolKprobe支持多个probe,所有表达式要用单引号括起来,表达式中,第一段字母p 表示probe函数入口,r表示probe函数返回位置,第二段为函数符号,该符号必须要在 tracing/available_filter_functions 中可以查找到的 ## 3.2、 获取函数入参 还是以_do_fork为例,我们可以查找到它的入参是: ```c #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,可以采用以下方法: ```bash #./coolKprobe.py 'p _do_fork comm=%0~(struct task_struct)->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="coolKprobe.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="coolKprobe.py" echo > /sys/kernel/debug/tracing/kprobe_events echo 0 > /sys/kernel/debug/tracing/tracing_on ``` 参数表达式中,第一个 comm是变量名,可以自己定义,%0 表示第一个入参,%1 表示第二入参,以此类推。~连接符号表示的是后面会紧跟结构化成员,coolKprobe会根据解析结果得到comm成员类型是string并显示出来。 ## 3.3 结构体级联和扩展 仍以 _do_fork 和 struct task_struct为例,在一个结构化表达式中,uesrs=%0**S**\~(struct task_struct)->mm->mm_users,入参编号%0与连接符~中间增加了一个S字母来指定整数显示格式。显示格式有SUX三种类型,分别对应有符号十进制、无符号十进制和十六进制。如果不指定,默认是X,即16进制 ```bash #级联指针和指定整数数据格式 ./coolKprobe.py 'p _do_fork comm=%0~(struct task_struct)->comm uesrs=%0S~(struct task_struct)->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="coolKprobe.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 …… #级联结构体成员 ./coolKprobe.py 'p _do_fork comm=%0~(struct task_struct)->comm node=%0~(struct task_struct)->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="coolKprobe.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 ``` ## 3.4 设置过滤器 过滤器需要放在表达式最后,以f:开头,可以使用括号和&& ||逻辑表达式进行组合,具体写法可以参考ftrace文档说明 ```bash ./coolKprobe.py 'p _do_fork comm=%0~(struct task_struct)->comm uesrs=%0S~(struct task_struct)->mm->mm_users f:comm==python' echo 'p:f0 _do_fork comm=+0xafc(%di):string uesrs=+0x48(+0x858(%di)):s32 ' >> /sys/kernel/debug/tracing/kprobe_events echo 'comm==python' > /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 python-16819 [002] d... 7465697.128585: f0: (_do_fork+0x0/0x3a0) comm="python" uesrs=4 python-16819 [004] d... 7465700.404283: f0: (_do_fork+0x0/0x3a0) comm="python" uesrs=4 python-16819 [000] d... 7465703.648028: f0: (_do_fork+0x0/0x3a0) comm="python" uesrs=4 …… #./coolKprobe.py 'p _do_fork comm=%0~(struct task_struct)->comm users=%0S~(struct task_struct)->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="coolKprobe.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进行过滤,该变量由系统提供,无需定义 ```bash #./coolKprobe.py 'p _do_fork comm=%0~(struct task_struct)->comm users=%0S~(struct task_struct)->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 ``` ## 3.5 函数内部追踪 以一下汇编代码为例,要获取偏移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 ``` ``` #./coolKprobe.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 ``` 同样的,也可以对寄存器对应的指针进行解析和过滤,这里就不在详细展开了 ## 3.6、函数返回值获取 这个和原kprobe方法一致,返回值用$retval 表示 ``` #./coolKprobe.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 ``` ## 3.7、skb解析处理 sk_buff 是linux网络协议栈重要的结构体,但是通过上面的方法,并不能直接解析到我们关注的报文内容,需要进行特殊处理。以追踪icmp接收ping报文为例,我们在__netif_receive_skb_core 函数中进行probe和过滤 ```bash #./coolKprobe.py 'p __netif_receive_skb_core proto=%0~$(struct iphdr)l3->protocol ip_src=%0~$(struct iphdr)l3->saddr ip_dst=%0~$(struct iphdr)->daddr type=%0~$(struct icmphdr)l3->type data=%0~$(struct icmphdr)l3->sdata[1] f:proto==1&&ip_src==127.0.0.1' echo 'p:f0 __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 data=+0x1e(+0xf0(%di)):x16 ' >> /sys/kernel/debug/tracing/kprobe_events echo 'proto==1&&ip_src==0x100007f' > /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 <...>-1673938 [003] d.s1 7469068.654066: f0: (__netif_receive_skb_core+0x0/0xa80) proto=0x1 ip_src=127.0.0.1 ip_dst=127.0.0.1 type=0x8 data=0x60e5 <...>-1673938 [003] d.s1 7469068.654108: f0: (__netif_receive_skb_core+0x0/0xa80) proto=0x1 ip_src=127.0.0.1 ip_dst=127.0.0.1 type=0x0 data=0x60e5 <...>-1673938 [003] d.s1 7469069.678101: f0: (__netif_receive_skb_core+0x0/0xa80) proto=0x1 ip_src=127.0.0.1 ip_dst=127.0.0.1 type=0x8 data=0x60e5 <...>-1673938 [003] d.s1 7469069.678147: f0: (__netif_receive_skb_core+0x0/0xa80) proto=0x1 ip_src=127.0.0.1 ip_dst=127.0.0.1 type=0x0 data=0x60e5 <...>-1673938 [003] d.s1 7469070.702089: f0: (__netif_receive_skb_core+0x0/0xa80) proto=0x1 ip_src=127.0.0.1 ip_dst=127.0.0.1 type=0x8 data=0x60e5 <...>-1673938 [003] d.s1 7469070.702134: f0: (__netif_receive_skb_core+0x0/0xa80) proto=0x1 ip_src=127.0.0.1 ip_dst=127.0.0.1 type=0x0 data=0x60e5 <...>-1673938 [003] d.s1 7469071.726073: f0: (__netif_receive_skb_core+0x0/0xa80) proto=0x1 ip_src=127.0.0.1 ip_dst=127.0.0.1 type=0x8 data=0x60e5 <...>-1673938 [003] d.s1 7469071.726114: f0: (__netif_receive_skb_core+0x0/0xa80) proto=0x1 ip_src=127.0.0.1 ip_dst=127.0.0.1 type=0x0 data=0x60e5 ``` ``` 协议的获取表达式为 proto=%0~$(struct iphdr)l3->protocol,和之前不一样的是, ~连接符号的后的结构体名左括号加了 $ 符号进行特殊标记(左标记,必须),表示需要 用该结构体来解析skb->data指针数据,结构体名和右括号后加了l3标记(右标记, 非必须),表示当前skb->data指向了TCP/IP 层3位置。 右标记有l2、l3、l4三个选项,也可以不加,默认为l3, 如 ip_dst=%0~$(struct iphdr)->daddr,就没有加右标记。 报文结构体有 '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 四类场景,位宽 分别对应 8 16 32 64. 数组下标是按照位宽进行对齐的,如实例表达式中的 data=%0~$(struct icmphdr)l3->sdata[1],sdata[1]表示要提取icmp报文中的2~3字节内容 coolKprobe 会对以 ip_xx开头的变量进行ipv4<->u32 , 如 ip_src=%0~$(struct iphdr)l3->saddr,会转成对应的IP格式。 对B16\_、B32\_、B64\_、b16\_、b32\_、b64\_开头的变量也会进行大小端转换,B开头按照 16进制输出,b以10进制输出。 ```