注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

kprobe实现基础  

2013-09-02 23:29:48|  分类: Linux内核 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、systemtap
查看这个功能的引入点是stap来探测一些系统调用的情况,虽然strace可以查看一个进程所有的系统调用,但是系统中进程对于某个系统调用的统计情况并没有好的方法来获得,例如一个系统中有用户正在写入一个文件,而此时又没有办法确定是谁在向文件中写入数据,此时我们系统能够在系统的底层设置一个断点,动态捕捉这个函数的调用者及上下文信息。或者通俗的说,就是一个简单的gdb调试器的功能,由于用户态调试无法进入内核,虽然strace可以获得系统调用情况,但是它并不是在系统空间中打断点,而只是通过内核提供的功能来实现。
更高级的调试可以使用kgdb或者虚拟机等来对内核进行调试,但是这些总是剑走偏锋,对于系统的要求比较高,搭建和实现的难度相对较大。其实在windows环境下也有高级的实时内核调试工具,那就是softice,这个工具我只是试用过一点点,比较容易蓝屏。
二、kprobe的实现基础
它的实现基础和gdb的实现机制一样,只是gdb依赖内核提供的功能,内核将断点命中时间捕捉封装之后发送给调试进程,而内核来说它没有更加底层的机制来依赖,所以只能自己完成,而这个完成的时机就是在内核的中断处理函数中。
在kprobe设置了内核地址(或者只是给出地址之后让内核来代劳查找地址)之后,内核毫不客气的在这个位置设置了断点指令,也就是将该位置的原始指令复制一份出来,然后将原始内容中写入断点指令,这个指令对于intel处理器来说,就是一个0xcc字节。
在2.6.21内核中,这个指令的拷贝在prepare函数中完成
int __kprobes arch_prepare_kprobe(struct kprobe *p)
{
    /* insn: must be on special executable page on i386. */
    p->ainsn.insn = get_insn_slot();
    if (!p->ainsn.insn)
        return -ENOMEM;

    memcpy(p->ainsn.insn, p->addr, MAX_INSN_SIZE * sizeof(kprobe_opcode_t));
    p->opcode = *p->addr;
    if (can_boost(p->addr)) {
        p->ainsn.boostable = 0;
    } else {
        p->ainsn.boostable = -1;
    }
    return 0;
}
这里并没有只是简单的拷贝出一个自己,而是拷贝了16个字节,这样满足一个条件,那就是它一定能够容纳一个完整的指令,包括操作数,这一点在后面会说明原因。
而断点的插入则在arm函数中完成,这个arm不是我们常见的嵌入式中的arm,而是武装,装备的意思:
typedef u8 kprobe_opcode_t;
#define BREAKPOINT_INSTRUCTION    0xcc
void __kprobes arch_arm_kprobe(struct kprobe *p)
{
    *p->addr = BREAKPOINT_INSTRUCTION;
    flush_icache_range((unsigned long) p->addr,
               (unsigned long) p->addr + sizeof(kprobe_opcode_t));
}
三、断点命中之后
在断点命中之后,此时内核算是捕捉到了这个地址被执行的事件,此时内核就可以执行在该地址上注册的事件回调函数,也就是kprobe中的pre_handler,如果该函数返回值为非零,表示内核不要再处理这个事件,事情就此结束,这种情况适用于使用setjmp和longjmp的机制,它可以直接设置跳转地址,而不用后续操作。
通常情况下,这个prehandler在执行probe之后返回非零,因为probe只是简单的查看,而不会修改原始的逻辑,虽然修改也是可行的。这里的问题恢复问题,就像malloc之后的free,这个操作往往比较复杂。
直观的想,我们把被断点指令替换的一个原始字节拷贝回去,继续执行就好了。但是这里的问题是在多核环境下,代码段是所有CPU共享的,此时如果将原始指令拷贝回去,其它CPU执行的时候就可能会miss这个断点。
此时kprobe采用的办法就是将这个地址直接转移到原来拷贝的16字节指令来执行,这个16字节足以保存所以intel处理器下所有的完整合法指令。也就是说,probe点的代码并不在原始位置执行,而是跳转到新的一个孤零零的一条指令处去执行。
这里马上就遇到了另一个问题,这个地方只有一条指令,执行完这条指令之后CPU继续执行肯定会跌入未知的深渊,这个问题也比较棘手。
kprobe采用的方法是在执行拷贝代码的时候设置CPU的TRAP标志,当这个标志置位之后,在CPU执行完下一条指令之后会被挂起,生成一个DEBUG中断,此时kprobe就可以再次做特殊处理。
事实上,kprobe中注册的posthandler就是在这个事件上下文下执行的。但是问题并没有就此结束,如果说拷贝的代码是一个call指令或者jmp指令,此时的eip将会产生一个跃迁、如果跳转的是相对地址,那么此时EIP中的数值已经错误。因为原始的代码段中的相对地址是相对于断点位置的地址,而此时指令放在拷贝出来的指令之后,此时的EIP明显是一个错误的相对地址。对于其他的无跳跃情况,例如mov指令,那么接下来的eip地址也是需要调整的,此时的调整同样由kprobe来手动修正,这个修正的代码为

/*
 * Called after single-stepping.  p->addr is the address of the
 * instruction whose first byte has been replaced by the "int 3"
 * instruction.  To avoid the SMP problems that can occur when we
 * temporarily put back the original opcode to single-step, we
 * single-stepped a copy of the instruction.  The address of this
 * copy is p->ainsn.insn.
 *
 * This function prepares to return from the post-single-step
 * interrupt.  We have to fix up the stack as follows:
 *
 * 0) Except in the case of absolute or indirect jump or call instructions,
 * the new eip is relative to the copied instruction.  We need to make
 * it relative to the original instruction.
 *
 * 1) If the single-stepped instruction was pushfl, then the TF and IF
 * flags are set in the just-pushed eflags, and may need to be cleared.
 *
 * 2) If the single-stepped instruction was a call, the return address
 * that is atop the stack is the address following the copied instruction.
 * We need to make it the address following the original instruction.
 *
 * This function also checks instruction size for preparing direct execution.
 */
static void __kprobes resume_execution(struct kprobe *p,
        struct pt_regs *regs, struct kprobe_ctlblk *kcb)
{
    unsigned long *tos = (unsigned long *)&regs->esp;
    unsigned long copy_eip = (unsigned long)p->ainsn.insn;
    unsigned long orig_eip = (unsigned long)p->addr;

    regs->eflags &= ~TF_MASK;
    switch (p->ainsn.insn[0]) {
    case 0x9c:        /* pushfl */
        *tos &= ~(TF_MASK | IF_MASK);
        *tos |= kcb->kprobe_old_eflags;
        break;
    case 0xc2:        /* iret/ret/lret */
    case 0xc3:
    case 0xca:
    case 0xcb:
    case 0xcf:
    case 0xea:        /* jmp absolute -- eip is correct */
        /* eip is already adjusted, no more changes required */
        p->ainsn.boostable = 1;
        goto no_change;
    case 0xe8:        /* call relative - Fix return addr */
        *tos = orig_eip + (*tos - copy_eip);
        break;
    case 0x9a:        /* call absolute -- same as call absolute, indirect */
        *tos = orig_eip + (*tos - copy_eip);
        goto no_change;如果跳转指令是绝对地址,则此时不用做任何修正
    case 0xff:
        if ((p->ainsn.insn[1] & 0x30) == 0x10) {
            /*
             * call absolute, indirect
             * Fix return addr; eip is correct.
             * But this is not boostable
             */
            *tos = orig_eip + (*tos - copy_eip);
            goto no_change;
        } else if (((p->ainsn.insn[1] & 0x31) == 0x20) ||    /* jmp near, absolute indirect */
               ((p->ainsn.insn[1] & 0x31) == 0x21)) {    /* jmp far, absolute indirect */
            /* eip is correct. And this is boostable */
            p->ainsn.boostable = 1;
            goto no_change;
        }
    default:
        break;
    }

    if (p->ainsn.boostable == 0) {
        if ((regs->eip > copy_eip) &&
            (regs->eip - copy_eip) + 5 < MAX_INSN_SIZE) {
            /*
             * These instructions can be executed directly if it
             * jumps back to correct address.
             */
            set_jmp_op((void *)regs->eip,
                   (void *)orig_eip + (regs->eip - copy_eip));
            p->ainsn.boostable = 1;
        } else {
            p->ainsn.boostable = -1;
        }
    }

    regs->eip = orig_eip + (regs->eip - copy_eip);这个括号内本质上是动态计算了拷贝过来的那条指令的长度,然后修正eip为断点位置的eip+断点指令处原始指令的长度,当然,对于绝对地址,此时跳转到了下面的no_change,不需要修正

no_change:
    return;
}
四、systemtap的实现
在systemtap的代码中,它就是注册了kprobe的prehandler代码,然后在其中统一回调所有用户注册的probe point。回到开始的问题,其实对于想看到那个进程在写入块设备文件,内核中有一个变量可以完成这个功能,不用使用stap,而且stap对内核的编译有一定要求,例如systemMap,内核模块的编译环境,最好还要有内核的调试信息,而有些机器上根本不具备这些条件,所以使用这种方法可能解决很多类似的问题,
echo 1 > /proc/sys/vm/block_dump
通过dmesg查看系统打印的信息
void submit_bio(int rw, struct bio *bio)

        if (unlikely(block_dump)) {
            char b[BDEVNAME_SIZE];
            printk(KERN_DEBUG "%s(%d): %s block %Lu on %s (%u sectors)\n",
            current->comm, task_pid_nr(current),
                (rw & WRITE) ? "WRITE" : "READ",
                (unsigned long long)bio->bi_sector,
                bdevname(bio->bi_bdev, b),
                count);
        }
或者通过lsof也可以看到那些进程在使用这个文件。
  评论这张
 
阅读(766)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017