Linux内核调试技术jprobe使用与实现.docx
- 文档编号:8699748
- 上传时间:2023-02-01
- 格式:DOCX
- 页数:17
- 大小:133.45KB
Linux内核调试技术jprobe使用与实现.docx
《Linux内核调试技术jprobe使用与实现.docx》由会员分享,可在线阅读,更多相关《Linux内核调试技术jprobe使用与实现.docx(17页珍藏版)》请在冰豆网上搜索。
Linux内核调试技术jprobe使用与实现
Linux内核调试技术——jprobe使用与实现
1、jprobe使用实例
使用jprobe探测函数的入参值,需要编写内核模块。
同kprobe一样,内核同样提供了jprobe的实例程序jprobe_example.c(位于sample/kprobes目录),该程序实现了探测do_fork函数入参的功能,用户可以以它为模板来探测其他函数(当然不是说什么函数都能探测的,限制同kprobe一样,另外需要注意的是一个被探测函数只能注册一个jprobe)。
在分析jprobe_example.c之前先熟悉一下jprobe的基本结构与API接口。
1.1、jprobe结构体与API介绍
structjprobe结构体定义如下:
[cpp]viewplaincopy在CODE上查看代码片派生到我的代码片
/*
*Specialprobetypethatusessetjmp-longjmptypetrickstoresume
*executionataspecifiedentrywithamatchingprototypecorresponding
*totheprobedfunction-atricktoenableargumentstobecome
*accessibleseamlesslybyprobehandlinglogic.
*Note:
*Becauseofthewaycompilersallocatestackspaceforlocalvariables
*etcupfront,regardlessofsub-scopeswithinafunction,thismirroring
*principlecurrentlyworksonlyforprobesplacedonfunctionentrypoints.
*/
structjprobe{
structkprobekp;
void*entry;/*probehandlingcodetojumpto*/
};
该结构非常的简单,仅包含了一个kprobe结构(因为它是基于kprobe实现的)和一个entry指针,它保存的是探测点执行回调函数的地址,当触发调用被探测函数时,保存到该指针的地址会作为目标地址跳转执行(probehandlingcodetojumpto),因此用户指定的探测函数得以执行。
相关的API如下:
[cpp]viewplaincopy在CODE上查看代码片派生到我的代码片
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函数的入参值。
下面详细分析:
[cpp]viewplaincopy在CODE上查看代码片派生到我的代码片
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;
}
printk(KERN_INFO"Plantedjprobeat%p,handleraddr%p\n",
my_jprobe.kp.addr,my_jprobe.entry);
return0;
}
staticvoid__exitjprobe_exit(void)
{
unregister_jprobe(&my_jprobe);
printk(KERN_INFO"jprobeat%punregistered\n",my_jprobe.kp.addr);
}
程序定义了一个structjprobe实例my_jprobe,指定被探测函数的名字是do_fork(可以修改它以达到探测其他函数的目的),然后探测回调函数为jdo_fork。
在模块的初始化函数中,调用register_jprobe函数向kprobe子系统注册my_jprobe,这样jprobe探测默认就启用了,最后在exit函数中调用unregister_jprobe函数卸载。
[cpp]viewplaincopy在CODE上查看代码片派生到我的代码片
/*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();
return0;
}
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
<6>[15817.551217]jprobe:
clone_flags=0x1200011,stack_start=0x0stack_size=0x0
<6>[15817.905328]jprobe:
clone_flags=0x1200011,stack_start=0x0stack_size=0x0
<6>[15822.684688]jprobe:
clone_flags=0x1200011,stack_start=0x0stack_size=0x0
<6>[15822.704001]jprobe:
clone_flags=0x1200011,stack_start=0x0stack_size=0x0
在加载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注册流程
[cpp]viewplaincopy在CODE上查看代码片派生到我的代码片
intregister_jprobe(structjprobe*jp)
{
returnregister_jprobes(&jp,1);
}
EXPORT_SYMBOL_GPL(register_jprobe);
register_jprobe函数只是register_jprobes的一个封装,主要注册功能由register_jprobes函数完成。
[cpp]viewplaincopy在CODE上查看代码片派生到我的代码片
intregister_jprobes(structjprobe**jps,intnum)
{
structjprobe*jp;
intret=0,i;
if(num<=0)
return-EINVAL;
for(i=0;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; jp->kp.break_handler=longjmp_break_handler; ret=register_kprobe(&jp->kp); }else ret=-EINVAL; if(ret<0){ 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中会有判断: [cpp]viewplaincopy在CODE上查看代码片派生到我的代码片 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架构实现 [cpp]viewplaincopy在CODE上查看代码片派生到我的代码片 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 regs->ARM_cpsr=cpsr; preempt_disable(); return1; } 首先再次明确入参structpt_regs*regs的含义是触发CPU异常前所保存的正常执行流上下文的寄存器值。 函数首先获取触发的jprobe结构实例,并调用get_kprobe_ctlblk取得当前CPU的kprobe_ctlblk结构全局变量,这个structkprobe_ctlblk结构定义在kprobe分析中已经见过,不过jprobe使用到了其中定义的另两个字段: [cpp]viewplaincopy在CODE上查看代码片派生到我的代码片 /*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: [cpp]viewplaincopy在CODE上查看代码片派生到我的代码片 if(! p->pre_handler||! p->pre_handler(p,regs)){ kcb->kprobe_status=KPROBE_HIT_SS; singlestep(p,regs,kcb); if(p->post_handler){ kcb->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函数,它是由嵌入汇编实现的 [cpp]viewplaincopy在CODE上查看代码片派生到我的代码片 void__kprobesjprobe_return(void) { structkprobe_ctlblk*kcb=get_kprobe_ctlblk(); __asm____volatile__( /* *Setupanemptypt_regs.FillSPandPCfieldsas *they'reneededbylongjmp_break_handler. * *WeallocatesomeslackbetweentheoriginalSPandstartof *ourfabricatedregs.Tobeprecisewewanttohaveworstcase *coveredwhichisSTMFDwithall16regssoweallocate2* *sizeof(struct_pt_regs)). * *Thisistopreventanysimulatedinstructionfromwriting *overtheregswhentheyareaccessingthestack. */ #ifdefCONFIG_THUMB2_KERNEL ... #else "subsp,%0,%1\n\t" #endif "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. */ #ifdefCONFIG_THUMB2_KERNEL ... #else "ldrr0,[sp,%4]\n\t" "msrcpsr_cxsf,r0\n\t" "ldmiasp,{r0-pc}\n\t" #endif : : "r"(kcb->jprobe_saved_regs.ARM_sp), "I"(sizeof(structpt_regs)*2), "J"(o
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Linux 内核 调试 技术 jprobe 使用 实现