Linux內核調試技術——kprobe使用與實現

Linux kprobes調試技術是內核開發者們專門爲了便於跟蹤內核函數執行狀態所設計的一種輕量級內核調試技術。利用kprobes技術,內核開發人員可以在內核的絕大多數指定函數中動態的插入探測點來收集所需的調試狀態信息而基本不影響內核原有的執行流程。kprobes技術目前提供了3種探測手段:kprobe、jprobe和kretprobe,其中jprobe和kretprobe是基於kprobe實現的,他們分別應用於不同的探測場景中。本文首先簡單描述這3種探測技術的原理與區別,然後主要圍繞其中的kprobe技術進行分析並給出一個簡單的實例介紹如何利用kprobe進行內核函數探測,最後分析kprobe的實現過程(jprobe和kretprobe會在後續的博文中進行分析)。

內核源碼:linux-4.1.15

實驗環境:CentOS(x86_64)、樹莓派1b


一、kprobes技術背景

開發人員在內核或者模塊的調試過程中,往往會需要要知道其中的一些函數有無被調用、何時被調用、執行是否正確以及函數的入參和返回值是什麼等等。比較簡單的做法是在內核代碼對應的函數中添加日誌打印信息,但這種方式往往需要重新編譯內核或模塊,重新啓動設備之類的,操作較爲複雜甚至可能會破壞原有的代碼執行過程。

而利用kprobes技術,用戶可以定義自己的回調函數,然後在內核或者模塊中幾乎所有的函數中(有些函數是不可探測的,例如kprobes自身的相關實現函數,後文會有詳細說明)動態的插入探測點,當內核執行流程執行到指定的探測函數時,會調用該回調函數,用戶即可收集所需的信息了,同時內核最後還會回到原本的正常執行流程。如果用戶已經收集足夠的信息,不再需要繼續探測,則同樣可以動態的移除探測點。因此kprobes技術具有對內核執行流程影響小和操作方便的優點。

kprobes技術包括的3種探測手段分別時kprobe、jprobe和kretprobe。首先kprobe是最基本的探測方式,是實現後兩種的基礎,它可以在任意的位置放置探測點(就連函數內部的某條指令處也可以),它提供了探測點的調用前、調用後和內存訪問出錯3種回調方式,分別是pre_handler、post_handler和fault_handler,其中pre_handler函數將在被探測指令被執行前回調,post_handler會在被探測指令執行完畢後回調(注意不是被探測函數),fault_handler會在內存訪問出錯時被調用;jprobe基於kprobe實現,它用於獲取被探測函數的入參值;最後kretprobe從名字種就可以看出其用途了,它同樣基於kprobe實現,用於獲取被探測函數的返回值。

kprobes的技術原理並不僅僅包含存軟件的實現方案,它也需要硬件架構提供支持。其中涉及硬件架構相關的是CPU的異常處理和單步調試技術,前者用於讓程序的執行流程陷入到用戶註冊的回調函數中去,而後者則用於單步執行被探測點指令,因此並不是所有的架構均支持,目前kprobes技術已經支持多種架構,包括i386、x86_64、ppc64、ia64、sparc64、arm、ppc和mips(有些架構實現可能並不完全,具體可參考內核的Documentation/kprobes.txt)。

kprobes的特點與使用限制:

1、kprobes允許在同一個被被探測位置註冊多個kprobe,但是目前jprobe卻不可以;同時也不允許以其他的jprobe回掉函數和kprobe的post_handler回調函數作爲被探測點。

2、一般情況下,可以探測內核中的任何函數,包括中斷處理函數。不過在kernel/kprobes.c和arch/*/kernel/kprobes.c程序中用於實現kprobes自身的函數是不允許被探測的,另外還有do_page_fault和notifier_call_chain;

3、如果以一個內聯函數爲探測點,則kprobes可能無法保證對該函數的所有實例都註冊探測點。由於gcc可能會自動將某些函數優化爲內聯函數,因此可能無法達到用戶預期的探測效果;

4、一個探測點的回調函數可能會修改被探測函數運行的上下文,例如通過修改內核的數據結構或者保存與struct pt_regs結構體中的觸發探測之前寄存器信息。因此kprobes可以被用來安裝bug修復代碼或者注入故障測試代碼;

5、kprobes會避免在處理探測點函數時再次調用另一個探測點的回調函數,例如在printk()函數上註冊了探測點,則在它的回調函數中可能再次調用printk函數,此時將不再觸發printk探測點的回調,僅僅時增加了kprobe結構體中nmissed字段的數值;

6、在kprobes的註冊和註銷過程中不會使用mutex鎖和動態的申請內存;

7、kprobes回調函數的運行期間是關閉內核搶佔的,同時也可能在關閉中斷的情況下執行,具體要視CPU架構而定。因此不論在何種情況下,在回調函數中不要調用會放棄CPU的函數(如信號量、mutex鎖等);

8、kretprobe通過替換返回地址爲預定義的trampoline的地址來實現,因此棧回溯和gcc內嵌函數__builtin_return_address()調用將返回trampoline的地址而不是真正的被探測函數的返回地址;

9、如果一個函數的調用此處和返回次數不相等,則在類似這樣的函數上註冊kretprobe將可能不會達到預期的效果,例如do_exit()函數會存在問題,而do_execve()函數和do_fork()函數不會;

10、如果當在進入和退出一個函數時,CPU運行在非當前任務所有的棧上,那麼往該函數上註冊kretprobe可能會導致不可預料的後果,因此,kprobes不支持在X86_64的結構下爲__switch_to()函數註冊kretprobe,將直接返回-EINVAL。


二、kprobe原理

下面來介紹一下kprobe是如何工作的。具體流程見下圖:


圖1 kprobe的工作流程

1、當用戶註冊一個探測點後,kprobe首先備份被探測點的對應指令,然後將原始指令的入口點替換爲斷點指令,該指令是CPU架構相關的,如i386和x86_64是int3,arm是設置一個未定義指令(目前的x86_64架構支持一種跳轉優化方案Jump Optimization,內核需開啓CONFIG_OPTPROBES選項,該種方案使用跳轉指令來代替斷點指令);

2、當CPU流程執行到探測點的斷點指令時,就觸發了一個trap,在trap處理流程中會保存當前CPU的寄存器信息並調用對應的trap處理函數,該處理函數會設置kprobe的調用狀態並調用用戶註冊的pre_handler回調函數,kprobe會向該函數傳遞註冊的struct kprobe結構地址以及保存的CPU寄存器信息;

3、隨後kprobe單步執行前面所拷貝的被探測指令,具體執行方式各個架構不盡相同,arm會在異常處理流程中使用模擬函數執行,而x86_64架構則會設置單步調試flag並回到異常觸發前的流程中執行;

4、在單步執行完成後,kprobe執行用戶註冊的post_handler回調函數;

5、最後,執行流程回到被探測指令之後的正常流程繼續執行。


三、kprobe使用實例

在分析kprobe的實現之前先來看一下如何利用kprobe對函數進行探測,以便於讓我們對kprobre所完成功能有一個比較清晰的認識。目前,使用kprobe可以通過兩種方式,第一種是開發人員自行編寫內核模塊,向內核註冊探測點,探測函數可根據需要自行定製,使用靈活方便;第二種方式是使用kprobes on ftrace,這種方式是kprobe和ftrace結合使用,即可以通過kprobe來優化ftrace來跟蹤函數的調用。下面來分別介紹:

1、編寫kprobe探測模塊

內核提供了一個struct kprobe結構體以及一系列的內核API函數接口,用戶可以通過這些接口自行實現探測回調函數並實現struct kprobe結構,然後將它註冊到內核的kprobes子系統中來達到探測的目的。同時在內核的samples/kprobes目錄下有一個例程kprobe_example.c描述了kprobe模塊最簡單的編寫方式,開發者可以以此爲模板編寫自己的探測模塊。

1.1、kprobe結構體與API介紹

struct kprobe結構體定義如下:

  1. struct kprobe {  
  2.     struct hlist_node hlist;  
  3.   
  4.     /* list of kprobes for multi-handler support */  
  5.     struct list_head list;  
  6.   
  7.     /*count the number of times this probe was temporarily disarmed */  
  8.     unsigned long nmissed;  
  9.   
  10.     /* location of the probe point */  
  11.     kprobe_opcode_t *addr;  
  12.   
  13.     /* Allow user to indicate symbol name of the probe point */  
  14.     const char *symbol_name;  
  15.   
  16.     /* Offset into the symbol */  
  17.     unsigned int offset;  
  18.   
  19.     /* Called before addr is executed. */  
  20.     kprobe_pre_handler_t pre_handler;  
  21.   
  22.     /* Called after addr is executed, unless... */  
  23.     kprobe_post_handler_t post_handler;  
  24.   
  25.     /* 
  26.      * ... called if executing addr causes a fault (eg. page fault). 
  27.      * Return 1 if it handled fault, otherwise kernel will see it. 
  28.      */  
  29.     kprobe_fault_handler_t fault_handler;  
  30.   
  31.     /* 
  32.      * ... called if breakpoint trap occurs in probe handler. 
  33.      * Return 1 if it handled break, otherwise kernel will see it. 
  34.      */  
  35.     kprobe_break_handler_t break_handler;  
  36.   
  37.     /* Saved opcode (which has been replaced with breakpoint) */  
  38.     kprobe_opcode_t opcode;  
  39.   
  40.     /* copy of the original instruction */  
  41.     struct arch_specific_insn ainsn;  
  42.   
  43.     /* 
  44.      * Indicates various status flags. 
  45.      * Protected by kprobe_mutex after this kprobe is registered. 
  46.      */  
  47.     u32 flags;  
  48. };  
其中各個字段的含義如下:

struct hlist_node hlist:被用於kprobe全局hash,索引值爲被探測點的地址;
struct list_head list:用於鏈接同一被探測點的不同探測kprobe;
kprobe_opcode_t *addr:被探測點的地址;
const char *symbol_name:被探測函數的名字;
unsigned int offset:被探測點在函數內部的偏移,用於探測函數內部的指令,如果該值爲0表示函數的入口;
kprobe_pre_handler_t pre_handler:在被探測點指令執行之前調用的回調函數;
kprobe_post_handler_t post_handler:在被探測指令執行之後調用的回調函數;
kprobe_fault_handler_t fault_handler:在執行pre_handler、post_handler或單步執行被探測指令時出現內存異常則會調用該回調函數;
kprobe_break_handler_t break_handler:在執行某一kprobe過程中觸發了斷點指令後會調用該函數,用於實現jprobe;
kprobe_opcode_t opcode:保存的被探測點原始指令;
struct arch_specific_insn ainsn:被複制的被探測點的原始指令,用於單步執行,架構強相關(可能包含指令模擬函數);
u32 flags:狀態標記。

涉及的API函數接口如下:

  1. int register_kprobe(struct kprobe *kp)      //向內核註冊kprobe探測點  
  2. void unregister_kprobe(struct kprobe *kp)   //卸載kprobe探測點  
  3. int register_kprobes(struct kprobe **kps, int num)     //註冊探測函數向量,包含多個探測點  
  4. void unregister_kprobes(struct kprobe **kps, int num)  //卸載探測函數向量,包含多個探測點  
  5. int disable_kprobe(struct kprobe *kp)       //臨時暫停指定探測點的探測  
  6. int enable_kprobe(struct kprobe *kp)        //恢復指定探測點的探測  

1.2、用例kprobe_example.c分析與演示

該用例函數非常簡單,它實現了內核函數do_fork的探測,該函數會在fork系統調用或者內核kernel_thread函數創建進程時被調用,觸發也十分的頻繁。下面來分析一下用例代碼:

  1. /* For each probe you need to allocate a kprobe structure */  
  2. static struct kprobe kp = {  
  3.     .symbol_name    = "do_fork",  
  4. };  
  5.   
  6. static int __init kprobe_init(void)  
  7. {  
  8.     int ret;  
  9.     kp.pre_handler = handler_pre;  
  10.     kp.post_handler = handler_post;  
  11.     kp.fault_handler = handler_fault;  
  12.   
  13.     ret = register_kprobe(&kp);  
  14.     if (ret < 0) {  
  15.         printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);  
  16.         return ret;  
  17.     }  
  18.     printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);  
  19.     return 0;  
  20. }  
  21.   
  22. static void __exit kprobe_exit(void)  
  23. {  
  24.     unregister_kprobe(&kp);  
  25.     printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);  
  26. }  
  27.   
  28. module_init(kprobe_init)  
  29. module_exit(kprobe_exit)  
  30. MODULE_LICENSE("GPL");  
程序中定義了一個struct kprobe結構實例kp並初始化其中的symbol_name字段爲“do_fork”,表明它將要探測do_fork函數。在模塊的初始化函數中,註冊了
pre_handler、post_handler和fault_handler這3個回調函數分別爲handler_pre、handler_post和handler_fault,最後調用register_kprobe註冊。在模塊的卸載函數中調用unregister_kprobe函數卸載kp探測點。
  1. static int handler_pre(struct kprobe *p, struct pt_regs *regs)  
  2. {  
  3. #ifdef CONFIG_X86  
  4.     printk(KERN_INFO "pre_handler: p->addr = 0x%p, ip = %lx,"  
  5.             " flags = 0x%lx\n",  
  6.         p->addr, regs->ip, regs->flags);  
  7. #endif  
  8. #ifdef CONFIG_PPC  
  9.     printk(KERN_INFO "pre_handler: p->addr = 0x%p, nip = 0x%lx,"  
  10.             " msr = 0x%lx\n",  
  11.         p->addr, regs->nip, regs->msr);  
  12. #endif  
  13. #ifdef CONFIG_MIPS  
  14.     printk(KERN_INFO "pre_handler: p->addr = 0x%p, epc = 0x%lx,"  
  15.             " status = 0x%lx\n",  
  16.         p->addr, regs->cp0_epc, regs->cp0_status);  
  17. #endif  
  18. #ifdef CONFIG_TILEGX  
  19.     printk(KERN_INFO "pre_handler: p->addr = 0x%p, pc = 0x%lx,"  
  20.             " ex1 = 0x%lx\n",  
  21.         p->addr, regs->pc, regs->ex1);  
  22. #endif  
  23.   
  24.     /* A dump_stack() here will give a stack backtrace */  
  25.     return 0;  
  26. }  
