kprobe实现原理解析

一、简介

       kprobe是内核的动态探测工具,几乎可以探测任何一条内核指令。kprobe根据探测点类型可分为三种: kprobes, jprobes和kretprobes (也叫返回探测点)。 kprobes是可以被插入到内核的任何指令位置的探测点,jprobes则只能被插入到一个内核函数的入口,而kretprobes则是在指定的内核函数返回时才被执行。

       kprobe根据实现原理也可以分为三种:基于动态ftrace的kprobe,基于int3的kprobe和基于jump相对跳转指令实现的kprobe。

这里主要介绍基于int3的kprobe实现以及kprobe和kretprobe的实现和使用。

内核版本基于3.10.33,x86_64平台。

二、注册流程

      一个kprobe探测实例是struct kprobe。

struct kprobe {       //哈希链表, 被静态全局变量kprobe_table管理, 每个被监测地址作为索引       
      struct hlist_node hlist; //如果一个地址存在多个kprobe则该哈希节点会用aggregate节点替代  
      struct list_head list;     //对于一个地址存在的多个kprobe的链表
      unsigned long nmissed;     //因断点指令不能重入处理, 当多个kprobe一起触发时会放弃执行后面的probe, 同时该计数增加   
      kprobe_opcode_t *addr;    //观察点对应的地址, 用户在调用注册接口时可以指定地址, 也可以传入函数名让内核自己查找   
      const char *symbol_name;//观察点对应的函数名, 在注册kprobe时会将其翻译为十六进制地址并修改addr  
      unsigned int offset; //相对于入口点地址的偏移, 会在计算addr以后再加上offset得到最终的addr    
      kprobe_pre_handler_t pre_handler;  //在执行kprobe地址addr指令之前执行的handler  
      kprobe_post_handler_t post_handler;  //在执行kprobe地址addr指令之后执行的handler 
      kprobe_fault_handler_t fault_handler; //异常处理句柄, 在执行pre_handler返回值非0时会调用 
 /* * ... called if breakpoint trap occurs in probe handler.       * Return 1 if it handled break, otherwise kernel will see it. 24      */ 
      kprobe_break_handler_t break_handler;    
      kprobe_opcode_t opcode;     //保存的操作码, 当注册kprobe后对应地址会用中断指令替代  
      struct arch_specific_insn ainsn;   //平台相关结构, 具体见下     
      u32 flags; 32 //状态标记, 被kprobe_mutex保护
}; 

   struct arch_specific_insn用来备份原来的探测指令的。

 struct arch_specific_insn {
     /* copy of the original instruction */
     kprobe_opcode_t *insn; //原指令opcode的拷贝
     /*
      * boostable = -1: This instruction type is not boostable.
      * boostable = 0: This instruction type is boostable.
      * boostable = 1: This instruction has been boosted: we have
      * added a relative jump after the instruction copy in insn,
      * so no single-step and fixup are needed (unless there's
      * a post_handler or break_handler).
      */
     int boostable;
     bool if_modifier;
 };
 typedef u8 kprobe_opcode_t

     register_kprobe函数完成kprobe的注册。

 int __kprobes register_kprobe(struct kprobe *p)
 {
     int ret;
     struct kprobe *old_p;
     struct module *probed_mod;
     kprobe_opcode_t *addr;
 
     /* Adjust probe address from symbol */
     addr = kprobe_addr(p);
     if (IS_ERR(addr))
         return PTR_ERR(addr);
     p->addr = addr;
 
     ret = check_kprobe_rereg(p);
     if (ret)
         return ret;
 
     /* User can pass only KPROBE_FLAG_DISABLED to register_kprobe */
     p->flags &= KPROBE_FLAG_DISABLED;
     p->nmissed = 0;
     INIT_LIST_HEAD(&p->list);
 
     ret = check_kprobe_address_safe(p, &probed_mod);
     if (ret)
         return ret;
 
     mutex_lock(&kprobe_mutex);
 
     old_p = get_kprobe(p->addr);
     if (old_p) {
         /* Since this may unoptimize old_p, locking text_mutex. */
         ret = register_aggr_kprobe(old_p, p);
         goto out;
     }
 
     mutex_lock(&text_mutex);    /* Avoiding text modification */
     ret = prepare_kprobe(p);
     mutex_unlock(&text_mutex);
     if (ret)
         goto out;
 
     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))                                                                                                                                            
         arm_kprobe(p);
 
     /* Try to optimize kprobe */
     try_to_optimize_kprobe(p);
 
 out:
     mutex_unlock(&kprobe_mutex);
 
     if (probed_mod)
         module_put(probed_mod);
      return ret;
}

     register_kprobe主要完成如下工作:

     (1)通过kprobe_addr函数获取要探测的具体位置。根据用户提供的symbol_name调用kallsyoms_lookup_name获取函数的首地址,再加上要探测的函数offset。这里的offset必须是一条汇编指令的开始地址,不能是指令的中间。

     (2)通过check_kprobe_rereg检测本kprobe时候已经注册,如果已经注册,直接返回完成注册。

     (3)调用check_kprobe_address_safe函数检查探测的地址。主要检查这几项:

       调用ftrace_location函数检测探测地址是否属于动态ftrace的探测点mcount段。一般是函数开始5个nop指令。如果是这个地址,那么置位p->flags的 KPROBE_FLAG_FTRACE,本次kprobe将走动态ftrace路径,不走int3异常。

       检查探测地址是否属于内核代码段,否则不能探测。

       是否在kprobe本身实现的关键函数地址内,是的话不允许探测。 

       探测地址不能是内核jump  label的保留地址。

       如果探测的是内核模块的地址,需要增加模块计数。没有这个计数的话,模块卸载之后,等到unregister_kprobe的时候会恢复原指令,或导致内核内存被被修改。

       如果该探测地址已经注册了kprobe,会调用register_aggr_kprobe注册一个aggregate kprobe,用来管理所有同一探测地址的的kprobe。

       (4)prepare_kprobe函数负责kprobe注册准备工作。


 static int __kprobes prepare_kprobe(struct kprobe *p)
 {
     if (!kprobe_ftrace(p))
         return arch_prepare_kprobe(p);
 
     return arch_prepare_kprobe_ftrace(p);
 }

     如果探测的是动态ftrace的mcount段地址,那么调用arch_prepare_kprobe_ftrace走ftrace kprobe的路径。

     负责调用arch_prepare_kprobe位int3 kprobe的准备工作。

     arch_prepare_kprobe首先调用can_probe检查探测地址是否地址所在代码的指令的边界地址,不是的话不能探测。

     调用get_insn_slot函数为kprobe的insn在x86的可执行page里申请一段内存用来备份完整原指令,kprobe执行流程中的单步环节需要在这里执行,所以需要执行权限。

     最后arch_copy_kprobe备份原指令到kprobe->ainsn.insn里面,并把原指令的opcode保存到kprobe->opcode。

    (5)将kprobe通过hlist字段添加到系统kprobe_table的hash table里面。

    (6)调用arm_kprobe函数替换原指令的opcode为int3指令(0xcc)。

     至此,kprobe注册完成。

三、kprobe的执行

   (1) int3指令属于x86上的断点指令,对应的异常属于trap类型,即异常恢复的时候执行int3后面的下一条指令。

    内核执行到被kprobe探测的指令,触发int3异常。

     

paranoidzeroentry_ist int3 do_int3 DEBUG_STACK
......
.macro paranoidzeroentry_ist sym do_sym ist
ENTRY(\sym)
    INTR_FRAME
    ASM_CLAC
    PARAVIRT_ADJUST_EXCEPTION_FRAME
    pushq_cfi $-1       /* ORIG_RAX: no syscall to restart */
    subq $ORIG_RAX-R15, %rsp
    CFI_ADJUST_CFA_OFFSET ORIG_RAX-R15
    call save_paranoid
    TRACE_IRQS_OFF_DEBUG
    movq %rsp,%rdi      /* pt_regs pointer */
    xorl %esi,%esi      /* no error code */
    subq $EXCEPTION_STKSZ, INIT_TSS_IST(\ist)
    call \do_sym
    addq $EXCEPTION_STKSZ, INIT_TSS_IST(\ist)
    jmp paranoid_exit   /* %ebx: no swapgs flag */
    CFI_ENDPROC
END(\sym)
.endm

    int3异常后进入ENTRY(int3),call_paranoid保存寄存到栈里,并将栈顶rsp和错误码0作为第一第二个参数调用do_int3函数。

(2)do_int3()

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

    if (notify_die(DIE_INT3, "int3", regs, error_code, X86_TRAP_BP,
            SIGTRAP) == NOTIFY_STOP)
        goto exit;
    ......

}

   调用notifier_die向通知链die_chain通知DIE_INT3事件。在int_kprobe的时候会向die_chain注册了回调函数kprobe_exceptions_notify函数,且为最高优先级。现在执行kprobe_exceptions_notify。

(3)kprobe_exceptions_notify根据DIE_INT3事件码调用kprobe_handler函数,kprobe_handler里kprobe的pre_handler执行。

   

 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));
     preempt_disable();
     kcb = get_kprobe_ctlblk();
     p = get_kprobe(addr);
 
     if (p) {
         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 (!p->pre_handler || !p->pre_handler(p, regs))
                 setup_singlestep(p, regs, kcb, 0);
             return 1;
        }
     } else if (*addr != BREAKPOINT_INSTRUCTION) {
         
         regs->ip = (unsigned long)addr;
         preempt_enable_no_resched();
         return 1;
     } 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;
         }
     } /* else: not a kprobe fault; let the kernel handle it */
 
     preempt_enable_no_resched();
     return 0;
 }
                                                                                                                               

       13行如果当前cpu上有其他kprobe正在运行,本次kprobe的handler不会执行,仅增加nmissed计数。

       16是正常流程,设置当前kprobe为正在运行的kprobe。如果有pre_handler,执行pre_handler。存在pre_handler并且pre_hanler返回值为0,调用setup_singlestep设置单步模式。函数开始是关抢占的,所以kprobe的pre_handler的执行上下文是关抢占的。

(4)被探测原指令单步执行

static void __kprobe 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();
         regs->ip = (unsigned long)p->ainsn.insn;
         preempt_enable_no_resched();
         return;
     }
 #endif
     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 */
     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 = (unsigned long)p->addr;
     else
         regs->ip = (unsigned long)p->ainsn.insn;
 }

        设置reg->flags的x86_EFLAGS_IF,如果原来指令本身就是int3指令,那么直接在原探测地址上执行单步。正常情况下,设置单步地址为kprobe备份指令所在的slot地址。

        设置完单步模式,kprobe_handler函数返回后,int3异常就正常返回了。int3异常会恢复到异常前的函数上下文,但此时EFLAGS寄存器被设置位单步模式,pc指向备份指令的所在的地址。会单步执行备份指令。

        单步执行完之后,会触发单步异常。

(5)单步异常里执行kprobe的post_handler

       备份指令单步执行完毕之后,会触发单步调试异常,对应int 1异常,因为和int3异常很类似,这里简要介绍下他的流程。int1异常触发后,进入debug函数,debug函数保存寄存器线程栈里,调用do_debug。do_debug调用notify_die向die_chain报告DIE_DEBUG事件,然后执行kprobe的通知回调函数kprobe_exceptions_notify。

       kprobe_exceptions_notify函数根据DIE_DEBUG事件码执行post_kprobe_handler函数。

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;
 
     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);
     }
 
     /* 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 (regs->flags & X86_EFLAGS_TF)
         return 0;
 
     return 1;
 }

       首先调用resume_execution为int1调试异常恢复到正常执行流程做准备。具体来说:

       清除regs->flags的X86_EFLAGS_TF标志,否则int1异常返回后,每执行一条指令都是单步模式。

       如果单步的备份指令是绝对跳转指令,那么regs->ip就是指向正确的下一条指令,无需修正。

       如果是单步的备份指令是相对跳转指令,那么需要修正regs->ip。执行完单步之后regs->ip的值是基于当前位置,也就是备份指令的位置+offset算出来的,所以要修正为基于原指令位置的偏移,加上kprobe->addr 和copy instruction的地址的差值即可。

       如果单步的备份指令是call指令,单步之后,call自动将返回地址push,也就是已经把备份指令的之后的位置入栈到regs->sp的位置,此时需要把regs->sp指向的内容替换位kprobe->addr所在指令的下一条指令的地址。

      如果kprobe->ainsn.boostable == 0,则在备份指令的下一条指令的位置构造一个jump指令,跳转到探测地址所在的指令的下一条指令的地址,前提是分配的slot空间要大于等于备份指令长度+5,5是相对跳转的指令的长度。设置成功后,kprobe->ainsn.boostable = 1。如果系统没有使能抢占,并且kprobe的post_handler位空,那么下次在int3异常中执行完pre_handler不需要在设置备份指令为单步模式,直接设置regs->ip位备份指令的位置,boost执行效率。

       继续kprobe_post_handler函数,执行kprobe->post_handler函数,函数最后开抢占。所以的kprobe的post_handler的执行上下文也是关抢占下执行的。

       int1异常返回后,恢复到正常执行流程。整个kprobe触发执行流程结束。

        

       

 

   

  

     

      

     

 

      

你可能感兴趣的:(Linux内核)