Linux内核调试技术jprobe使用与实现Word格式.docx

上传人:b****6 文档编号:21835943 上传时间:2023-02-01 格式:DOCX 页数:17 大小:133.45KB
下载 相关 举报
Linux内核调试技术jprobe使用与实现Word格式.docx_第1页
第1页 / 共17页
Linux内核调试技术jprobe使用与实现Word格式.docx_第2页
第2页 / 共17页
Linux内核调试技术jprobe使用与实现Word格式.docx_第3页
第3页 / 共17页
Linux内核调试技术jprobe使用与实现Word格式.docx_第4页
第4页 / 共17页
Linux内核调试技术jprobe使用与实现Word格式.docx_第5页
第5页 / 共17页
点击查看更多>>
下载资源
资源描述

Linux内核调试技术jprobe使用与实现Word格式.docx

《Linux内核调试技术jprobe使用与实现Word格式.docx》由会员分享,可在线阅读,更多相关《Linux内核调试技术jprobe使用与实现Word格式.docx(17页珍藏版)》请在冰豆网上搜索。

Linux内核调试技术jprobe使用与实现Word格式.docx

该结构非常的简单,仅包含了一个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

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 职业教育 > 职业技术培训

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1