Linux下 kprobe工具的使用

此处转载:

一、Kprobe简单介绍

kprobe是一个动态地收集调试和性能信息的工具,它从Dprobe项目派生而来,是一种非破坏性工具,用户用它差点儿能够跟踪不论什么函数或被运行的指令以及一些异步事件(如timer)。

它的基本工作机制是:用户指定一个探測点。并把一个用户定义的处理函数关联到该探測点。当内核运行到该探測点时,对应的关联函数被运行。然后继续运行正常的代码路径。


kprobe实现了三种类型的探測点: kprobes, jprobes和kretprobes (也叫返回探測点)。

kprobes是能够被插入到内核的不论什么指令位置的探測点,jprobes则仅仅能被插入到一个内核函数的入口,而kretprobes则是在指定的内核函数返回时才被运行。

一般。使用kprobe的程序实现作一个内核模块。模块的初始化函数来负责安装探測点。退出函数卸载那些被安装的探測点。kprobe提供了接口函数(APIs)来安装或卸载探測点。

眼下kprobe支持例如以下架构:i386、x86_64、ppc64、ia64(不支持对slot1指令的探測)、sparc64 (返回探測还没有实现)。

二、Kprobe实现原理

当安装一个kprobes探測点时。kprobe首先备份被探測的指令,然后使用断点指令(即在i386和x86_64的int3指令)来代替被探測指令的头一个或几个字节。当CPU运行到探測点时,将因运行断点指令而运行trap操作,那将导致保存CPU的寄存器,调用对应的trap处理函数。而trap处理函数将调用对应的notifier_call_chain(内核中一种异步工作机制)中注冊的全部notifier函数。kprobe正是通过向trap对应的notifier_call_chain注冊关联到探測点的处理函数来实现探測处理的。

当kprobe注冊的notifier被运行时,它首先运行关联到探測点的pre_handler函数,并把对应的kprobe struct和保存的寄存器作为该函数的參数,接着,kprobe单步运行被探測指令的备份。最后,kprobe运行post_handler。等全部这些运行完成后。紧跟在被探測指令后的指令流将被正常运行。

kretprobe也使用了kprobes来实现,当用户调用register_kretprobe()时,kprobe在被探測函数的入口建立了一个探測点。当运行到探測点时,kprobe保存了被探測函数的返回地址并代替返回地址为一个trampoline的地址,kprobe在初始化时定义了该trampoline而且为该trampoline注冊了一个kprobe,当被探測函数运行它的返回指令时。控制传递到该trampoline,因此kprobe已经注冊的相应于trampoline的处理函数将被运行。而该处理函数会调用用户关联到该kretprobe上的处理函数。处理完成后,设置指令寄存器指向已经备份的函数返回地址。因而原来的函数返回被正常运行。

被探測函数的返回地址保存在类型为kretprobe_instance的变量中。结构kretprobe的maxactive字段指定了被探測函数能够被同一时候探測的实例数,函数register_kretprobe()将预分配指定数量的kretprobe_instance。假设被探測函数是非递归的而且调用时已经保持了自旋锁(spinlock),那么maxactive为1就足够了; 假设被探測函数是非递归的且执行时是抢占失效的,那么maxactive为NR_CPUS就能够了;假设maxactive被设置为小于等于0, 它被设置到缺省值(假设抢占使能, 即配置了 CONFIG_PREEMPT,缺省值为10和2*NR_CPUS中的最大值,否则缺省值为NR_CPUS)。

假设maxactive被设置的太小了,一些探測点的运行可能被丢失,可是不影响系统的正常运行,在结构kretprobe中nmissed字段将记录被丢失的探測点运行数,它在返回探測点被注冊时设置为0,每次当运行探測函数而没有kretprobe_instance可用时,它就加1。

三、Kprobe注冊函数

kprobe为每一类型的探測点提供了注冊和卸载函数。



1.register_kprobe

它用于注冊一个kprobes类型的探測点,其函数原型为:

int register_kprobe(struct kprobe *kp);

为了使用该函数。用户须要在源文件里包括头文件linux/kprobes.h。

该函数的參数是struct kprobe类型的指针。struct kprobe包括了字段addr、pre_handler、post_handler和fault_handler,addr指定探測点的位置,pre_handler指定运行到探測点时运行的处理函数,post_handler指定运行完探測点后运行的处理函数。fault_handler指定错误处理函数,当在运行pre_handler、post_handler以及被探測函数期间错误发生时。它会被调用。在调用该注冊函数前。用户必须先设置好struct kprobe的这些字段,用户能够指定不论什么处理函数为NULL。

该注冊函数会在kp->addr地址处注冊一个kprobes类型的探測点,当运行到该探測点时,将调用函数kp->pre_handler。运行完被探測函数后,将调用kp->post_handler。假设在运行kp->pre_handler或kp->post_handler时或在单步跟踪被探測函数期间错误发生,将调用kp->fault_handler。



该函数成功时返回0,否则返回负的错误码。

探測点处理函数pre_handler的原型例如以下:

int pre_handler(struct kprobe *p, struct pt_regs *regs);

用户必须依照该原型參数格式定义自己的pre_handler,当然函数名取决于用户自己。

參数p就是指向该处理函数关联到的kprobes探測点的指针,能够在该函数内部引用该结构的不论什么字段。就如同在使用调用register_kprobe时传递的那个參数。參数regs指向执行到探測点时保存的寄存器内容。kprobe负责在调用pre_handler时传递这些參数,用户不必关心,仅仅是要知道在该函数内你能訪问这些内容。

一般地,它应当始终返回0。除非用户知道自己在做什么。



探測点处理函数post_handler的原型例如以下:

void post_handler(struct kprobe *p, struct pt_regs *regs,
	unsigned long flags);