handler_pre回調函數的第一個入參是註冊的struct kprobe探測實例,第二個參數是保存的觸發斷點前的寄存器狀態,它在do_fork函數被調用之前被調用,該函數僅僅是打印了被探測點的地址,保存的個別寄存器參數。由於受CPU架構影響,這裏對不同的架構進行了宏區分(雖然沒有實現arm架構的,但是支持的,可以自行添加);
  1. /* kprobe post_handler: called after the probed instruction is executed */  
  2. static void handler_post(struct kprobe *p, struct pt_regs *regs,  
  3.                 unsigned long flags)  
  4. {  
  5. #ifdef CONFIG_X86  
  6.     printk(KERN_INFO "post_handler: p->addr = 0x%p, flags = 0x%lx\n",  
  7.         p->addr, regs->flags);  
  8. #endif  
  9. #ifdef CONFIG_PPC  
  10.     printk(KERN_INFO "post_handler: p->addr = 0x%p, msr = 0x%lx\n",  
  11.         p->addr, regs->msr);  
  12. #endif  
  13. #ifdef CONFIG_MIPS  
  14.     printk(KERN_INFO "post_handler: p->addr = 0x%p, status = 0x%lx\n",  
  15.         p->addr, regs->cp0_status);  
  16. #endif  
  17. #ifdef CONFIG_TILEGX  
  18.     printk(KERN_INFO "post_handler: p->addr = 0x%p, ex1 = 0x%lx\n",  
  19.         p->addr, regs->ex1);  
  20. #endif  
  21. }  
handler_post回調函數的前兩個入參同handler_pre,第三個參數目前尚未使用,全部爲0;該函數在do_fork函數調用之後被調用,這裏打印的內容同handler_pre類似。
  1. /* 
  2.  * fault_handler: this is called if an exception is generated for any 
  3.  * instruction within the pre- or post-handler, or when Kprobes 
  4.  * single-steps the probed instruction. 
  5.  */  
  6. static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)  
  7. {  
  8.     printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",  
  9.         p->addr, trapnr);  
  10.     /* Return 0 because we don't handle the fault. */  
  11.     return 0;  
  12. }  
handler_fault回調函數會在執行handler_pre、handler_post或單步執行do_fork時出現錯誤時調用,這裏第三個參數時具體發生錯誤的trap number,與架構相關,例如i386的page fault爲14。

下面將它編譯成模塊在我的x86(CentOS 3.10)環境下進行演示,首先確保架構和內核已經支持kprobes,開啓以下選項(一般都是默認開啓的):

Symbol: KPROBES [=y]                            
Type  : boolean                                 
Prompt: Kprobes                                 
  Location:                                     
(3) -> General setup                            
  Defined at arch/Kconfig:37                    
  Depends on: MODULES [=y] && HAVE_KPROBES [=y] 
  Selects: KALLSYMS [=y]                        
 
Symbol: HAVE_KPROBES [=y]                       
Type  : boolean                                 
  Defined at arch/Kconfig:174                   
  Selected by: X86 [=y]    

然後使用以下Makefile單獨編譯kprobe_example.ko模塊:

  1. obj-m := kprobe_example.o  
  2.   
  3. CROSS_COMPILE=''  
  4.   
  5. KDIR := /lib/modules/$(shell uname -r)/build  
  6. all:  
  7.         make -C $(KDIR) M=$(PWD) modules   
  8. clean:  
  9.         rm -f *.ko *.o *.mod.o *.mod.c .*.cmd *.symvers  modul*  
加載到內核中後,隨便在終端上敲一個命令,可以看到dmesg中打印如下信息:

<6>pre_handler: p->addr = 0xc0439cc0, ip = c0439cc1, flags = 0x246
<6>post_handler: p->addr = 0xc0439cc0, flags = 0x246

<6>pre_handler: p->addr = 0xc0439cc0, ip = c0439cc1, flags = 0x246
<6>post_handler: p->addr = 0xc0439cc0, flags = 0x246
<6>pre_handler: p->addr = 0xc0439cc0, ip = c0439cc1, flags = 0x246
<6>post_handler: p->addr = 0xc0439cc0, flags = 0x246

可以看到被探測點的地址爲0xc0439cc0,用以下命令確定這個地址就是do_fork的入口地址。

[root@apple kprobes]# cat /proc/kallsyms | grep do_fork
c0439cc0 T do_fork


2、使用kprobe on ftrace來跟蹤函數和調用棧

這種方式用戶通過/sys/kernel/debug/tracing/目錄下的trace等屬性文件來探測用戶指定的函數,用戶可添加kprobe支持的任意函數並設置探測格式與過濾條件,無需再編寫內核模塊,使用更爲簡便,但需要內核的debugfs和ftrace功能的支持。

首先,在使用前需要保證開啓以下內核選項:

Symbol: FTRACE [=y]                                                                                            
Type  : boolean                                                                                                
Prompt: Tracers                                                                                                
  Location:                                                                                                    
(5) -> Kernel hacking                                                                                          
  Defined at kernel/trace/Kconfig:132                                                                          
  Depends on: TRACING_SUPPORT [=y] 

Symbol: KPROBE_EVENT [=y]                                                                                      
Type  : boolean                                                                                                
Prompt: Enable kprobes-based dynamic events                                                                    
  Location:                                                                                                    
    -> Kernel hacking                                                                                          
(1)   -> Tracers (FTRACE [=y])                                                                                 
  Defined at kernel/trace/Kconfig:405                                                                          
  Depends on: TRACING_SUPPORT [=y] && FTRACE [=y] && KPROBES [=y] && HAVE_REGS_AND_STACK_ACCESS_API [=y]       
  Selects: TRACING [=y] && PROBE_EVENTS [=y]  

Symbol: HAVE_KPROBES_ON_FTRACE [=y]                                                                            
Type  : boolean                                                                                                
  Defined at arch/Kconfig:183                                                                                  
  Selected by: X86 [=y]                                                                                        
 
Symbol: KPROBES_ON_FTRACE [=y]                                                                                 
Type  : boolean                                                                                                
  Defined at arch/Kconfig:79                                                                                   
  Depends on: KPROBES [=y] && HAVE_KPROBES_ON_FTRACE [=y] && DYNAMIC_FTRACE_WITH_REGS [=y]

然後需要將debugfs文件系統掛在到/sys/kernel/debug/目錄下:

# mount -t debugfs nodev /sys/kernel/debug/

此時/sys/kernel/debug/tracing目錄下就出現了若干個文件和目錄用於用戶設置要跟蹤的函數以及過濾條件等等,這裏我主要關注以下幾個文件:

1、配置屬性文件:kprobe_events
2、查詢屬性文件:trace和trace_pipe
3、使能屬性文件:events/kprobes/<GRP>/<EVENT>/enabled
4、過濾屬性文件:events/kprobes/<GRP>/<EVENT>/filter
5、格式查詢屬性文件:events/kprobes/<GRP>/<EVENT>/format
6、事件統計屬性文件:kprobe_profile

其中配置屬性文件用於用戶配置要探測的函數以及探測的方式與參數,在配置完成後,會在events/kprobes/目錄下生成對應的目錄;其中會生成enabled、format、filter和id這4個文件,其中的enable屬性文件用於控制探測的開啓或關閉,filter用於設置過濾條件,format可以查看當前的輸出格式,最後id可以查看當前probe event的ID號。然後若被探測函數被執行流程觸發調用,用戶可以通過trace屬性文件進行查看。最後通過kprobe_profile屬性文件可以查看探測命中次數和丟失次數(probe hits and probe miss-hits)。

下面來看看各個屬性文件的常用操作方式(其中具體格式和參數方面的細節可以查看內核的Documentation/trace/kprobetrace.txt文件,描述非常詳細):

1、kprobe_events

該屬性文件支持3中格式的輸入:

  p[:[GRP/]EVENT] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS]——設置一個probe探測點
  r[:[GRP/]EVENT] [MOD:]SYM[+0] [FETCHARGS]                         ——設置一個return probe探測點
  -:[GRP/]EVENT                                                 ——刪除一個探測點

各個字段的含義如下: 

GRP : Group name. If omitted, use "kprobes" for it.               ——指定後會在events/kprobes目錄下生成對應名字的目錄,一般不設
 EVENT : Event name. If omitted, the event name is generated based on SYM+offs or MEMADDR.    ——指定後會在events/kprobes/<GRP>目錄下生成對應名字的目錄
 MOD : Module name which has given SYM.                             ——模塊名,一般不設
 SYM[+offs] : Symbol+offset where the probe is inserted.                 ——指定被探測函數和偏移
 MEMADDR : Address where the probe is inserted.                            ——指定被探測的內存絕對地址

 FETCHARGS : Arguments. Each probe can have up to 128 args.                         ——指定要獲取的參數信息
  %REG : Fetch register REG                                                                                       ——獲取指定寄存器值
  @ADDR : Fetch memory at ADDR (ADDR should be in kernel)                    ——獲取指定內存地址的值
  @SYM[+|-offs] : Fetch memory at SYM +|- offs (SYM should be a data symbol)    ——獲取全局變量的值
  $stackN : Fetch Nth entry of stack (N >= 0)                                                                 ——獲取指定棧空間值,即sp寄存器+N後的位置值
  $stack : Fetch stack address.                                                                                            ——獲取sp寄存器值
  $retval : Fetch return value.(*)                                                                                             ——獲取返回值,僅用於return probe
  +|-offs(FETCHARG) : Fetch memory at FETCHARG +|- offs address.(**)                  ——以下可以由於獲取指定地址的結構體參數內容,可以設定具體的參數名和偏移地址
  NAME=FETCHARG : Set NAME as the argument name of FETCHARG.
  FETCHARG:TYPE : Set TYPE as the type of FETCHARG. Currently, basic types     ——設置參數的類型,可以支持字符串和比特類型
 (u8/u16/u32/u64/s8/s16/s32/s64), "string" and bitfield
 are supported.

2、events/kprobes/<GRP>/<EVENT>/enabled

開啓探測:echo 1 > events/kprobes/<GRP>/<EVENT>/enabled

暫停探測:echo 0 > events/kprobes/<GRP>/<EVENT>/enabled

3、events/kprobes/<GRP>/<EVENT>/filter

該屬性文件用於設置過濾條件,可以減少trace中輸出的信息,它支持的格式和C語言的表達式類似,支持 ==,!=,>,<,>=,<=判斷,並且支持與&&,或||,還有()。


下面還是以do_fork()函數爲例來舉例看一下具體如何使用(實驗環境:樹莓派1b):

1、設置配置屬性

首先添加配置探測點:

root@apple:~# echo 'p:myprobe do_fork clone_flags=%r0 stack_start=%r1 stack_size=%r2 parent_tidptr=%r3 child_tidptr=+0($stack)' > /sys/kernel/debug/tracing/kprobe_events

root@apple:~# echo 'r:myretprobe do_fork $retval' >> /sys/kernel/debug/tracing/kprobe_events

這裏註冊probe和retprobe,其中probe中設定了獲取do_fork()函數的入參值(注意這裏的參數信息根據不同CPU架構的函數參數傳遞規則強相關,根據ARM遵守的ATPCS規則,函數入參1~4通過r0~r3寄存器傳遞,多餘的參數通過棧傳遞),由於入參爲5個,所以前4個通過寄存器獲取,最後一個通過棧獲取。

現可通過format文件查看探測的輸出格式:

root@apple:/sys/kernel/debug/tracing# cat events/kprobes/myprobe/format 
name: myprobe
ID: 1211
format:
        field:unsigned short common_type;       offset:0;       size:2; signed:0;
        field:unsigned char common_flags;       offset:2;       size:1; signed:0;
        field:unsigned char common_preempt_count;       offset:3;       size:1; signed:0;
        field:int common_pid;   offset:4;       size:4; signed:1;

        field:unsigned long __probe_ip; offset:8;       size:4; signed:0;
        field:u32 clone_flags;  offset:12;      size:4; signed:0;
        field:u32 stack_start;  offset:16;      size:4; signed:0;
        field:u32 stack_size;   offset:20;      size:4; signed:0;
        field:u32 parent_tidptr;        offset:24;      size:4; signed:0;
        field:u32 child_tidptr; offset:28;      size:4; signed:0;

print fmt: "(%lx) clone_flags=0x%x stack_start=0x%x stack_size=0x%x parent_tidptr=0x%x child_tidptr=0x%x", REC->__probe_ip, REC->clone_flags, REC->stack_start, REC->stack_size, REC->parent_tidptr, REC->child_tidptr

root@apple:/sys/kernel/debug/tracing# cat events/kprobes/myretprobe/format     
name: myretprobe
ID: 1212
format:
        field:unsigned short common_type;       offset:0;       size:2; signed:0;
        field:unsigned char common_flags;       offset:2;       size:1; signed:0;
        field:unsigned char common_preempt_count;       offset:3;       size:1; signed:0;
        field:int common_pid;   offset:4;       size:4; signed:1;

        field:unsigned long __probe_func;       offset:8;       size:4; signed:0;
        field:unsigned long __probe_ret_ip;     offset:12;      size:4; signed:0;
        field:u32 arg1; offset:16;      size:4; signed:0;

print fmt: "(%lx <- %lx) arg1=0x%x", REC->__probe_func, REC->__probe_ret_ip, REC->arg1

2、開啓探測並觸發函數調用

往對應的enable函數中寫入1用以開啓探測功能:

root@apple:/sys/kernel/debug/tracing# echo 1 > events/kprobes/myprobe/enable 

root@apple:/sys/kernel/debug/tracing# echo 1 > events/kprobes/myretprobe/enable 

然後在終端上敲幾條命令和建立一個ssh鏈接觸發進程創建do_fork函數調用,並通過trace屬性文件獲取函數調用時的探測情況

root@apple:/sys/kernel/debug/tracing# cat trace     

# tracer: nop
......
            bash-513   [000] d... 15726.746135: myprobe: (do_fork+0x0/0x380) clone_flags=0x1200011 stack_start=0x0 stack_size=0x0 parent_tidptr=0x0 child_tidptr=0xb6f43278
            bash-513   [000] d... 15726.746691: myretprobe: (SyS_clone+0x2c/0x34 <- do_fork) arg1=0x226
            bash-513   [000] d... 15727.296153: myprobe: (do_fork+0x0/0x380) clone_flags=0x1200011 stack_start=0x0 stack_size=0x0 parent_tidptr=0x0 child_tidptr=0xb6f43278
            bash-513   [000] d... 15727.296713: myretprobe: (SyS_clone+0x2c/0x34 <- do_fork) arg1=0x227
            bash-513   [000] d... 15728.356149: myprobe: (do_fork+0x0/0x380) clone_flags=0x1200011 stack_start=0x0 stack_size=0x0 parent_tidptr=0x0 child_tidptr=0xb6f43278
            bash-513   [000] d... 15728.356705: myretprobe: (SyS_clone+0x2c/0x34 <- do_fork) arg1=0x228
            bash-513   [000] d... 15731.596195: myprobe: (do_fork+0x0/0x380) clone_flags=0x1200011 stack_start=0x0 stack_size=0x0 parent_tidptr=0x0 child_tidptr=0xb6f43278
            bash-513   [000] d... 15731.596756: myretprobe: (SyS_clone+0x2c/0x34 <- do_fork) arg1=0x229
            sshd-520   [000] d... 17755.999223: myprobe: (do_fork+0x0/0x380) clone_flags=0x1200011 stack_start=0x0 stack_size=0x0 parent_tidptr=0x0 child_tidptr=0xb6fac068
            sshd-520   [000] d... 17755.999943: myretprobe: (SyS_clone+0x2c/0x34 <- do_fork) arg1=0x22d

