嵌入式与Linux那些事

电子技术应用专栏作家——嵌入式与Linux那些事。关注嵌入式与Linux的校招社招,本人整理了《嵌入式软件工程师笔试面试指南》PDF,平时发布嵌入式与Linux相关的实用技术文章

kprobes(二)使用方法

0
阅读(1438)

前言

上一节介绍了kprobe的基本概念,下面我们将使用几个具体的例子,看下kprobe在实际使用中有那些应用场景。

kprobe

内核的samples/kprobe目录下有kprobe相关的例子,我们以这些例子为基础,简单修改下。

查看函数的入参

我们所有的例子都是探测do_sys_open() 或者_do_fork(),以下是内核中的源码。

image.png

image.png

image.png

实际调试中经常需要调查函数使用的变量的值。要在kprobes的侦测器内显示某个函数的局部变量的值,需要一些技巧,原因是在printk的参数中无法直接指定变量名,因此必须给侦测器函数提供一个pt_regs结构,其中保存了指定地址的命令执行时的寄存器信息。

当然,不同架构下该结构的成员变量不尽相同,但用该结构可以显示变量等更为详细的信息。

ARM64,ARM32,X86的寄存器及其访问方式可以看文末的目录

image.png

image.png

image.png

我们以内核目录下的例程做一个简单修改,探测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分别存放dfdpathname的值。

image.png

image.png

image.png

任意位置通过变量名获取信息

kprobes拥有更加强大的功能,那就是它能在内核的任意地址插入侦测器。此外,侦测器可以在任意地址的指令执行之前或之后执行,或者前后都执行。

因此,应当观察汇编代码,找到源代码中想要调查的位置对应于编译后的二进制文件中的什么地址,并调查希望显示的变量保存在哪个寄存器、哪个内存地址。

通常,我们希望在函数执行的过程中变量,即打印一些流程中的东西,而不是函数本身被调用,此时我们不能简单设置 kprobe->symbol_name 函数名字 ,假设我们期望获取 _do_fork函数变量 nr 的值:

将vmlinux进行反汇编,找出_do_fork的地址。

image.png


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

另外,反汇编能力就是多看汇编以及找到几个关键点(例如常量,跳转语句)就能定位到汇编对应的源码了,这里不再展开了。

image.png

image.png

image.png

image.png

jprobe

与kprobes相比,jprobes能更容易地获取传给函数的参数。有几点需要注意:

  1. 处理程序应该有与被探测函数相同的参数列表和返回类型;

  2. 返回之前,必须调用jprobe_return()(处理程序实际上从未返回,因为jprobe_return()将控制权返回给Kprobes) 。

image.png

image.png

使用kprobes时,必须通过寄存器或栈才能计算出参数的值。此外,计算方法还依赖于架构。

如果使用jprobes,那么无须了解架构的详细知识,也能简单地查看参数的值。

编译加载驱动程序

image.png


kretprobe

kretprobe 也是基于kprobe的,相比于kprobe和jprobe,实现相对复杂。下面我们以内核目录下的例程,简单分析下。

image.png

image.png

image.png


image.png

  1. 其中我们可以看到 struct kretprobe 结构体中 有struct kprobe成员(kretprobe时基于 kprobe实现的)。handler:用户自定义回调函数,被探测函数返回后被调用,一般在这个函数中获取被探测函数的返回值。

  2. entry_handler:用户自定义回调函数,这是Kretprobes 提供了一个可选的用户指定的处理程序,它在函数入口上运行。 每当 kretprobe 放置在函数入口处的 kprobe 被命中时,都会调用用户定义的 entry_handler,如果有的话。 如果 entry_handler 返回 0(成功),则保证在函数返回时调用相应的返回处理程序。 如果 entry_handler 返回非零错误,则 Kprobes 将返回地址保持原样,并且 kretprobe 对该特定函数实例没有进一步的影响。

  3. maxactive:被探测函数可以同时活动的最大实例数。来指定可以同时探测多少个指定函数的实例。register_kretprobe() 预分配指定数量的 kretprobe_instance 对象。

  4. nmissed:跟踪被探测函数的返回被忽略的次数(maxactive设置的过低)。

  5. data_size:表示kretprobe私有数据的大小,在注册kretprobe时会根据该大小预留空间。

  6. free_instances :表示空闲的kretprobe运行实例链表,它链接了本kretprobe的空闲实例struct kretprobe_instance结构体表示。