前两个參数与pre_handler同样。最后一个參数flags总是0。

错误处理函数fault_handler的原刑例如以下:

int fault_handler(struct kprobe *p, struct pt_regs *regs, int trapnr);
前两个參数与pre_handler同样,第三个參数trapnr是与错误处理相关的架构依赖的trap号(比如,对于i386,通常的保护错误是13。而页失效错误是14)。

假设成功地处理了异常。它应当返回1。
2 . register_kretprobe

该函数用于注冊类型为kretprobes的探測点。它的原型例如以下:
int register_kretprobe(struct kretprobe *rp);

为了使用该函数,用户须要在源文件里包括头文件linux/kprobes.h。

该注冊函数的參数为struct kretprobe类型的指针,用户在调用该函数前必须定义一个struct kretprobe的变量并设置它的kp.addr、handler以及maxactive字段。kp.addr指定探測点的位置,handler指定探測点的处理函数。maxactive指定能够同一时候执行的最大处理函数实例数,它应当被恰当设置。否则可能丢失探測点的某些执行。

该注冊函数在地址rp->kp.addr注冊一个kretprobe类型的探測点。当被探測函数返回时。rp->handler会被调用。

假设成功,它返回0,否则返回负的错误码。



kretprobe处理函数的原型例如以下:

int kretprobe_handler(struct kretprobe_instance *ri, struct pt_regs *regs);
參数regs指向保存的寄存器。ri指向类型为struct kretprobe_instance的变量,该结构的ret_addr字段表示返回地址,rp指向对应的kretprobe_instance变量,task字段指向对应的task_struct。结构struct kretprobe_instance是注冊函数register_kretprobe依据用户指定的maxactive值来分配的。kprobe负责在调用kretprobe处理函数时传递对应的kretprobe_instance。

3. 相应于每个注冊函数。有相应的卸载函数。

void unregister_kprobe(struct kprobe *kp);
void unregister_jprobe(struct jprobe *jp);
void unregister_kretprobe(struct kretprobe *rp);

上面是相应与三种探測点类型的卸载函数。当使用探測点的模块卸载或须要卸载已经注冊的探測点时,须要使用相应的卸载函数来卸载已经注冊的探測点。kp,jp和rp分别为指向结构struct kprobe,struct jprobe和struct kretprobe的指针。它们应当指向调用相应的注冊函数时使用的那个结构。也就说注冊和卸载必须针对相同的探測点。否则会导致系统崩溃。这些卸载函数能够在注冊后的不论什么时刻调用。

四 、Kprobe限制

kprobe同意在同一地址注冊多个kprobes,可是不能同一时候在该地址上有多个jprobes。



通常,用户能够在内核的不论什么位置注冊探測点,特别是能够对中断处理函数注冊探測点,可是也有一些例外。

假设用户尝试在实现kprobe的代码(包含kernel/kprobes.c和arch/*/kernel/kprobes.c以及do_page_fault和notifier_call_chain)中注冊探測点。register_*probe将返回-EINVAL.

假设为一个内联(inline)函数注冊探測点,kprobe无法保证对该函数的全部实例都注冊探測点,由于gcc可能隐式地内联一个函数。因此,要记住,用户可能看不到预期的探測点的运行。



一个探測点处理函数可以改动被探測函数的上下文,如改动内核数据结构,寄存器等。因此,kprobe可以用来安装bug解决代码或注入一些错误或測试代码。



假设一个探測处理函数调用了还有一个探測点,该探測点的处理函数不将执行,可是它的nmissed数将加1。

多个探測点处理函数或同一处理函数的多个实例可以在不同的CPU上同一时候执行。

除了注冊和卸载,kprobe不会使用mutexe或分配内存。



探測点处理函数在执行时是失效抢占的。依赖于特定的架构,探測点处理函数执行时也可能是中断失效的。因此,对于不论什么探測点处理函数,不要使用导致睡眠或进程调度的不论什么内核函数(如尝试获得semaphore)。



kretprobe是通过代替返回地址为提前定义的trampoline的地址来实现的。因此栈回溯和gcc内嵌函数__builtin_return_address()调用将返回trampoline的地址而不是真正的被探測函数的返回地址。



假设一个函数的调用次数与它的返回次数不同样,那么在该函数上注冊的kretprobe探測点可能产生无法预料的结果(do_exit()就是一个典型的样例,但do_execve() 和 do_fork()没有问题)。



当进入或退出一个函数时,假设CPU正执行在一个非当前任务全部的栈上,那么该函数的kretprobe探測可能产生无法预料的结果,因此kprobe并不支持在x86_64上对__switch_to()的返回探測。假设用户对它注冊探測点,注冊函数将返回-EINVAL。

五、怎样在内核中引入Kprobe

kprobe已经被包括在2.6内核中。可是仅仅有最新的内核才提供了上面描写叙述的所有功能,因此假设读者想实验本文附带的内核模块,须要最新的内核,作者在2.6.18内核上測试的这些代码。内核缺省时并没有使能kprobe,因此用户需使能它。

为了使能kprobe。用户必须在编译内核时设置CONFIG_KPROBES,即选择在“Instrumentation Support“中的“Kprobes”项。假设用户希望动态载入和卸载使用kprobe的模块,还必须确保“Loadable module support” (CONFIG_MODULES)和“Module unloading” (CONFIG_MODULE_UNLOAD)设置为y。假设用户还想使用kallsyms_lookup_name()来得到被探測函数的地址,也要确保CONFIG_KALLSYMS设置为y,当然设置CONFIG_KALLSYMS_ALL为y将更好。

六、Kprobe使用实例

演示样例见内核原码: samples/kprobes/

原文地址:https://www.cnblogs.com/llguanli/p/7238680.html