Kprobes源码分析----kprobe的处理

    在探测点注册kprobe后,会在执行到探测点的指令时触发断点异常(trap 3)。kprobes在断点异常的通知链die_chain上注册了自己的处理函数,这个函数就是kprobe_exception_notify()。kprobe_exception_notify()不仅会接收到断点异常的通知,还会接收到调试异常(trap 1)和保护异常的通知。这篇文章主要围绕这个函数的处理来展开。

    1.断点异常处理

   断点是int 3指令触发的,系统中这个异常是由int3()函数来处理,这个函数在entry_32.S中定义。断点异常的处理函数int3()是在trap_init()中注册,注册的时候使用的门类型是中断门,这意味着在处理断点异常时是要关闭中断的,这和普通的异常处理是不同的。在关中断的情况下,CPU仍然可以接收到NMI和CPU引发的异常,包括断点异常。
    int3()中主要是调用do_int3()在处理。do_int3()中在由内核处理前,会通知注册在die_chian通知链上的模块,如果这个异常由内核其他模块处理,内核就不再处理了,源码如下所示:
dotraplinkage void __kprobes do_int3( struct pt_regs *regs, long error_code)
{
# ifdef CONFIG_KPROBES
    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);
}
    如果断点异常是由kprobe引发的, kprobe_exception_notify()会返回NOTIFY_STOP,不再由内核处理。通过前面的分析,我们可以看出 kprobe_exception_notify()是在关中断的情况下调用的,所以这个函数的处理要尽可能地短,不能发生调度,也不能出现会导致睡眠的操作(例如获取互斥锁或信号量)。
    断点异常发生时,通知链上的通知类型为DIE_INT3,这种类型在kprobe_exception_notify()中会调用kprobe_handler()来处理,如下所示:
int __kprobes kprobe_exceptions_notify( struct notifier_block *self,
                       unsigned long val, void *data)
{
......
    switch (val) {
    case DIE_INT3 :
        if (kprobe_handler(args - >regs))
            ret = NOTIFY_STOP;
        break;
    case DIE_DEBUG :
......
    return ret;
}
    kprobe_handler()中会处理普通的kprobe处理流程,即调用pre_handler接口,然后开始单步执行指令。由于在调用kprobe的handler的时候,handler中有可能会触发断点异常(虽然是关闭了中断,但是异常还是会处理的),所以kprobe_handler()也要处理kprobe重入的问题。重入的问题,这里我们只关注普通的kprobe处理流程,代码如下所示:
static int __kprobes kprobe_handler( struct pt_regs *regs)
{
    kprobe_opcode_t *addr;
    struct kprobe *p;
    struct kprobe_ctlblk *kcb;

    addr = (kprobe_opcode_t *)(regs - >ip - sizeof(kprobe_opcode_t));
    if ( *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 = ( unsigned long)addr;
        return 1;
    }

    /*
     * 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();
    p = get_kprobe(addr);

    if (p) {
        if (kprobe_running()) {
                    .......
        } 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.
             */

            if ( !p - >pre_handler || !p - >pre_handler(p, regs))
                setup_singlestep(p, regs, kcb);
            return 1;
        }
    } else if (kprobe_running()) {
        ......
    } /* else: not a kprobe fault; let the kernel handle it */

    preempt_enable_no_resched();
    return 0;
}
    执行完断点指令后,指令指针IP会指向下一个指令的位置,所以这里要使用regs->ip减去断点指令的长度,得到断点指令的地址,存在局部变量addr中。如果addr处的指令不是断点指令,kprobes就不会再处理了,将指令指针指向探测点的位置,然后返回。断点异常处理后,CPU会从addr处的指令开始执行。这种情况可能是断点指令在发生断点异常后被另一个CPU移除,或者是探测点被禁止。
    如果断点指令没有被移除,则kprobes会继续处理。kprobes会首先调用get_kprobe_ctlblk()获取kprobe控制块,获取的变量是一个per-cpu变量,这个变量中会存储kprobe处理的状态,还有可能保存处理kprobe时的寄存器信息等。
    接着会调用get_kprobe()获取addr的位置注册的kprobe。如果kprobe不存在,并且没有kprobe正在处理,则表示addr的位置没有注册kprobe,并且也不是jprobes的处理(kprobe不存在,但是有kprobe正在处理)。
    如果addr处有一个对应的kprobe,则会调用set_current_kprobe()将addr处的kprobe设置到per-cpu变量current_kprobe中,并且把TF(单步执行)和IF(开关中断)标志保存到kprobe控制块中。将kprobe变量保存到current_kprobe中,这样就可以通过这个变量来判断是否当前CPU上正在处理kprobe。
    在kprobe的处理过程中要记录当前kprobe处理的状态,在调用pre_handler之前,状态会设置为KPROBE_HIT_ACTIVE。
    如果在注册kprobe时指定了pre_handler接口,则会调用用户指定的接口。如果pre_handler接口返回1,则不会进行单步执行的过程,post_handler接口也不会被调用(依赖于单步执行过程),直接结束kprobe的处理。

    2.单步执行

    单步执行由setup_singlestep()函数来启动,它做的主要工作就是调用prepare_singlestep()来为单步执行做准备,然后把kprobe的处理状态变为KPROBE_HIT_SS,如下所示:
