kprobes(二)使用方法
0赞前言
上一节介绍了kprobe的基本概念,下面我们将使用几个具体的例子,看下kprobe在实际使用中有那些应用场景。
kprobe
内核的samples/kprobe目录下有kprobe相关的例子,我们以这些例子为基础,简单修改下。
查看函数的入参
我们所有的例子都是探测do_sys_open()
或者_do_fork()
,以下是内核中的源码。
实际调试中经常需要调查函数使用的变量的值。要在kprobes的侦测器内显示某个函数的局部变量的值,需要一些技巧,原因是在printk的参数中无法直接指定变量名,因此必须给侦测器函数提供一个pt_regs
结构,其中保存了指定地址的命令执行时的寄存器信息。
当然,不同架构下该结构的成员变量不尽相同,但用该结构可以显示变量等更为详细的信息。
ARM64,ARM32,X86的寄存器及其访问方式可以看文末的目录
我们以内核目录下的例程做一个简单修改,探测do_filp_open
函数,当打开testfile文件时,自动打印出文件的路径。
为了减少无效信息的打印,我们将handler_post,handler_fault直接注释掉。
当探测点do_filp_open
命中时,Kprobes调用handler_pre
。在handler_pre
根据struct filename *pathname
来获得文件的名字。
在x86_64架构中,函数的参数从左到右分别保存在rdi、rsi、rdx、rcx、r8、r9中,因此查看rdi和rsi就能得到第1个、第2个参数的值。
同理,在ARM64架构中, 函数的参数1~参数8分别保存到 X0~X7 寄存器中 ,剩下的参数从右往左依次入栈。因此,X0和X1分别存放dfd
, pathname
的值。
任意位置通过变量名获取信息
kprobes拥有更加强大的功能,那就是它能在内核的任意地址插入侦测器。此外,侦测器可以在任意地址的指令执行之前或之后执行,或者前后都执行。
因此,应当观察汇编代码,找到源代码中想要调查的位置对应于编译后的二进制文件中的什么地址,并调查希望显示的变量保存在哪个寄存器、哪个内存地址。
通常,我们希望在函数执行的过程中变量,即打印一些流程中的东西,而不是函数本身被调用,此时我们不能简单设置 kprobe->symbol_name
函数名字 ,假设我们期望获取 _do_fork
函数变量 nr
的值:
将vmlinux进行反汇编,找出_do_fork的地址。
nr
变量 是 函数pid_vnr
的返回值(也是子进程的pid) ,根据ARM调用规范,调用完成pid_vnr()
后,寄存器x0
存放的就是其函数返回值。
参考:ARM64调用标准 https://blog.51cto.com/u_15333820/3452605
通过反汇编可以知道,pid_vnr
在 ffffff80080ba930
地址处被调用,因此,侦测器的插入地址就是在ffffff80080ba930
之后,并且x0
被改变之前。只要符合这两个条件,放在哪里都无所谓。
因此,我们将kprobe的点设置为ffffff80080ba934
,然后获取 x0
,就能获取变量nr的值。
.offset
是探测点相对于_do_fork
的偏移,在注册时指定。我们这里的 offset = ffffff80080ba934 - ffffff80080ba83c = F8
。
另外,反汇编能力就是多看汇编以及找到几个关键点(例如常量,跳转语句)就能定位到汇编对应的源码了,这里不再展开了。
jprobe
与kprobes相比,jprobes能更容易地获取传给函数的参数。有几点需要注意:
处理程序应该有与被探测函数相同的参数列表和返回类型;
返回之前,必须调用
jprobe_return()
(处理程序实际上从未返回,因为jprobe_return()
将控制权返回给Kprobes) 。
使用kprobes时,必须通过寄存器或栈才能计算出参数的值。此外,计算方法还依赖于架构。
如果使用jprobes,那么无须了解架构的详细知识,也能简单地查看参数的值。
编译加载驱动程序
kretprobe
kretprobe 也是基于kprobe的,相比于kprobe和jprobe,实现相对复杂。下面我们以内核目录下的例程,简单分析下。
其中我们可以看到
struct kretprobe
结构体中 有struct kprobe
成员(kretprobe
时基于kprobe
实现的)。handler
:用户自定义回调函数,被探测函数返回后被调用,一般在这个函数中获取被探测函数的返回值。entry_handler
:用户自定义回调函数,这是Kretprobes
提供了一个可选的用户指定的处理程序,它在函数入口上运行。 每当kretprobe
放置在函数入口处的kprobe
被命中时,都会调用用户定义的entry_handler
,如果有的话。 如果entry_handler
返回 0(成功),则保证在函数返回时调用相应的返回处理程序。 如果entry_handler
返回非零错误,则 Kprobes 将返回地址保持原样,并且kretprobe
对该特定函数实例没有进一步的影响。maxactive
:被探测函数可以同时活动的最大实例数。来指定可以同时探测多少个指定函数的实例。register_kretprobe()
预分配指定数量的kretprobe_instance
对象。nmissed
:跟踪被探测函数的返回被忽略的次数(maxactive设置的过低)。data_size
:表示kretprobe私有数据的大小,在注册kretprobe时会根据该大小预留空间。free_instances
:表示空闲的kretprobe
运行实例链表,它链接了本kretprobe
的空闲实例struct kretprobe_instance
结构体表示。
这个结构体表示
kretprobe
的运行实例,前文说过被探测函数在跟踪期间可能存在并发执行的现象,因此kretprobe使用一个kretprobe_instance
来跟踪一个执行流,支持的上限为maxactive
。在没有触发探测时,所有的kretprobe_instance
实例都保存在free_instances
表中,每当有执行流触发一次kretprobe
探测,都会从该表中取出一个空闲的kretprobe_instance
实例用来跟踪。kretprobe_instance
结构提中的rp指针指向所属的kretprobe;ret_addr
用于保存原始被探测函数的返回地址(后文会看到被探测函数返回地址会被暂时替换);task用于绑定其跟踪的进程;
data
保存用户使用的kretprobe
私有数据,它会在整个kretprobe
探测运行期间在entry_handler
和handler
回调函数之间进行传递(一般用于实现统计被探测函数的执行耗时)。
register_kretprobe
kretprobe
探测点的blackpoint
,用来表示不支持kretprobe探测的函数的信息。name表示该函数名,addr表示该函数的地址。
最后调用 register_kprobe(&rp->kp),注册kprobe点,可以看出kretprobe也是基于kprobe机制实现的,kretprobe也是一种特殊形式的kprobe。
kretprobe注册完成后就默认启动探测。
pre_handler_kretprobe
pre_handler_kretprobe
这个函数是内核自己定义的,内核已经指定该回调函数,不支持用户自定义。这个 kprobe pre_handler
在每个 kretprobe
中注册。 当探针命中时,它将设置返回探针。
arch_prepare_kretprobe
arch_prepare_kretprobe(ri, regs)
该函数架构相关,struct kretprobe_instance
结构体 的 ret_addr
成员用于保存并替换regs
中的返回地址。返回地址被替换为kretprobe_trampoline
。
pre_handler
回调函数会为kretprobe
探测函数执行的返回值做准备工作,其中最主要的就是替换掉正常流程的返回地址,让被探测函数在执行之后能够跳转到kretprobe
设计的函数 kretprobe_trampoline
中去。
kretprobe_trampoline
pre_handler_kretprobe
函数返回后,kprobe流程接着执行singlestep流程并返回到正常的执行流程,被探测函数(do_fork)继续执行,直到它执行完毕并返回。
由于返回地址被替换为kretprobe_trampoline
,所以跳转到kretprobe_trampoline
执行,该函数架构相关且有嵌入汇编实现。
该函数会获取被探测函数的寄存器信息并调用用户定义的回调函数输出其中的返回值,最后函数返回正常的执行流程。
(4)ri->rp->handler(ri, regs)
表示执行用户态自定义的回调函数handler
(用来获取_do_fork
函数的返回值),handler回调函数执行完毕以后,调用recycle_rp_inst
函数将当前的kretprobe_instance
实例从kretprobe_inst_table
哈希表释放,重新链入free_instances
中,以备后面kretprobe
触发时使用,另外如果kretprobe
已经被注销则将它添加到销毁表中待销毁。
(5)trampoline_handler
函数执行完后,返回被探测函数的原始返回地址,执行流程再次回到kretprobe_trampoline
函数中,将保存的 sp 替换为真实的返回地址。 从rax寄存器中取出原始的返回地址,然后恢复原始函数调用栈空间,最后跳转到原始返回地址执行,至此函数调用的流程就回归正常流程了,整个kretprobe探测结束。
(4) 将 lr寄存器中的trampoline
地址替换为实际的 orig_ret_addr
返回地址。 从x0
寄存器中取出原始的返回地址,然后恢复原始函数调用栈空间,最后跳转到原始返回地址执行,至此函数调用的流程就回归正常流程了,整个kretprobe探测结束。
Kprobe-based Event Tracing
这些事件类似于基于tracepoint的事件。与Tracepoint不同,它是基于kprobes(kprobe和kretprobe)的。所以它可以探测任何kprobes可以探测的地方。与基于Tracepoint的事件不同的是,它可以动态地添加和删除。
要启用这个功能,在编译内核时CONFIG_KPROBE_EVENTS=y
与 Event Tracing
类似,这不需要通过current_tracer
来激活。可以通过/sys/kernel/debug/tracing/kprobe_events
添加探测点,并通过/sys/kernel/debug/tracing/events/kprobes/<EVENT>/enable
来启用它。
你也可以使用/sys/kernel/debug/tracing/dynamic_events
,而不是kprobe_events
。该接口也将提供对其他动态事件的统一访问。
Synopsis of kprobe_events
kprobe和内核的ftrac结合使用,需要对内核进行配置,然后添加探测点、进行探测、查看结果。
ARM64
原文链接:https://mp.weixin.qq.com/s/315jT9Ft-3CZPW-ga5WMQA
电子技术应用专栏作家 嵌入式与Linux那些事