Linux内核调试工具Kprobe机制的研究
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
Linux内核调试工具Kprobe机制的研究
李清干* 邵作之
(华北电力大学计算机科学与技术学院,北京 102206)
摘 要:在传统内核调试过程中,我们经常使用print k作为内核调试的一种方法,但这种方法执行速度相对较慢,在对响应时间要求比较严格的内核控制路径(如中断)中不宜采用它。
Kprobe(K erne l probe)与2 6内核结合起来提供了一个动态插入pri n t k的轻量级、无干扰而且强大的装置,使用K probe不需要经常重新引导和重新编译内核就可以完成这一任务。
本文重点介绍K probe调试机制原理以及同内核接口。
关键词:L inux内核;K probe;探测点
一、Kp robe机制简介
K probe是IB M公司开发的一个轻量级调试工具,它允许内核运行时通过加载模块设置探测器,内核运行到探测点时便执行其相应的处理函数,这是以前的所有工具都做不到的。
K probe作为一个动态地收集调试和性能信息的工具,它从Dprobe项目派生而来,是一种非破坏性工具,用户用它几乎可以跟踪任何函数或被执行的指令以及一些异步事件(如ti m er)。
它的基本工作机制是:用户指定一个探测点,并把一个用户定义的处理函数关联到该探测点,当内核执行到该探测点时,相应的关联函数被执行,然后继续执行正常的代码路径。
使用K probe可以轻松地通过收集处理器寄存器和全局数据结构等调试信息。
开发者甚至可以使用K probe来修改寄存器值和全局数据结构的值。
K probe实现了三种类型的探测点:kprobes、j probes和kretprobes(也叫返回探测点)。
kprobes是可以被插入到内核的任何指令位置的探测点,j probes则只能被插入到一个内核函数的入口,而kre t probes则是在指定的内核函数返回时才被执行。
这三种类型可以用在不同情况的上下文中。
二、Kp robe机制实现原理
当安装一个kprobes探测点时,K probe首先备份被探测的指令,然后使用断点指令(即在i386和x86_64的i nt3指令)来取代被探测指令的头一个或几个字节。
当CPU执行到探测点时,将因运行断点指令而执行trap操作,那将导致保存CPU的寄存器,调用相应的trap处理函数,而trap 处理函数将调用相应的no tifi er_call_cha i n(内核中一种异步工作机制)中注册的所有notifier函数,kprobe正是通过向trap对应的no tifi er_ca ll_cha i n注册关联到探测点的处理函数来实现探测处理的。
当kprobe注册的notifier被执行时,它首先执行关联到探测点的pre_handler函数,并把相应的kprobe struct和保存的寄存器作为该函数的参数,接着, kprobe单步执行被探测指令的备份,最后,kprobe执行post_hand l e r。
等所有这些运行完毕后,紧跟在被探测指令后的指令流j probe通过注册kprobes在被探测函数入口来实现,它能无缝地访问被探测函数的参数。
j probe处理函数应当和被探测函数有同样的原型,而且该处理函数在函数末必须调用kprobe提供的函数jprobe_re t urn()。
当执行到该探测点时,kprobe备份CPU寄存器和栈的一些部分,然后修改指令寄存器指向j probe处理函数,当执行该j probe处理函数时,寄存器和栈内容与执行真正的被探测函数一模一样,因此它不需要任何特别的处理就能访问函数参数,在该处理函数执行到最后时,它调用j probe_ret u rn(),那导致寄存器和栈恢复到执行探测点时的状态,因此被探测函数能被正常运行。
需要注意,被探测函数的参数可能通过栈传递,也可能通过寄存器传递,但是j probe对于两种情况都能工作,因为它既备份了栈,又备份了寄存器,当然,前提是j probe处理函数原型必须与被探测函数完全一样。
kretprobe也使用了kprobes来实现,当用户调用reg ister_kretprobe()时,kprobe在被探测函数的入口建立了一个探测点,当执行到探测点时,kprobe保存了被探测函数的返回地址并取代返回地址为一个tra m po li ne的地址, kprobe在初始化时,定义了该tra m po line并且为该tra m po li ne 注册了一个kprobe,当被探测函数执行它的返回指令时,控制传递到该trampo li ne,因此kprobe已经注册的对应于tra m po li ne的处理函数将被执行,而该处理函数会调用用户关联到该kre t probe上的处理函数,处理完毕后,设置指令寄存器指向已经备份的函数返回地址,因而原来的函数返回被正常执行。
被探测函数的返回地址保存在类型为kretprobe_i n stance的变量中,结构kretprobe的m axacti v e字段指定了被探测函数可以被同时探测的实例数,函数reg i ster_kretprobe ()将预分配指定数量的kretprobe_instance。
如果被探测函数是非递归的并且调用时已经保持了自旋锁(sp i nlock),那么m axacti v e为1就足够了;如果被探测函数是非递归的且运行时是抢占失效的,那么m ax acti ve为NR_CPU S就可以
349
中国电力教育 2008年研究综述与技术论坛专刊*作者简介:李清干,男,华北电力大学计算机科学与技术学院硕士研究生。
了;如果m axacti ve被设置为小于等于0,它被设置到缺省值(如果抢占使能,即配置了CON F I G_PREE M PT,缺省值为10和2*NR_CPU S中的最大值,否则缺省值为NR_ CPU S)。
如果m axactive被设置的太小了,一些探测点的执行可能被丢失,但是不影响系统的正常运行,在结构kretprobe 中n m i ssed字段将记录被丢失的探测点执行数,它在返回探测点被注册时设置为0,每次当执行探测函数而没有kret probe_i nstance可用时,它就加1。
三、Kp robe接口函数注册与卸载
K probe为每一类型的探测点提供了注册和卸载函数。
1 reg ist e r_kprobe
它用于注册一个kprobes类型的探测点,其函数原型为:
int regis t er_kprobe(struc t kprobe*kp);
为了使用该函数,用户需要在源文件中包含头文件li nux/kprobes h。
该函数的参数是struct kprobe类型的指针。
其中addr指定探测点的位置,pre_handler指定执行到探测点时执行的处理函数,post_handler指定执行完探测点后执行的处理函数,fault_handler指定错误处理函数,当在执行pre_handler、post_handler以及被探测函数期间发生错误时,它会被调用。
在调用该注册函数前,用户必须先设置好struct kprobe的这些字段,用户可以指定任何处理函数为NU LL。
该注册函数会在kp->addr地址处注册一个kprobes类型的探测点,当执行到该探测点时,将调用函数kp-> pre_hand l e r,执行完被探测函数后,将调用kp->post_ handler。
如果在执行kp->pre_handler或kp->po st_ handler时或在单步跟踪被探测函数期间发生错误,将调用kp->fault_handler。
该函数成功时返回0,否则返回负的错误码。
探测点处理函数pre_handler的原型如下:
int pre_handler(struct kprobe*p,struct pt_regs*r egs)
用户必须按照该原型参数格式定义自己的pre_handler,当然函数名取决于用户自己。
参数p就是指向该处理函数关联到的kprobes探测点的指针,可以在该函数内部引用该结构的任何字段,就如同在使用调用reg ister_kprobe时传递的那个参数。
参数reg s指向运行到探测点时保存的寄存器内容。
kprobe负责在调用pre_hand ler时传递这些参数,用户不必关心,只是要知道在该函数内就能访问这些内容,一般地,它应当始终返回0。
探测点处理函数post_handler的原型如下:
void post_hand l e r(s truc t kprobe*p,s truc t pt_regs*r egs, uns i g ned long fl a gs)
前两个参数与pre_handler相同,最后一个参数flags总是0。
错误处理函数fau lt_hand l er的原型如下:
int fault_handler(struct kprobe*p,struct pt_regs*regs,int trapnr)
前两个参数与pre_handler相同,第三个参数trapnr是与错误处理相关的架构依赖的trap号(例如,对于i386,通常的保护错误是13,而页失效错误是14)。
如果成功地处理了异常,它应当返回1。
2 reg ist e r_j p robe
该函数用于注册jprobes类型的探测点,它的原型如下:
int regis t er_jprobe(struct jpr obe*jp)
为了使用该函数,用户需要在源文件中包含头文件li nux/kprobes h。
用户在调用该注册函数前需要定义一个struct j probe类型的变量并设置它的kp addr和entry字段,kp addr指定探测点的位置,它必须是被探测函数的第一条指令的地址, entry指定探测点的处理函数,该处理函数的参数表和返回类型应当与被探测函数完全相同,而且它必须正好在返回前调用jprobe_re t urn()。
如果被探测函数被声明为as m li nkag e、fastcall或影响参数传递的任何其他形式,那么相应的处理函数也必须声明为相应的形式。
该注册函数在j p->kp addr注册一个j probes类型的探测点,当内核运行到该探测点时,j p->entry指定的函数会被执行。
如果成功,该函数返回0,否则返回负的错误码。
3 reg ist e r_kre t p robe
该函数用于注册类型为kre tprobes的探测点,它的原型如下:
int regis t er_kret p r obe(s truc t kretprobe*rp)
为了使用该函数,用户需要在源文件中包含li nux/ kprobes h。
该注册函数的参数为struct kre tprobe类型的指针,用户在调用该函数前必须定义一个struct kretprobe的变量并设置它的kp addr、hand l er以及m axac ti ve字段,kp addr指定探测点的位置,hand l e r指定探测点的处理函数,maxactive指定可以同时运行的最大处理函数实例数,它应当被恰当设置,否则可能丢失探测点的某些运行。
该注册函数在地址rp->kp addr注册一个k retprobe类型的探测点,当被探测函数返回时,rp->hand l e r会被调用。
如果成功,它返回0,否则返回负的错误码。
kretprobe处理函数的原型如下:
int kret p r obe_ha ndler(str uct kretpr obe_i n st a nce*r,i struc t pt_ regs*r egs)
参数regs指向保存的寄存器,ri指向类型为struct kret probe_i nstance的变量,该结构的ret_addr字段表示返回地址,rp指向相应的kre t probe_instance变量,task字段指向相应的task_struc t。
结构struct kretprobe_i nstance是注册函数reg i ster_kretprobe根据用户指定的m axacti ve值来分配的, kprobe负责在调用kretprobe处理函数时传递相应的kretpro be_i nstance。
4 un reg ist e r*p robe
对应于每一个注册函数,有相应的卸载函数。
350
void unreg i s ter_kprobe(s truc t kprobe*kp)
void unreg i s ter_j p robe(struct jprobe*jp)
void unreg i s ter_kretprobe(struct kretprobe*rp)
上面是对应与三种探测点类型的卸载函数,当使用探测点的模块卸载或需要卸载已经注册的探测点时,需要使用相应的卸载函数来卸载已经注册的探测点,kp,jp和rp 分别为指向结构struct kprobe、struc t jprobe和struct kretprobe 的指针,它们应当指向调用对应的注册函数时使用的那个结构,也就说注册和卸载必须针对同样的探测点,否则会导致系统崩溃。
这些卸载函数可以在注册后的任何时刻调用。
四、结束语
本文主要介绍了收集和调试内核的K probe工具工作机制,它可以将断点插入到正在运行的内核之中。
使用K probe的好处有很多。
不需要重新编译和重新引导内核就可以插入pri n t k,为了进行调试,可以记录处理器寄存器的日志,甚至进行修改 不会干扰系统。
类似地,可以轻松地收集处理器寄存器和全局数据结构等调试信息,开发者可以使用K probe来修改寄存器值和全局数据结构的值。
您甚至可以使用K probe调试S M P系统上的竞态条件 避免了自己重新编译和重新引导的所有麻烦,内核调试将比以往更为快速和简单。
参考文献:
[1]陈燕晖,罗宇.L i nux内核数据采集的一种有效方法.计算机应用与软件,2006,6.
[2]Dan i el P.Bovet,M arco Cesat,i Unders t and i ng t h e L i nux K ern e,l 2nd Ed iti on,OpR eill y,2002.
[3]Kprob e project.h tt p://www-124.i bm.co m.
[4]L i nux T race Too l k itP roj ect,h tt p://www.opersys.co m/LTT/.
(上接第348页)
硬件(IB M370)和原有操作系统(VM/370)的结构都支持虚拟机。
采用虚拟机法增强操作系统的安全性时,硬件特性对虚拟机的实现非常关键,它要求原系统的硬件和结构都要支持虚拟机,因此用这种方法开发安全操作系统的局限性很大。
2 改进/增强法
在现有操作系统的基础上,对其内核和应用程序进行面向安全策略的分析,然后加入安全机制,经改造、开发后的安全操作系统基本上保持原操作系统的用户接口界面。
由于改进/增强法是在现有系统的基础上开发增强安全性的,受其体系结构和现有应用程序的限制,所以很难达到很高(如B3级以上)的安全级别。
但这种方法不破坏原系统的体系结构,开发代价小,且能很好地保持原操作系统的用户接口界面和系统效率。
3 仿真法
对现有操作系统的内核做面向安全策略的修改,然后在安全内核与原有操作系统用户接口界面中间,再编写一层仿真程序。
这样,在建立安全内核时,可以不必受现有的应用程序的限制,且可以完全自由地定义操作系统的仿真程序与安全核之间的接口。
但采用这种方法要同时设计仿真程序和安全内核,还要受顶层。
操作系统接口的限制。
另外,根据安全策略,有些操作系统的接口功能不安全,从而不能仿真,有些接口尽管安全,但仿真实现特别困难。
三、总结
L inux安全操作系统实现了多级安全保护,即在原有的基础上增加了强制访问控制、安全审计、特权管理等安全模块,这样就使系统的安全性能得到增强。
本文在原理和技术实现上都给与了详细的分析。
参考文献:
[1]杨沿航,孙冰心.操作系统的安全策略.东北林业大学学报.
[2]王亚辉,衷克定,于鹦.一种基LI NUX操作系统的安全增强实现思路.计算机应用与软件.
[3]张衡,张毓森,路卫东.L i nux安全增强系统中能力机制的实现与评估[J].系统仿真学报,2004,16(1):79-81.
351。