static void __kprobes setup_singlestep( struct kprobe *p, struct pt_regs *regs,
                       struct kprobe_ctlblk *kcb)
{
.......
    prepare_singlestep(p, regs);
    kcb - >kprobe_status = KPROBE_HIT_SS;
}
    prepare_singlesetp()中的处理也很简单,主要是将标志寄存器的TF标志位置1,然后就是设置单步执行指令的地址。如果原始的指令就是断点指令,会将指令指针指向探测点的位置;如果不是,则从保存的指令开始。如果原始的指令真的是断点指令,则会发生kprobe重入,重入时的处理后面再讲,这里先跳过。

     3.调试异常处理

    TF标志置1时,CPU每执行完一条指令就产生调试异常(trap 1)。调试异常由debug()函数处理,该函数在entry_32.S中定义,主要是调用do_debug()函数来完成的。do_debug()中会通知注册在die_chian通知链上模块,通知的类型为DIE_DEBUG。Kprobes注册的处理函数仍然是kprobe_exception_notify(),对应的处理如下所示:
int __kprobes kprobe_exceptions_notify( struct notifier_block *self,
                       unsigned long val, void *data)
{
        ......
    switch (val) {
        .......
    case DIE_DEBUG :
        if (post_kprobe_handler(args - >regs))
            ret = NOTIFY_STOP;
        break;
    .......
    return ret;
}
    post_kprobe_handler()中主要是恢复原来的处理流程,并且会调用注册时指定的post_handler接口,源码如下所示:
static int __kprobes post_kprobe_handler( struct pt_regs *regs)
{
        .......
    resume_execution(cur, regs, kcb);
    regs - >flags |= kcb - >kprobe_saved_flags;

    if ((kcb - >kprobe_status != KPROBE_REENTER) && cur - >post_handler) {
        kcb - >kprobe_status = KPROBE_HIT_SSDONE;
        cur - >post_handler(cur, regs, 0);
    }
        .......
    reset_current_kprobe();
        .......

    return 1;
}
    为了在异常处理后从探测点之后开始执行,在这里需要调用resume_execution()来将指令指针IP指向探测点之后的位置,在resume_execution()中还会清除TF标志。在kprobe处理完成后,要恢复之前保存的寄存器标志位,这里主要是TF和IF标志。
    如果kprobe控制块的状态是KPROBE_REENTER,即发生了kprobe重入,则不会调用指定的post_handler接口。
    在调用完指定的post_handler接口后,kprobe的处理就完成了。最后调用reset_current_kprobe()将current_kprobe(per-cpu变量)置为NULL,表示kprobe的处理完成了。

    4.kprobe重入

    kprobe重入是指在kprobe的处理过程中又触发了断点异常,这种情况有可能是用户指定的pre_handler或post_handler接口,或者探测点处的指令本身就是断点指令。
    如果第二次发生断点异常的位置没有注册kprobe,kprobe_handler()中会调用正在处理的kprobe的break_handler接口,源码如下所示:
static int __kprobes kprobe_handler( struct pt_regs *regs)
{
    ......
    p = get_kprobe(addr);

    if (p) {
        .......
    } else if (kprobe_running()) {
        p = __get_cpu_var(current_kprobe);
        if (p - >break_handler && p - >break_handler(p, regs)) {
            setup_singlestep(p, regs, kcb);
            return 1;
        }
    } /* else: not a kprobe fault; let the kernel handle it */
        .......
    return 0;
}
    如果指定了break_handler接口并且返回1,则会开始单步执行保存的指令,顺着普通的kprobe流程继续处理。如果在处理kprobe的过程中又发生了断点异常,这也算是一种“异常”(jprobes就利用了这点,或者是专门为jprobes准备的),这种情况需要由break_handler接口来处理。如果处理成功,即返回1,则表示可以继续处理。
    如果第二次发生断点异常的位置也注册了kprobe,kprobe_handler()中会调用reenter_kprobe()来处理,源码如下所示:
static int __kprobes reenter_kprobe( struct kprobe *p, struct pt_regs *regs,
                     struct kprobe_ctlblk *kcb)
{
    switch (kcb - >kprobe_status) {
    case KPROBE_HIT_SSDONE :
# ifdef CONFIG_X86_64
        /* TODO: Provide re-entrancy from post_kprobes_handler() and
         * avoid exception stack corruption while single-stepping on
         * the instruction of the new probe.
         */

        arch_disarm_kprobe(p);
        regs - >ip = ( unsigned long)p - >addr;
        reset_current_kprobe();
        preempt_enable_no_resched();
        break;
# endif
    case KPROBE_HIT_ACTIVE :
        save_previous_kprobe(kcb);
        set_current_kprobe(p, regs, kcb);
        kprobes_inc_nmissed_count(p);
        prepare_singlestep(p, regs);
        kcb - >kprobe_status = KPROBE_REENTER;
        break;
    case KPROBE_HIT_SS :
        if (p == kprobe_running()) {
            regs - >flags &= ~X86_EFLAGS_TF;
            regs - >flags |= kcb - >kprobe_saved_flags;
            return 0;
        } else {
            /* A probe has been hit in the codepath leading up
             * to, or just after, single-stepping of a probed
             * instruction. This entire codepath should strictly
             * reside in .kprobes.text section. Raise a warning
             * to highlight this peculiar case.
             */

        }
    default :
        /* impossible cases */
        WARN_ON( 1);
        return 0;
    }

    return 1;
}
    reenter_kprobe()中会根据当前kprobe处理的状态(kcb->kprobe_status)来做具体的处理。
    如果是KPROBE_HIT_SSDONE状态,说明是在调试异常中调用用户指定的post_handler接口时,第二次触发了断点异常,相当于是断点异常把调试异常给中断了。如果不是X86-64环境,处理和KPROBE_HIT_ACTIVE状态的处理一样。根据代码中的TODO注释,x86-64环境中单步执行新的kprobe指令时会导致异常栈崩溃,具体的原因现在不清楚,如果有知道的,麻烦告知一下。如果是在x86-64下,会调用arch_disarm_kprobe()将触发二次断点异常的kprobe设置的断点指令恢复成原始的指令,相当于是把当前处理的kprobe给禁止掉。然后将指令指针指向触发二次异常的探测点位置(此时已经没有kprobe设置的断点指令),并且调用reset_current_kprobe()将current_kprobe设置为NULL,这样前一个kprobe的处理相当于也结束了。注意,这里的处理完成后,后面还会回到调试异常的处理中,也就是返回到post_kprobe_handler()函数中。
    如果是KPROBE_HIT_ACTIVE状态,是在调用pre_handler接口时触发的断点异常。这种情况下会调用save_previous_kprobe()将前一个kprobe的信息保存到kprobe控制块kprobe_ctlblk(per-cpu变量)中,接着会调用set_current_kprobe()将当前处理的kprobe设置到current_kprobe中,最后开始单步执行新的kprobe处的指令。由于发生了kprobe重入,所以要将kprobe控制块的状态设置为KPROBE_REENTER。此时要开始新的kprobe的处理,把前一个kprobe的处理给挂起了,后面的处理流程和普通的kprobe处理流程相同。在当前的kprobe处理完成后,会继续前一个kprobe的处理,但是由于发生了重入,所以前一个kprobe的post_handler接口就不会调用了。
    如果是KPROBE_HIT_SS,是在单步执行保存的探测点指令时触发的断点异常。如果触发断点异常的指令就是探测点处的指令,此时处理的kprobe和kprobe_running()返回的kprobe是同一个,这样情况下会取消单步执行,恢复保存的寄存器标志。此时会返回0,表示由内核来处理这种情况,因为这个指令不是krobe设置上去的。如果在探测点之后的指令中,这种函数是不应该被探测的,应该放在.kprobes.text section中。此时kprobes会产生一个警告,并且返回0,由内核来处理这种情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值