Linux内核调试技术jprobe使用与实现Word格式.docx
《Linux内核调试技术jprobe使用与实现Word格式.docx》由会员分享,可在线阅读,更多相关《Linux内核调试技术jprobe使用与实现Word格式.docx(17页珍藏版)》请在冰豆网上搜索。
该结构非常的简单,仅包含了一个kprobe结构(因为它是基于kprobe实现的)和一个entry指针,它保存的是探测点执行回调函数的地址,当触发调用被探测函数时,保存到该指针的地址会作为目标地址跳转执行(probehandlingcodetojumpto),因此用户指定的探测函数得以执行。
相关的API如下:
intregister_jprobe(structjprobe*jp)//向内核注册jprobe探测点
voidunregister_jprobe(structjprobe*jp)//卸载jprobe探测点
intregister_jprobes(structjprobe**jps,intnum)//注册探测函数向量,包含多个不同探测点
voidunregister_jprobes(structjprobe**jps,intnum)//卸载探测函数向量,包含多个不同探测点
intdisable_jprobe(structjprobe*jp)//临时暂停指定探测点的探测
intenable_jprobe(structjprobe*jp)//恢复指定探测点的探测
1.2、示例jprobe_example分析与演示
同kprobe_example.c一样,该示例程序仍以do_fork作为被探测函数进行探测。
当创建进程时,探测函数会调用它打印出do_fork函数的入参值。
下面详细分析:
staticstructjprobemy_jprobe={
.entry=jdo_fork,
.kp={
.symbol_name="
do_fork"
},
staticint__initjprobe_init(void)
{
intret;
ret=register_jprobe(&
my_jprobe);
if(ret<
0){
printk(KERN_INFO"
register_jprobefailed,returned%d\n"
ret);
return-1;
}
Plantedjprobeat%p,handleraddr%p\n"
my_jprobe.kp.addr,my_jprobe.entry);
return0;
}
staticvoid__exitjprobe_exit(void)
unregister_jprobe(&
jprobeat%punregistered\n"
my_jprobe.kp.addr);
程序定义了一个structjprobe实例my_jprobe,指定被探测函数的名字是do_fork(可以修改它以达到探测其他函数的目的),然后探测回调函数为jdo_fork。
在模块的初始化函数中,调用register_jprobe函数向kprobe子系统注册my_jprobe,这样jprobe探测默认就启用了,最后在exit函数中调用unregister_jprobe函数卸载。
/*Proxyroutinehavingthesameargumentsasactualdo_fork()routine*/
staticlongjdo_fork(unsignedlongclone_flags,unsignedlongstack_start,
unsignedlongstack_size,int__user*parent_tidptr,
int__user*child_tidptr)
pr_info("
jprobe:
clone_flags=0x%lx,stack_start=0x%lx"
"
stack_size=0x%lx\n"
clone_flags,stack_start,stack_size);
/*Alwaysendwithacalltojprobe_return().*/
jprobe_return();
jdo_fork函数也仅仅打印出了在调用do_fork函数时传入的clone_flags、stack_start和stack_size这三个入参值,整个实现非常简单直观,但是有两点需要注意:
1)探测回调函数的入参必须同被探测函数的一致,否则无法达到探测函数入参的目的,例如此处的jdo_fork函数入参unsignedlongclone_flags、unsignedlongstack_start、unsignedlongstack_size、int__user*parent_tidptr和int__user*child_tidptr同do_fork函数是完全一致的(注意返回值固定为long类型)。
2)在回调函数执行完毕以后,必须调用jprobe_return函数(注释中也有强调),否则执行流程就回不到正常的执行流程中了,这一点后文会详细分析。
下面在x86_64环境下演示该程序的实际效果(环境配置请参考前一篇博文):
<
6>
[15817.544375]jprobe:
clone_flags=0x1200011,stack_start=0x0stack_size=0x0
[15817.551217]jprobe:
[15817.905328]jprobe:
[15822.684688]jprobe:
[15822.704001]jprobe:
在加载jprobe_example.ko模块以后,在终端随便敲几个命令触发进程创建,内核打印出以上message,可以看到do_fork的入参就被非常容易的获取到了,其他函数的探测也类似,不再详细描述。
2、jprobe实现分析
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,其主要流程如下图:
图1jpobe注册流程
intregister_jprobe(structjprobe*jp)
returnregister_jprobes(&
jp,1);
EXPORT_SYMBOL_GPL(register_jprobe);
register_jprobe函数只是register_jprobes的一个封装,主要注册功能由register_jprobes函数完成。
intregister_jprobes(structjprobe**jps,intnum)
structjprobe*jp;
intret=0,i;
if(num<
=0)
return-EINVAL;
for(i=0;
i<
num;
i++){
unsignedlongaddr,offset;
jp=jps[i];
addr=arch_deref_entry_point(jp->
entry);
/*Verifyprobepointisafunctionentrypoint*/
if(kallsyms_lookup_size_offset(addr,NULL,&
offset)&
&
offset==0){
jp->
kp.pre_handler=setjmp_pre_handler;
kp.break_handler=longjmp_break_handler;
ret=register_kprobe(&
jp->
kp);
}else
ret=-EINVAL;
if(i>
0)
unregister_jprobes(jps,i);
break;
returnret;
EXPORT_SYMBOL_GPL(register_jprobes);
函数是一个循环,对每个jprobe执行相同的注册流程,首先从jp->
entry中取出探测回调函数的地址,对它进行验证。
kallsyms_lookup_size_offset函数的作用是从内核或者模块的符号表中找到addr地址所在的符号,找到后会通过offset值返回addr与符号起始的偏移,这偏移值必须为0,即必须为一个函数的入口。
若条件符合,则设置kprobe的pre_handler和break_handler这两个回调函数setjmp_pre_handler和longjmp_break_handler,最后调用register_kprobe函数注册kprobe。
可见jprobe的注册流程非常的简单,它的本质就是注册一个kprobe,利用kprobe机制实现探测,只是探测回调函数并非用户自己定义,使用jprobe私有的而已。
在注册完成后,jprobe(kprobe)机制启动,当函数调用流程执行到被探测函数时就会触发jprobe(kprobe)探测。
最后需要注意的是,jprobe是不能在同一个被探测点注册多个的,在kprobe的注册流程register_kprobe->
register_aggr_kprobe->
add_new_kprobe中会有判断:
if(p->
break_handler){
if(ap->
break_handler)
return-EEXIST;
2.3、触发jprobe探测
基于kprobe机制,在执行到被探测函数后,会触发CPU异常,按照kprobe的执行流程,由kprobe_handler函数调用到pre_handler回调函数,即setjmp_pre_handler。
该函数架构相关,它根据架构的不同进行一些栈或者寄存器相关的操作,保存现场以备调用结束后恢复,随后跳转到用户定的jprobe->
entry处执行,在打印出用户需要的信息后,返回原有正常的流程继续执行。
主要流程如下图:
2.3.1、arm架构实现
int__kprobessetjmp_pre_handler(structkprobe*p,structpt_regs*regs)
structjprobe*jp=container_of(p,structjprobe,kp);
structkprobe_ctlblk*kcb=get_kprobe_ctlblk();
longsp_addr=regs->
ARM_sp;
longcpsr;
kcb->
jprobe_saved_regs=*regs;
memcpy(kcb->
jprobes_stack,(void*)sp_addr,MIN_STACK_SIZE(sp_addr));
regs->
ARM_pc=(long)jp->
entry;
cpsr=regs->
ARM_cpsr|PSR_I_BIT;
#ifdefCONFIG_THUMB2_KERNEL
/*SetcorrectThumbstateincpsr*/
if(regs->
ARM_pc&
1)
cpsr|=PSR_T_BIT;
else
cpsr&
=~PSR_T_BIT;
#endif
ARM_cpsr=cpsr;
preempt_disable();
return1;
首先再次明确入参structpt_regs*regs的含义是触发CPU异常前所保存的正常执行流上下文的寄存器值。
函数首先获取触发的jprobe结构实例,并调用get_kprobe_ctlblk取得当前CPU的kprobe_ctlblk结构全局变量,这个structkprobe_ctlblk结构定义在kprobe分析中已经见过,不过jprobe使用到了其中定义的另两个字段:
/*per-cpukprobecontrolblock*/
structkprobe_ctlblk{
unsignedintkprobe_status;
structprev_kprobeprev_kprobe;
structpt_regsjprobe_saved_regs;
charjprobes_stack[MAX_STACK_SIZE];
其中jprobe_saved_regs用于保存寄存器信息,jprobes_stack则用于保存栈信息,它们用于在jprobe返回时恢复调用探测前的上下文,这一点从setjmp_pre_handler函数的前两行就可以看出。
先提个问题,为何kprobe不需要保存原上下文信息而jprobe需要?
函数接下来修改传入的ARM_pc值为用户指定的探测回调函数地址,注意这个值本来在正常的kprobe流程中是要被设置为正常流程的下一条指令的(执行完kprobe流程后就会回到原流程继续执行),这里在kprobe的整个流程结束后就不会回到原流程执行了,而是会进入到用户指定的探测函数执行。
函数然后修改入参的CPSR寄存器值,置位PSR_I_BIT,表示禁用中断,最后禁止抢占并返回1。
回到kprobe_handler函数中看返回1后接下来kprobe就不会执行singlestep和调用post_handler回调函数了,注意也不会调用reset_current_kprobe函数复位当前执行的kprobe为NULL:
if(!
p->
pre_handler||!
pre_handler(p,regs)){
kprobe_status=KPROBE_HIT_SS;
singlestep(p,regs,kcb);
if(p->
post_handler){
kprobe_status=KPROBE_HIT_SSDONE;
p->
post_handler(p,regs,0);
reset_current_kprobe();
在kprobe_handler流程返回后,执行流程进入到了用户指定的探测函数执行,对于前文中的jprobe_example程序来说就是jdo_fork函数。
提第二个问题,被探测函数的入参值是如何获取的?
从setjmp_pre_handler的实现可以看出,该函数仅仅修改了kprobe的返回地址,并没有修改栈和其他的寄存器值,因此在CPU跳转到jdo_fork执行时,它的寄存器和栈中的内容同原本调用do_fork函数时几乎是一模一样的(仅仅是禁用了中断而已),因此不论是通过寄存器传参还是通过压栈的方式传参,用户在定义jdo_fork函数时只需要将函数入参定义的同do_fork一样就可以轻轻松松的获取到原有的入参值了。
另外从这里的实现可以看出另外一个信息,jprobe的回调执行上下文同原函数执行的上下文是一样的,这点不同于kprobe,kprobe的回调函数执行的上下文是在CPU异常的中断上下文。
最后由于探测函数(jdo_fork)是在kprobe_handler流程执行完成后跳转执行的,跳过了single_step流程,这也就说它不能利用原有kprobe的机制回到原始执行流程中去执行,需要另想他法,其实在setjmp_pre_handler函数中保存的寄存器pt_regs就是用于这个目的的,也就解释了前文中提出的第一个问题,接下来详细分析。
回到探测函数jdo_fork中,用户在获取需要的信息后,接下来进入现场恢复的流程,其中的关键部分就是jdo_fork函数最后调用的jprobe_return函数,它是由嵌入汇编实现的
void__kprobesjprobe_return(void)
__asm____volatile__(
/*
*Setupanemptypt_regs.FillSPandPCfieldsas
*they'
reneededbylongjmp_break_handler.
*
*WeallocatesomeslackbetweentheoriginalSPandstartof
*ourfabricatedregs.Tobeprecisewewanttohaveworstcase
*coveredwhichisSTMFDwithall16regssoweallocate2*
*sizeof(struct_pt_regs)).
*Thisistopreventanysimulatedinstructionfromwriting
*overtheregswhentheyareaccessingthestack.
...
#else
subsp,%0,%1\n\t"
ldrr0,="
__stringify(JPROBE_MAGIC_ADDR)"
\n\t"
str%0,[sp,%2]\n\t"
strr0,[sp,%3]\n\t"
movr0,sp\n\t"
blkprobe_handler\n\t"
*Returntothecontextsavedbysetjmp_pre_handler
*andrestoredbylongjmp_break_handler.
ldrr0,[sp,%4]\n\t"
msrcpsr_cxsf,r0\n\t"
ldmiasp,{r0-pc}\n\t"
:
r"
(kcb->
jprobe_saved_regs.ARM_sp),
I"
(sizeof(structpt_regs)*2),
J"
(o