image.png

  1. 这个结构体表示kretprobe的运行实例,前文说过被探测函数在跟踪期间可能存在并发执行的现象,因此kretprobe使用一个kretprobe_instance来跟踪一个执行流,支持的上限为maxactive。在没有触发探测时,所有的kretprobe_instance实例都保存在free_instances表中,每当有执行流触发一次kretprobe探测,都会从该表中取出一个空闲的kretprobe_instance实例用来跟踪。

  2. kretprobe_instance结构提中的rp指针指向所属的kretprobe;

  3. ret_addr用于保存原始被探测函数的返回地址(后文会看到被探测函数返回地址会被暂时替换);

  4. task用于绑定其跟踪的进程;

  5. data保存用户使用的kretprobe私有数据,它会在整个kretprobe探测运行期间在entry_handlerhandler回调函数之间进行传递(一般用于实现统计被探测函数的执行耗时)。

register_kretprobe

kretprobe探测点的blackpoint,用来表示不支持kretprobe探测的函数的信息。name表示该函数名,addr表示该函数的地址。

image.png

image.png

image.png

最后调用 register_kprobe(&rp->kp),注册kprobe点,可以看出kretprobe也是基于kprobe机制实现的,kretprobe也是一种特殊形式的kprobe。

kretprobe注册完成后就默认启动探测。

pre_handler_kretprobe

pre_handler_kretprobe这个函数是内核自己定义的,内核已经指定该回调函数,不支持用户自定义。这个 kprobe pre_handler 在每个 kretprobe 中注册。 当探针命中时,它将设置返回探针。

image.png

image.png

arch_prepare_kretprobe

arch_prepare_kretprobe(ri, regs)该函数架构相关,struct kretprobe_instance结构体 的 ret_addr 成员用于保存并替换regs中的返回地址。返回地址被替换为kretprobe_trampoline

image.png

image.png


image.png

pre_handler回调函数会为kretprobe探测函数执行的返回值做准备工作,其中最主要的就是替换掉正常流程的返回地址,让被探测函数在执行之后能够跳转到kretprobe设计的函数 kretprobe_trampoline中去。

kretprobe_trampoline

pre_handler_kretprobe函数返回后,kprobe流程接着执行singlestep流程并返回到正常的执行流程,被探测函数(do_fork)继续执行,直到它执行完毕并返回。

由于返回地址被替换为kretprobe_trampoline,所以跳转到kretprobe_trampoline执行,该函数架构相关且有嵌入汇编实现。

该函数会获取被探测函数的寄存器信息并调用用户定义的回调函数输出其中的返回值,最后函数返回正常的执行流程。

image.png

image.png

image.png

image.png

image.png

(4)ri->rp->handler(ri, regs)表示执行用户态自定义的回调函数handler(用来获取_do_fork函数的返回值),handler回调函数执行完毕以后,调用recycle_rp_inst函数将当前的kretprobe_instance实例从kretprobe_inst_table哈希表释放,重新链入free_instances中,以备后面kretprobe触发时使用,另外如果kretprobe已经被注销则将它添加到销毁表中待销毁。

image.png

(5)trampoline_handler函数执行完后,返回被探测函数的原始返回地址,执行流程再次回到kretprobe_trampoline函数中,将保存的 sp 替换为真实的返回地址。 从rax寄存器中取出原始的返回地址,然后恢复原始函数调用栈空间,最后跳转到原始返回地址执行,至此函数调用的流程就回归正常流程了,整个kretprobe探测结束。

image.png

image.png

image.png

image.png

(4) 将 lr寄存器中的trampoline地址替换为实际的 orig_ret_addr 返回地址。 从x0寄存器中取出原始的返回地址,然后恢复原始函数调用栈空间,最后跳转到原始返回地址执行,至此函数调用的流程就回归正常流程了,整个kretprobe探测结束。

image.png

image.png

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结合使用,需要对内核进行配置,然后添加探测点、进行探测、查看结果。


image.png



image.pngimage.png

ARM64

image.png

image.png


原文链接:https://mp.weixin.qq.com/s/315jT9Ft-3CZPW-ga5WMQA

微信图片_20220708145705.jpg

电子技术应用专栏作家 嵌入式与Linux那些事