zoukankan      html  css  js  c++  java
  • Linux内核调试技术——jprobe使用与实现

    摘自:https://blog.csdn.net/luckyapple1028/article/details/54350410

    前一篇博文介绍了kprobes的原理与kprobe的使用与实现方式,本文介绍kprobes中的第二种探测技术jprobe,它基于kprobe实现,不能在函数的任意位置插入探测点,只能在函数的入口处探测,一般用于监测函数的入参值。本文首先通过一个简单的示例介绍jprobe的使用方式,然后通过源码详细分析jprobe的实现流程。

    内核源码:Linux-4.1.x

    实验环境:Fedora25(x86_64)、树莓派1b

    1、jprobe使用实例


    使用jprobe探测函数的入参值,需要编写内核模块。同kprobe一样,内核同样提供了jprobe的实例程序jprobe_example.c(位于sample/kprobes目录),该程序实现了探测do_fork函数入参的功能,用户可以以它为模板来探测其他函数(当然不是说什么函数都能探测的,限制同kprobe一样,另外需要注意的是一个被探测函数只能注册一个jprobe)。在分析jprobe_example.c之前先熟悉一下jprobe的基本结构与API接口。

    1.1、jprobe结构体与API介绍


    struct jprobe结构体定义如下:

    /*
    * Special probe type that uses setjmp-longjmp type tricks to resume
    * execution at a specified entry with a matching prototype corresponding
    * to the probed function - a trick to enable arguments to become
    * accessible seamlessly by probe handling logic.
    * Note:
    * Because of the way compilers allocate stack space for local variables
    * etc upfront, regardless of sub-scopes within a function, this mirroring
    * principle currently works only for probes placed on function entry points.
    */
    struct jprobe {
    struct kprobe kp;
    void *entry; /* probe handling code to jump to */
    };
    该结构非常的简单,仅包含了一个kprobe结构(因为它是基于kprobe实现的)和一个entry指针,它保存的是探测点执行回调函数的地址,当触发调用被探测函数时,保存到该指针的地址会作为目标地址跳转执行(probe handling code to jump to),因此用户指定的探测函数得以执行。
    相关的API如下:

    int register_jprobe(struct jprobe *jp) //向内核注册jprobe探测点
    void unregister_jprobe(struct jprobe *jp) //卸载jprobe探测点
    int register_jprobes(struct jprobe **jps, int num) //注册探测函数向量,包含多个不同探测点
    void unregister_jprobes(struct jprobe **jps, int num) //卸载探测函数向量,包含多个不同探测点
    int disable_jprobe(struct jprobe *jp) //临时暂停指定探测点的探测
    int enable_jprobe(struct jprobe *jp) //恢复指定探测点的探测


    1.2、示例jprobe_example分析与演示

    同kprobe_example.c一样,该示例程序仍以do_fork作为被探测函数进行探测。当创建进程时,探测函数会调用它打印出do_fork函数的入参值。下面详细分析:

    static struct jprobe my_jprobe = {
    .entry = jdo_fork,
    .kp = {
    .symbol_name = "do_fork",
    },
    };

    static int __init jprobe_init(void)
    {
    int ret;

    ret = register_jprobe(&my_jprobe);
    if (ret < 0) {
    printk(KERN_INFO "register_jprobe failed, returned %d ", ret);
    return -1;
    }
    printk(KERN_INFO "Planted jprobe at %p, handler addr %p ",
    my_jprobe.kp.addr, my_jprobe.entry);
    return 0;
    }

    static void __exit jprobe_exit(void)
    {
    unregister_jprobe(&my_jprobe);
    printk(KERN_INFO "jprobe at %p unregistered ", my_jprobe.kp.addr);
    }
    程序定义了一个struct jprobe实例my_jprobe,指定被探测函数的名字是do_fork(可以修改它以达到探测其他函数的目的),然后探测回调函数为jdo_fork。在模块的初始化函数中,调用register_jprobe函数向kprobe子系统注册my_jprobe,这样jprobe探测默认就启用了,最后在exit函数中调用unregister_jprobe函数卸载。
    /* Proxy routine having the same arguments as actual do_fork() routine */
    static long jdo_fork(unsigned long clone_flags, unsigned long stack_start,
    unsigned long stack_size, int __user *parent_tidptr,
    int __user *child_tidptr)
    {
    pr_info("jprobe: clone_flags = 0x%lx, stack_start = 0x%lx "
    "stack_size = 0x%lx ", clone_flags, stack_start, stack_size);

    /* Always end with a call to jprobe_return(). */
    jprobe_return();
    return 0;
    }
    jdo_fork函数也仅仅打印出了在调用do_fork函数时传入的clone_flags、stack_start和stack_size这三个入参值,整个实现非常简单直观,但是有两点需要注意:
    1)探测回调函数的入参必须同被探测函数的一致,否则无法达到探测函数入参的目的,例如此处的jdo_fork函数入参unsigned long clone_flags、unsigned long stack_start、unsigned long stack_size、int __user *parent_tidptr和int __user *child_tidptr同do_fork函数是完全一致的(注意返回值固定为long类型)。

    2)在回调函数执行完毕以后,必须调用jprobe_return函数(注释中也有强调),否则执行流程就回不到正常的执行流程中了,这一点后文会详细分析。

    下面在x86_64环境下演示该程序的实际效果(环境配置请参考前一篇博文):

    <6>[15817.544375] jprobe: clone_flags = 0x1200011, stack_start = 0x0 stack_size = 0x0
    <6>[15817.551217] jprobe: clone_flags = 0x1200011, stack_start = 0x0 stack_size = 0x0
    <6>[15817.905328] jprobe: clone_flags = 0x1200011, stack_start = 0x0 stack_size = 0x0
    <6>[15822.684688] jprobe: clone_flags = 0x1200011, stack_start = 0x0 stack_size = 0x0
    <6>[15822.704001] jprobe: clone_flags = 0x1200011, stack_start = 0x0 stack_size = 0x0

    在加载jprobe_example.ko模块以后,在终端随便敲几个命令触发进程创建,内核打印出以上message,可以看到do_fork的入参就被非常容易的获取到了,其他函数的探测也类似,不再详细描述。

    2、jprobe实现分析

    jpeobe的实现基于kprobe,因此这里将在前一篇博文《Linux内核调试技术——kprobe使用与实现》的基础之上分析它的实现,述主要包括jprobe注册流程和触发探测流程,涉及kprobe的部分不再详细描。

    2.1、jprobe实现原理


    利用kprobe,jprobe是一种特殊形式的kprobe,它有自己的pre_handler和break_handler回调函数,其中pre_handler回调函数负责保存原始调用上下文并为调用用户指定的探测函数jprobe->entry准备环境,然后跳转到jprobe->entry执行(被探测函数的入参信息在此得到输出),接着再次触发kprobe流程,在break_handler函数中恢复原始上下文,最后返回正常执行流程。

    2.2、注册一个jprobe实例


    jprobe探测模块调用register_jprobe函数向内核注册一个jprobe实例,代码路径kernel/kprobes.c,其主要流程如下图:

    图1 jpobe注册流程

    int register_jprobe(struct jprobe *jp)
    {
    return register_jprobes(&jp, 1);
    }
    EXPORT_SYMBOL_GPL(register_jprobe);
    register_jprobe函数只是register_jprobes的一个封装,主要注册功能由register_jprobes函数完成。

    int register_jprobes(struct jprobe **jps, int num)
    {
    struct jprobe *jp;
    int ret = 0, i;

    if (num <= 0)
    return -EINVAL;
    for (i = 0; i < num; i++) {
    unsigned long addr, offset;
    jp = jps[i];
    addr = arch_deref_entry_point(jp->entry);

    /* Verify probepoint is a function entry point */
    if (kallsyms_lookup_size_offset(addr, NULL, &offset) &&
    offset == 0) {
    jp->kp.pre_handler = setjmp_pre_handler;
    jp->kp.break_handler = longjmp_break_handler;
    ret = register_kprobe(&jp->kp);
    } else
    ret = -EINVAL;

    if (ret < 0) {
    if (i > 0)
    unregister_jprobes(jps, i);
    break;
    }
    }
    return ret;
    }
    EXPORT_SYMBOL_GPL(register_jprobes);
    函数是一个循环,对每个jprobe执行相同的注册流程,首先从jp->entry中取出探测回调函数的地址,对它进行验证。kallsyms_lookup_size_offset函数的作用是从内核或者模块的符号表中找到addr地址所在的符号,找到后会通过offset值返回addr与符号起始的偏移,这偏移值必须为0,即必须为一个函数的入口。若条件符合,则设置kprobe的pre_handler和break_handler这两个回调函数setjmp_pre_handler和longjmp_break_handler,最后调用register_kprobe函数注册kprobe。

    可见jprobe的注册流程非常的简单,它的本质就是注册一个kprobe,利用kprobe机制实现探测,只是探测回调函数并非用户自己定义,使用jprobe私有的而已。在注册完成后,jprobe(kprobe)机制启动,当函数调用流程执行到被探测函数时就会触发jprobe(kprobe)探测。

    最后需要注意的是,jprobe是不能在同一个被探测点注册多个的,在kprobe的注册流程register_kprobe->register_aggr_kprobe->add_new_kprobe中会有判断:

    if (p->break_handler) {
    if (ap->break_handler)
    return -EEXIST;

    2.3、触发jprobe探测


    基于kprobe机制,在执行到被探测函数后,会触发CPU异常,按照kprobe的执行流程,由kprobe_handler函数调用到pre_handler回调函数,即setjmp_pre_handler。该函数架构相关,它根据架构的不同进行一些栈或者寄存器相关的操作,保存现场以备调用结束后恢复,随后跳转到用户定的jprobe->entry处执行,在打印出用户需要的信息后,返回原有正常的流程继续执行。主要流程如下图:

    图2 jprobe触发流程

    2.3.1、arm架构实现

    int __kprobes setjmp_pre_handler(struct kprobe *p, struct pt_regs *regs)
    {
    struct jprobe *jp = container_of(p, struct jprobe, kp);
    struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();
    long sp_addr = regs->ARM_sp;
    long cpsr;

    kcb->jprobe_saved_regs = *regs;
    memcpy(kcb->jprobes_stack, (void *)sp_addr, MIN_STACK_SIZE(sp_addr));
    regs->ARM_pc = (long)jp->entry;

    cpsr = regs->ARM_cpsr | PSR_I_BIT;
    #ifdef CONFIG_THUMB2_KERNEL
    /* Set correct Thumb state in cpsr */
    if (regs->ARM_pc & 1)
    cpsr |= PSR_T_BIT;
    else
    cpsr &= ~PSR_T_BIT;
    #endif
    regs->ARM_cpsr = cpsr;

    preempt_disable();
    return 1;
    }
    首先再次明确入参struct pt_regs *regs的含义是触发CPU异常前所保存的正常执行流上下文的寄存器值。函数首先获取触发的jprobe结构实例,并调用get_kprobe_ctlblk取得当前CPU的kprobe_ctlblk结构全局变量,这个struct kprobe_ctlblk结构定义在kprobe分析中已经见过,不过jprobe使用到了其中定义的另两个字段:
    /* per-cpu kprobe control block */
    struct kprobe_ctlblk {
    unsigned int kprobe_status;
    struct prev_kprobe prev_kprobe;
    struct pt_regs jprobe_saved_regs;
    char jprobes_stack[MAX_STACK_SIZE];
    };
    其中jprobe_saved_regs用于保存寄存器信息,jprobes_stack则用于保存栈信息,它们用于在jprobe返回时恢复调用探测前的上下文,这一点从setjmp_pre_handler函数的前两行就可以看出。先提个问题,为何kprobe不需要保存原上下文信息而jprobe需要?
    函数接下来修改传入的ARM_pc值为用户指定的探测回调函数地址,注意这个值本来在正常的kprobe流程中是要被设置为正常流程的下一条指令的(执行完kprobe流程后就会回到原流程继续执行),这里在kprobe的整个流程结束后就不会回到原流程执行了,而是会进入到用户指定的探测函数执行。

    函数然后修改入参的CPSR寄存器值,置位PSR_I_BIT,表示禁用中断,最后禁止抢占并返回1。回到kprobe_handler函数中看返回1后接下来kprobe就不会执行singlestep和调用post_handler回调函数了,注意也不会调用reset_current_kprobe函数复位当前执行的kprobe为NULL:

    if (!p->pre_handler || !p->pre_handler(p, regs)) {
    kcb->kprobe_status = KPROBE_HIT_SS;
    singlestep(p, regs, kcb);
    if (p->post_handler) {
    kcb->kprobe_status = KPROBE_HIT_SSDONE;
    p->post_handler(p, regs, 0);
    }
    reset_current_kprobe();
    }
    在kprobe_handler流程返回后,执行流程进入到了用户指定的探测函数执行,对于前文中的jprobe_example程序来说就是jdo_fork函数。提第二个问题,被探测函数的入参值是如何获取的?
    从setjmp_pre_handler的实现可以看出,该函数仅仅修改了kprobe的返回地址,并没有修改栈和其他的寄存器值,因此在CPU跳转到jdo_fork执行时,它的寄存器和栈中的内容同原本调用do_fork函数时几乎是一模一样的(仅仅是禁用了中断而已),因此不论是通过寄存器传参还是通过压栈的方式传参,用户在定义jdo_fork函数时只需要将函数入参定义的同do_fork一样就可以轻轻松松的获取到原有的入参值了。另外从这里的实现可以看出另外一个信息,jprobe的回调执行上下文同原函数执行的上下文是一样的,这点不同于kprobe,kprobe的回调函数执行的上下文是在CPU异常的中断上下文。

    最后由于探测函数(jdo_fork)是在kprobe_handler流程执行完成后跳转执行的,跳过了single_step流程,这也就说它不能利用原有kprobe的机制回到原始执行流程中去执行,需要另想他法,其实在setjmp_pre_handler函数中保存的寄存器pt_regs就是用于这个目的的,也就解释了前文中提出的第一个问题,接下来详细分析。

    回到探测函数jdo_fork中,用户在获取需要的信息后,接下来进入现场恢复的流程,其中的关键部分就是jdo_fork函数最后调用的jprobe_return函数,它是由嵌入汇编实现的

    void __kprobes jprobe_return(void)
    {
    struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();

    __asm__ __volatile__ (
    /*
    * Setup an empty pt_regs. Fill SP and PC fields as
    * they're needed by longjmp_break_handler.
    *
    * We allocate some slack between the original SP and start of
    * our fabricated regs. To be precise we want to have worst case
    * covered which is STMFD with all 16 regs so we allocate 2 *
    * sizeof(struct_pt_regs)).
    *
    * This is to prevent any simulated instruction from writing
    * over the regs when they are accessing the stack.
    */
    #ifdef CONFIG_THUMB2_KERNEL
    ...
    #else
    "sub sp, %0, %1 "
    #endif
    "ldr r0, ="__stringify(JPROBE_MAGIC_ADDR)" "
    "str %0, [sp, %2] "
    "str r0, [sp, %3] "
    "mov r0, sp "
    "bl kprobe_handler "

    /*
    * Return to the context saved by setjmp_pre_handler
    * and restored by longjmp_break_handler.
    */
    #ifdef CONFIG_THUMB2_KERNEL
    ...
    #else
    "ldr r0, [sp, %4] "
    "msr cpsr_cxsf, r0 "
    "ldmia sp, {r0 - pc} "
    #endif
    :
    : "r" (kcb->jprobe_saved_regs.ARM_sp),
    "I" (sizeof(struct pt_regs) * 2),
    "J" (offsetof(struct pt_regs, ARM_sp)),
    "J" (offsetof(struct pt_regs, ARM_pc)),
    "J" (offsetof(struct pt_regs, ARM_cpsr)),
    "J" (offsetof(struct pt_regs, ARM_lr))
    : "memory", "cc");
    }
    这里模拟出了一个假的pt_regs结构体,仅仅填充了其中的sp和pc字段(后文中的longjmp_break_handler函数需要),其中pc的值为JPROBE_MAGIC_ADDR,然后长跳转到kprobe_handler执行,kprobe_handler函数判断当前已经有kprobe正在运行了,因此进入以下调用流程:
    } else if (cur) {
    /* We probably hit a jprobe. Call its break handler. */
    if (cur->break_handler && cur->break_handler(cur, regs)) {
    kcb->kprobe_status = KPROBE_HIT_SS;
    singlestep(cur, regs, kcb);
    if (cur->post_handler) {
    kcb->kprobe_status = KPROBE_HIT_SSDONE;
    cur->post_handler(cur, regs, 0);
    }
    }
    reset_current_kprobe();
    首先调用kprobe的break_handler回调函数,即longjmp_break_handler函数:
    int __kprobes longjmp_break_handler(struct kprobe *p, struct pt_regs *regs)
    {
    struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();
    long stack_addr = kcb->jprobe_saved_regs.ARM_sp;
    long orig_sp = regs->ARM_sp;
    struct jprobe *jp = container_of(p, struct jprobe, kp);

    if (regs->ARM_pc == JPROBE_MAGIC_ADDR) {
    if (orig_sp != stack_addr) {
    struct pt_regs *saved_regs =
    (struct pt_regs *)kcb->jprobe_saved_regs.ARM_sp;
    printk("current sp %lx does not match saved sp %lx ",
    orig_sp, stack_addr);
    printk("Saved registers for jprobe %p ", jp);
    show_regs(saved_regs);
    printk("Current registers ");
    show_regs(regs);
    BUG();
    }
    *regs = kcb->jprobe_saved_regs;
    memcpy((void *)stack_addr, kcb->jprobes_stack,
    MIN_STACK_SIZE(stack_addr));
    preempt_enable_no_resched();
    return 1;
    }
    return 0;
    }
    这个函数很简单,首先会判断sp的值和保存的sp值是都是一样的,若不一样则报BUG,否则恢复保存在kprobe_ctlblk结构体中的寄存器值和栈,最后启用内核抢占,至此jprobe的处理流程全部完毕,接下来会回到kprobe的kprobe_handler中继续完成本次kprobe,执行单步执行single_step和post_handler,最后回到原流程执行。由此可见,在用户定义的探测函数末尾,必须要调用jprobe_return函数,否则代码的执行就“飞了”,再也回不到原有的流程中去了。

    2.3.2、x86_64架构实现


    int setjmp_pre_handler(struct kprobe *p, struct pt_regs *regs)
    {
    struct jprobe *jp = container_of(p, struct jprobe, kp);
    unsigned long addr;
    struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();

    kcb->jprobe_saved_regs = *regs;
    kcb->jprobe_saved_sp = stack_addr(regs);
    addr = (unsigned long)(kcb->jprobe_saved_sp);

    /*
    * As Linus pointed out, gcc assumes that the callee
    * owns the argument space and could overwrite it, e.g.
    * tailcall optimization. So, to be absolutely safe
    * we also save and restore enough stack bytes to cover
    * the argument area.
    */
    memcpy(kcb->jprobes_stack, (kprobe_opcode_t *)addr,
    MIN_STACK_SIZE(addr));
    regs->flags &= ~X86_EFLAGS_IF;
    trace_hardirqs_off();
    regs->ip = (unsigned long)(jp->entry);

    /*
    * jprobes use jprobe_return() which skips the normal return
    * path of the function, and this messes up the accounting of the
    * function graph tracer to get messed up.
    *
    * Pause function graph tracing while performing the jprobe function.
    */
    pause_graph_tracing();
    return 1;
    }
    NOKPROBE_SYMBOL(setjmp_pre_handler);
    x86_64架构的实现整体同arm的大同小异,函数首先同样是保存现场,然后关闭中断并设置IP寄存器的值为jp->entry,最后返回1,这样在kprobe_int3_handler函数会跳过single_step。
    /*
    * 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, 0);
    return 1;
    于是在kprobe调用流程结束后跳转到用户的探测函数执行。在来看jprobe_return函数的实现:
    void jprobe_return(void)
    {
    struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();

    asm volatile (
    #ifdef CONFIG_X86_64
    " xchg %%rbx,%%rsp "
    #else
    " xchgl %%ebx,%%esp "
    #endif
    " int3 "
    " .globl jprobe_return_end "
    " jprobe_return_end: "
    " nop "::"b"
    (kcb->jprobe_saved_sp):"memory");
    }
    同arm的实现不同,这里使用int3指令再次触发CPU3异常,并且异常出的地址已经不再是BREAKPOINT_INSTRUCTION了,所以会进入到kprobe_int3_handler的以下流程执行:
    } else if (kprobe_running()) {
    p = __this_cpu_read(current_kprobe);
    if (p->break_handler && p->break_handler(p, regs)) {
    if (!skip_singlestep(p, regs, kcb))
    setup_singlestep(p, regs, kcb, 0);
    return 1;
    }
    同样是调用kprobe的break_handler回调函数执行,也即是longjmp_break_handler函数。
    int longjmp_break_handler(struct kprobe *p, struct pt_regs *regs)
    {
    struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();
    u8 *addr = (u8 *) (regs->ip - 1);
    struct jprobe *jp = container_of(p, struct jprobe, kp);
    void *saved_sp = kcb->jprobe_saved_sp;

    if ((addr > (u8 *) jprobe_return) &&
    (addr < (u8 *) jprobe_return_end)) {
    if (stack_addr(regs) != saved_sp) {
    struct pt_regs *saved_regs = &kcb->jprobe_saved_regs;
    printk(KERN_ERR
    "current sp %p does not match saved sp %p ",
    stack_addr(regs), saved_sp);
    printk(KERN_ERR "Saved registers for jprobe %p ", jp);
    show_regs(saved_regs);
    printk(KERN_ERR "Current registers ");
    show_regs(regs);
    BUG();
    }
    /* It's OK to start function graph tracing again */
    unpause_graph_tracing();
    *regs = kcb->jprobe_saved_regs;
    memcpy(saved_sp, kcb->jprobes_stack, MIN_STACK_SIZE(saved_sp));
    preempt_enable_no_resched();
    return 1;
    }
    return 0;
    }
    longjmp_break_handler函数同arm实现基本一致,恢复代码的原有上下文,打开内核抢占,最后交回给kprobe继续执行后面的single_step和恢复流程。不过值的注意的是第一条判断语句,由于本次int3异常是在jprobe_return函数中触发的,因此longjmp_break_handler函数的struct pt_regs *regs入参值是在调用jprobe_return函数环境上下文中的寄存器值,因此addr一定是在jprobe_return函数的地址范围内,所以以此判断本次调用的有效性,防止误入。

    3、总结


    jprobe探测技术基于kprobe实现,是kprobes三种探测技术中的第二种,内核开发人员可以用它来探测内核函数的调用以及调用时的入参值,使用非常方便。本文介绍了jprobe探测工具的使用方式及其原理,并通过源码分析了arm架构和x86_64架构下它的实现方式。下一篇博文将介绍kprobes中的最后一种用来探测函数返回值的kretprobe探测技术。

  • 相关阅读:
    习题10-2 递归求阶乘和(15 分)
    在过滤器中得到模型状态信息
    理解OAuth 2.0
    asp.net mvc 控制器的依赖注入(使用Ninject)
    web.routing 学习
    深度优先和广度优先的基础应用
    数的全排
    C# 表达式树
    C#中RSA的简单使用
    select into 和insert into select
  • 原文地址:https://www.cnblogs.com/LiuYanYGZ/p/12643706.html
Copyright © 2011-2022 走看看