從輸出中可以看到do_fork函數由bash(PID=513) 和sshd(PID=520)進程調用,同時執行的CPU爲0,調用do_fork函數是入參值分別是stack_start=0x0 stack_size=0x0 parent_tidptr=0x0 child_tidptr=0xbxxxxxxx,同時輸出函數返回上層SyS_clone系統調用的nr值。

如果輸出太多了,想要清除就向trace中寫0即可

root@apple:/sys/kernel/debug/tracing# echo 0 > trace  

3、使用filter進行過濾

例如想要把前面列出的PID爲513調用信息的給過濾掉,則向filter中寫入如下的命令即可:

root@apple:/sys/kernel/debug/tracing# echo common_pid!=513 > events/kprobes/myprobe/filter 

root@apple:/sys/kernel/debug/tracing# cat trace
# tracer: nop
......
            bash-513   [000] d... 24456.536804: myretprobe: (SyS_clone+0x2c/0x34 <- do_fork) arg1=0x245
        kthreadd-2     [000] d... 24598.655935: myprobe: (do_fork+0x0/0x380) clone_flags=0x800711 stack_start=0xc003d69c stack_size=0xc58982a0 parent_tidptr=0x0 child_tidptr=0x0
        kthreadd-2     [000] d... 24598.656133: myretprobe: (kernel_thread+0x38/0x40 <- do_fork) arg1=0x246
            bash-513   [000] d... 24667.676717: myretprobe: (SyS_clone+0x2c/0x34 <- do_fork) arg1=0x247

如此就不會在打印PID爲513的進程調用信息了,這裏的參數可以參考前面的format中輸出的,例如想指定輸出特定clone_flags值,則可以輸入clone_flags=xxx即可。

最後補充一點,若此時需要查看函數調用的棧信息(stacktrace),可以使用如下命令激活stacktrace輸出:

root@apple:/sys/kernel/debug/tracing# echo stacktrace > trace_options

root@apple:/sys/kernel/debug/tracing# cat trace                                
......
            bash-508   [000] d...   449.276093: myprobe: (do_fork+0x0/0x380) clone_flags=0x1200011 stack_start=0x0 stack_size=0x0 parent_tidptr=0x0 child_tidptr=0xb6f86278
            bash-508   [000] d...   449.276126: <stack trace>
 => do_fork


四、kprobe實現源碼分析

在瞭解了kprobe的基本原理和使用後,現在從源碼的角度來詳細分析它是如何實現的。主要包括kprobes的初始化、註冊kprobe和觸發kprobe(包括arm結構和x86_64架構的回調函數和single-step單步執行)。

1、kprobes初始化


圖 2 kprobes初始化流程 

kprobes作爲一個模塊,其初始化函數爲init_kprobes,代碼路徑kernel/kprobes.c

  1. static int __init init_kprobes(void)  
  2. {  
  3.     int i, err = 0;  
  4.   
  5.     /* FIXME allocate the probe table, currently defined statically */  
  6.     /* initialize all list heads */  
  7.     for (i = 0; i < KPROBE_TABLE_SIZE; i++) {  
  8.         INIT_HLIST_HEAD(&kprobe_table[i]);  
  9.         INIT_HLIST_HEAD(&kretprobe_inst_table[i]);  
  10.         raw_spin_lock_init(&(kretprobe_table_locks[i].lock));  
  11.     }  
  12.   
  13.     err = populate_kprobe_blacklist(__start_kprobe_blacklist,  
  14.                     __stop_kprobe_blacklist);  
  15.     if (err) {  
  16.         pr_err("kprobes: failed to populate blacklist: %d\n", err);  
  17.         pr_err("Please take care of using kprobes.\n");  
  18.     }  
  19.   
  20.     if (kretprobe_blacklist_size) {  
  21.         /* lookup the function address from its name */  
  22.         for (i = 0; kretprobe_blacklist[i].name != NULL; i++) {  
  23.             kprobe_lookup_name(kretprobe_blacklist[i].name,  
  24.                        kretprobe_blacklist[i].addr);  
  25.             if (!kretprobe_blacklist[i].addr)  
  26.                 printk("kretprobe: lookup failed: %s\n",  
  27.                        kretprobe_blacklist[i].name);  
  28.         }  
  29.     }  
  30.   
  31. #if defined(CONFIG_OPTPROBES)  
  32. #if defined(__ARCH_WANT_KPROBES_INSN_SLOT)  
  33.     /* Init kprobe_optinsn_slots */  
  34.     kprobe_optinsn_slots.insn_size = MAX_OPTINSN_SIZE;  
  35. #endif  
  36.     /* By default, kprobes can be optimized */  
  37.     kprobes_allow_optimization = true;  
  38. #endif  
  39.   
  40.     /* By default, kprobes are armed */  
  41.     kprobes_all_disarmed = false;  
  42.   
  43.     err = arch_init_kprobes();  
  44.     if (!err)  
  45.         err = register_die_notifier(&kprobe_exceptions_nb);  
  46.     if (!err)  
  47.         err = register_module_notifier(&kprobe_module_nb);  
  48.   
  49.     kprobes_initialized = (err == 0);  
  50.   
  51.     if (!err)  
  52.         init_test_probes();  
  53.     return err;  
  54. }  
首先初始化hash表的各個鏈表頭,用來保存後面調用kprobe_register函數註冊的struct kprobes實例(會使用探測地址作爲索引),同時初始化kretprobe用到的自旋鎖。
接下來調用populate_kprobe_blacklist函數將kprobe實現相關的代碼函數保存到kprobe_blacklist這個鏈表中去,用於後面註冊探測點時判斷使用,注意這裏的__start_kprobe_blacklist和__stop_kprobe_blacklist定義在arch/arm/kernel/vmlinux.lds.h中的.init.rodata段中,其中保存了_kprobe_blacklist段信息:
  1. #define KPROBE_BLACKLIST()  . = ALIGN(8);                     \  
  2.                 VMLINUX_SYMBOL(__start_kprobe_blacklist) = .; \  
  3.                 *(_kprobe_blacklist)                  \  
  4.                 VMLINUX_SYMBOL(__stop_kprobe_blacklist) = .;  
  5.   
  6. #define INIT_DATA                           \  
  7.     *(.init.data)                           \  
  8. ......  
  9.     *(.init.rodata)                         \  
  10. ......  
  11.     KPROBE_BLACKLIST()                      \  
  12. ......  
而_kprobe_blacklist段中保存了實現kprobes的關鍵代碼路徑,這些代碼是不可以被kprobe自己所探測的,在源碼定義相關函數時使用NOKPROBE_SYMBOL宏將函數放到這個段中:
  1. #define __NOKPROBE_SYMBOL(fname)            \  
  2. static unsigned long __used             \  
  3.     __attribute__((section("_kprobe_blacklist")))   \  
  4.     _kbl_addr_##fname = (unsigned long)fname;  
  5. #define NOKPROBE_SYMBOL(fname)  __NOKPROBE_SYMBOL(fname)  
例如其中的get_kprobe函數:
  1. struct kprobe *get_kprobe(void *addr)  
  2. {  
  3. ......  
  4. }  
  5. NOKPROBE_SYMBOL(get_kprobe);  

回到init_kprobes函數中繼續分析,接下來的片段是kretprobe相關的代碼,用來覈對kretprobe_blacklist中定義的函數是否存在,這裏kretprobe_blacklist_size變量默認爲0;接下來初始化3個全局變量,kprobes_all_disarmed用於表示是否啓用kprobe機制,這裏默認設置爲啓用;隨後調用arch_init_kprobes進行架構相關的初始化,x86架構的實現爲空,arm架構的實現如下:

  1. int __init arch_init_kprobes()  
  2. {  
  3.     arm_probes_decode_init();  
  4. #ifdef CONFIG_THUMB2_KERNEL  
  5.     register_undef_hook(&kprobes_thumb16_break_hook);  
  6.     register_undef_hook(&kprobes_thumb32_break_hook);  
  7. #else  
  8.     register_undef_hook(&kprobes_arm_break_hook);  
  9. #endif  
  10.     return 0;  
  11. }  
由於沒有啓用THUMB2模式,這裏arm_probes_decode_init主要是獲取PC和當前執行地址偏移值(ARM的流水線機制一般爲8)以及設置相關寄存器值獲取方式等代碼;而register_undef_hook函數向全局undef_hook鏈表註冊了一個未定義指令異常處理的鉤子,相關的結構體如下:
  1. static struct undef_hook kprobes_arm_break_hook = {  
  2.     .instr_mask = 0x0fffffff,  
  3.     .instr_val  = KPROBE_ARM_BREAKPOINT_INSTRUCTION,  
  4.     .cpsr_mask  = MODE_MASK,  
  5.     .cpsr_val   = SVC_MODE,  
  6.     .fn     = kprobe_trap_handler,  
  7. };  
這樣在觸發未定義指令KPROBE_ARM_BREAKPOINT_INSTRUCTION(機器碼0x07f001f8)時將會調用到這裏的kprobe_trap_handler函數。

再次回到init_kprobes函數,接下來分別註冊die和module的內核通知鏈kprobe_exceptions_nb和kprobe_module_nb:

  1. static struct notifier_block kprobe_exceptions_nb = {  
  2.     .notifier_call = kprobe_exceptions_notify,  
  3.     .priority = 0x7fffffff /* we need to be notified first */  
  4. };  
  1. static struct notifier_block kprobe_module_nb = {  
  2.     .notifier_call = kprobes_module_callback,  
  3.     .priority = 0  
  4. };  
其中kprobe_exceptions_nb的優先級很高,如此在執行回調函數和單步執行被探測指令期間若發生了內存異常,將優先調用kprobe_exceptions_notify函數處理(架構相關,x86會調用kprobe的fault回調函數,而arm則爲空);註冊module notify回調kprobes_module_callback函數的作用是若當某個內核模塊發生卸載操作時有必要檢測並移除註冊到該模塊函數的探測點。

最後init_kprobes函數置位kprobes_initialized標識,初始化完成。


2、註冊一個kprobe實例

kprobe探測模塊調用register_kprobe向kprobe子系統註冊一個kprobe探測點實例,代碼路徑kernel/kprobes.c


圖 3 kprobe註冊流程

  1. int register_kprobe(struct kprobe *p)  
  2. {  
  3.     int ret;  
  4.     struct kprobe *old_p;  
  5.     struct module *probed_mod;  
  6.     kprobe_opcode_t *addr;  
  7.   
  8.     /* Adjust probe address from symbol */  
  9.     addr = kprobe_addr(p);  
  10.     if (IS_ERR(addr))  
  11.         return PTR_ERR(addr);  
  12.     p->addr = addr;  
  13.   
  14.     ret = check_kprobe_rereg(p);  
  15.     if (ret)  
  16.         return ret;  
  17.   
  18.     /* User can pass only KPROBE_FLAG_DISABLED to register_kprobe */  
  19.     p->flags &= KPROBE_FLAG_DISABLED;  
  20.     p->nmissed = 0;  
  21.     INIT_LIST_HEAD(&p->list);  
  22.   
  23.     ret = check_kprobe_address_safe(p, &probed_mod);  
  24.     if (ret)  
  25.         return ret;  
  26.   
  27.     mutex_lock(&kprobe_mutex);  
  28.   
  29.     old_p = get_kprobe(p->addr);  
  30.     if (old_p) {  
  31.         /* Since this may unoptimize old_p, locking text_mutex. */  
  32.         ret = register_aggr_kprobe(old_p, p);  
  33.         goto out;  
  34.     }  
  35.   
  36.     mutex_lock(&text_mutex);    /* Avoiding text modification */  
  37.     ret = prepare_kprobe(p);  
  38.     mutex_unlock(&text_mutex);  
  39.     if (ret)  
  40.         goto out;  
  41.   
  42.     INIT_HLIST_NODE(&p->hlist);  
  43.     hlist_add_head_rcu(&p->hlist,  
  44.                &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]);  
  45.   
  46.     if (!kprobes_all_disarmed && !kprobe_disabled(p))  
  47.         arm_kprobe(p);  
  48.   
  49.     /* Try to optimize kprobe */  
  50.     try_to_optimize_kprobe(p);  
  51.   
  52. out:  
  53.     mutex_unlock(&kprobe_mutex);  
  54.   
  55.     if (probed_mod)  
  56.         module_put(probed_mod);  
  57.   
  58.     return ret;  
  59. }  
  60. EXPORT_SYMBOL_GPL(register_kprobe);  
函數首先調用kprobe_addr函數初始化被探測點的地址p->addr。因爲一般的探測模塊並不會指定想要探測的addr地址,同kprobe_example例程一樣通過傳入函數名來指定要探測的函數,kprobe_addr函數的作用就是將函數名轉換爲最終的被探測地址:
  1. #define kprobe_lookup_name(name, addr) \  
  2.     addr = ((kprobe_opcode_t *)(kallsyms_lookup_name(name)))  
  3.       
  4. static kprobe_opcode_t *kprobe_addr(struct kprobe *p)  
  5. {  
  6.     kprobe_opcode_t *addr = p->addr;  
  7.   
  8.     if ((p->symbol_name && p->addr) ||  
  9.         (!p->symbol_name && !p->addr))  
  10.         goto invalid;  
  11.   
  12.     if (p->symbol_name) {  
  13.         kprobe_lookup_name(p->symbol_name, addr);  
  14.         if (!addr)  
  15.             return ERR_PTR(-ENOENT);  
  16.     }  
  17.   
  18.     addr = (kprobe_opcode_t *)(((char *)addr) + p->offset);  
  19.     if (addr)  
  20.         return addr;  
  21.   
  22. invalid:  
  23.     return ERR_PTR(-EINVAL);  
  24. }  

kprobe_addr首先對入參進行檢查,不允許函數名和地址同時設置或同時爲空的情況;如果用戶指定被探測函數名則調用kallsyms_lookup_name函數根據函數名查找其運行的虛擬地址;最後加上指定的探測偏移值作爲最終的被探測地址。當然在絕大多數的情況下,offset值被用戶設置爲0,即用戶探測指定函數的入口,但是也不排除用戶想要探測某一函數內部的某一條指令。

回到register_kprobe函數中,下面調用check_kprobe_rereg函數防止同一個kprobe實例被重複註冊,其中check_kprobe_rereg->__get_valid_kprobe調用流程將根據addr地址值搜索全局hash表並查看是否有同樣的kprobe實例已經在表中了。

隨後register_kprobe函數繼續初始化kprobe的flags、nmissed字段和list鏈表(flag只允許用戶傳遞KPROBE_FLAG_DISABLED,表示註冊的kprobe默認是不啓用的),然後調用check_kprobe_address_safe函數檢測被探測地址是否可探測:

  1. static int check_kprobe_address_safe(struct kprobe *p,  
  2.                      struct module **probed_mod)  
  3. {  
  4.     int ret;  
  5.   
  6.     ret = arch_check_ftrace_location(p);  
  7.     if (ret)  
  8.         return ret;  
  9.     jump_label_lock();  
  10.     preempt_disable();  
  11.   
  12.     /* Ensure it is not in reserved area nor out of text */  
  13.     if (!kernel_text_address((unsigned long) p->addr) ||  
  14.         within_kprobe_blacklist((unsigned long) p->addr) ||  
  15.         jump_label_text_reserved(p->addr, p->addr)) {  
  16.         ret = -EINVAL;  
  17.         goto out;  
  18.     }  
  19.   
  20.     /* Check if are we probing a module */  
  21.     *probed_mod = __module_text_address((unsigned long) p->addr);  
  22.     if (*probed_mod) {  
  23.         /* 
  24.          * We must hold a refcount of the probed module while updating 
  25.          * its code to prohibit unexpected unloading. 
  26.          */  
  27.         if (unlikely(!try_module_get(*probed_mod))) {  
  28.             ret = -ENOENT;  
  29.             goto out;  
  30.         }  
  31.   
  32.         /* 
  33.          * If the module freed .init.text, we couldn't insert 
  34.          * kprobes in there. 
  35.          */  
  36.         if (within_module_init((unsigned long)p->addr, *probed_mod) &&  
  37.             (*probed_mod)->state != MODULE_STATE_COMING) {  
  38.             module_put(*probed_mod);  
  39.             *probed_mod = NULL;  
  40.             ret = -ENOENT;  
  41.         }  
  42.     }  
  43. out:  
  44.     preempt_enable();  
  45.     jump_label_unlock();  
  46.   
  47.     return ret;  
  48. }  
首先調用arch_check_ftrace_location確認是否探測地址已經被ftrace跟蹤,若是且在開啓了CONFIG_KPROBES_ON_FTRACE內核選項的情況下在該kprobe實例的flags上置位KPROBE_FLAG_FTRACE符號,表示本kprobe已使用ftrace。
然後上鎖並竟用內核搶佔,開始進入地址有效性檢測流程,首先判斷以下3個條件,必須全部滿足:1、被探測地址在內核的地址段中;2、地址不在kprobe的黑名單之中;3、不在jump lable保留的地址空間中(內核jump lable特性使用?)。其中第一點比較好理解,函數實現如下:
  1. int kernel_text_address(unsigned long addr)  
  2. {  
  3.     if (core_kernel_text(addr))  
  4.         return 1;  
  5.     if (is_module_text_address(addr))  
  6.         return 1;  
  7.     return is_ftrace_trampoline(addr);  
  8. }  

被探測的函數當然要在內核的text(_stext ~ _etext)段中,由於非內核啓動時刻,不包括init text段;然後模塊的text段和init text段也都可以,最後如果在ftrace動態分配的trampoline地址空間中也是滿足的。

其中第二點中的blacklist黑名單指的是實現kprobes的關鍵代碼路徑,只有不在該黑名單中的函數纔可以被探測:

  1. bool __weak arch_within_kprobe_blacklist(unsigned long addr)  
  2. {  
  3.     /* The __kprobes marked functions and entry code must not be probed */  
  4.     return addr >= (unsigned long)__kprobes_text_start &&  
  5.            addr < (unsigned long)__kprobes_text_end;  
  6. }  
  7.   
  8. static bool within_kprobe_blacklist(unsigned long addr)  
  9. {  
  10.     struct kprobe_blacklist_entry *ent;  
  11.   
  12.     if (arch_within_kprobe_blacklist(addr))  
  13.         return true;  
  14.     /* 
  15.      * If there exists a kprobe_blacklist, verify and 
  16.      * fail any probe registration in the prohibited area 
  17.      */  
  18.     list_for_each_entry(ent, &kprobe_blacklist, list) {  
  19.         if (addr >= ent->start_addr && addr < ent->end_addr)  
  20.             return true;  
  21.     }  
  22.   
  23.     return false;  
  24. }  

主要包含兩個方面,一是架構相關的kprobe關鍵代碼路徑,他們被保存在__kprobes_text_start~__kprobes_text_end段中,二是kprobe_blacklist鏈表,該鏈表前面在kprobe初始化過程中已經看到了。

首先__kprobes_text_start和__kprobes_text_end被定義在include/asm-generic/Vmlinux.lds.h中,使用宏__kprobes標記的函數被歸入該.kprobes.text段:

  1. #define KPROBES_TEXT                            \  
  2.         ALIGN_FUNCTION();                   \  
  3.         VMLINUX_SYMBOL(__kprobes_text_start) = .;       \  
  4.         *(.kprobes.text)                    \  
  5.         VMLINUX_SYMBOL(__kprobes_text_end) = .;  
  1. #ifdef CONFIG_KPROBES  
  2. # define __kprobes  __attribute__((__section__(".kprobes.text")))  
簡單的總結一下:即使用宏NOKPROBE_SYMBOL和宏__kprobes標記的內核函數不可以被探測kprobe。

回到check_kprobe_address_safe函數中,若滿足了以上三點,接下來判斷被探測地址是否屬於某一個內核模塊的init_text段或core_text段:

  1. struct module *__module_text_address(unsigned long addr)  
  2. {  
  3.     struct module *mod = __module_address(addr);  
  4.     if (mod) {  
  5.         /* Make sure it's within the text section. */  
  6.         if (!within(addr, mod->module_init, mod->init_text_size)  
  7.             && !within(addr, mod->module_core, mod->core_text_size))  
  8.             mod = NULL;  
  9.     }  
  10.     return mod;  
  11. }  
判斷若屬於某一個模塊的話則增加這個模塊的引用計數以防止模塊被意外動態卸載,同時不允許在已經完成加載模塊的init_text段中的函數註冊kprobe(因爲在模塊加載完成後init_text段的內存已經被free掉了)。最後若模塊獲取成功,它將通過probed_mod參數返回給register_kprobe用於錯誤處理流程。

以上判斷都通過之後重新打開內核搶佔並解鎖,回到register_kprobe函數繼續註冊流程。接下來嘗試從全局hash表中查找是否之前已經爲同一個被探測地址註冊了kprobe探測點,若已註冊則調用register_aggr_kprobe函數繼續註冊流程,該流程稍後再分析。現假設是初次註冊,則調用prepare_kprobe函數,該函數會根據被探測地址是否已經被ftrace了而進入不同的流程,這裏假設沒有啓用ftrace,則直接調用arch_prepare_kprobe函數進入架構相關的註冊流程,先看一下x86架構的實現:

  1. int arch_prepare_kprobe(struct kprobe *p)  
  2. {  
  3.     if (alternatives_text_reserved(p->addr, p->addr))  
  4.         return -EINVAL;  
  5.   
  6.     if (!can_probe((unsigned long)p->addr))  
  7.         return -EILSEQ;  
  8.     /* insn: must be on special executable page on x86. */  
  9.     p->ainsn.insn = get_insn_slot();  
  10.     if (!p->ainsn.insn)  
  11.         return -ENOMEM;  
  12.   
  13.     return arch_copy_kprobe(p);  
  14. }  
首先對於smp系統,被探測地址不能被用於smp-alternatives,非smp無此要求;然後判斷該被探測地址的指令有效並調用get_insn_slot函數申請用於拷貝原始指令的指令slot內存空間,最後調用arch_copy_kprobe函數執行指令複製動作。
  1. static int arch_copy_kprobe(struct kprobe *p)  
  2. {  
  3.     int ret;  
  4.   
  5.     /* Copy an instruction with recovering if other optprobe modifies it.*/  
  6.     ret = __copy_instruction(p->ainsn.insn, p->addr);  
  7.     if (!ret)  
  8.         return -EINVAL;  
  9.   
  10.     /* 
  11.      * __copy_instruction can modify the displacement of the instruction, 
  12.      * but it doesn't affect boostable check. 
  13.      */  
  14.     if (can_boost(p->ainsn.insn))  
  15.         p->ainsn.boostable = 0;  
  16.     else  
  17.         p->ainsn.boostable = -1;  
  18.   
  19.     /* Check whether the instruction modifies Interrupt Flag or not */  
  20.     p->ainsn.if_modifier = is_IF_modifier(p->ainsn.insn);  
  21.   
  22.     /* Also, displacement change doesn't affect the first byte */  
  23.     p->opcode = p->ainsn.insn[0];  
  24.   
  25.     return 0;  
  26. }  
函數首先調用__copy_instruction將kprobe->addr被探測地址的指令拷貝到kprobe->ainsn.insn保存起來(可能會對指令做適當的修改),然後初始化kprobe->ainsn結構體,最後將指令的第一個字節保存到kprobe->opcode字段中(x86架構的kprobe_opcode_t是u8類型的)。
再來看一下arm架構的實現方式(已去除CONFIG_THUMB2_KERNEL相關部分的代碼):
  1. int __kprobes arch_prepare_kprobe(struct kprobe *p)  
  2. {  
  3.     kprobe_opcode_t insn;  
  4.     kprobe_opcode_t tmp_insn[MAX_INSN_SIZE];  
  5.     unsigned long addr = (unsigned long)p->addr;  
  6.     bool thumb;  
  7.     kprobe_decode_insn_t *decode_insn;  
  8.     const union decode_action *actions;  
  9.     int is;  
  10.     const struct decode_checker **checkers;  
  11.   
  12.     if (in_exception_text(addr))  
  13.         return -EINVAL;  
  14.   
  15. #ifdef CONFIG_THUMB2_KERNEL  
  16.     ......  
  17. #else /* !CONFIG_THUMB2_KERNEL */  
  18.     thumb = false;  
  19.     if (addr & 0x3)  
  20.         return -EINVAL;  
  21.     insn = __mem_to_opcode_arm(*p->addr);  
  22.     decode_insn = arm_probes_decode_insn;  
  23.     actions = kprobes_arm_actions;  
  24.     checkers = kprobes_arm_checkers;  
  25. #endif  
  26.   
  27.     p->opcode = insn;  
  28.     p->ainsn.insn = tmp_insn;  
  29.   
  30.     switch ((*decode_insn)(insn, &p->ainsn, true, actions, checkers)) {  
  31.     case INSN_REJECTED: /* not supported */  
  32.         return -EINVAL;  
  33.   
  34.     case INSN_GOOD:     /* instruction uses slot */  
  35.         p->ainsn.insn = get_insn_slot();  
  36.         if (!p->ainsn.insn)  
  37.             return -ENOMEM;  
  38.         for (is = 0; is < MAX_INSN_SIZE; ++is)  
  39.             p->ainsn.insn[is] = tmp_insn[is];  
  40.         flush_insns(p->ainsn.insn,  
  41.                 sizeof(p->ainsn.insn[0]) * MAX_INSN_SIZE);  
  42.         p->ainsn.insn_fn = (probes_insn_fn_t *)  
  43.                     ((uintptr_t)p->ainsn.insn | thumb);  
  44.         break;  
  45.   
  46.     case INSN_GOOD_NO_SLOT: /* instruction doesn't need insn slot */  
  47.         p->ainsn.insn = NULL;  
  48.         break;  
  49.     }  
  50.   
  51.     /* 
  52.      * Never instrument insn like 'str r0, [sp, +/-r1]'. Also, insn likes 
  53.      * 'str r0, [sp, #-68]' should also be prohibited. 
  54.      * See __und_svc. 
  55.      */  
  56.     if ((p->ainsn.stack_space < 0) ||  
  57.             (p->ainsn.stack_space > MAX_STACK_SIZE))  
  58.         return -EINVAL;  
  59.   
  60.     return 0;  
  61. }  
首先檢測被探測地址不能在異常代碼段中並且地址必須是4字節對齊的,隨後取出被探測點的指令保存在kprobe->opcode中,並調用arm_probes_decode_insn函數來判斷被探測的指令是什麼類型的:
  1. /* Return: 
  2.  *   INSN_REJECTED     If instruction is one not allowed to kprobe, 
  3.  *   INSN_GOOD         If instruction is supported and uses instruction slot, 
  4.  *   INSN_GOOD_NO_SLOT If instruction is supported but doesn't use its slot. 
  5.  * 
  6.  * For instructions we don't want to kprobe (INSN_REJECTED return result): 
  7.  *   These are generally ones that modify the processor state making 
  8.  *   them "hard" to simulate such as switches processor modes or 
  9.  *   make accesses in alternate modes.  Any of these could be simulated 
  10.  *   if the work was put into it, but low return considering they 
  11.  *   should also be very rare. 
  12.  */  
  13. enum probes_insn __kprobes  
  14. arm_probes_decode_insn(probes_opcode_t insn, struct arch_probes_insn *asi,  
  15.                bool emulate, const union decode_action *actions,  
  16.                const struct decode_checker *checkers[])  
  17. {  
  18.     asi->insn_singlestep = arm_singlestep;  
  19.     asi->insn_check_cc = probes_condition_checks[insn>>28];  
  20.     return probes_decode_insn(insn, asi, probes_decode_arm_table, false,  
  21.                   emulate, actions, checkers);  
  22. }  

該arm_probes_decode_insn調用流程會對kprobe->ainsn結構進行初始化(該結構架構相關),其中函數指針insn_singlestep初始化爲arm_singlestep,它用於kprobe觸發後的單步執行,而函數insn_check_cc初始化爲probes_condition_checks[insn>>28],它是一個函數指針數組,以指令的高4位爲索引,用於kprobe觸發後進行條件異常檢測。

  1. probes_check_cc * const probes_condition_checks[16] = {  
  2.     &__check_eq, &__check_ne, &__check_cs, &__check_cc,  
  3.     &__check_mi, &__check_pl, &__check_vs, &__check_vc,  
  4.     &__check_hi, &__check_ls, &__check_ge, &__check_lt,  
  5.     &__check_gt, &__check_le, &__check_al, &__check_al  
  6. };  

現以do_fork函數爲例,來看一下這裏的insn_check_cc函數指針初始化爲那個函數了:

反彙編vmlinux後找到do_fork,對應的入口地址爲0xc0022798,彙編指令爲mov,機器碼爲e1a0c00d,計算後值爲0xe=15,因此選中的條件異常檢測處理函數爲__check_al;

  1. c0022798 <do_fork>:  
  2. do_fork():  
  3. c0022798:       e1a0c00d        mov     ip, sp  

如果用戶探測的並不是函數的入口地址,而是函數內部的某一條指令,則可能會選中其他的檢測函數,例如movne指令選中的就是__check_ne,moveq指令選中的就是__check_eq等等。

回到arm_probes_decode_insn函數中,然後調用probes_decode_insn函數判斷指令的類型並初始化單步執行函數指針insn_handler,最後返回INSN_REJECTED、INSN_GOOD和INSN_GOOD_NO_SLOT這三種類型(如果是INSN_GOOD還會拷貝指令填充ainsn.insn字段)。該函數的註釋中對其描述的已經比較詳細了,對於諸如某些會修改處理器工作狀態的指令會返回INSN_REJECTED表示不支持,另外INSN_GOOD是需要slot的指令,INSN_GOOD_NO_SLOT是不需要slot的指令。

回到arch_prepare_kprobe函數中,會對返回的指令類型做不同的處理,若是INSN_GOOD類型則同x86類似,調用get_insn_slot申請內存空間並將前面存放在tmp_insn中的指令拷貝到kprobe->ainsn.insn中,然後flush icache。

如此被探測點指令就被拷貝保存起來了。架構相關的初始化完成以後,接下來register_kprobe函數初始化kprobe的hlist字段並將它添加到全局的hash表中。然後判斷如果kprobes_all_disarmed爲false並且kprobe沒有被disable(在kprobe的初始化函數中該kprobes_all_disarmed值默認爲false),則調用arm_kprobe函數,它會把觸發trap的指令寫到被探測點處替換原始指令。

  1. static void arm_kprobe(struct kprobe *kp)  
  2. {  
  3.     if (unlikely(kprobe_ftrace(kp))) {  
  4.         arm_kprobe_ftrace(kp);  
  5.         return;  
  6.     }  
  7.     /* 
  8.      * Here, since __arm_kprobe() doesn't use stop_machine(), 
  9.      * this doesn't cause deadlock on text_mutex. So, we don't 
  10.      * need get_online_cpus(). 
  11.      */  
  12.     mutex_lock(&text_mutex);  
  13.     __arm_kprobe(kp);  
  14.     mutex_unlock(&text_mutex);  
  15. }  
這裏假設不適用ftrace和optimize kprobe特性,將直接調用架構相關的函數arch_arm_kprobe,其中x86的實現如下:
  1. void arch_arm_kprobe(struct kprobe *p)  
  2. {  
  3.     text_poke(p->addr, ((unsigned char []){BREAKPOINT_INSTRUCTION}), 1);  
  4. }  
直接調用text_poke函數將addr地址處的指令替換爲BREAKPOINT_INSTRUCTION指令(機器碼是0xCC),當正常執行流程執行到這條指令後就會觸發int3中斷,進而進入探測回調流程。再看一下arm的實現流程:
  1. void __kprobes arch_arm_kprobe(struct kprobe *p)  
  2. {  
  3.     unsigned int brkp;  
  4.     void *addr;  
  5.   
  6.     if (IS_ENABLED(CONFIG_THUMB2_KERNEL)) {  
  7.         ......  
  8.     } else {  
  9.         kprobe_opcode_t insn = p->opcode;  
  10.   
  11.         addr = p->addr;  
  12.         brkp = KPROBE_ARM_BREAKPOINT_INSTRUCTION;  
  13.   
  14.         if (insn >= 0xe0000000)  
  15.             brkp |= 0xe0000000;  /* Unconditional instruction */  
  16.         else  
  17.             brkp |= insn & 0xf0000000;  /* Copy condition from insn */  
  18.     }  
  19.   
  20.     patch_text(addr, brkp);  
  21. }   

arm架構的實現中替換的指令爲KPROBE_ARM_BREAKPOINT_INSTRUCTION(機器碼是0x07f001f8),然後還會根據被替換指令做一定的調整,最後調用patch_text函數執行替換動作。繼續以kprobe_example例程中的do_fork函數爲例,從前文中反彙編可知,地址0xc0022798處的“mov     ip, sp”指令被替換KPROBE_ARM_BREAKPOINT_INSTRUCTION指令,可從pre_handler回調函數中打印的地址得到印證:

<6>[   57.386132] [do_fork] pre_handler: p->addr = 0xc0022798, pc = 0xc0022798, cpsr = 0x80000013
<6>[   57.386167] [do_fork] post_handler: p->addr = 0xc0022798, cpsr = 0x80000013

前文中看到KPROBE_ARM_BREAKPOINT_INSTRUCTION指令在init_kprobes函數的執行流程中已經爲它註冊了一個異常處理函數kprobe_trap_handler,因此當正常執行流程執行到KPROBE_ARM_BREAKPOINT_INSTRUCTION指令後將觸發異常,進而調用kprobe_trap_handler開始回調流程。

至此kprobe的註冊流程分析完畢,再回頭分析對一個已經被註冊過kprobe的探測點註冊新的kprobe的執行流程,即register_aggr_kprobe函數:

  1. /* 
  2.  * This is the second or subsequent kprobe at the address - handle 
  3.  * the intricacies 
  4.  */  
  5. static int register_aggr_kprobe(struct kprobe *orig_p, struct kprobe *p)  
  6. {  
  7.     int ret = 0;  
  8.     struct kprobe *ap = orig_p;  
  9.   
  10.     /* For preparing optimization, jump_label_text_reserved() is called */  
  11.     jump_label_lock();  
  12.     /* 
  13.      * Get online CPUs to avoid text_mutex deadlock.with stop machine, 
  14.      * which is invoked by unoptimize_kprobe() in add_new_kprobe() 
  15.      */  
  16.     get_online_cpus();  
  17.     mutex_lock(&text_mutex);  
  18.   
  19.     if (!kprobe_aggrprobe(orig_p)) {  
  20.         /* If orig_p is not an aggr_kprobe, create new aggr_kprobe. */  
  21.         ap = alloc_aggr_kprobe(orig_p);  
  22.         if (!ap) {  
  23.             ret = -ENOMEM;  
  24.             goto out;  
  25.         }  
  26.         init_aggr_kprobe(ap, orig_p);  
  27.     } else if (kprobe_unused(ap))  
  28.         /* This probe is going to die. Rescue it */  
  29.         reuse_unused_kprobe(ap);  
  30.   
  31.     if (kprobe_gone(ap)) {  
  32.         /* 
  33.          * Attempting to insert new probe at the same location that 
  34.          * had a probe in the module vaddr area which already 
  35.          * freed. So, the instruction slot has already been 
  36.          * released. We need a new slot for the new probe. 
  37.          */  
  38.         ret = arch_prepare_kprobe(ap);  
  39.         if (ret)  
  40.             /* 
  41.              * Even if fail to allocate new slot, don't need to 
  42.              * free aggr_probe. It will be used next time, or 
  43.              * freed by unregister_kprobe. 
  44.              */  
  45.             goto out;  
  46.   
  47.         /* Prepare optimized instructions if possible. */  
  48.         prepare_optimized_kprobe(ap);  
  49.   
  50.         /* 
  51.          * Clear gone flag to prevent allocating new slot again, and 
  52.          * set disabled flag because it is not armed yet. 
  53.          */  
  54.         ap->flags = (ap->flags & ~KPROBE_FLAG_GONE)  
  55.                 | KPROBE_FLAG_DISABLED;  
  56.     }  
  57.   
  58.     /* Copy ap's insn slot to p */  
  59.     copy_kprobe(ap, p);  
  60.     ret = add_new_kprobe(ap, p);  
  61.   
  62. out:  
  63.     mutex_unlock(&text_mutex);  
  64.     put_online_cpus();  
  65.     jump_label_unlock();  
  66.   
  67.     if (ret == 0 && kprobe_disabled(ap) && !kprobe_disabled(p)) {  
  68.         ap->flags &= ~KPROBE_FLAG_DISABLED;  
  69.         if (!kprobes_all_disarmed)  
  70.             /* Arm the breakpoint again. */  
  71.             arm_kprobe(ap);  
  72.     }  
  73.     return ret;  
  74. }  
在前文中看到,該函數會在對同一個被探測地址註冊多個kprobe實例時會被調用到,該函數會引入一個kprobe aggregator的概念,即由一個統一的kprobe實例接管所有註冊到該地址的kprobe。這個函數的註釋非常詳細,並不難理解,來簡單分析一下:
函數的第一個入參orig_p是在全局hash表中找到的已經註冊的kprobe實例,第二個入參是本次需要註冊的kprobe實例。首先在完成了必要的上鎖操作後就調用kprobe_aggrprobe函數檢查orig_p是否是一個aggregator。
  1. /* Return true if the kprobe is an aggregator */  
  2. static inline int kprobe_aggrprobe(struct kprobe *p)  
  3. {  
  4.     return p->pre_handler == aggr_pre_handler;  
  5. }  
它通過kprobe的pre_handler回調判斷,如果是aggregator則它的pre_handler回調函數會被替換成aggr_pre_handler函數。一般對於第二次註冊kprobe的情況顯然是不會滿足條件的,會調用alloc_aggr_kprobe函數創建一個,對於沒有開啓CONFIG_OPTPROBES選項的情況,alloc_aggr_kprobe僅僅是分配了一塊內存空間,然後調用init_aggr_kprobe函數初始化這個aggr kprobe。
  1. /* 
  2.  * Fill in the required fields of the "manager kprobe". Replace the 
  3.  * earlier kprobe in the hlist with the manager kprobe 
  4.  */  
  5. static void init_aggr_kprobe(struct kprobe *ap, struct kprobe *p)  
  6. {  
  7.     /* Copy p's insn slot to ap */  
  8.     copy_kprobe(p, ap);  
  9.     flush_insn_slot(ap);  
  10.     ap->addr = p->addr;  
  11.     ap->flags = p->flags & ~KPROBE_FLAG_OPTIMIZED;  
  12.     ap->pre_handler = aggr_pre_handler;  
  13.     ap->fault_handler = aggr_fault_handler;  
  14.     /* We don't care the kprobe which has gone. */  
  15.     if (p->post_handler && !kprobe_gone(p))  
  16.         ap->post_handler = aggr_post_handler;  
  17.     if (p->break_handler && !kprobe_gone(p))  
  18.         ap->break_handler = aggr_break_handler;  
  19.   
  20.     INIT_LIST_HEAD(&ap->list);  
  21.     INIT_HLIST_NODE(&ap->hlist);  
  22.   
  23.     list_add_rcu(&p->list, &ap->list);  
  24.     hlist_replace_rcu(&p->hlist, &ap->hlist);  
  25. }  
可以看到,這個aggr kprobe中的各個字段基本就是從orig_p中拷貝過來的,包括opcode和ainsn這兩個備份指令的字段以及addr和flags字段,但是其中的4個回調函數會被初始化爲aggr kprobe所特有的addr_xxx_handler,這幾個函數後面會具體分析。接下來函數會初始化aggr kprobe的兩個鏈表頭,然後將自己添加到鏈表中去,並替換掉orig_p。

回到register_aggr_kprobe函數中,如果本次是第二次以上向同一地址註冊kprobe實例,則此時的orig_p已經是aggr kprobe了,則會調用kprobe_unused函數判斷該kprobe是否爲被使用,若是則調用reuse_unused_kprobe函數重新啓用,但是對於沒有開啓CONFIG_OPTPROBES選項的情況,邏輯上是不存在這種情況的,因此reuse_unused_kprobe函數的實現僅僅是一段打印後就立即觸發BUG_ON。

  1. /* There should be no unused kprobes can be reused without optimization */  
  2. static void reuse_unused_kprobe(struct kprobe *ap)  
  3. {  
  4.     printk(KERN_ERR "Error: There should be no unused kprobe here.\n");  
  5.     BUG_ON(kprobe_unused(ap));  
  6. }  

繼續往下分析,下面來討論aggr kprobe被kill掉的情況,顯然只有在第三次及以上註冊同一地址可能會出現這樣的情況。針對這一種情況,這裏同初次註冊kprobe的調用流程類似,首先調用arch_prepare_kprobe做架構相關初始化,保存被探測地址的機器指令,然後調用prepare_optimized_kprobe啓用optimized_kprobe,最後清除KPROBE_FLAG_GONE的標記。

接下來調用再次copy_kprobe將aggr kprobe中保存的指令opcode和ainsn字段拷貝到本次要註冊的kprobe的對應字段中,然後調用add_new_kprobe函數將新註冊的kprobe鏈入到aggr kprobe的list鏈表中:

  1. /* 
  2. * Add the new probe to ap->list. Fail if this is the 
  3. * second jprobe at the address - two jprobes can't coexist 
  4. */  
  5. static int add_new_kprobe(struct kprobe *ap, struct kprobe *p)  
  6. {  
  7.     BUG_ON(kprobe_gone(ap) || kprobe_gone(p));  
  8.   
  9.     if (p->break_handler || p->post_handler)  
  10.         unoptimize_kprobe(ap, true);    /* Fall back to normal kprobe */  
  11.   
  12.     if (p->break_handler) {  
  13.         if (ap->break_handler)  
  14.             return -EEXIST;  
  15.         list_add_tail_rcu(&p->list, &ap->list);  
  16.         ap->break_handler = aggr_break_handler;  
  17.     } else  
  18.         list_add_rcu(&p->list, &ap->list);  
  19.     if (p->post_handler && !ap->post_handler)  
  20.         ap->post_handler = aggr_post_handler;  
  21.   
  22.     return 0;  
  23. }  
注意最主要的就是add list,只是如果新註冊的kprobe設定了break_handler回調函數,會將其插入鏈表的末尾併爲aggr kprobe設定break handler回調函數aggr_break_handler;與此同時若新註冊的kprobe設定了post_handler,也同樣爲aggr kprobe設定post handler回調函數aggr_post_handler。

回到register_aggr_kprobe函數,在out標號處繼續執行,下面會進入if條件判斷,啓用aggr kprobe,然後調用前文中分析過的arm_kprobe函數替換被探測地址的機器指令爲BREAKPOINT_INSTRUCTION指令。

至此整個kprobe註冊流程分析結束,下面來分析以上註冊的探測回調函數是如何被執行的以及被探測指令是如何被單步執行的。


3、觸發kprobe探測和回調

前文中,從register_kprobe函數註冊kprobe的流程已經看到,用戶指定的被探測函數入口地址處的指令已經被替換成架構相關的BREAKPOINT_INSTRUCTION指令,若是正常的代碼流程執行到該指令,將會觸發異常,進入架構相關的異常處理函數,kprobe註冊的回調函數及被探測函數的單步執行流程均在該流程中執行。由於不同架構實現存在差別,下面分別來分析,首先先分析arm架構的執行流程:

3.1、arm架構實現

前文中已經分析了內核已經爲KPROBE_ARM_BREAKPOINT_INSTRUCTION指令註冊了異常處理回調函數kprobe_trap_handler,因此在執行這條指令時會觸發以下調用流程:__und_svc->__und_svc_fault->__und_fault->do_undefinstr()->call_undef_hook():

  1. static int __kprobes kprobe_trap_handler(struct pt_regs *regs, unsigned int instr)  
  2. {  
  3.     unsigned long flags;  
  4.     local_irq_save(flags);  
  5.     kprobe_handler(regs);  
  6.     local_irq_restore(flags);  
  7.     return 0;  
  8. }  
call_undef_hook()調用未定義指令的回調函數,對於KPROBE_ARM_BREAKPOINT_INSTRUCTION指令即調用到kprobe_trap_handler函數,其中入參struct pt_regs *regs保存的時執行異常指令時的寄存器信息,同時該函數在處理kprobe的流程時會禁用掉本地CPU的中斷。

