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 }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM