您的位置:首页 > 运维架构 > Linux

Kprobe添加linux内核钩子定位bug

2017-08-23 00:07 3355 查看

一、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。等所有这些运行完毕后,紧跟在被探测指令后的指令流将被正常执行。

jprobe通过注册kprobes在被探测函数入口的来实现,它能无缝地访问被探测函数的参数。jprobe处理函数应当和被探测函数有同样的原型,而且该处理函数在函数末必须调用kprobe提供的函数jprobe_return()。当执行到该探测点时,kprobe备份CPU寄存器和栈的一些部分,然后修改指令寄存器指向jprobe处理函数,当执行该jprobe处理函数时,寄存器和栈内容与执行真正的被探测函数一模一样,因此它不需要任何特别的处理就能访问函数参数, 在该处理函数执行到最后时,它调用jprobe_return(),那导致寄存器和栈恢复到执行探测点时的状态,因此被探测函数能被正常运行。需要注意,被探测函数的参数可能通过栈传递,也可能通过寄存器传递,但是jprobe对于两种情况都能工作,因为它既备份了栈,又备份了寄存器,当然,前提是jprobe处理函数原型必须与被探测函数完全一样。

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_jprobe

该函数用于注册jprobes类型的探测点,它的原型如下:

int register_jprobe(struct jprobe *jp);

为了使用该函数,用户需要在源文件中包含头文件linux/kprobes.h。

用户在调用该注册函数前需要定义一个struct jprobe类型的变量并设置它的kp.addr和entry字段,kp.addr指定探测点的位置,它必须是被探测函数的第一条指令的地址,entry指定探测点的处理函数,该处理函数的参数表和返回类型应当与被探测函数完全相同,而且它必须正好在返回前调用jprobe_return()。如果被探测函数被声明为asmlinkage、fastcall或影响参数传递的任何其他形式,那么相应的处理函数也必须声明为相应的形式。

该注册函数在jp->kp.addr注册一个jprobes类型的探测点,当内核运行到该探测点时,jp->entry指定的函数会被执行。

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

3.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。

4.unregister_*probe

对应于每一个注册函数,有相应的卸载函数。

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使用实例

本文附带的包包含了三个示例模块,kprobe-exam.c是kprobes使用示例,jprobe-exam.c是jprobes使用示例,kretprobe-exam.c是kretprobes使用示例,读者可以下载该包并执行如下指令来实验这些模块:

$ tar -jxvf kprobes-examples.tar.bz2
$ cd kprobes-examples
$ make
…
$ su -
…
$ insmod kprobe-example.ko
$ dmesg
…
$ rmmod kprobe-example
$ dmesg
…
$ insmod jprobe-example.ko
$ cat kprobe-example.c
$dmesg
…
$ rmmod jprobe-example
$ dmesg
…
$ insmod kretprobe-example.ko
$ dmesg
…
$ ls -Rla / > /dev/null &
$ dmesg
…
$ rmmod kretprobe-example
$ dmesg
…
$

示例模块kprobe-exame.c探测schedule()函数,在探测点执行前后分别输出当前正在运行的进程、所在的CPU以及preempt_count(),当卸载该模块时将输出该模块运行时间以及发生的调度次数。这是该模块在作者系统上的输出:

kprobe registered
current task on CPU#1: swapper (before scheduling), preempt_count = 0
current task on CPU#1: swapper (after scheduling), preempt_count = 0
current task on CPU#0: insmod (before scheduling), preempt_count = 0
current task on CPU#0: insmod (after scheduling), preempt_count = 0
current task on CPU#1: klogd (before scheduling), preempt_count = 0
current task on CPU#1: klogd (after scheduling), preempt_count = 0
current task on CPU#1: klogd (before scheduling), preempt_count = 0
current task on CPU#1: klogd (after scheduling), preempt_count = 0
current task on CPU#1: klogd (before scheduling), preempt_count = 0
…
Scheduling times is 5918 during of 7655 milliseconds.
kprobe unregistered

示例模块jprobe-exam.c是一个jprobes探测例子,它示例了获取系统调用open的参数,但读者不要试图在实际的应用中这么使用,因为copy_from_user可能导致睡眠,而kprobe并不允许在探测点处理函数中这么做(请参看前面内容了解详细描述)。

这是该模块在作者系统上的输出:

Registered a jprobe.
process 'cat' call open('/etc/ld.so.cache', 0, 0)
process 'cat' call open('/lib/libc.so.6', 0, -524289)
process 'cat' call open('/usr/lib/locale/locale-archive', 32768, 1)
process 'cat' call open('/usr/share/locale/locale.alias', 0, 438)
process 'cat' call open('/usr/lib/locale/en_US.UTF-8/LC_CTYPE', 0, 0)
process 'cat' call open('/usr/lib/locale/en_US.utf8/LC_CTYPE', 0, 0)
process 'cat' call open('/usr/lib/gconv/gconv-modules.cache', 0, 0)
process 'cat' call open('kprobe-exam.c', 32768, 0)
…
process 'rmmod' call open('/etc/ld.so.cache', 0, 0)
process 'rmmod' call open('/lib/libc.so.6', 0, -524289)
process 'rmmod' call open('/proc/modules', 0, 438)
jprobe unregistered

示例模块kretprobe-exam.c是一个返回探测例子,它探测系统调用open并输出返回值小于0的情况。它也有意设置maxactive为1,以便示例丢失探测运行的情况,当然,只有系统并发运行多个sys_open才可能导致这种情况,因此,读者需要有SMP的系统或者有超线程支持才能看到这种情况。如果读者比较仔细,会看到在前面的命令有”ls -Rla / > /dev/null & ,那是专门为了导致出现丢失探测运行的。

这是该模块在作者系统上的输出:

Registered a return probe.
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
…
kretprobe unregistered
Missed 11 sys_open probe instances.

 

 

 

 

Kprobe机制是内核提供的一种调试机制,它提供了一种方法,能够在不修改现有代码的基础上,灵活的跟踪内核函数的执行。它的基本工作原理是:用户指定一个探测点,并把一个用户定义的处理函数关联到该探测点,当内核执行到该探测点时,相应的关联函数被执行,然后继续执行正常的代码路径。

     Kprobe提供了三种形式的探测点,一种是最基本的kprobe,能够在指定代码执行前、执行后进行探测,但此时不能访问被探测函数内的相关变量信息;一种是jprobe,用于探测某一函数的入口,并且能够访问对应的函数参数;一种是kretprobe,用于完成指定函数返回值的探测功能。其中最基本的就是kprobe机制,jprobe以及kretprobe的实现都依赖于kprobe,但其代码的实现都很巧妙,强烈建议每一个内核爱好者阅读。
    
    好了,闲话少叙,开始上代码:
  首先是struct kprobe结构,每一个探测点的基本结构。

点击(此处)折叠或打开

struct kprobe
{

    /*用于保存kprobe的全局hash表,以被探测的addr为key*/

    struct hlist_node hlist;

    /* list of kprobes for multi-handler support */

    /*当对同一个探测点存在多个探测函数时,所有的函数挂在这条链上*/

    struct list_head
list;

    /*count the number of times this probe was temporarily disarmed */

    unsigned long nmissed;

    /* location of the probe point */

    /*被探测的目标地址*/

    kprobe_opcode_t *addr;

    /* Allow user to indicate symbol name of the probe point */

    /*symblo_name的存在,允许用户指定函数名而非确定的地址*/

    const char*symbol_name;

    /* Offset into the symbol */

    /*如果被探测点为函数内部某个指令,需要使用addr + offset的方式*/

    unsigned int offset;

    /* Called before addr is executed. */

    /*探测函数,在目标探测点执行之前调用*/

    kprobe_pre_handler_t pre_handler;

    /* Called after addr is executed, unless... */

    /*探测函数,在目标探测点执行之后调用*/

    kprobe_post_handler_t post_handler;

    /*

     * ... called if executing addr causes a fault (eg. page fault).

     * Return 1 if it handled fault, otherwise kernel will see it.

     */

    kprobe_fault_handler_t fault_handler;

    /*

     * ... called if breakpoint trap occurs in probe handler.

     * Return 1 if it handled break, otherwise kernel will see it.

     */

    kprobe_break_handler_t break_handler;

    /*opcode 以及 ainsn 用于保存被替换的指令码*/

    

    /* Saved opcode (which has been replaced with breakpoint) */

    kprobe_opcode_t opcode;

    /* copy of the original instruction */

    struct arch_specific_insn ainsn;

    /*

     * Indicates various status flags.

     * Protected by kprobe_mutex after this kprobe is registered.

     */

    u32 flags;

};

    对于kprobe功能的实现主要利用了内核中的两个功能特性:异常(尤其是int 3),单步执行(EFLAGS中的TF标志)。

    大概的流程:
 1)在注册探测点的时候,对被探测函数的指令码进行替换,替换为int 3的指令码;
 2)在执行int 3的异常执行中,通过通知链的方式调用kprobe的异常处理函数;
 3)在kprobe的异常出来函数中,判断是否存在pre_handler钩子,存在则执行;
 4)执行完后,准备进入单步调试,通过设置EFLAGS中的TF标志位,并且把异常返回的地址修改为保存的原指令码;
 5)代码返回,执行原有指令,执行结束后触发单步异常;
 6)在单步异常的处理中,清除单步标志,执行post_handler流程,并最终返回;

    下面又进入代码时间,首先看一下kprobe模块的初始化代码,初始化代码主要做了两件事:标记出哪些代码是不能被探测的,这些代码属于kprobe实现的关键代码;注册通知链到die_notifier,用于接收异常通知。

点击(此处)折叠或打开

初始化代码位于kernel/kprobes.c中

static int __init init_kprobes(void)

{

    int i, err= 0;

        ....

     /*kprobe_blacklist中保存的是kprobe实现的关键代码路径,这些函数不应该被kprobe探测*/

    /*

     * Lookup and populate the kprobe_blacklist.

     *

     * Unlike the kretprobe blacklist, we'll need to determine

     * the range of addresses that belong to the said functions,

     * since a kprobe need not necessarily be at the beginning

     * of a function.

     */

    for (kb= kprobe_blacklist; kb->name!=
NULL; kb++){

        kprobe_lookup_name(kb->name, addr);

        if (!addr)

            continue;

        kb->start_addr=
(unsignedlong)addr;

        symbol_name = kallsyms_lookup(kb->start_addr,

                &size,&offset,
&modname, namebuf);

        if (!symbol_name)

            kb->range= 0;

        else

            kb->range=
size;

    }

        ....

    if (!err)

        /*注册通知链到die_notifier,用于接收int 3的异常信息*/

        err = register_die_notifier(&kprobe_exceptions_nb);

         ....

}

其中的通知链:

static struct notifier_block kprobe_exceptions_nb=
{

    .notifier_call
= kprobe_exceptions_notify,

    /*优先级最高,保证最先执行*/

    .priority
= 0x7fffffff /* we need to be notified first */

};

    kprobe的注册流程register_kprobe。

点击(此处)折叠或打开

int __kprobes register_kprobe(struct kprobe*p)

{

    int ret = 0;

    struct kprobe
*old_p;

    struct module
*probed_mod;

    kprobe_opcode_t *addr;

    /*获取被探测点的地址,指定了symbol_name,则从kallsyms中获取;指定了offset,则返回addr + offset*/

    addr = kprobe_addr(p);

    if (!addr)

        return
-EINVAL;

    p->addr= addr;

    /*判断同一个kprobe是否被重复注册*/

    ret = check_kprobe_rereg(p);

    if (ret)

        return ret;

    jump_label_lock();

    preempt_disable();

    /*判断被注册的函数是否位于内核的代码段内,或位于不能探测的kprobe实现路径中*/

    if (!kernel_text_address((unsignedlong)
p->addr)||

     in_kprobes_functions((unsignedlong) p->addr)||

     ftrace_text_reserved(p->addr, p->addr)||

     jump_label_text_reserved(p->addr, p->addr))

        goto fail_with_jump_label;

    /* User can pass only KPROBE_FLAG_DISABLED to register_kprobe */

    p->flags&= KPROBE_FLAG_DISABLED;

    /*

     * Check if are we probing a module.

     */

    /*判断被探测的地址是否属于某一个模块,并且位于模块的text section内*/

    probed_mod = __module_text_address((unsignedlong) p->addr);

    if (probed_mod){

        /*如果被探测的为模块地址,首先要增加模块的引用计数*/

        /*

         * We must hold a refcount of the probed module while updating

         * its code to prohibit unexpected unloading.

         */

        if (unlikely(!try_module_get(probed_mod)))

            goto fail_with_jump_label;

        /*

         * If the module freed .init.text, we couldn't insert

         * kprobes in there.

         */

        /*如果被探测的地址位于模块的init地址段内,但该段代码区间已被释放,则直接退出*/

        if (within_module_init((unsignedlong)p->addr,
probed_mod)&&

         probed_mod->state!= MODULE_STATE_COMING){

            module_put(probed_mod);

            goto fail_with_jump_label;

        }

    }

    preempt_enable();

    jump_label_unlock();

    p->nmissed= 0;

    INIT_LIST_HEAD(&p->list);

    mutex_lock(&kprobe_mutex);

    jump_label_lock();/* needed to call jump_label_text_reserved() */

    get_online_cpus();    /* For avoiding text_mutex deadlock. */

    mutex_lock(&text_mutex);

    /*判断在同一个探测点是否已经注册了其他的探测函数*/

    old_p = get_kprobe(p->addr);

    if (old_p){

        /* Since this may unoptimize old_p, locking text_mutex. */

        /*如果已经存在注册过的kprobe,则将探测点的函数修改为aggr_pre_handler,并将所有的handler挂载到其链表上,由其负责所有handler函数的执行*/

        ret = register_aggr_kprobe(old_p, p);

        goto out;

    }

    /* 分配特定的内存地址用于保存原有的指令

     * 按照内核注释,被分配的地址必须must be on special executable page on x86.

     * 该地址被保存在kprobe->ainsn.insn

     */

    ret = arch_prepare_kprobe(p);

    if (ret)

        goto out;

    /*将kprobe加入到相应的hash表内*/

    INIT_HLIST_NODE(&p->hlist);

    hlist_add_head_rcu(&p->hlist,

         &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]);

    if (!kprobes_all_disarmed&&
!kprobe_disabled(p))

/*将探测点的指令码修改为int 3指令*/
        __arm_kprobe(p);

    /* Try to optimize kprobe */

    try_to_optimize_kprobe(p);

out:

    mutex_unlock(&text_mutex);

    put_online_cpus();

    jump_label_unlock();

    mutex_unlock(&kprobe_mutex);

    if (probed_mod)

        module_put(probed_mod);

    return ret;

fail_with_jump_label:

    preempt_enable();

    jump_label_unlock();

    return -EINVAL;

    注册完毕,就开始kprobe的执行流程了。对于该探测点,由于其起始指令已经被修改为int3,因此在执行到该地址时,必然会触发3号中断向量的处理流程do_int3.

点击(此处)折叠或打开

/* May run on IST stack. */

dotraplinkage void __kprobes do_int3(struct pt_regs*regs,
long error_code)

{

#ifdef CONFIG_KGDB_LOW_LEVEL_TRAP

    if (kgdb_ll_trap(DIE_INT3,"int3", regs,
error_code, 3, SIGTRAP)

            == NOTIFY_STOP)

        return;

#endif /* CONFIG_KGDB_LOW_LEVEL_TRAP */

#ifdef CONFIG_KPROBES

    /*在这里以DIE_INT3,通知kprobe注册的通知链*/
    if (notify_die(DIE_INT3,"int3", regs,
error_code, 3, SIGTRAP)

            == NOTIFY_STOP)

        return;

#else

    if (notify_die(DIE_TRAP,"int3", regs,
error_code, 3, SIGTRAP)

            == NOTIFY_STOP)

        return;

#endif

    preempt_conditional_sti(regs);

    do_trap(3, SIGTRAP,"int3", regs, error_code,NULL);

    preempt_conditional_cli(regs);

}

    在do_int3中触发kprobe注册的通知链函数,kprobe_exceptions_notify。由于kprobe以及jprobe等机制的处理核心都在此函数内,这里只针对kprobe的流程进行分析:进入函数的原因是DIE_INT3,并且是第一次进入该函数。

点击(此处)折叠或打开

int __kprobes kprobe_exceptions_notify(struct notifier_block*self,

                 unsigned long val,void
*data)

{

    struct die_args
*args = data;

    int ret = NOTIFY_DONE;

    if (args->regs&& user_mode_vm(args->regs))

        return ret;

    switch (val){

    case DIE_INT3:

/*对于kprobe,进入kprobe_handle*/
        if (kprobe_handler(args->regs))

            ret = NOTIFY_STOP;

        break;

    case DIE_DEBUG:

        if (post_kprobe_handler(args->regs)){

            /*

             * Reset the BS bit in dr6 (pointed by args->err) to

             * denote completion of processing

             */

            (*(unsignedlong
*)ERR_PTR(args->err))&=
~DR_STEP;

            ret = NOTIFY_STOP;

        }

        break;

    case DIE_GPF:

        /*

         * To be potentially processing a kprobe fault and to

         * trust the result from kprobe_running(), we have

         * be non-preemptible.

         */

        if (!preemptible()&&
kprobe_running()&&

         kprobe_fault_handler(args->regs, args->trapnr))

            ret = NOTIFY_STOP;

        break;

    default:

        break;

    }

    return ret;

}

点击(此处)折叠或打开

static int __kprobes kprobe_handler(struct pt_regs*regs)

{

    kprobe_opcode_t *addr;

    struct kprobe
*p;

    struct kprobe_ctlblk
*kcb;

    /*对于int 3中断,其被Intel定义为Trap,那么异常发生时EIP寄存器内指向的为异常指令的后一条指令*/
    addr = (kprobe_opcode_t*)(regs->ip-
sizeof(kprobe_opcode_t));

    /*

     * We don't want to be preempted for the entire

     * duration of kprobe processing. We conditionally

     * re-enable preemption at the end of this function,

     * and also in reenter_kprobe() and setup_singlestep().

     */

    preempt_disable();

    kcb = get_kprobe_ctlblk();

    /*获取addr对应的kprobe*/
    p = get_kprobe(addr);

    if (p){

/*如果异常的进入是由kprobe导致,则进入reenter_kprobe(jprobe需要,到时候分析)*/
        if (kprobe_running()){

            if
(reenter_kprobe(p, regs, kcb))

                return 1;

        } else{

            set_current_kprobe(p, regs, kcb);

            kcb->kprobe_status= KPROBE_HIT_ACTIVE;

            /*

             * If we have no pre-handler or it returned 0, we

             * continue with normal processing. If we have a

             * pre-handler and it returned non-zero, it prepped

             * for calling the break_handler below on re-entry

             * for jprobe processing, so get out doing nothing

             * more here.

             */

    /*执行在此地址上挂载的pre_handle函数*/
            if
(!p->pre_handler||
!p->pre_handler(p, regs))

/*设置单步调试模式,为post_handle函数的执行做准备*/
                setup_singlestep(p, regs, kcb, 0);

            return 1;

        }

    } elseif
(*addr!= BREAKPOINT_INSTRUCTION){

        /*

         * The breakpoint instruction was removed right

         * after we hit it. Another cpu has removed

         * either a probepoint or a debugger breakpoint

         * at this address. In either case, no further

         * handling of this interrupt is appropriate.

         * Back up over the (now missing) int3 and run

         * the original instruction.

         */

        regs->ip=
(unsignedlong)addr;

        preempt_enable_no_resched();

        return 1;

    } elseif
(kprobe_running()){

        p = __this_cpu_read(current_kprobe);

        if (p->break_handler&& p->break_handler(p,
regs)){

            setup_singlestep(p, regs, kcb, 0);

            return 1;

        }

    } /* else: not a kprobe fault; let the kernel handle it */

    preempt_enable_no_resched();

    return 0;

}

点击(此处)折叠或打开

static void __kprobes setup_singlestep(struct kprobe*p,
struct pt_regs *regs,

                 struct kprobe_ctlblk
*kcb, int reenter)

{

    if (setup_detour_execution(p, regs, reenter))

        return;

#if !defined(CONFIG_PREEMPT)

    if (p->ainsn.boostable==
1 &&
!p->post_handler){

        /* Boost up -- we can execute copied instructions directly */

        if (!reenter)

            reset_current_kprobe();

        /*

         * Reentering boosted probe doesn't reset current_kprobe,

         * nor set current_kprobe, because it doesn't use single

         * stepping.

         */

        regs->ip=
(unsignedlong)p->ainsn.insn;

        preempt_enable_no_resched();

        return;

    }

#endif

    /*jprobe*/
    if (reenter){

        save_previous_kprobe(kcb);

        set_current_kprobe(p, regs, kcb);

        kcb->kprobe_status= KPROBE_REENTER;

    } else

        kcb->kprobe_status= KPROBE_HIT_SS;

    /* Prepare real single stepping */

    /*准备单步模式,设置EFLAGS的TF标志位,清楚IF标志位(禁止中断)*/
    clear_btf();

    regs->flags|= X86_EFLAGS_TF;

    regs->flags&=
~X86_EFLAGS_IF;

    /* single step inline if the instruction is an int3 */

    if (p->opcode== BREAKPOINT_INSTRUCTION)

        regs->ip=
(unsignedlong)p->addr;

    else

/*设置异常返回的指令为保存的被探测点的指令*/
        regs->ip=
(unsignedlong)p->ainsn.insn;

}

     对应kprobe,pre_handle的执行就结束了,按照代码,程序开始执行保存的被探测点的指令,由于开启了单步调试模式,执行完指令后会继续触发异常,这次的是do_debug异常处理流程。

点击(此处)折叠或打开

dotraplinkage void __kprobes do_debug(struct pt_regs*regs,
long error_code)

{

    ....

    /*在do_debug中,以DIE_DEBUG再一次触发kprobe的通知链*/
    if (notify_die(DIE_DEBUG,"debug", regs,
PTR_ERR(&dr6), error_code,

                            SIGTRAP)
== NOTIFY_STOP)

        return;
   
    ....
    return;

}

点击(此处)折叠或打开

/*对于kprobe_exceptions_notify,其DIE_DEBUG处理流程*/
case DIE_DEBUG:

        if (post_kprobe_handler(args->regs)){

            /*

             * Reset the BS bit in dr6 (pointed by args->err) to

             * denote completion of processing

             */

            (*(unsignedlong
*)ERR_PTR(args->err))&=
~DR_STEP;

            ret = NOTIFY_STOP;

        }

        break;

static int __kprobes post_kprobe_handler(struct pt_regs*regs)

{

    struct kprobe
*cur = kprobe_running();

    struct kprobe_ctlblk
*kcb = get_kprobe_ctlblk();

    if (!cur)

        return 0;

    /*设置异常返回的EIP为下一条需要执行的指令*/

    resume_execution(cur, regs, kcb);

    /*恢复异常执行前的EFLAGS*/

    regs->flags|= kcb->kprobe_saved_flags;

    /*执行post_handler函数*/

    if ((kcb->kprobe_status!=
KPROBE_REENTER)&& cur->post_handler){

        kcb->kprobe_status= KPROBE_HIT_SSDONE;

        cur->post_handler(cur, regs, 0);

    }

    /* Restore back the original saved kprobes variables and continue. */

    if (kcb->kprobe_status== KPROBE_REENTER){

        restore_previous_kprobe(kcb);

        goto out;

    }

    reset_current_kprobe();

out:

    preempt_enable_no_resched();

    /*

     * if somebody else is singlestepping across a probe point, flags

     * will have TF set, in which case, continue the remaining processing

     * of do_debug, as if this is not a probe hit.

     */

    if (regs->flags& X86_EFLAGS_TF)

        return 0;

    return 1;

}

    至此,一个典型的kprobe的流程已经执行完毕了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: