kprobe原理与实现笔记

很久以前挖的坑, 现在还没填上, 也许以后再详细分析吧.


kprobe是内核提供的代码跟踪工具, 其使用方法见Documentation/kprobes.txt, 此处做个简要说明. kprobe允许你在任何内核程序位置动态打断并收集调试信息. 你可以在几乎热河内核代码地址陷入中断, 指定断点触发时的处理程序. 当前有三类探测方式: kprobes, jprobes与kretprobes(也叫return probes). 一个kprobe可以插入到内核任意指令里, 一个jprobe可以插入到内核函数的入口并提供方便的函数参数获取方式, 一个return probe在指定函数返回时使用.
通常情况下基于kprobe的调试手段被包装成一个内核模块. 模块初始化函数初始化(注册)一个或多个probes, 退出函数注销它们. 一个注册函数如register_kprobe()指定probe插入的位置及probe触发时的处理程序.
register_/unregister_*probes()函数集对应一组不同的*probes的注册与注销函数, 当你需要一次注销多个probes时这些函数加速注销处理.
1.1 kprobe原理
当一个kprobe注册后, 会复制监测地址的指令并将监测地址起始的单个(或多个)字节指令替换为一个中断指令(如x86平台的int3).
当CPU触发断点指令时, 一个中断会产生, CPU的寄存器被保存, 控制权通过notifier_call_chain转移到kprobes, kprobes执行pre_handler关联kprobe, 传递句柄, kprobe结构地址与寄存器.
接着kprobes单步运行它保存的指令拷贝(直接单步运行原始指令会更简单, 但kprobes可能会短暂的移除断点指令, 这会造成一个小的时间窗口当其它CPU也路过该probe点).
再单步执行指令后kprobes执行post_handler(如果kprobe有该回调句柄). 程序继续从probe点之后继续执行.

更多实现说明见Documentation/kprobes.txt.
demo代码见samples/kprobes/kprobe_example.c.

以下分析kprobes数据结构与源码, 仅分析kprobe部分, jprobe与kretprobe不额外详述, 毕竟这块代码还是比较简单的.
 
数据结构:

 1 struct kprobe { 
 2     //哈希链表, 被静态全局变量kprobe_table管理, 每个被监测地址作为索引 
 3     //如果一个地址存在多个kprobe则该哈希节点会用aggregate节点替代 
 4     struct hlist_node hlist; 
 5     //对于一个地址存在的多个kprobe的链表 
 6     struct list_head list; 
 7     //因断点指令不能重入处理, 当多个kprobe一起触发时会放弃执行后面的probe, 同时该计数增加 
 8     unsigned long nmissed; 
 9     //观察点对应的地址, 用户在调用注册接口时可以指定地址, 也可以传入函数名让内核自己查找 
10     kprobe_opcode_t *addr; 
11     //观察点对应的函数名, 在注册kprobe时会将其翻译为十六进制地址并修改addr 
12     const char *symbol_name; 
13     //相对于入口点地址的偏移, 会在计算addr以后再加上offset得到最终的addr 
14     unsigned int offset; 
15     //在执行kprobe地址addr指令之前执行的handler 
16     kprobe_pre_handler_t pre_handler; 
17     //在执行kprobe地址addr指令之后执行的handler 
18     kprobe_post_handler_t post_handler; 
19     //异常处理句柄, 在执行pre_handler返回值非0时会调用 
20     kprobe_fault_handler_t fault_handler; 
21     /* 
22      * ... called if breakpoint trap occurs in probe handler. 
23      * Return 1 if it handled break, otherwise kernel will see it. 
24      */ 
25     kprobe_break_handler_t break_handler; 
26     //保存的操作码, 当注册kprobe后对应地址会用中断指令替代 
27     kprobe_opcode_t opcode; 
28     //平台相关结构, 具体见下 
29     struct arch_specific_insn ainsn; 
30     //状态标记, 被kprobe_mutex保护 
31     u32 flags; 
32 }; 
33 struct arch_specific_insn { 
34     //回调处理的指令, 具体见arch_prepare_kprobe中的初始化 
35     kprobe_opcode_t             *insn; 
36     kprobe_insn_handler_t       *insn_handler; 
37     //指令的条件检查(通过取指令高4位)回调 
38     kprobe_check_cc             *insn_check_cc; 
39     //单步调试回调 
40     kprobe_insn_singlestep_t    *insn_singlestep; 
41     kprobe_insn_fn_t            *insn_fn; 
42 }; 

 

注册kprobe:

 1 (kernel/kprobes.c)int __kprobes register_kprobe(struct kprobe *p) 
 2 { 
 3     //获取观察点在内核文件的地址 
 4     //注意传入的kprobe结构中symbol与addr有且只能有一个为有效值, 否则返回错误EINVAL 
 5     //如果传入的是函数名symbol则内核会查找符号表并翻译为指令地址, 查找不到也返回错误ENOENT 
 6     addr = kprobe_addr(p); 
 7     //判断观察点是否已注册, 判断方式是先根据addr索引kprobe_table查找是否该地址已被监测 
 8     //如存在节点再根据传入的kprobe指针p索引kprobe->list查找是否有与p相同的节点 
 9     //如已注册p则返回错误EINVAL 
10     ret = check_kprobe_rereg(p); 
11     //初始化p的成员, 注意对于flags成员用户只能传递一个标记KPROBE_FLAG_DISABLED 
12     p->flags &= KPROBE_FLAG_DISABLED; 
13     p->nmissed = 0; 
14     INIT_LIST_HEAD(&p->list); 
15     //校验kprobe地址是否安全, 观察点地址首先要满足以下要求, 不符合的一律返回EINVAL: 
16     //1. 必须在内核代码段(内核本身或驱动模块代码) 
17     //2. 不能是禁止kprobe的函数(函数声明带__kprobes属性或在kprobe_blacklist中的函数) 
18     //3. 不能是与跳转相关的代码段(不太理解这个jump table是什么, 看定义不是在数据段里吗?) 
19     //对于观察点地址落在驱动模块代码中的情况还需注意两点: 
20     //1. 增加模块引用计数防止在更新模块代码时模块被意外卸载(在函数返回时释放) 
21     //2. 不能是模块.init.text段 
22     ret = check_kprobe_address_safe(p, &probed_mod); 
23     mutex_lock(&kprobe_mutex); 
24     //获取地址对应的kprobe对象, 如不存在即第一次设置该地址的观察点, 反之则注册kprobe集合 
25     //register_aggr_kprobe首先判断old_p的pre_handler是否为aggr_pre_handler 
26     //如不相等即该地址第二次注册, 使用alloc_aggr_kprobe/init_aggr_kprobe初始化一个集合节点 
27     //如果集合节点标记为GONE还需重新准备架构相关代码(详见下文), 最后将新对象加入list链表 
28     old_p = get_kprobe(p->addr); 
29     if (old_p) { 
30         ret = register_aggr_kprobe(old_p, p); 
31         goto out; 
32     } 
33     //准备架构相关代码, 因当前config未定义KPROBES_ON_FTRACE 
34     //prepare_kprobe实际调用arch_prepare_kprobe, 该函数具体分析见下文 
35     mutex_lock(&text_mutex); 
36     ret = prepare_kprobe(p); 
37     mutex_unlock(&text_mutex); 
38     //初始化哈希表节点 
39     INIT_HLIST_NODE(&p->hlist); 
40     hlist_add_head_rcu(&p->hlist, 
41         &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]); 
42     //将观察点地址的指令替换为未定义指令 
43     //如在非Thumb态就使用KPROBE_ARM_BREAKPOINT_INSTRUCTION(0x07f001f8) 
44     //替换后刷新icache, 之后执行到该指令时会进入中断(在init_kprobes中注册), 实现中断处理 
45     if (!kprobes_all_disarmed && !kprobe_disabled(p)) 
46         arm_kprobe(p); 
47 } 
48 (arch/arm/kernel/kprobes.c)int __kprobes arch_prepare_kprobe(struct kprobe *p) 
49 { 
50     //如在异常代码中直接返回 
51     if (in_exception_text(addr)) 
52         return -EINVAL; 
53     //未定义THUMB2_KERNEL, 忽略相关代码 
54     //因ARM RISC架构必定为4字节指令且不支持THUMB保证最低位为零, 所以此处判断低二位为零 
55     //因不支持THUMB指令, decode_insn函数指针直接赋值为arm_kprobe_decode_insn 
56     thumb = false; 
57     if (addr & 0x3) 
58         return -EINVAL; 
59     insn = *p->addr; 
60     decode_insn = arm_kprobe_decode_insn; 
61     //保存操作码 
62     p->opcode = insn; 
63     p->ainsn.insn = tmp_insn; 
64     //解码指令步骤, 因未定义THUMB2_KERNEL此处执行arm_kprobe_decode_insn 
65     //首先初始化p->ainsn, 注意asi->insn是执行指令数组 
66     //数组第一个成员是观察点指令的不带条件判断的部分(因条件处理在外部) 
67     //数组第二个成员固定是0xe1a0f00e, 即mov pc, lr指令, 用来跳转回原接口 
68     //指令模拟的重点在数组kprobe_decode_arm_table, 对照ARM手册逐个分析, 具体就不展开了 
69     //最后返回该指令是否可kprobe, INSN_REJECTED即不可kprobe, 一般为修改程序状态的指令 
70     //INSN_GOOD为需要额外slot, 则需额外分配地址有限制的内存, 详见get_insn_slot 
71     //INSN_GOOD_NO_SLOT即无需内存的指令 
72     switch ((*decode_insn)(insn, &p->ainsn)) { 
73     case INSN_REJECTED: 
74         return -EINVAL; 
75     case INSN_GOOD: 
76         p->ainsn.insn = get_insn_slot(); 
77         if (!p->ainsn.insn) 
78             return -ENOMEM; 
79         for (is = 0; is < MAX_INSN_SIZE; ++is) 
80             p->ainsn.insn[is] = tmp_insn[is]; 
81         flush_insns(p->ainsn.insn, 
82             sizeof(p->ainsn.insn[0]) * MAX_INSN_SIZE); 
83         p->ainsn.insn_fn = (kprobe_insn_fn_t *) 
84             ((uintptr_t)p->ainsn.insn | thumb); 
85         break; 
86     case INSN_GOOD_NO_SLOT: 
87         p->ainsn.insn = NULL; 
88         break; 
89     } 
90 } 

 

初始化kprobe:

 1 (kernel/kprobes.c)static int __init init_kprobes(void) 
 2 { 
 3     //初始化哈希表节点 
 4     for (i = 0; i < KPROBE_TABLE_SIZE; i++) { 
 5         INIT_HLIST_HEAD(&kprobe_table[i]); 
 6         INIT_HLIST_HEAD(&kretprobe_inst_table[i]); 
 7         raw_spin_lock_init(&(kretprobe_table_locks[i].lock)); 
 8     } 
 9     //初始化kprobe黑名单(非__krpobe属性又不能被kprobe的函数) 
10     for (kb = kprobe_blacklist; kb->name != NULL; kb++) { 
11         kprobe_lookup_name(kb->name, addr); 
12         if (!addr) 
13             continue; 
14         kb->start_addr = (unsigned long)addr; 
15         symbol_name = kallsyms_lookup(kb->start_addr, 
16             &size, &offset, &modname, namebuf); 
17         if (!symbol_name) 
18             kb->range = 0; 
19         else 
20             kb->range = size; 
21     } 
22     kprobes_all_disarmed = false; 
23     //架构相关初始化, 调用两个函数arm_kprobe_decode_init与register_undef_hook 
24     //前者主要是arm指令相关初始化, 其中find_str_pc_offset是计算str pc指令带来多少pc偏移 
25     //因为ARM是三级流水线(取指译码执行), PC地址实际领先执行指令, str pc时就需要减去偏移 
26     //如果ARM架构大于7统一是8, 否则就需要计算实际偏移, 具体计算方式很简单, 不详述 
27     //剩下两个判断没仔细看, 反正大于v7的架构都是空定义 
28     //register_undef_hook是核心, 它注册了上文的KPROBE_ARM_BREAKPOINT_INSTRUCTION异常中断 
29     //其中undef_hook是针对所有未定义指令的hook的链表头 
30     err = arch_init_kprobes(); 
31     //下面两个notifier没仔细看, 不知道注册了有什么用, 先放放 
32     if (!err) 
33         err = register_die_notifier(&kprobe_exceptions_nb); 
34     if (!err) 
35         err = register_module_notifier(&kprobe_module_nb); 
36 } 

 

kprobe中断处理:

  1 (arch/arm/kernel/traps.c)asmlinkage void __exception do_undefinstr(struct pt_regs *regs) 
  2 { 
  3     pc = (void __user *)instruction_pointer(regs); 
  4     //未定义THUMB2_KERNEL去除相关代码 
  5     if (processor_mode(regs) == SVC_MODE) { 
  6         instr = *(u32 *) pc; 
  7     } else if (thumb_mode(regs)) { 
  8         if (get_user(instr, (u16 __user *)pc)) 
  9             goto die_sig; 
 10         if (is_wide_instruction(instr)) { 
 11             unsigned int instr2; 
 12             if (get_user(instr2, (u16 __user *)pc+1)) 
 13                 goto die_sig; 
 14             instr <<= 16; 
 15             instr |= instr2; 
 16         } 
 17     } else if (get_user(instr, (u32 __user *)pc)) { 
 18         goto die_sig; 
 19     } 
 20     //遍历钩子链表找到符合的钩子调用回调处理, 此处即kprobe_trap_handler 
 21     if (call_undef_hook(regs, instr) == 0) 
 22         return; 
 23     info.si_signo = SIGILL; 
 24     info.si_errno = 0; 
 25     info.si_code  = ILL_ILLOPC; 
 26     info.si_addr  = pc; 
 27     arm_notify_die("Oops - undefined instruction", regs, &info, 0, 6); 
 28 } 
 29 (arch/arm/kernel/traps.c)static int __kprobes kprobe_trap_handler(struct pt_regs *regs, unsigned int instr) 
 30 { 
 31     //kprobe中断处理是时关中断的! 
 32     local_irq_save(flags); 
 33     kprobe_handler(regs); 
 34     local_irq_restore(flags); 
 35 } 
 36 (arch/arm/kernel/kprobes.c)void __kprobes kprobe_handler(struct pt_regs *regs) 
 37 { 
 38     //这里两个变量的定义都是对于每个CPU的, why? 
 39     kcb = get_kprobe_ctlblk(); 
 40     cur = kprobe_running(); 
 41     //未定义THUMB2_KERNEL去除相关代码 
 42     p = get_kprobe((kprobe_opcode_t *)regs->ARM_pc); 
 43     //获取pc地址所对应的kprobe对象, 如果p存在即存在kprobe 
 44     //如果p不存在但cur存在则可能对应存在jprobe(因为cur存在即有probe但又没有该地址的kprobe, 只可能是jprobe) 
 45     //否则probe已被移除, 什么都不做, 等退出后看当前地址指令是否会恢复 
 46     if (p) { 
 47         //kprobe存在又区分两种情况, 如果cur也存在即之前还有kprobe在执行 
 48         //则kprobe->nmissed增加, 正常运行断点指令后退出, 不执行pre_handler/post_handler 
 49         //否则即当前kprobe是唯一中断的kprobe, 先设置当前CPU的current_kprobe与状态 
 50         //然后先执行pre_handler, 根据其返回值(为0)正常运行或(为1)直接跳转异常处理句柄 
 51         //最后复位全局current_kprobe 
 52         if (cur) { 
 53             /* Kprobe is pending, so we're recursing. */ 
 54             switch (kcb->kprobe_status) { 
 55             case KPROBE_HIT_ACTIVE: 
 56             case KPROBE_HIT_SSDONE: 
 57                 /* A pre- or post-handler probe got us here. */ 
 58                 kprobes_inc_nmissed_count(p); 
 59                 save_previous_kprobe(kcb); 
 60                 set_current_kprobe(p); 
 61                 kcb->kprobe_status = KPROBE_REENTER; 
 62                 singlestep(p, regs, kcb); 
 63                 restore_previous_kprobe(kcb); 
 64                 break; 
 65             default: 
 66                 /* impossible cases */ 
 67                 BUG(); 
 68             } 
 69         } else if (p->ainsn.insn_check_cc(regs->ARM_cpsr)) { 
 70             /* Probe hit and conditional execution check ok. */ 
 71             set_current_kprobe(p); 
 72             kcb->kprobe_status = KPROBE_HIT_ACTIVE; 
 73             /* 
 74              * If we have no pre-handler or it returned 0, we 
 75              * continue with normal processing.  If we have a 
 76              * pre-handler and it returned non-zero, it prepped 
 77              * for calling the break_handler below on re-entry, 
 78              * so get out doing nothing more here. 
 79              */ 
 80             if (!p->pre_handler || !p->pre_handler(p, regs)) { 
 81                 kcb->kprobe_status = KPROBE_HIT_SS; 
 82                 singlestep(p, regs, kcb); 
 83                 if (p->post_handler) { 
 84                     kcb->kprobe_status = KPROBE_HIT_SSDONE; 
 85                     p->post_handler(p, regs, 0); 
 86                 } 
 87                 reset_current_kprobe(); 
 88             } 
 89         } else { 
 90             /* 
 91              * Probe hit but conditional execution check failed, 
 92              * so just skip the instruction and continue as if 
 93              * nothing had happened. 
 94              */ 
 95             singlestep_skip(p, regs); 
 96         } 
 97     } else if (cur) { 
 98         /* We probably hit a jprobe.  Call its break handler. */ 
 99         if (cur->break_handler && cur->break_handler(cur, regs)) { 
100             kcb->kprobe_status = KPROBE_HIT_SS; 
101             singlestep(cur, regs, kcb); 
102             if (cur->post_handler) { 
103                 kcb->kprobe_status = KPROBE_HIT_SSDONE; 
104                 cur->post_handler(cur, regs, 0); 
105             } 
106         } 
107         reset_current_kprobe(); 
108     } else { 
109         /* 
110          * The probe was removed and a race is in progress. 
111          * There is nothing we can do about it.  Let's restart 
112          * the instruction.  By the time we can restart, the 
113          * real instruction will be there. 
114          */ 
115     } 
116 }

 

posted @ 2018-03-25 15:59  Five100Miles  阅读(2439)  评论(0编辑  收藏  举报