Linux内核调试技术——kprobe使用与实现(一)
Linux内核调试技术——kprobe使用与实现(二)
Linux内核调试技术——kprobe使用与实现(三)
Linux内核调试技术——kprobe使用与实现(四)
Linux内核调试技术——kprobe使用与实现(五)
本文介绍kprobes中的第二种探测技术jprobe,它基于kprobe实现,不能在函数的任意位置插入探测点,只能在函数的入口处探测,一般用于监测函数的入参值。
1、jprobe使用实例
使用jprobe探测函数的入参值,需要编写内核模块。同kprobe一样,内核同样提供了jprobe的实例程序jprobe_example.c(位于sample/kprobes目录),该程序实现了探测do_fork函数入参的功能,用户可以以它为模板来探测其他函数(当然不是说什么函数都能探测的,限制同kprobe一样,另外需要注意的是一个被探测函数只能注册一个jprobe)。在分析jprobe_example.c之前先熟悉一下jprobe的基本结构与API接口。
1.1、jprobe结构体与API介绍
struct jprobe结构体定义如下:
该结构非常的简单,仅包含了一个kprobe结构(因为它是基于kprobe实现的)和一个entry指针,它保存的是探测点执行回调函数的地址,当触发调用被探测函数时,保存到该指针的地址会作为目标地址跳转执行(probe handling code to jump to),因此用户指定的探测函数得以执行。
相关的API如下:
int register_jprobe(struct jprobe *jp)
//向内核注册jprobe探测点
void unregister_jprobe(struct jprobe*jp)
//卸载jprobe探测点
int register_jprobes(struct jprobe **jps,int num)
//注册探测函数向量,包含多个不同探测点
void unregister_jprobes(struct jprobe **jps,int num)
//卸载探测函数向量,包含多个不同探测点
int disable_jprobe(struct jprobe *jp)
//临时暂停指定探测点的探测
int enable_jprobe(struct jprobe *jp)
//恢复指定探测点的探测
1.2、示例jprobe_example分析与演示
同kprobe_example.c一样,该示例程序仍以do_fork作为被探测函数进行探测。当创建进程时,探测函数会调用它打印出do_fork函数的入参值。下面详细分析:
程序定义了一个struct jprobe实例my_jprobe,指定被探测函数的名字是do_fork(可以修改它以达到探测其他函数的目的),然后探测回调函数为jdo_fork。在模块的初始化函数中,调用register_jprobe函数向kprobe子系统注册my_jprobe,这样jprobe探测默认就启用了,最后在exit函数中调用unregister_jprobe函数卸载。
jdo_fork函数也仅仅打印出了在调用do_fork函数时传入的clone_flags、stack_start和stack_size这三个入参值,整个实现非常简单直观,但是有两点需要注意:
1)探测回调函数的入参必须同被探测函数的一致,否则无法达到探测函数入参的目的,例如此处的jdo_fork函数入参unsigned long clone_flags、unsigned long stack_start、unsigned longstack_size、int __user *parent_tidptr和int __user *child_tidptr同do_fork函数是完全一致的(注意返回值固定为long类型)。
2)在回调函数执行完毕以后,必须调用jprobe_return函数(注释中也有强调),否则执行流程就回不到正常的执行流程中了,这一点后文会详细分析。
下面在x86_64环境下演示该程序的实际效果
在加载jprobe_example.ko模块以后,在终端随便敲几个命令触发进程创建,内核打印出以上message,可以看到do_fork的入参就被非常容易的获取到了,其他函数的探测也类似,不再详细描述。
2、jprobe实现分析
jpeobe的实现基于kprobe,在其基础之上分析它的实现,述主要包括jprobe注册流程和触发探测流程,涉及kprobe的部分不再详细描。
2.1、jprobe实现原理
利用kprobe,jprobe是一种特殊形式的kprobe,它有自己的pre_handler和break_handler回调函数,其中pre_handler回调函数负责保存原始调用上下文并为调用用户指定的探测函数jprobe->entry准备环境,然后跳转到jprobe->entry执行(被探测函数的入参信息在此得到输出),接着再次触发kprobe流程,在break_handler函数中恢复原始上下文,最后返回正常执行流程。
2.2、注册一个jprobe实例
jprobe探测模块调用register_jprobe函数向内核注册一个jprobe实例,代码路径kernel/kprobes.c,其主要流程如下图:
可见jprobe的注册流程非常的简单,它的本质就是注册一个kprobe,利用kprobe机制实现探测,只是探测回调函数并非用户自己定义,使用jprobe私有的而已。在注册完成后,jprobe(kprobe)机制启动,当函数调用流程执行到被探测函数时就会触发jprobe(kprobe)探测。
最后需要注意的是,jprobe是不能在同一个被探测点注册多个的,在kprobe的注册流程register_kprobe->register_aggr_kprobe->add_new_kprobe中会有判断
2.3、触发jprobe探测
基于kprobe机制,在执行到被探测函数后,会触发CPU异常,按照kprobe的执行流程,由kprobe_handler函数调用到pre_handler回调函数,即setjmp_pre_handler。该函数架构相关,它根据架构的不同进行一些栈或者寄存器相关的操作,保存现场以备调用结束后恢复,随后跳转到用户定的jprobe->entry处执行,在打印出用户需要的信息后,返回原有正常的流程继续执行。主要流程如下图:
x86平台下代码如下:
函数首先同样是保存现场,然后关闭中断并设置IP寄存器的值为jp->entry,最后返回1,这样在kprobe_int3_handler函数会跳过single_step。
于是在kprobe调用流程结束后跳转到用户的探测函数执行。在来看jprobe_return函数的实现:
这里使用int3指令再次触发CPU3异常,并且异常出的地址已经不再是BREAKPOINT_INSTRUCTION了,所以会进入到kprobe_int3_handler的以下流程执行:
同样是调用kprobe的break_handler回调函数执行,也即是longjmp_break_handler函数。
longjmp_break_handler函数恢复代码的原有上下文,打开内核抢占,最后交回给kprobe继续执行后面的single_step和恢复流程。不过值的注意的是第一条判断语句,由于本次int3异常是在jprobe_return函数中触发的,因此longjmp_break_handler函数的struct pt_regs *regs入参值是在调用jprobe_return函数环境上下文中的寄存器值,因此addr一定是在jprobe_return函数的地址范围内,所以以此判断本次调用的有效性,防止误入。
3、总结
jprobe探测技术基于kprobe实现,是kprobes三种探测技术中的第二种,内核开发人员可以用它来探测内核函数的调用以及调用时的入参值,使用非常方便。