2013年12月25日星期三

Kprobes源码分析----kprobe的注册

    kprobes是一个动态地收集调试和性能信息的工具,使用它几乎可以跟踪任何函数或被执行的指令。它的机制也很简单,就是将被探测的位置的指令替换为断点指令(不考虑jmp优化),断点指令被执行后会通过notifier_call_chain机制来通知kprobes,kprobes会首先调用用户指定的pre_handler接口。执行pre_handler接口后会单步执行原始的指令,如果用户也指定了post_handler接口,会在调用post_handler接口后结束处理。
    基本的处理过程如下图所示:

    如果你没接触过kprobes,上面提到的pre_handler接口和post_handler接口可能让你疑惑,所以首先我们来介绍下和这些接口相关的kprobe结构。

    1.kprobe结构

    当你要探测一个函数时,需要首先分配一个kprobe实例,然后设置要探测的函数名称或地址,以及自己的处理函数。kprobe结构描述了探测点的位置以及要做的处理操作,其结构定义如下:
struct kprobe {
    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 */
    const char *symbol_name;
    /* Offset into the symbol */
    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;
    /* 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;
};
    其成员说明如下:
    hlist:所有注册的kprobe都会添加到kprobe_table哈希表中,hlist成员用来链接到某个槽位中。
    list:如果在同一个位置注册了多个kprobe,这些kprobe会形成一个队列,队首是一个特殊的kprobe实例,list成员用来用来链接到这个队列中。当探测点被触发时,队首的kprobe实例中注册的handler会逐个遍历队列中注册的handler。
    nmissed:记录当前的probe没有被处理的次数。
    addr:这个成员有两个作用,一个是用户在注册前指定探测点的基地址(加上偏移得到真实的地址),另一个是在注册后保存探测点的实际地址。在注册前,这个可以不指定,由kprobes来初始化。如果没有指定,必须指定探测的位置的符号信息,例如函数名。
    symbol_name:探测点的符号名称。名称和地址不能同时指定,否则注册时会返回EINVAL错误。
    offset:探测点相对于addr地址的偏移
    pre_handler:这个接口在断点异常触发之后,开始单步执行原始的指令之前被调用
    post_handler:在单步执行原始的指令后会被调用
    fault_handler:如果执行过程中出错,则调用该接口。如果返回1,则表示错误由kprobes处理,否则由内核来处理。
    break_handler:在调用probe的处理函数(比如pre_handler接口)时触发了断点异常会调用该接口,。断点异常是通过中断门来处理的(参见trap_init()),在调用相应的处理函数前会自动关闭中断。关中断的情况下虽然不会接收可屏蔽的中断,但是CPU引发的异常或者NMI还是会接收到,所以有可能会发生断点异常处理嵌套,jprobes的实现就用到了这点。
    opcode:原始指令,在被替换为断点指令(X86下是int 3指令)前保存。
    ainsn:保存了探测点原始指令的拷贝。这里拷贝的指令要比opcode中存储的指令多,拷贝的大小为MAX_INSN_SIZE * sizeof(kprobe_opcode_t)。
    flags:探测点的标志,可取的值为KPROBE_FLAG_GONE和​KPROBE_FLAG_DISABLED。如果设置了KPROBE_FLAG_GONE标志,表示断点指令被移除;如果设置了 KPROBE_FLAG_DISABLED,则表示只注册probe,但是并不启用它,也就是说在断点异常触发时并不会调用该probe的接口。

    2.探测点的注册----register_kprobe()

    2.1 探测点地址的计算
register_kprobe()用来在指定的位置注册探测点。它会首先调用kprobe_addr()计算需要插入探测点的地址,源码如下所示:
int __kprobes register_kprobe(struct kprobe *p)
{
    int ret = 0;
    struct kprobe *old_p;
    struct module *probed_mod;
    kprobe_opcode_t *addr;

    addr = kprobe_addr(p);
    if (!addr)
        return -EINVAL;
    p->addr = addr;
    ......
}
    kprobe_addr()会首先检查用户是否设置探测点的地址,如果指定了地址,并且指定了符号信息(symbol_name成员也设置了),它会直接返回NULL,不会去检查符号信息和指定的地址是否一致。通过这里的代码不难发现,如果同时指定了符号信息和地址,register_kprobe()会返回EINVAL错误。
   如果没有指定探测点的地址,而是指定了符号信息,kprobe_addr()会调用kprobe_lookup_name()在内核符号表中查找符号对应的地址,在找到对应的符号地址后,加上偏移就得到探测点的实际位置。
  如果只指定了探测点的地址,kprobe_addr()会将这个地址直接加上偏移返回。
  kprobe_addr()返回的地址会被设置到kprobe的addr成员中,注册后通过这个成员就可以拿到被探测位置的地址。利用这个特性,你可以通过kprobe来获取内核中某一个函数的运行时地址。假设你在编写一个内核模块,但是你要使用函数内核没有导出,这个特性就很有用了。
    2.2 检查探测点地址
   计算探测点的地址后,接着就是要检查这个地址是否可以被探测,源码如下所示:
    ......
    preempt_disable();
    if (!kernel_text_address((unsigned long) p->addr) ||
        in_kprobes_functions((unsigned long) p->addr)) {
        preempt_enable();
        return -EINVAL;
    }

    /* User can pass only KPROBE_FLAG_DISABLED to register_kprobe */
    p->flags &= KPROBE_FLAG_DISABLED;

    /*
     * Check if are we probing a module.
     */

    probed_mod = __module_text_address((unsigned long) 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))) {
            preempt_enable();
            return -EINVAL;
        }
        /*
         * If the module freed .init.text, we couldn't insert
         * kprobes in there.
         */

        if (within_module_init((unsigned long)p->addr, probed_mod) &&
            probed_mod->state != MODULE_STATE_COMING) {
            module_put(probed_mod);
            preempt_enable();
            return -EINVAL;
        }
    }
    preempt_enable();
    ......
    kprobes只能用作内核函数的探测,所以在注册前必须检查探测点的地址是否是在内核地址空间中。探测点的地址要么是在内核映像中(_stext和_etext之间,如果是在系统启动阶段,要在_sinittext和_einittext之间),要么是是在某个内核模块的地址空间中。具体的判断是在kernel_text_address()函数中处理的。
    实现kprobes的内核函数或者一些特殊的函数是不能探测的,所以还要进一步检测探测点的地址是否在这些限制空间内,这是由in_kprobes_functions()函数来处理的。kprobes把所有不能探测的函数都放在.kprobes.text section(使用__kprobes宏)中,这个段的起始地址是__kprobes_text_start和__kprobes_text_end。如果探测的地址在__kprobes_text_start和__kprobes_text_end之间,会返回EINVAL错误。某些函数也是不能探测的,但是已经放在其他section了,这些函数会放在kprobe_blacklist数组中。如果用户探测的函数在这个黑名单中,也会返回EINVAL错误。2.6.32中只有preempt_schedule()函数在kprobe_blacklist数组中,3.12中增加了native_get_debugreg()irq_entries_start()common_interrupt()mcount()
    如果探测点的地址是在一个内核模块中,需要增加对该模块的引用,以免模块提前卸载。但是如果模块已经开始卸载,此时也是不能注册探测点的,同样会返回EINVAL错误。模块中.init.text section占用的内存,在模块加载后,如果不再使用是可以被释放掉的。如果探测点在模块的.init.text section中,而该section已经被释放,此时是不能注册探测点的,同样会返回EINVAL错误。
    前面说了一大堆会返回EINVAL错误的情况,是时候要总结一下了。在以下情况中,会注册失败,返回EINVAL错误:
      1)    函数被__kprobes宏修饰的函数,这些函数会被放在.kprobes.text section中。这种类型大多数是实现kprobes的代码
      2)    kprobe_blacklist数组中列出的函数。2.6.32中只有preempt_schedule()函数,最新的3.12内核中又增加了native_get_debugreg()、irq_entries_start()、    
             common_interrupt()、mcount()。
      3)    要探测的地址位于一个模块的.init.text section,但是这个section已经被释放。
2.3 注册kprobe
    经过前面的处理后,可以开始执行真正的注册操作了,源码如下所示:
    p->nmissed = 0;
    INIT_LIST_HEAD(&p->list);
    mutex_lock(&kprobe_mutex);
    old_p = get_kprobe(p->addr);
    if (old_p) {
        ret = register_aggr_kprobe(old_p, p);
        goto out;
    }

    mutex_lock(&text_mutex);
    ret = arch_prepare_kprobe(p);
    if (ret)
        goto out_unlock_text;

    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))
        arch_arm_kprobe(p);

out_unlock_text:
    mutex_unlock(&text_mutex);
out:
    mutex_unlock(&kprobe_mutex);
    在初始化kprobe的相关成员后,首先要获取kprobe_mutex互斥锁,这个互斥锁用来保护kprobe_table哈希表,系统中所有已注册的kprobe实例都保存在这个哈希表中。
    kprobes允许在同一个探测点注册多个kprobe,如果调用get_kprobe()能找到一个kprobe实例,说明已经在当前的探测点注册了一个kprobe,这种情况下会调用register_aggr_kprobe()来处理。如果已注册的kprobe只有一个,register_aggr_kprobe()会分配一个新的kprobe实例,将已注册的kprobe中的内容拷贝到新分配的kprobe中,然后把已注册的kprobe的hanlder设置为kprobes提供的特殊的hanlder,这些handler在异常触发时会逐个遍历用户注册的kprobe中的handler。接着会将新分配的kprobe(不是当前调用register_kprobe()注册的那个)链接到前面已经注册的kprobe,形成一个队列。之后如果还有新的kprobe注册到当前的探测点,会直接添加到队列的末尾。注意,前面描述的register_aggr_kprobe()过程中还没有操作当前要注册的kprobe实例,在前面的操作都做完之后,就直接把新注册的kprobe实例添加到队尾,然后返回。这个过程中结构的变化如下图所示:

    如果在当前探测点没有注册过kprobe,则调用arch_prepare_kprobe()将被探测位置的指令保存到kprobe结构的ainsn成员中,并且被探测位置的第一条指令保存到opcode成员中。
    接着就可以将krobe实例添加到kprobe_table哈希表中,这个操作必须在将探测点的指令替换成断点指令前完成,否则有可能在注册完成前断点异常已经发生了。
    经过前面的处理,现在就剩下最后一步了,将探测点的指令替换为断点指令。这是通过调用text_poke()来完成,它可以将替换指定位置的指令。不过在调用text_poke()替换指令前,必须要获取text_mutex互斥锁。text_poke()真是太好用了,可以做很多事情.......
  至此,整个注册过程就完成了。

没有评论:

发表评论