kprobe_handler函數的實現比較長,分段來看:

  1. /* 
  2.  * Called with IRQs disabled. IRQs must remain disabled from that point 
  3.  * all the way until processing this kprobe is complete.  The current 
  4.  * kprobes implementation cannot process more than one nested level of 
  5.  * kprobe, and that level is reserved for user kprobe handlers, so we can't 
  6.  * risk encountering a new kprobe in an interrupt handler. 
  7.  */  
  8. void __kprobes kprobe_handler(struct pt_regs *regs)  
  9. {  
  10.     struct kprobe *p, *cur;  
  11.     struct kprobe_ctlblk *kcb;  
  12.   
  13.     kcb = get_kprobe_ctlblk();  
  14.     cur = kprobe_running();  
  15.   
  16. #ifdef CONFIG_THUMB2_KERNEL  
  17.     ......  
  18. #else /* ! CONFIG_THUMB2_KERNEL */  
  19.     p = get_kprobe((kprobe_opcode_t *)regs->ARM_pc);  
  20. #endif  
註釋中說明了當前arm架構的kprobe實現不支持在中斷中多層kprobe重入,因此爲了防止在處理一個kprobe期間由於中斷可能會導致多次觸發kprobe的情況,所以需要禁用中斷。函數首先調用get_kprobe_ctlblk函數獲取本cpu的per_cpu結構體變量kprobe_ctlblk,該結構體是架構相關的,arm的定義如下:
  1. /* per-cpu kprobe control block */  
  2. struct kprobe_ctlblk {  
  3.     unsigned int kprobe_status;  
  4.     struct prev_kprobe prev_kprobe;  
  5.     struct pt_regs jprobe_saved_regs;  
  6.     char jprobes_stack[MAX_STACK_SIZE];  
  7. };  
其中保存了kprobe的一些狀態信息以及jpboe用到的字段,目前需要關注的是其中的kprobe_status和prev_kprobe字段,其中kprobe_status代表了當前kprobe的處理狀態,一共包括以下幾種:
  1. #define KPROBE_HIT_ACTIVE   0x00000001      //開始處理kprobe  
  2. #define KPROBE_HIT_SS       0x00000002      //kprobe單步執行階段  
  3. #define KPROBE_REENTER      0x00000004      //重複觸發kprobe  
  4. #define KPROBE_HIT_SSDONE   0x00000008      //kprobe單步執行階段結束  

而prev_kprobe則是用於在kprobe重入情況下保存當前正在處理的kprobe實例和狀態的。內核爲每個cpu都定義了一個該類型全局變量。然後調用kprobe_running函數獲取當前cpu上正在處理的kprobe:

  1. /* kprobe_running() will just return the current_kprobe on this CPU */  
  2. static inline struct kprobe *kprobe_running(void)  
  3. {  
  4.     return (__this_cpu_read(current_kprobe));  
  5. }  
這裏的current_kprobe也是一個per_cpu變量,其中保存了當前cpu正在處理的kprobe實例,若沒有正在處理的則爲NULL。下面調用get_kprobe函數獲取本次要處理的kprobe,入參是regs->ARM_pc,即觸發異常指令所在的地址,也就是被探測點的地址,利用它就可以在全局hash表中找到註冊的kprobe實例了。接下來根據cur和p的存在情況進行多分支處理:

1、p和cur的kprobe實例同時存在

  1. /* Kprobe is pending, so we're recursing. */  
  2. switch (kcb->kprobe_status) {  
  3. case KPROBE_HIT_ACTIVE:  
  4. case KPROBE_HIT_SSDONE:  
  5.     /* A pre- or post-handler probe got us here. */  
  6.     kprobes_inc_nmissed_count(p);  
  7.     save_previous_kprobe(kcb);  
  8.     set_current_kprobe(p);  
  9.     kcb->kprobe_status = KPROBE_REENTER;  
  10.     singlestep(p, regs, kcb);  
  11.     restore_previous_kprobe(kcb);  
  12.     break;  
  13. default:  
  14.     /* impossible cases */  
  15.     BUG();  
  16. }  
這種情況屬於kprobe重入的情況,即在運行kprobe回調函數或單步執行被探測指令時又一次觸發了kprobe。對於重入,目前流程只能處理在前一kprobe執行回調函數時引發的kprobe重入,對於在單步執行階段引發的重入就直接報BUG。具體的處理流程爲:首先調用kprobes_inc_nmissed_count遞增當前要處理kprobe的nmissed值(如果是aggr kprobe則會遍歷鏈表將註冊到同地址的所有kprobe的nmissed值都加1);然後調用save_previous_kprobe函數將當前時刻已經在處理的kprobe(cur)及狀態保存到kcb->prev_kprobe字段中去;
  1. static void __kprobes save_previous_kprobe(struct kprobe_ctlblk *kcb)  
  2. {  
  3.     kcb->prev_kprobe.kp = kprobe_running();  
  4.     kcb->prev_kprobe.status = kcb->kprobe_status;  
  5. }  
然後調用set_current_kprobe函數將本次需要處理的kprobe(p)設置到current_kprobe的per_cpu變量中去,並且更新kprobe_status狀態爲KPROBE_REENTER,表示存在重入情況;接下來調用singlestep函數啓動單步執行,這個函數稍後再看;最後調用restore_previous_kprobe函數恢復前面所保存的kprobe。
  1. static void __kprobes save_previous_kprobe(struct kprobe_ctlblk *kcb)  
  2. {  
  3.     kcb->prev_kprobe.kp = kprobe_running();  
  4.     kcb->prev_kprobe.status = kcb->kprobe_status;  
  5. }  

注意,以上重入的處理流程僅僅是單步執行了被探測的函數,並不會調用kprobe的pre_handle回調函數(遞增nmissed字段的原因就在此),因此用戶並不會感知到kprobe被實際觸發了

2、p存在但cur不存在

  1. else if (p->ainsn.insn_check_cc(regs->ARM_cpsr)) {  
  2.     /* Probe hit and conditional execution check ok. */  
  3.     set_current_kprobe(p);  
  4.     kcb->kprobe_status = KPROBE_HIT_ACTIVE;  
  5.   
  6.     /* 
  7.      * If we have no pre-handler or it returned 0, we 
  8.      * continue with normal processing.  If we have a 
  9.      * pre-handler and it returned non-zero, it prepped 
  10.      * for calling the break_handler below on re-entry, 
  11.      * so get out doing nothing more here. 
  12.      */  
  13.     if (!p->pre_handler || !p->pre_handler(p, regs)) {  
  14.         kcb->kprobe_status = KPROBE_HIT_SS;  
  15.         singlestep(p, regs, kcb);  
  16.         if (p->post_handler) {  
  17.             kcb->kprobe_status = KPROBE_HIT_SSDONE;  
  18.             p->post_handler(p, regs, 0);  
  19.         }  
  20.         reset_current_kprobe();  
  21.     }  
  22. else {  
  23.     /* 
  24.      * Probe hit but conditional execution check failed, 
  25.      * so just skip the instruction and continue as if 
  26.      * nothing had happened. 
  27.      */  
  28.     singlestep_skip(p, regs);  
  29. }  

這種情況就是最爲一般的情況,即當前kprobe是首次觸發,前面並沒有其他的kprobe流程正在處理。這裏會首先調用p->ainsn.insn_check_cc註冊函數來進行條件異常檢測,這個函數在前文註冊kprobe的流程中已經看到根據不同的被探測指令被註冊成不同的函數了,入參是觸發異常時的cpsr程序狀態寄存器值。

對於前文中看到的do_fork函數入口彙編指令mov設置的__check_al檢測函數來說,它將永遠返回true,而movne指令的__check_ne檢測函數則會對cpsr進行判斷:

  1. static unsigned long __kprobes __check_ne(unsigned long cpsr)  
  2. {  
  3.     return (~cpsr) & PSR_Z_BIT;  
  4. }  

(1)如果條件異常檢測通過,那也同樣調用set_current_kprobe函數設置當前正在處理的kprobe並更新kprobe狀態標識爲KPROBE_HIT_ACTIVE,表明開始處理該kprobe。接下來就到關鍵的回調和單步執行流程了,首先判斷kprobe的pre_handler函數是否被註冊,在註冊的情況下調用它。對於單kprobe註冊的情況很簡單了,直接調用註冊函數即可(這樣前面kprobe_example中handler_pre函數就在此調用),但是對於前文中分析的多kprobe註冊的情況(aggr kprobe),則會調用到aggr_pre_handler函數:

  1. /* 
  2.  * Aggregate handlers for multiple kprobes support - these handlers 
  3.  * take care of invoking the individual kprobe handlers on p->list 
  4.  */  
  5. static int aggr_pre_handler(struct kprobe *p, struct pt_regs *regs)  
  6. {  
  7.     struct kprobe *kp;  
  8.   
  9.     list_for_each_entry_rcu(kp, &p->list, list) {  
  10.         if (kp->pre_handler && likely(!kprobe_disabled(kp))) {  
  11.             set_kprobe_instance(kp);  
  12.             if (kp->pre_handler(kp, regs))  
  13.                 return 1;  
  14.         }  
  15.         reset_kprobe_instance();  
  16.     }  
  17.     return 0;  
  18. }  
  19. NOKPROBE_SYMBOL(aggr_pre_handler);  
該函數的功能很直觀,即遍歷aggr_kprobe->list鏈表中的各個同註冊地址的kprobe實例,然後調用它們自己的pre_handler回調函數,這裏的aggr_kprobe僅僅起到了一個管理分配的作用。其中set_kprobe_instance和reset_kprobe_instance函數的作用是設置和恢復kprobe_instance這個per_cpu變量,這個變量在aggr_fault_handler和aggr_break_handler回調函數中會用到,應爲發生異常時,需要定位到當前正在處理哪一個kprobe。
  1. /* We have preemption disabled.. so it is safe to use __ versions */  
  2. static inline void set_kprobe_instance(struct kprobe *kp)  
  3. {  
  4.     __this_cpu_write(kprobe_instance, kp);  
  5. }  
  6.   
  7. static inline void reset_kprobe_instance(void)  
  8. {  
  9.     __this_cpu_write(kprobe_instance, NULL);  
  10. }  
回到kprobe_handler函數繼續往下分析,如果pre_handler執行成功或者不存在pre_handler回調函數則將kprobe當前處理狀態設置爲KPROBE_HIT_SS,表示開始進入單步執行階段。隨後調用singlestep函數單步執行”原始被探測指令“,完畢後繼續判斷post_handler回調函數是否存在,若存在則設置當前狀態爲KPROBE_HIT_SSDONE,表示單步執行階段執行結束,然後調用post_handler回調函數(前文kprobe_example總的handler_post就在此調用)。post_handler同pre_handler一樣,對與aggr kprobe會調用aggr_post_handler函數,由於實現類似,這裏就不再贅述了。在執行完所有的回調後,最後調用reset_current_kprobe函數恢復current_kprobe變量。

這裏可能會存在這樣的疑問,爲什麼kcb->kprobe_status = KPROBE_HIT_SSDONE;這條狀態賦值語句會放在條件判斷內部,而不是在單步執行完以後?其實對於當前的上下文邏輯來看效果是一樣的,因爲若沒有註冊post_handler,就會立即執行reset_current_kprobe函數解除kprobe的綁定,因此不會對邏輯產生影響。

(2)如果條件異常檢測不通過則調用singlestep_skip函數跳過當前的指令,繼續執行後面的指令,就像什麼都沒有發生過一樣

  1. static void __kprobes  
  2. singlestep_skip(struct kprobe *p, struct pt_regs *regs)  
  3. {  
  4. #ifdef CONFIG_THUMB2_KERNEL  
  5.     ......  
  6. #else  
  7.     regs->ARM_pc += 4;  
  8. #endif  
  9. }  
該函數僅僅修改了regs結構中的PC值,在kprobe處理結束後將從被探測指令之後的指令繼續執行。這裏就有一個疑問,如果不執行被探測點的原始指令,直接執行之後的指令難道不會出問題嗎?

3、p不存在但cur存在

  1. else if (cur) {  
  2.     /* We probably hit a jprobe.  Call its break handler. */  
  3.     if (cur->break_handler && cur->break_handler(cur, regs)) {  
  4.         kcb->kprobe_status = KPROBE_HIT_SS;  
  5.         singlestep(cur, regs, kcb);  
  6.         if (cur->post_handler) {  
  7.             kcb->kprobe_status = KPROBE_HIT_SSDONE;  
  8.             cur->post_handler(cur, regs, 0);  
  9.         }  
  10.     }  
  11.     reset_current_kprobe();  
這種情況一般用於jprobe實現,函數調用cur kprobe的break_handler回調函數且在break_handler返回非0的情況下啓動單步執行和執行post_handler回調,最後一樣調用reset_current_kprobe函數解除cur kprobe綁定。該流程先不做詳細推演分析,後面分析jprobe實現時再細細分析。

4、p和cur都不存在

  1. else {  
  2.     /* 
  3.      * The probe was removed and a race is in progress. 
  4.      * There is nothing we can do about it.  Let's restart 
  5.      * the instruction.  By the time we can restart, the 
  6.      * real instruction will be there. 
  7.      */  
  8. }  
這種情況表示當前kprobe已經被註銷了,但是可能在註銷的過程中(註銷的過程並不是原子操作)可能被其他執行流程搶佔進而觸發該kprobe,對於這種情況什麼都不需要做,直接返回即可。

至此arm架構的kprobe觸發及處理整體流程就分析完了。下面分析x86_64架構的實現,總體大同小異,其中的相同之處就不再分析了。

3.2、x86_64架構實現

  1. /* May run on IST stack. */  
  2. dotraplinkage void notrace do_int3(struct pt_regs *regs, long error_code)  
  3. {  
  4.     ......  
  5.   
  6. #ifdef CONFIG_KPROBES  
  7.     if (kprobe_int3_handler(regs))  
  8.         goto exit;  
  9. #endif  
  10.     ......  
  11. }  
  12. NOKPROBE_SYMBOL(do_int3);  
x86_64架構下,執行到前文中替換的BREAKPOINT_INSTRUCTION指令後將觸發INT3中斷,進而調用到do_int3函數。do_init3函數做的事情比較多,但是和kprobe相關的僅代碼中列出的這1處,下面來看kprobe_int3_handler函數,這個函數同arm結構的kprobe_handler函數很像,依然分段來分析:
  1. /* 
  2.  * Interrupts are disabled on entry as trap3 is an interrupt gate and they 
  3.  * remain disabled throughout this function. 
  4.  */  
  5. int kprobe_int3_handler(struct pt_regs *regs)  
  6. {  
  7.     kprobe_opcode_t *addr;  
  8.     struct kprobe *p;  
  9.     struct kprobe_ctlblk *kcb;  
  10.   
  11.     if (user_mode(regs))  
  12.         return 0;  
  13.   
  14.     addr = (kprobe_opcode_t *)(regs->ip - sizeof(kprobe_opcode_t));  
  15.     /* 
  16.      * We don't want to be preempted for the entire 
  17.      * duration of kprobe processing. We conditionally 
  18.      * re-enable preemption at the end of this function, 
  19.      * and also in reenter_kprobe() and setup_singlestep(). 
  20.      */  
  21.     preempt_disable();  
  22.   
  23.     kcb = get_kprobe_ctlblk();  
  24.     p = get_kprobe(addr);  

本地中斷在處理kprobe期間依然被禁止,同時調用user_mode函數確保本處理函數處理的int3中斷是在內核態執行流程期間被觸發的(因爲kprobe不會從用戶態觸發),這裏之所以要做這麼一個判斷是因爲同arm定義特殊未處理指令回調函數不同,這裏的do_int3要通用的多,並不是單獨爲kprobe所設計的。然後獲取被探測指令的地址保存到addr中(對於int3中斷,其被Intel定義爲trap,那麼異常發生時EIP寄存器內指向的爲異常指令的後一條指令),同時會禁用內核搶佔,註釋中說明在reenter_kprobe和單步執行時會有選擇的重新開啓內核搶佔。接下來下面同arm一樣獲取當前cpu的kprobe_ctlblk控制結構體和本次要處理的kprobe實例p,然後根據不同的情況進行不同分支的處理。在繼續分析前先來看一下x86_64架構kprobe_ctlblk結構體的定義

  1. /* per-cpu kprobe control block */  
  2. struct kprobe_ctlblk {  
  3.     unsigned long kprobe_status;  
  4.     unsigned long kprobe_old_flags;  
  5.     unsigned long kprobe_saved_flags;  
  6.     unsigned long *jprobe_saved_sp;  
  7.     struct pt_regs jprobe_saved_regs;  
  8.     kprobe_opcode_t jprobes_stack[MAX_STACK_SIZE];  
  9.     struct prev_kprobe prev_kprobe;  
  10. };  
該定義比arm結構的多一些字段,其中kprobe_status字段不變,kprobe_old_flags和kprobe_saved_flags字段用於保存寄存器pt_regs的flag標識。

下面回到函數中根據不同的情況分別分析:

1、p存在且curent_kprobe存在

對於kprobe重入的情況,調用reenter_kprobe函數單獨處理:

  1. if (kprobe_running()) {  
  2.     if (reenter_kprobe(p, regs, kcb))  
  3.         return 1;  
  1. /* 
  2.  * We have reentered the kprobe_handler(), since another probe was hit while 
  3.  * within the handler. We save the original kprobes variables and just single 
  4.  * step on the instruction of the new probe without calling any user handlers. 
  5.  */  
  6. static int reenter_kprobe(struct kprobe *p, struct pt_regs *regs,  
  7.               struct kprobe_ctlblk *kcb)  
  8. {  
  9.     switch (kcb->kprobe_status) {  
  10.     case KPROBE_HIT_SSDONE:  
  11.     case KPROBE_HIT_ACTIVE:  
  12.     case KPROBE_HIT_SS:  
  13.         kprobes_inc_nmissed_count(p);  
  14.         setup_singlestep(p, regs, kcb, 1);  
  15.         break;  
  16.     case KPROBE_REENTER:  
  17.         /* A probe has been hit in the codepath leading up to, or just 
  18.          * after, single-stepping of a probed instruction. This entire 
  19.          * codepath should strictly reside in .kprobes.text section. 
  20.          * Raise a BUG or we'll continue in an endless reentering loop 
  21.          * and eventually a stack overflow. 
  22.          */  
  23.         printk(KERN_WARNING "Unrecoverable kprobe detected at %p.\n",  
  24.                p->addr);  
  25.         dump_kprobe(p);  
  26.         BUG();  
  27.     default:  
  28.         /* impossible cases */  
  29.         WARN_ON(1);  
  30.         return 0;  
  31.     }  
  32.   
  33.     return 1;  
  34. }  
  35. NOKPROBE_SYMBOL(reenter_kprobe);  
這個流程同arm實現的很像,只不過對於KPROBE_HIT_SS階段不會報BUG,也同KPROBE_HIT_SSDONE和KPROBE_HIT_ACTIVE一樣,遞增nmissed值並調用setup_singlestep函數進入單步處理流程(該函數最後一個入參此時設置爲1,針對reenter的情況會將kprobe_status狀態設置爲KPROBE_REENTER並調用save_previous_kprobe執行保存當前kprobe的操作)。對於KPROBE_REENTER階段還是直接報BUG。注意最後函數會返回1,do_int3也會直接返回,表示該中斷已被kprobe截取並處理,無需再處理其他分支。

2、p存在但curent_kprobe不存在

  1. else {  
  2.     set_current_kprobe(p, regs, kcb);  
  3.     kcb->kprobe_status = KPROBE_HIT_ACTIVE;  
  4.   
  5.     /* 
  6.      * If we have no pre-handler or it returned 0, we 
  7.      * continue with normal processing.  If we have a 
  8.      * pre-handler and it returned non-zero, it prepped 
  9.      * for calling the break_handler below on re-entry 
  10.      * for jprobe processing, so get out doing nothing 
  11.      * more here. 
  12.      */  
  13.     if (!p->pre_handler || !p->pre_handler(p, regs))  
  14.         setup_singlestep(p, regs, kcb, 0);  
  15.     return 1;  
  16. }  

這是一般最通用的kprobe執行流程,首先調用set_current_kprobe綁定p爲當前正在處理的kprobe:

  1. static nokprobe_inline void  
  2. set_current_kprobe(struct kprobe *p, struct pt_regs *regs,  
  3.            struct kprobe_ctlblk *kcb)  
  4. {  
  5.     __this_cpu_write(current_kprobe, p);  
  6.     kcb->kprobe_saved_flags = kcb->kprobe_old_flags  
  7.         = (regs->flags & (X86_EFLAGS_TF | X86_EFLAGS_IF));  
  8.     if (p->ainsn.if_modifier)  
  9.         kcb->kprobe_saved_flags &= ~X86_EFLAGS_IF;  
  10. }  
這裏在設置current_kprobe全局變量的同時,還會同時設置kprobe_saved_flags和kprobe_old_flags的flag值,它們用於具體的架構指令相關處理。接下來處理pre_handler回調函數,有註冊的話就調用執行,然後調用setup_singlestep啓動單步執行。在調試完成後直接返回1,注意這裏並沒有向arm實現那樣直接調用post_handler回調函數並解除kprobe綁定,因爲x86_64架構的post_handler採用另一種方式調用,後文會講到。

3、p不存在且被探測地址的指令也不是BREAKPOINT_INSTRUCTION

  1. else if (*addr != BREAKPOINT_INSTRUCTION) {  
  2.     /* 
  3.      * The breakpoint instruction was removed right 
  4.      * after we hit it.  Another cpu has removed 
  5.      * either a probepoint or a debugger breakpoint 
  6.      * at this address.  In either case, no further 
  7.      * handling of this interrupt is appropriate. 
  8.      * Back up over the (now missing) int3 and run 
  9.      * the original instruction. 
  10.      */  
  11.     regs->ip = (unsigned long)addr;  
  12.     preempt_enable_no_resched();  
  13.     return 1;  

這種情況表示kprobe可能已經被其他CPU註銷了,則讓他執行原始指令即可,因此這裏設置regs->ip值爲addr並重新開啓內核搶佔返回1。

4、p不存在但curent_kprobe存在

  1. else if (kprobe_running()) {  
  2.     p = __this_cpu_read(current_kprobe);  
  3.     if (p->break_handler && p->break_handler(p, regs)) {  
  4.         if (!skip_singlestep(p, regs, kcb))  
  5.             setup_singlestep(p, regs, kcb, 0);  
  6.         return 1;  
  7.     }  

這種情況一般用於實現jprobe,因此會調用curent_kprobe的break_handler回調函數,然後在break_handler返回非0的情況下執行單步執行,最後返回1。具體在jprobe實現中再詳細分析。

以上x86_64架構的kprobe觸發及回調整體流程分析完畢,可以看到基本的觸發條件和處理流程和arm架構的實現還是差不多的,和架構相關的一些細節有所不同。同時也並沒有看到post_handle的回調流程和kprobe的解綁定流程,由於實現同arm不同,以上遺留的兩點會在後文分析。接下來分析被探測指令的單步執行過程。


4、單步執行

單步執行其實就是執行被探測點的原始指令,涉及的主要函數即前文中分析kprobe觸發及處理流程時遺留的singlestep函數(arm)和setup_singlestep函數(x86),它們的實現原理完全不同,其中會涉及許多cpu架構相關的知識,因此會比較晦澀。下面從原理角度逐一分析,並不涉及太多架構相關的細節:

4.1、arm架構實現

arm架構單步執行的原理並不非常複雜(但是實現非常複雜),它本質上所做的就是執行被探測點的被替換前的“原始指令”,但是當前的上下文已經是kprobe的執行上下文了,不再是原始指令所處的上下文,所以單步執行流程無法直接執行原始指令,而是會調用其他函數來模擬實現原始指令以達到相同的效果,因此涉及的函數很多,基本每條不同的彙編指令都有不同的模擬函數。
  1. static inline void __kprobes  
  2. singlestep(struct kprobe *p, struct pt_regs *regs, struct kprobe_ctlblk *kcb)  
  3. {  
  4.     p->ainsn.insn_singlestep(p->opcode, &p->ainsn, regs);  
  5. }  
singlestep函數直接調用保存在arch_probes_insn結構中的insn_singlestep函數指針(該指針在註冊kprobe時由arm_probes_decode_insn函數負責初始化),即arm_singlestep函數。入參爲保存的被探測點指令、arch_probes_insn結構地址及寄存器參數。
  1. static void __kprobes arm_singlestep(probes_opcode_t insn,  
  2.         struct arch_probes_insn *asi, struct pt_regs *regs)  
  3. {  
  4.     regs->ARM_pc += 4;  
  5.     asi->insn_handler(insn, asi, regs);  
  6. }  
首先讓寄存器參數中的PC加4,表示kprobe處理完成後將跳過觸發kprobe時的KPROBE_ARM_BREAKPOINT_INSTRUCTION指令繼續執行。然後調用insn_handler函數指針中設置的註冊函數,該函數指針由probes_decode_insn函數根據不同的原始指令被設置爲不同的處理函數,它們被定義在kprobes_arm_actions數組中:
  1. const union decode_action kprobes_arm_actions[NUM_PROBES_ARM_ACTIONS] = {  
  2.     [PROBES_PRELOAD_IMM] = {.handler = probes_simulate_nop},  
  3.     [PROBES_PRELOAD_REG] = {.handler = probes_simulate_nop},  
  4.     [PROBES_BRANCH_IMM] = {.handler = simulate_blx1},  
  5.     [PROBES_MRS] = {.handler = simulate_mrs},  
  6.     [PROBES_BRANCH_REG] = {.handler = simulate_blx2bx},  
  7.     [PROBES_CLZ] = {.handler = emulate_rd12rm0_noflags_nopc},  
  8.     [PROBES_SATURATING_ARITHMETIC] = {  
  9.         .handler = emulate_rd12rn16rm0_rwflags_nopc},  
  10.     [PROBES_MUL1] = {.handler = emulate_rdlo12rdhi16rn0rm8_rwflags_nopc},  
  11.     [PROBES_MUL2] = {.handler = emulate_rd16rn12rm0rs8_rwflags_nopc},  
  12.     [PROBES_SWP] = {.handler = emulate_rd12rn16rm0_rwflags_nopc},  
  13.     [PROBES_LDRSTRD] = {.handler = emulate_ldrdstrd},  
  14.     ......  
  15. }  
這裏的函數衆多就不一一分析了,現仍然以do_fork函數的入口指令“mov     ip, sp”爲例,調用的函數爲simulate_mov_ipsp:
  1. void __kprobes simulate_mov_ipsp(probes_opcode_t insn,  
  2.         struct arch_probes_insn *asi, struct pt_regs *regs)  
  3. {  
  4.     regs->uregs[12] = regs->uregs[13];  
  5. }  
這裏的uregs[12]即ARM_ip,uregs[13]即ARM_sp,可見simulate_mov_ipsp函數僅僅是模擬實現“mov     ip, sp”指令而已,對觸發kprobe前的寄存器狀態進行處理。當然這只是其中一個簡單的例子,對於其他一些複雜的多週期指令其模擬函數會實現的比較複雜,甚至有一些無法模擬的指令在註冊時probes_decode_insn函數就會返回INSN_REJECTED了。

以上arm架構下實現同原始指令同樣效果的單步執行就分析完了,在kprobe流程執行完成後,恢復到regs中保存的上下文後就會從ARM_pc處繼續取指執行了。這裏雖然只分析了mov指令的單步執行,但其他的指令的處理流程類似,若想要了解箇中細節可以通過ftrace工具進行跟蹤。

4.2、x86_64架構實現

x86_64架構的單步執行函數與arm架構的原理不同,其主要原理是:當程序執行到某條想要單獨執行CPU指令時,在執行之前產生一次CPU異常,此時把異常返回時的CPU的EFLAGS寄存器的TF(調試位)位置爲1,把IF(中斷屏蔽位)標誌位置爲0,然後把EIP指向單步執行的指令。當單步指令執行完成後,CPU會自動產生一次調試異常(由於TF被置位)。此時,Kprobes會利用debug異常,執行post_handler()。下面來簡單看一下:

  1. static void setup_singlestep(struct kprobe *p, struct pt_regs *regs,  
  2.                  struct kprobe_ctlblk *kcb, int reenter)  
  3. {  
  4.     if (setup_detour_execution(p, regs, reenter))  
  5.         return;  
  6.   
  7.     ......  
  8.       
  9.     if (reenter) {  
  10.         save_previous_kprobe(kcb);  
  11.         set_current_kprobe(p, regs, kcb);  
  12.         kcb->kprobe_status = KPROBE_REENTER;  
  13.     } else  
  14.         kcb->kprobe_status = KPROBE_HIT_SS;  
  15.     /* Prepare real single stepping */  
  16.     clear_btf();  
  17.     regs->flags |= X86_EFLAGS_TF;  
  18.     regs->flags &= ~X86_EFLAGS_IF;  
  19.     /* single step inline if the instruction is an int3 */  
  20.     if (p->opcode == BREAKPOINT_INSTRUCTION)  
  21.         regs->ip = (unsigned long)p->addr;  
  22.     else  
  23.         regs->ip = (unsigned long)p->ainsn.insn;  
  24. }  
首先在前文中已經介紹了,函數的最後一個入參reenter表示是否重入,對於重入的情況那就調用save_previous_kprobe函數保存當前正在運行的kprobe,然後綁定p和current_kprobe並設置kprobe_status爲KPROBE_REENTER;對於非重入的情況則設置kprobe_status爲KPROBE_HIT_SS。

接下來考試準備單步執行,首先設置regs->flags中的TF位並清空IF位,同時把int3異常返回的指令寄存器地址改爲前面保存的被探測指令,當int3異常返回時這些設置就會生效,即立即執行保存的原始指令(注意這裏是在觸發int3之前原來的上下文中執行,因此直接執行原始指令即可,無需特別的模擬操作)。該函數返回後do_int3函數立即返回,由於cpu的標識寄存器被設置,在單步執行完被探測指令後立即觸發debug異常,進入debug異常處理函數do_debug。

  1. dotraplinkage void do_debug(struct pt_regs *regs, long error_code)  
  2. {  
  3.     ......  
  4.       
  5. #ifdef CONFIG_KPROBES  
  6.     if (kprobe_debug_handler(regs))  
  7.         goto exit;  
  8. #endif  
  9.   
  10.     ......  
  11.   
  12. exit:  
  13.     ist_exit(regs, prev_state);  
  14. }  
  1. /* 
  2.  * Interrupts are disabled on entry as trap1 is an interrupt gate and they 
  3.  * remain disabled throughout this function. 
  4.  */  
  5. int kprobe_debug_handler(struct pt_regs *regs)  
  6. {  
  7.     struct kprobe *cur = kprobe_running();  
  8.     struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();  
  9.   
  10.     if (!cur)  
  11.         return 0;  
  12.   
  13.     resume_execution(cur, regs, kcb);  
  14.     regs->flags |= kcb->kprobe_saved_flags;  
  15.   
  16.     if ((kcb->kprobe_status != KPROBE_REENTER) && cur->post_handler) {  
  17.         kcb->kprobe_status = KPROBE_HIT_SSDONE;  
  18.         cur->post_handler(cur, regs, 0);  
  19.     }  
  20.   
  21.     /* Restore back the original saved kprobes variables and continue. */  
  22.     if (kcb->kprobe_status == KPROBE_REENTER) {  
  23.         restore_previous_kprobe(kcb);  
  24.         goto out;  
  25.     }  
  26.     reset_current_kprobe();  
  27. out:  
  28.     preempt_enable_no_resched();  
  29.   
  30.     /* 
  31.      * if somebody else is singlestepping across a probe point, flags 
  32.      * will have TF set, in which case, continue the remaining processing 
  33.      * of do_debug, as if this is not a probe hit. 
  34.      */  
  35.     if (regs->flags & X86_EFLAGS_TF)  
  36.         return 0;  
  37.   
  38.     return 1;  
  39. }  
  40. NOKPROBE_SYMBOL(kprobe_debug_handler);  
首先調用resume_execution函數將debug異常返回的下一條指令設置爲被探測之後的指令,這樣異常返回後程序的流程就會按正常的流程繼續執行;然後恢復kprobe執行前保存的flags標識;接下來如果kprobe不是重入的並且設置了post_handler回調函數,就設置kprobe_status狀態爲KPROBE_HIT_SSDONE並調用post_handler函數;如果是重入的kprobe則調用restore_previous_kprobe函數恢復之前保存的kprobe。最後調用reset_current_kprobe函數解除本kprobe和current_kprobe的綁定,如果本kprobe由單步執行觸發,則說明do_debug異常處理還有其他流程帶處理,返回0,否則返回1。

以上x86_64的單步執行和post_handler回調分析完畢,簡單總結一下和arm架構的實現區別:arm結構的單步執行被探測指令是在異常處理上下文中進行的,因此需要使用單獨的函數來模擬實現原始命令所操作的流程,而x86_64架構則利用了cpu提供的單步調試技術,使得原始指令在正常的原上下文中執行,而兩個回調函數則分別在int3和debug兩次異常處理流程中執行。

至此,kprobe的一般處理流程就分析完了,最後分析一下剩下的最後一個回調函數fault_handler。


5、出錯回調

出錯會調函數fault_handler會在執行pre_handler、single_step和post_handler期間觸發內存異常時被調用,對應的調用函數爲kprobe_fault_handler,它同樣時架構相關的,分別來看一下:

5.1、arm調用流程

do_page_fault->notify_page_fault

  1. static inline int notify_page_fault(struct pt_regs *regs, unsigned int fsr)  
  2. {  
  3.     int ret = 0;  
  4.   
  5.     if (!user_mode(regs)) {  
  6.         /* kprobe_running() needs smp_processor_id() */  
  7.         preempt_disable();  
  8.         if (kprobe_running() && kprobe_fault_handler(regs, fsr))  
  9.             ret = 1;  
  10.         preempt_enable();  
  11.     }  
  12.   
  13.     return ret;  
  14. }  
可見在觸發缺頁異常之後,若當前正在處理kprobe流程期間,會調用kprobe_fault_handler進行處理。
  1. int __kprobes kprobe_fault_handler(struct pt_regs *regs, unsigned int fsr)  
  2. {  
  3.     struct kprobe *cur = kprobe_running();  
  4.     struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();  
  5.   
  6.     switch (kcb->kprobe_status) {  
  7.     case KPROBE_HIT_SS:  
  8.     case KPROBE_REENTER:  
  9.         /* 
  10.          * We are here because the instruction being single 
  11.          * stepped caused a page fault. We reset the current 
  12.          * kprobe and the PC to point back to the probe address 
  13.          * and allow the page fault handler to continue as a 
  14.          * normal page fault. 
  15.          */  
  16.         regs->ARM_pc = (long)cur->addr;  
  17.         if (kcb->kprobe_status == KPROBE_REENTER) {  
  18.             restore_previous_kprobe(kcb);  
  19.         } else {  
  20.             reset_current_kprobe();  
  21.         }  
  22.         break;  
  23.   
  24.     case KPROBE_HIT_ACTIVE:  
  25.     case KPROBE_HIT_SSDONE:  
  26.         /* 
  27.          * We increment the nmissed count for accounting, 
  28.          * we can also use npre/npostfault count for accounting 
  29.          * these specific fault cases. 
  30.          */  
  31.         kprobes_inc_nmissed_count(cur);  
  32.   
  33.         /* 
  34.          * We come here because instructions in the pre/post 
  35.          * handler caused the page_fault, this could happen 
  36.          * if handler tries to access user space by 
  37.          * copy_from_user(), get_user() etc. Let the 
  38.          * user-specified handler try to fix it. 
  39.          */  
  40.         if (cur->fault_handler && cur->fault_handler(cur, regs, fsr))  
  41.             return 1;  
  42.         break;  
  43.   
  44.     default:  
  45.         break;  
  46.     }  
  47.   
  48.     return 0;  
  49. }  
kprobe_fault_handler函數會找到當前正在處理的kprobe,然後根據處理狀態的不同本別處理。首先若是單步執行或是重入的情況,則說明單步執行是發生了內存錯誤,則復位當前正在處理的kprobe,同時設置PC指針爲異常觸發指令地址,就好像它是一個普通的缺頁異常,由內核後續的處理流程處理;若是執行pre_handler和post_handler回調函數期間出錯,則遞增kprobe的nmiss字段值,然後調用fault_handler回調函數執行用戶指定的操作,如果fault_handler函數返回0則會由內核繼續處理page fault,否則表示fault_handler函數已經執行了修復操作,do_page_fault會直接返回。

5.2、x86_64調用流程

1、do_page_fault->__do_page_fault->kprobes_fault

  1. static nokprobe_inline int kprobes_fault(struct pt_regs *regs)  
  2. {  
  3.     int ret = 0;  
  4.   
  5.     /* kprobe_running() needs smp_processor_id() */  
  6.     if (kprobes_built_in() && !user_mode(regs)) {  
  7.         preempt_disable();  
  8.         if (kprobe_running() && kprobe_fault_handler(regs, 14))  
  9.             ret = 1;  
  10.         preempt_enable();  
  11.     }  
  12.   
  13.     return ret;  
  14. }  
這個缺頁異常的調用流程同arm實現的幾乎完全一樣,就不贅述了。

2、do_general_protection->notify_die->kprobe_exceptions_notify

  1. int kprobe_exceptions_notify(struct notifier_block *self, unsigned long val,  
  2.                  void *data)  
  3. {  
  4.     struct die_args *args = data;  
  5.     int ret = NOTIFY_DONE;  
  6.   
  7.     if (args->regs && user_mode(args->regs))  
  8.         return ret;  
  9.   
  10.     if (val == DIE_GPF) {  
  11.         /* 
  12.          * To be potentially processing a kprobe fault and to 
  13.          * trust the result from kprobe_running(), we have 
  14.          * be non-preemptible. 
  15.          */  
  16.         if (!preemptible() && kprobe_running() &&  
  17.             kprobe_fault_handler(args->regs, args->trapnr))  
  18.             ret = NOTIFY_STOP;  
  19.     }  
  20.     return ret;  
  21. }  
前文中init_kprobes初始化時會註冊die內核通知鏈kprobe_exceptions_nb,它的回調函數爲kprobe_exceptions_notify,在內核觸發DIE_GPF類型的notify_die時,該函數會調用kprobe_fault_handler進行處理。下面來簡單看一下x86_64架構的kprobe_fault_handler函數實現:
  1. int kprobe_fault_handler(struct pt_regs *regs, int trapnr)  
  2. {  
  3.     struct kprobe *cur = kprobe_running();  
  4.     struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();  
  5.   
  6.     if (unlikely(regs->ip == (unsigned long)cur->ainsn.insn)) {  
  7.         /* This must happen on single-stepping */  
  8.         WARN_ON(kcb->kprobe_status != KPROBE_HIT_SS &&  
  9.             kcb->kprobe_status != KPROBE_REENTER);  
  10.         /* 
  11.          * We are here because the instruction being single 
  12.          * stepped caused a page fault. We reset the current 
  13.          * kprobe and the ip points back to the probe address 
  14.          * and allow the page fault handler to continue as a 
  15.          * normal page fault. 
  16.          */  
  17.         regs->ip = (unsigned long)cur->addr;  
  18.         regs->flags |= kcb->kprobe_old_flags;  
  19.         if (kcb->kprobe_status == KPROBE_REENTER)  
  20.             restore_previous_kprobe(kcb);  
  21.         else  
  22.             reset_current_kprobe();  
  23.         preempt_enable_no_resched();  
  24.     } else if (kcb->kprobe_status == KPROBE_HIT_ACTIVE ||  
  25.            kcb->kprobe_status == KPROBE_HIT_SSDONE) {  
  26.         /* 
  27.          * We increment the nmissed count for accounting, 
  28.          * we can also use npre/npostfault count for accounting 
  29.          * these specific fault cases. 
  30.          */  
  31.         kprobes_inc_nmissed_count(cur);  
  32.   
  33.         /* 
  34.          * We come here because instructions in the pre/post 
  35.          * handler caused the page_fault, this could happen 
  36.          * if handler tries to access user space by 
  37.          * copy_from_user(), get_user() etc. Let the 
  38.          * user-specified handler try to fix it first. 
  39.          */  
  40.         if (cur->fault_handler && cur->fault_handler(cur, regs, trapnr))  
  41.             return 1;  
  42.   
  43.         /* 
  44.          * In case the user-specified fault handler returned 
  45.          * zero, try to fix up. 
  46.          */  
  47.         if (fixup_exception(regs))  
  48.             return 1;  
  49.   
  50.         /* 
  51.          * fixup routine could not handle it, 
  52.          * Let do_page_fault() fix it. 
  53.          */  
  54.     }  
  55.   
  56.     return 0;  
  57. }  
流程基本同arm實現的完全一致,唯一不同之處在於如果fault_handler函數返回0,即沒有修復內存異常,則會直接調用fixup_exception函數嘗試修復。

以上fault_handler回調函數分析完畢。

五、總結

kprobes內核探測技術作爲一種內核代碼的跟蹤及調試手段,開發人員可以動態的跟蹤內核函數的執行,相較與傳統的添加內核日誌等調試手段,它具有操作簡單,使用靈活,對原始代碼破壞小等多方面優勢。本文首先介紹了kprobes的技術背景,然後介紹了其中kprobe技術使用方法並且通過源代碼詳細分析了arm架構和x86_64架構的原理和實現方式。下一篇博文將介紹基於kprobe實現的jprobe內核跟蹤技術。


參考文獻:1、http://blog.chinaunix.NET/uid-20662820-id-3795534.html

2、http://blog.csdn.net/panfengyun12345/article/details/19480567

3、Documentation/kprobes.txt

4、Documentation/trace/kprobetrace.txt

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章