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結構體定義如下:
- struct kprobe {
- struct hlist_node hlist;
- /* list of kprobes for multi-handler support */
- struct list_head list;
- /*count the number of times this probe was temporarily disarmed */
- unsigned long nmissed;
- /* location of the probe point */
- kprobe_opcode_t *addr;
- /* Allow user to indicate symbol name of the probe point */
- const char *symbol_name;
- /* Offset into the symbol */
- unsigned int offset;
- /* Called before addr is executed. */
- kprobe_pre_handler_t pre_handler;
- /* Called after addr is executed, unless... */
- kprobe_post_handler_t post_handler;
- /*
- * ... called if executing addr causes a fault (eg. page fault).
- * Return 1 if it handled fault, otherwise kernel will see it.
- */
- kprobe_fault_handler_t fault_handler;
- /*
- * ... called if breakpoint trap occurs in probe handler.
- * Return 1 if it handled break, otherwise kernel will see it.
- */
- kprobe_break_handler_t break_handler;
- /* Saved opcode (which has been replaced with breakpoint) */
- kprobe_opcode_t opcode;
- /* copy of the original instruction */
- struct arch_specific_insn ainsn;
- /*
- * Indicates various status flags.
- * Protected by kprobe_mutex after this kprobe is registered.
- */
- u32 flags;
- };
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函數接口如下:
- int register_kprobe(struct kprobe *kp) //向內核註冊kprobe探測點
- void unregister_kprobe(struct kprobe *kp) //卸載kprobe探測點
- int register_kprobes(struct kprobe **kps, int num) //註冊探測函數向量,包含多個探測點
- void unregister_kprobes(struct kprobe **kps, int num) //卸載探測函數向量,包含多個探測點
- int disable_kprobe(struct kprobe *kp) //臨時暫停指定探測點的探測
- int enable_kprobe(struct kprobe *kp) //恢復指定探測點的探測
1.2、用例kprobe_example.c分析與演示
該用例函數非常簡單,它實現了內核函數do_fork的探測,該函數會在fork系統調用或者內核kernel_thread函數創建進程時被調用,觸發也十分的頻繁。下面來分析一下用例代碼:
- /* For each probe you need to allocate a kprobe structure */
- static struct kprobe kp = {
- .symbol_name = "do_fork",
- };
- static int __init kprobe_init(void)
- {
- int ret;
- kp.pre_handler = handler_pre;
- kp.post_handler = handler_post;
- kp.fault_handler = handler_fault;
- ret = register_kprobe(&kp);
- if (ret < 0) {
- printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
- return ret;
- }
- printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);
- return 0;
- }
- static void __exit kprobe_exit(void)
- {
- unregister_kprobe(&kp);
- printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
- }
- module_init(kprobe_init)
- module_exit(kprobe_exit)
- MODULE_LICENSE("GPL");
pre_handler、post_handler和fault_handler這3個回調函數分別爲handler_pre、handler_post和handler_fault,最後調用register_kprobe註冊。在模塊的卸載函數中調用unregister_kprobe函數卸載kp探測點。
- static int handler_pre(struct kprobe *p, struct pt_regs *regs)
- {
- #ifdef CONFIG_X86
- printk(KERN_INFO "pre_handler: p->addr = 0x%p, ip = %lx,"
- " flags = 0x%lx\n",
- p->addr, regs->ip, regs->flags);
- #endif
- #ifdef CONFIG_PPC
- printk(KERN_INFO "pre_handler: p->addr = 0x%p, nip = 0x%lx,"
- " msr = 0x%lx\n",
- p->addr, regs->nip, regs->msr);
- #endif
- #ifdef CONFIG_MIPS
- printk(KERN_INFO "pre_handler: p->addr = 0x%p, epc = 0x%lx,"
- " status = 0x%lx\n",
- p->addr, regs->cp0_epc, regs->cp0_status);
- #endif
- #ifdef CONFIG_TILEGX
- printk(KERN_INFO "pre_handler: p->addr = 0x%p, pc = 0x%lx,"
- " ex1 = 0x%lx\n",
- p->addr, regs->pc, regs->ex1);
- #endif
- /* A dump_stack() here will give a stack backtrace */
- return 0;
- }
- /* kprobe post_handler: called after the probed instruction is executed */
- static void handler_post(struct kprobe *p, struct pt_regs *regs,
- unsigned long flags)
- {
- #ifdef CONFIG_X86
- printk(KERN_INFO "post_handler: p->addr = 0x%p, flags = 0x%lx\n",
- p->addr, regs->flags);
- #endif
- #ifdef CONFIG_PPC
- printk(KERN_INFO "post_handler: p->addr = 0x%p, msr = 0x%lx\n",
- p->addr, regs->msr);
- #endif
- #ifdef CONFIG_MIPS
- printk(KERN_INFO "post_handler: p->addr = 0x%p, status = 0x%lx\n",
- p->addr, regs->cp0_status);
- #endif
- #ifdef CONFIG_TILEGX
- printk(KERN_INFO "post_handler: p->addr = 0x%p, ex1 = 0x%lx\n",
- p->addr, regs->ex1);
- #endif
- }
- /*
- * fault_handler: this is called if an exception is generated for any
- * instruction within the pre- or post-handler, or when Kprobes
- * single-steps the probed instruction.
- */
- static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
- {
- printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",
- p->addr, trapnr);
- /* Return 0 because we don't handle the fault. */
- return 0;
- }
下面將它編譯成模塊在我的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模塊:
- obj-m := kprobe_example.o
- CROSS_COMPILE=''
- KDIR := /lib/modules/$(shell uname -r)/build
- all:
- make -C $(KDIR) M=$(PWD) modules
- clean:
- rm -f *.ko *.o *.mod.o *.mod.c .*.cmd *.symvers modul*
<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
- static int __init init_kprobes(void)
- {
- int i, err = 0;
- /* FIXME allocate the probe table, currently defined statically */
- /* initialize all list heads */
- for (i = 0; i < KPROBE_TABLE_SIZE; i++) {
- INIT_HLIST_HEAD(&kprobe_table[i]);
- INIT_HLIST_HEAD(&kretprobe_inst_table[i]);
- raw_spin_lock_init(&(kretprobe_table_locks[i].lock));
- }
- err = populate_kprobe_blacklist(__start_kprobe_blacklist,
- __stop_kprobe_blacklist);
- if (err) {
- pr_err("kprobes: failed to populate blacklist: %d\n", err);
- pr_err("Please take care of using kprobes.\n");
- }
- if (kretprobe_blacklist_size) {
- /* lookup the function address from its name */
- for (i = 0; kretprobe_blacklist[i].name != NULL; i++) {
- kprobe_lookup_name(kretprobe_blacklist[i].name,
- kretprobe_blacklist[i].addr);
- if (!kretprobe_blacklist[i].addr)
- printk("kretprobe: lookup failed: %s\n",
- kretprobe_blacklist[i].name);
- }
- }
- #if defined(CONFIG_OPTPROBES)
- #if defined(__ARCH_WANT_KPROBES_INSN_SLOT)
- /* Init kprobe_optinsn_slots */
- kprobe_optinsn_slots.insn_size = MAX_OPTINSN_SIZE;
- #endif
- /* By default, kprobes can be optimized */
- kprobes_allow_optimization = true;
- #endif
- /* By default, kprobes are armed */
- kprobes_all_disarmed = false;
- err = arch_init_kprobes();
- if (!err)
- err = register_die_notifier(&kprobe_exceptions_nb);
- if (!err)
- err = register_module_notifier(&kprobe_module_nb);
- kprobes_initialized = (err == 0);
- if (!err)
- init_test_probes();
- return err;
- }
接下來調用populate_kprobe_blacklist函數將kprobe實現相關的代碼函數保存到kprobe_blacklist這個鏈表中去,用於後面註冊探測點時判斷使用,注意這裏的__start_kprobe_blacklist和__stop_kprobe_blacklist定義在arch/arm/kernel/vmlinux.lds.h中的.init.rodata段中,其中保存了_kprobe_blacklist段信息:
- #define KPROBE_BLACKLIST() . = ALIGN(8); \
- VMLINUX_SYMBOL(__start_kprobe_blacklist) = .; \
- *(_kprobe_blacklist) \
- VMLINUX_SYMBOL(__stop_kprobe_blacklist) = .;
- #define INIT_DATA \
- *(.init.data) \
- ......
- *(.init.rodata) \
- ......
- KPROBE_BLACKLIST() \
- ......
- #define __NOKPROBE_SYMBOL(fname) \
- static unsigned long __used \
- __attribute__((section("_kprobe_blacklist"))) \
- _kbl_addr_##fname = (unsigned long)fname;
- #define NOKPROBE_SYMBOL(fname) __NOKPROBE_SYMBOL(fname)
- struct kprobe *get_kprobe(void *addr)
- {
- ......
- }
- NOKPROBE_SYMBOL(get_kprobe);
回到init_kprobes函數中繼續分析,接下來的片段是kretprobe相關的代碼,用來覈對kretprobe_blacklist中定義的函數是否存在,這裏kretprobe_blacklist_size變量默認爲0;接下來初始化3個全局變量,kprobes_all_disarmed用於表示是否啓用kprobe機制,這裏默認設置爲啓用;隨後調用arch_init_kprobes進行架構相關的初始化,x86架構的實現爲空,arm架構的實現如下:
- int __init arch_init_kprobes()
- {
- arm_probes_decode_init();
- #ifdef CONFIG_THUMB2_KERNEL
- register_undef_hook(&kprobes_thumb16_break_hook);
- register_undef_hook(&kprobes_thumb32_break_hook);
- #else
- register_undef_hook(&kprobes_arm_break_hook);
- #endif
- return 0;
- }
- static struct undef_hook kprobes_arm_break_hook = {
- .instr_mask = 0x0fffffff,
- .instr_val = KPROBE_ARM_BREAKPOINT_INSTRUCTION,
- .cpsr_mask = MODE_MASK,
- .cpsr_val = SVC_MODE,
- .fn = kprobe_trap_handler,
- };
再次回到init_kprobes函數,接下來分別註冊die和module的內核通知鏈kprobe_exceptions_nb和kprobe_module_nb:
- static struct notifier_block kprobe_exceptions_nb = {
- .notifier_call = kprobe_exceptions_notify,
- .priority = 0x7fffffff /* we need to be notified first */
- };
- static struct notifier_block kprobe_module_nb = {
- .notifier_call = kprobes_module_callback,
- .priority = 0
- };
最後init_kprobes函數置位kprobes_initialized標識,初始化完成。
2、註冊一個kprobe實例
kprobe探測模塊調用register_kprobe向kprobe子系統註冊一個kprobe探測點實例,代碼路徑kernel/kprobes.c
圖 3 kprobe註冊流程
- int register_kprobe(struct kprobe *p)
- {
- int ret;
- struct kprobe *old_p;
- struct module *probed_mod;
- kprobe_opcode_t *addr;
- /* Adjust probe address from symbol */
- addr = kprobe_addr(p);
- if (IS_ERR(addr))
- return PTR_ERR(addr);
- p->addr = addr;
- ret = check_kprobe_rereg(p);
- if (ret)
- return ret;
- /* User can pass only KPROBE_FLAG_DISABLED to register_kprobe */
- p->flags &= KPROBE_FLAG_DISABLED;
- p->nmissed = 0;
- INIT_LIST_HEAD(&p->list);
- ret = check_kprobe_address_safe(p, &probed_mod);
- if (ret)
- return ret;
- mutex_lock(&kprobe_mutex);
- old_p = get_kprobe(p->addr);
- if (old_p) {
- /* Since this may unoptimize old_p, locking text_mutex. */
- ret = register_aggr_kprobe(old_p, p);
- goto out;
- }
- mutex_lock(&text_mutex); /* Avoiding text modification */
- ret = prepare_kprobe(p);
- mutex_unlock(&text_mutex);
- if (ret)
- goto out;
- INIT_HLIST_NODE(&p->hlist);
- hlist_add_head_rcu(&p->hlist,
- &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]);
- if (!kprobes_all_disarmed && !kprobe_disabled(p))
- arm_kprobe(p);
- /* Try to optimize kprobe */
- try_to_optimize_kprobe(p);
- out:
- mutex_unlock(&kprobe_mutex);
- if (probed_mod)
- module_put(probed_mod);
- return ret;
- }
- EXPORT_SYMBOL_GPL(register_kprobe);
- #define kprobe_lookup_name(name, addr) \
- addr = ((kprobe_opcode_t *)(kallsyms_lookup_name(name)))
- static kprobe_opcode_t *kprobe_addr(struct kprobe *p)
- {
- kprobe_opcode_t *addr = p->addr;
- if ((p->symbol_name && p->addr) ||
- (!p->symbol_name && !p->addr))
- goto invalid;
- if (p->symbol_name) {
- kprobe_lookup_name(p->symbol_name, addr);
- if (!addr)
- return ERR_PTR(-ENOENT);
- }
- addr = (kprobe_opcode_t *)(((char *)addr) + p->offset);
- if (addr)
- return addr;
- invalid:
- return ERR_PTR(-EINVAL);
- }
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函數檢測被探測地址是否可探測:
- static int check_kprobe_address_safe(struct kprobe *p,
- struct module **probed_mod)
- {
- int ret;
- ret = arch_check_ftrace_location(p);
- if (ret)
- return ret;
- jump_label_lock();
- preempt_disable();
- /* Ensure it is not in reserved area nor out of text */
- if (!kernel_text_address((unsigned long) p->addr) ||
- within_kprobe_blacklist((unsigned long) p->addr) ||
- jump_label_text_reserved(p->addr, p->addr)) {
- ret = -EINVAL;
- goto out;
- }
- /* Check if are we probing a module */
- *probed_mod = __module_text_address((unsigned long) p->addr);
- if (*probed_mod) {
- /*
- * We must hold a refcount of the probed module while updating
- * its code to prohibit unexpected unloading.
- */
- if (unlikely(!try_module_get(*probed_mod))) {
- ret = -ENOENT;
- goto out;
- }
- /*
- * If the module freed .init.text, we couldn't insert
- * kprobes in there.
- */
- if (within_module_init((unsigned long)p->addr, *probed_mod) &&
- (*probed_mod)->state != MODULE_STATE_COMING) {
- module_put(*probed_mod);
- *probed_mod = NULL;
- ret = -ENOENT;
- }
- }
- out:
- preempt_enable();
- jump_label_unlock();
- return ret;
- }
然後上鎖並竟用內核搶佔,開始進入地址有效性檢測流程,首先判斷以下3個條件,必須全部滿足:1、被探測地址在內核的地址段中;2、地址不在kprobe的黑名單之中;3、不在jump lable保留的地址空間中(內核jump lable特性使用?)。其中第一點比較好理解,函數實現如下:
- int kernel_text_address(unsigned long addr)
- {
- if (core_kernel_text(addr))
- return 1;
- if (is_module_text_address(addr))
- return 1;
- return is_ftrace_trampoline(addr);
- }
被探測的函數當然要在內核的text(_stext ~ _etext)段中,由於非內核啓動時刻,不包括init text段;然後模塊的text段和init text段也都可以,最後如果在ftrace動態分配的trampoline地址空間中也是滿足的。
其中第二點中的blacklist黑名單指的是實現kprobes的關鍵代碼路徑,只有不在該黑名單中的函數纔可以被探測:
- bool __weak arch_within_kprobe_blacklist(unsigned long addr)
- {
- /* The __kprobes marked functions and entry code must not be probed */
- return addr >= (unsigned long)__kprobes_text_start &&
- addr < (unsigned long)__kprobes_text_end;
- }
- static bool within_kprobe_blacklist(unsigned long addr)
- {
- struct kprobe_blacklist_entry *ent;
- if (arch_within_kprobe_blacklist(addr))
- return true;
- /*
- * If there exists a kprobe_blacklist, verify and
- * fail any probe registration in the prohibited area
- */
- list_for_each_entry(ent, &kprobe_blacklist, list) {
- if (addr >= ent->start_addr && addr < ent->end_addr)
- return true;
- }
- return false;
- }
主要包含兩個方面,一是架構相關的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段:
- #define KPROBES_TEXT \
- ALIGN_FUNCTION(); \
- VMLINUX_SYMBOL(__kprobes_text_start) = .; \
- *(.kprobes.text) \
- VMLINUX_SYMBOL(__kprobes_text_end) = .;
- #ifdef CONFIG_KPROBES
- # define __kprobes __attribute__((__section__(".kprobes.text")))
回到check_kprobe_address_safe函數中,若滿足了以上三點,接下來判斷被探測地址是否屬於某一個內核模塊的init_text段或core_text段:
- struct module *__module_text_address(unsigned long addr)
- {
- struct module *mod = __module_address(addr);
- if (mod) {
- /* Make sure it's within the text section. */
- if (!within(addr, mod->module_init, mod->init_text_size)
- && !within(addr, mod->module_core, mod->core_text_size))
- mod = NULL;
- }
- return mod;
- }
以上判斷都通過之後重新打開內核搶佔並解鎖,回到register_kprobe函數繼續註冊流程。接下來嘗試從全局hash表中查找是否之前已經爲同一個被探測地址註冊了kprobe探測點,若已註冊則調用register_aggr_kprobe函數繼續註冊流程,該流程稍後再分析。現假設是初次註冊,則調用prepare_kprobe函數,該函數會根據被探測地址是否已經被ftrace了而進入不同的流程,這裏假設沒有啓用ftrace,則直接調用arch_prepare_kprobe函數進入架構相關的註冊流程,先看一下x86架構的實現:
- int arch_prepare_kprobe(struct kprobe *p)
- {
- if (alternatives_text_reserved(p->addr, p->addr))
- return -EINVAL;
- if (!can_probe((unsigned long)p->addr))
- return -EILSEQ;
- /* insn: must be on special executable page on x86. */
- p->ainsn.insn = get_insn_slot();
- if (!p->ainsn.insn)
- return -ENOMEM;
- return arch_copy_kprobe(p);
- }
- static int arch_copy_kprobe(struct kprobe *p)
- {
- int ret;
- /* Copy an instruction with recovering if other optprobe modifies it.*/
- ret = __copy_instruction(p->ainsn.insn, p->addr);
- if (!ret)
- return -EINVAL;
- /*
- * __copy_instruction can modify the displacement of the instruction,
- * but it doesn't affect boostable check.
- */
- if (can_boost(p->ainsn.insn))
- p->ainsn.boostable = 0;
- else
- p->ainsn.boostable = -1;
- /* Check whether the instruction modifies Interrupt Flag or not */
- p->ainsn.if_modifier = is_IF_modifier(p->ainsn.insn);
- /* Also, displacement change doesn't affect the first byte */
- p->opcode = p->ainsn.insn[0];
- return 0;
- }
再來看一下arm架構的實現方式(已去除CONFIG_THUMB2_KERNEL相關部分的代碼):
- int __kprobes arch_prepare_kprobe(struct kprobe *p)
- {
- kprobe_opcode_t insn;
- kprobe_opcode_t tmp_insn[MAX_INSN_SIZE];
- unsigned long addr = (unsigned long)p->addr;
- bool thumb;
- kprobe_decode_insn_t *decode_insn;
- const union decode_action *actions;
- int is;
- const struct decode_checker **checkers;
- if (in_exception_text(addr))
- return -EINVAL;
- #ifdef CONFIG_THUMB2_KERNEL
- ......
- #else /* !CONFIG_THUMB2_KERNEL */
- thumb = false;
- if (addr & 0x3)
- return -EINVAL;
- insn = __mem_to_opcode_arm(*p->addr);
- decode_insn = arm_probes_decode_insn;
- actions = kprobes_arm_actions;
- checkers = kprobes_arm_checkers;
- #endif
- p->opcode = insn;
- p->ainsn.insn = tmp_insn;
- switch ((*decode_insn)(insn, &p->ainsn, true, actions, checkers)) {
- case INSN_REJECTED: /* not supported */
- return -EINVAL;
- case INSN_GOOD: /* instruction uses slot */
- p->ainsn.insn = get_insn_slot();
- if (!p->ainsn.insn)
- return -ENOMEM;
- for (is = 0; is < MAX_INSN_SIZE; ++is)
- p->ainsn.insn[is] = tmp_insn[is];
- flush_insns(p->ainsn.insn,
- sizeof(p->ainsn.insn[0]) * MAX_INSN_SIZE);
- p->ainsn.insn_fn = (probes_insn_fn_t *)
- ((uintptr_t)p->ainsn.insn | thumb);
- break;
- case INSN_GOOD_NO_SLOT: /* instruction doesn't need insn slot */
- p->ainsn.insn = NULL;
- break;
- }
- /*
- * Never instrument insn like 'str r0, [sp, +/-r1]'. Also, insn likes
- * 'str r0, [sp, #-68]' should also be prohibited.
- * See __und_svc.
- */
- if ((p->ainsn.stack_space < 0) ||
- (p->ainsn.stack_space > MAX_STACK_SIZE))
- return -EINVAL;
- return 0;
- }
- /* Return:
- * INSN_REJECTED If instruction is one not allowed to kprobe,
- * INSN_GOOD If instruction is supported and uses instruction slot,
- * INSN_GOOD_NO_SLOT If instruction is supported but doesn't use its slot.
- *
- * For instructions we don't want to kprobe (INSN_REJECTED return result):
- * These are generally ones that modify the processor state making
- * them "hard" to simulate such as switches processor modes or
- * make accesses in alternate modes. Any of these could be simulated
- * if the work was put into it, but low return considering they
- * should also be very rare.
- */
- enum probes_insn __kprobes
- arm_probes_decode_insn(probes_opcode_t insn, struct arch_probes_insn *asi,
- bool emulate, const union decode_action *actions,
- const struct decode_checker *checkers[])
- {
- asi->insn_singlestep = arm_singlestep;
- asi->insn_check_cc = probes_condition_checks[insn>>28];
- return probes_decode_insn(insn, asi, probes_decode_arm_table, false,
- emulate, actions, checkers);
- }
該arm_probes_decode_insn調用流程會對kprobe->ainsn結構進行初始化(該結構架構相關),其中函數指針insn_singlestep初始化爲arm_singlestep,它用於kprobe觸發後的單步執行,而函數insn_check_cc初始化爲probes_condition_checks[insn>>28],它是一個函數指針數組,以指令的高4位爲索引,用於kprobe觸發後進行條件異常檢測。
- probes_check_cc * const probes_condition_checks[16] = {
- &__check_eq, &__check_ne, &__check_cs, &__check_cc,
- &__check_mi, &__check_pl, &__check_vs, &__check_vc,
- &__check_hi, &__check_ls, &__check_ge, &__check_lt,
- &__check_gt, &__check_le, &__check_al, &__check_al
- };
現以do_fork函數爲例,來看一下這裏的insn_check_cc函數指針初始化爲那個函數了:
反彙編vmlinux後找到do_fork,對應的入口地址爲0xc0022798,彙編指令爲mov,機器碼爲e1a0c00d,計算後值爲0xe=15,因此選中的條件異常檢測處理函數爲__check_al;
- c0022798 <do_fork>:
- do_fork():
- 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的指令寫到被探測點處替換原始指令。
- static void arm_kprobe(struct kprobe *kp)
- {
- if (unlikely(kprobe_ftrace(kp))) {
- arm_kprobe_ftrace(kp);
- return;
- }
- /*
- * Here, since __arm_kprobe() doesn't use stop_machine(),
- * this doesn't cause deadlock on text_mutex. So, we don't
- * need get_online_cpus().
- */
- mutex_lock(&text_mutex);
- __arm_kprobe(kp);
- mutex_unlock(&text_mutex);
- }
- void arch_arm_kprobe(struct kprobe *p)
- {
- text_poke(p->addr, ((unsigned char []){BREAKPOINT_INSTRUCTION}), 1);
- }
- void __kprobes arch_arm_kprobe(struct kprobe *p)
- {
- unsigned int brkp;
- void *addr;
- if (IS_ENABLED(CONFIG_THUMB2_KERNEL)) {
- ......
- } else {
- kprobe_opcode_t insn = p->opcode;
- addr = p->addr;
- brkp = KPROBE_ARM_BREAKPOINT_INSTRUCTION;
- if (insn >= 0xe0000000)
- brkp |= 0xe0000000; /* Unconditional instruction */
- else
- brkp |= insn & 0xf0000000; /* Copy condition from insn */
- }
- patch_text(addr, brkp);
- }
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函數:
- /*
- * This is the second or subsequent kprobe at the address - handle
- * the intricacies
- */
- static int register_aggr_kprobe(struct kprobe *orig_p, struct kprobe *p)
- {
- int ret = 0;
- struct kprobe *ap = orig_p;
- /* For preparing optimization, jump_label_text_reserved() is called */
- jump_label_lock();
- /*
- * Get online CPUs to avoid text_mutex deadlock.with stop machine,
- * which is invoked by unoptimize_kprobe() in add_new_kprobe()
- */
- get_online_cpus();
- mutex_lock(&text_mutex);
- if (!kprobe_aggrprobe(orig_p)) {
- /* If orig_p is not an aggr_kprobe, create new aggr_kprobe. */
- ap = alloc_aggr_kprobe(orig_p);
- if (!ap) {
- ret = -ENOMEM;
- goto out;
- }
- init_aggr_kprobe(ap, orig_p);
- } else if (kprobe_unused(ap))
- /* This probe is going to die. Rescue it */
- reuse_unused_kprobe(ap);
- if (kprobe_gone(ap)) {
- /*
- * Attempting to insert new probe at the same location that
- * had a probe in the module vaddr area which already
- * freed. So, the instruction slot has already been
- * released. We need a new slot for the new probe.
- */
- ret = arch_prepare_kprobe(ap);
- if (ret)
- /*
- * Even if fail to allocate new slot, don't need to
- * free aggr_probe. It will be used next time, or
- * freed by unregister_kprobe.
- */
- goto out;
- /* Prepare optimized instructions if possible. */
- prepare_optimized_kprobe(ap);
- /*
- * Clear gone flag to prevent allocating new slot again, and
- * set disabled flag because it is not armed yet.
- */
- ap->flags = (ap->flags & ~KPROBE_FLAG_GONE)
- | KPROBE_FLAG_DISABLED;
- }
- /* Copy ap's insn slot to p */
- copy_kprobe(ap, p);
- ret = add_new_kprobe(ap, p);
- out:
- mutex_unlock(&text_mutex);
- put_online_cpus();
- jump_label_unlock();
- if (ret == 0 && kprobe_disabled(ap) && !kprobe_disabled(p)) {
- ap->flags &= ~KPROBE_FLAG_DISABLED;
- if (!kprobes_all_disarmed)
- /* Arm the breakpoint again. */
- arm_kprobe(ap);
- }
- return ret;
- }
函數的第一個入參orig_p是在全局hash表中找到的已經註冊的kprobe實例,第二個入參是本次需要註冊的kprobe實例。首先在完成了必要的上鎖操作後就調用kprobe_aggrprobe函數檢查orig_p是否是一個aggregator。
- /* Return true if the kprobe is an aggregator */
- static inline int kprobe_aggrprobe(struct kprobe *p)
- {
- return p->pre_handler == aggr_pre_handler;
- }
- /*
- * Fill in the required fields of the "manager kprobe". Replace the
- * earlier kprobe in the hlist with the manager kprobe
- */
- static void init_aggr_kprobe(struct kprobe *ap, struct kprobe *p)
- {
- /* Copy p's insn slot to ap */
- copy_kprobe(p, ap);
- flush_insn_slot(ap);
- ap->addr = p->addr;
- ap->flags = p->flags & ~KPROBE_FLAG_OPTIMIZED;
- ap->pre_handler = aggr_pre_handler;
- ap->fault_handler = aggr_fault_handler;
- /* We don't care the kprobe which has gone. */
- if (p->post_handler && !kprobe_gone(p))
- ap->post_handler = aggr_post_handler;
- if (p->break_handler && !kprobe_gone(p))
- ap->break_handler = aggr_break_handler;
- INIT_LIST_HEAD(&ap->list);
- INIT_HLIST_NODE(&ap->hlist);
- list_add_rcu(&p->list, &ap->list);
- hlist_replace_rcu(&p->hlist, &ap->hlist);
- }
回到register_aggr_kprobe函數中,如果本次是第二次以上向同一地址註冊kprobe實例,則此時的orig_p已經是aggr kprobe了,則會調用kprobe_unused函數判斷該kprobe是否爲被使用,若是則調用reuse_unused_kprobe函數重新啓用,但是對於沒有開啓CONFIG_OPTPROBES選項的情況,邏輯上是不存在這種情況的,因此reuse_unused_kprobe函數的實現僅僅是一段打印後就立即觸發BUG_ON。
- /* There should be no unused kprobes can be reused without optimization */
- static void reuse_unused_kprobe(struct kprobe *ap)
- {
- printk(KERN_ERR "Error: There should be no unused kprobe here.\n");
- BUG_ON(kprobe_unused(ap));
- }
繼續往下分析,下面來討論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鏈表中:
- /*
- * Add the new probe to ap->list. Fail if this is the
- * second jprobe at the address - two jprobes can't coexist
- */
- static int add_new_kprobe(struct kprobe *ap, struct kprobe *p)
- {
- BUG_ON(kprobe_gone(ap) || kprobe_gone(p));
- if (p->break_handler || p->post_handler)
- unoptimize_kprobe(ap, true); /* Fall back to normal kprobe */
- if (p->break_handler) {
- if (ap->break_handler)
- return -EEXIST;
- list_add_tail_rcu(&p->list, &ap->list);
- ap->break_handler = aggr_break_handler;
- } else
- list_add_rcu(&p->list, &ap->list);
- if (p->post_handler && !ap->post_handler)
- ap->post_handler = aggr_post_handler;
- return 0;
- }
回到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():
- static int __kprobes kprobe_trap_handler(struct pt_regs *regs, unsigned int instr)
- {
- unsigned long flags;
- local_irq_save(flags);
- kprobe_handler(regs);
- local_irq_restore(flags);
- return 0;
- }
kprobe_handler函數的實現比較長,分段來看:
- /*
- * Called with IRQs disabled. IRQs must remain disabled from that point
- * all the way until processing this kprobe is complete. The current
- * kprobes implementation cannot process more than one nested level of
- * kprobe, and that level is reserved for user kprobe handlers, so we can't
- * risk encountering a new kprobe in an interrupt handler.
- */
- void __kprobes kprobe_handler(struct pt_regs *regs)
- {
- struct kprobe *p, *cur;
- struct kprobe_ctlblk *kcb;
- kcb = get_kprobe_ctlblk();
- cur = kprobe_running();
- #ifdef CONFIG_THUMB2_KERNEL
- ......
- #else /* ! CONFIG_THUMB2_KERNEL */
- p = get_kprobe((kprobe_opcode_t *)regs->ARM_pc);
- #endif
- /* per-cpu kprobe control block */
- struct kprobe_ctlblk {
- unsigned int kprobe_status;
- struct prev_kprobe prev_kprobe;
- struct pt_regs jprobe_saved_regs;
- char jprobes_stack[MAX_STACK_SIZE];
- };
- #define KPROBE_HIT_ACTIVE 0x00000001 //開始處理kprobe
- #define KPROBE_HIT_SS 0x00000002 //kprobe單步執行階段
- #define KPROBE_REENTER 0x00000004 //重複觸發kprobe
- #define KPROBE_HIT_SSDONE 0x00000008 //kprobe單步執行階段結束
而prev_kprobe則是用於在kprobe重入情況下保存當前正在處理的kprobe實例和狀態的。內核爲每個cpu都定義了一個該類型全局變量。然後調用kprobe_running函數獲取當前cpu上正在處理的kprobe:
- /* kprobe_running() will just return the current_kprobe on this CPU */
- static inline struct kprobe *kprobe_running(void)
- {
- return (__this_cpu_read(current_kprobe));
- }
1、p和cur的kprobe實例同時存在
- /* Kprobe is pending, so we're recursing. */
- switch (kcb->kprobe_status) {
- case KPROBE_HIT_ACTIVE:
- case KPROBE_HIT_SSDONE:
- /* A pre- or post-handler probe got us here. */
- kprobes_inc_nmissed_count(p);
- save_previous_kprobe(kcb);
- set_current_kprobe(p);
- kcb->kprobe_status = KPROBE_REENTER;
- singlestep(p, regs, kcb);
- restore_previous_kprobe(kcb);
- break;
- default:
- /* impossible cases */
- BUG();
- }
- static void __kprobes save_previous_kprobe(struct kprobe_ctlblk *kcb)
- {
- kcb->prev_kprobe.kp = kprobe_running();
- kcb->prev_kprobe.status = kcb->kprobe_status;
- }
- static void __kprobes save_previous_kprobe(struct kprobe_ctlblk *kcb)
- {
- kcb->prev_kprobe.kp = kprobe_running();
- kcb->prev_kprobe.status = kcb->kprobe_status;
- }
注意,以上重入的處理流程僅僅是單步執行了被探測的函數,並不會調用kprobe的pre_handle回調函數(遞增nmissed字段的原因就在此),因此用戶並不會感知到kprobe被實際觸發了。
2、p存在但cur不存在
- } else if (p->ainsn.insn_check_cc(regs->ARM_cpsr)) {
- /* Probe hit and conditional execution check ok. */
- set_current_kprobe(p);
- kcb->kprobe_status = KPROBE_HIT_ACTIVE;
- /*
- * If we have no pre-handler or it returned 0, we
- * continue with normal processing. If we have a
- * pre-handler and it returned non-zero, it prepped
- * for calling the break_handler below on re-entry,
- * so get out doing nothing more here.
- */
- if (!p->pre_handler || !p->pre_handler(p, regs)) {
- kcb->kprobe_status = KPROBE_HIT_SS;
- singlestep(p, regs, kcb);
- if (p->post_handler) {
- kcb->kprobe_status = KPROBE_HIT_SSDONE;
- p->post_handler(p, regs, 0);
- }
- reset_current_kprobe();
- }
- } else {
- /*
- * Probe hit but conditional execution check failed,
- * so just skip the instruction and continue as if
- * nothing had happened.
- */
- singlestep_skip(p, regs);
- }
這種情況就是最爲一般的情況,即當前kprobe是首次觸發,前面並沒有其他的kprobe流程正在處理。這裏會首先調用p->ainsn.insn_check_cc註冊函數來進行條件異常檢測,這個函數在前文註冊kprobe的流程中已經看到根據不同的被探測指令被註冊成不同的函數了,入參是觸發異常時的cpsr程序狀態寄存器值。
對於前文中看到的do_fork函數入口彙編指令mov設置的__check_al檢測函數來說,它將永遠返回true,而movne指令的__check_ne檢測函數則會對cpsr進行判斷:
- static unsigned long __kprobes __check_ne(unsigned long cpsr)
- {
- return (~cpsr) & PSR_Z_BIT;
- }
(1)如果條件異常檢測通過,那也同樣調用set_current_kprobe函數設置當前正在處理的kprobe並更新kprobe狀態標識爲KPROBE_HIT_ACTIVE,表明開始處理該kprobe。接下來就到關鍵的回調和單步執行流程了,首先判斷kprobe的pre_handler函數是否被註冊,在註冊的情況下調用它。對於單kprobe註冊的情況很簡單了,直接調用註冊函數即可(這樣前面kprobe_example中handler_pre函數就在此調用),但是對於前文中分析的多kprobe註冊的情況(aggr kprobe),則會調用到aggr_pre_handler函數:
- /*
- * Aggregate handlers for multiple kprobes support - these handlers
- * take care of invoking the individual kprobe handlers on p->list
- */
- static int aggr_pre_handler(struct kprobe *p, struct pt_regs *regs)
- {
- struct kprobe *kp;
- list_for_each_entry_rcu(kp, &p->list, list) {
- if (kp->pre_handler && likely(!kprobe_disabled(kp))) {
- set_kprobe_instance(kp);
- if (kp->pre_handler(kp, regs))
- return 1;
- }
- reset_kprobe_instance();
- }
- return 0;
- }
- NOKPROBE_SYMBOL(aggr_pre_handler);
- /* We have preemption disabled.. so it is safe to use __ versions */
- static inline void set_kprobe_instance(struct kprobe *kp)
- {
- __this_cpu_write(kprobe_instance, kp);
- }
- static inline void reset_kprobe_instance(void)
- {
- __this_cpu_write(kprobe_instance, NULL);
- }
這裏可能會存在這樣的疑問,爲什麼kcb->kprobe_status = KPROBE_HIT_SSDONE;這條狀態賦值語句會放在條件判斷內部,而不是在單步執行完以後?其實對於當前的上下文邏輯來看效果是一樣的,因爲若沒有註冊post_handler,就會立即執行reset_current_kprobe函數解除kprobe的綁定,因此不會對邏輯產生影響。
(2)如果條件異常檢測不通過則調用singlestep_skip函數跳過當前的指令,繼續執行後面的指令,就像什麼都沒有發生過一樣
- static void __kprobes
- singlestep_skip(struct kprobe *p, struct pt_regs *regs)
- {
- #ifdef CONFIG_THUMB2_KERNEL
- ......
- #else
- regs->ARM_pc += 4;
- #endif
- }
3、p不存在但cur存在
- } else if (cur) {
- /* We probably hit a jprobe. Call its break handler. */
- if (cur->break_handler && cur->break_handler(cur, regs)) {
- kcb->kprobe_status = KPROBE_HIT_SS;
- singlestep(cur, regs, kcb);
- if (cur->post_handler) {
- kcb->kprobe_status = KPROBE_HIT_SSDONE;
- cur->post_handler(cur, regs, 0);
- }
- }
- reset_current_kprobe();
4、p和cur都不存在
- } else {
- /*
- * The probe was removed and a race is in progress.
- * There is nothing we can do about it. Let's restart
- * the instruction. By the time we can restart, the
- * real instruction will be there.
- */
- }
至此arm架構的kprobe觸發及處理整體流程就分析完了。下面分析x86_64架構的實現,總體大同小異,其中的相同之處就不再分析了。
3.2、x86_64架構實現
- /* May run on IST stack. */
- dotraplinkage void notrace do_int3(struct pt_regs *regs, long error_code)
- {
- ......
- #ifdef CONFIG_KPROBES
- if (kprobe_int3_handler(regs))
- goto exit;
- #endif
- ......
- }
- NOKPROBE_SYMBOL(do_int3);
- /*
- * Interrupts are disabled on entry as trap3 is an interrupt gate and they
- * remain disabled throughout this function.
- */
- int kprobe_int3_handler(struct pt_regs *regs)
- {
- kprobe_opcode_t *addr;
- struct kprobe *p;
- struct kprobe_ctlblk *kcb;
- if (user_mode(regs))
- return 0;
- addr = (kprobe_opcode_t *)(regs->ip - sizeof(kprobe_opcode_t));
- /*
- * We don't want to be preempted for the entire
- * duration of kprobe processing. We conditionally
- * re-enable preemption at the end of this function,
- * and also in reenter_kprobe() and setup_singlestep().
- */
- preempt_disable();
- kcb = get_kprobe_ctlblk();
- 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結構體的定義
- /* per-cpu kprobe control block */
- struct kprobe_ctlblk {
- unsigned long kprobe_status;
- unsigned long kprobe_old_flags;
- unsigned long kprobe_saved_flags;
- unsigned long *jprobe_saved_sp;
- struct pt_regs jprobe_saved_regs;
- kprobe_opcode_t jprobes_stack[MAX_STACK_SIZE];
- struct prev_kprobe prev_kprobe;
- };
下面回到函數中根據不同的情況分別分析:
1、p存在且curent_kprobe存在
對於kprobe重入的情況,調用reenter_kprobe函數單獨處理:
- if (kprobe_running()) {
- if (reenter_kprobe(p, regs, kcb))
- return 1;
- /*
- * We have reentered the kprobe_handler(), since another probe was hit while
- * within the handler. We save the original kprobes variables and just single
- * step on the instruction of the new probe without calling any user handlers.
- */
- static int reenter_kprobe(struct kprobe *p, struct pt_regs *regs,
- struct kprobe_ctlblk *kcb)
- {
- switch (kcb->kprobe_status) {
- case KPROBE_HIT_SSDONE:
- case KPROBE_HIT_ACTIVE:
- case KPROBE_HIT_SS:
- kprobes_inc_nmissed_count(p);
- setup_singlestep(p, regs, kcb, 1);
- break;
- case KPROBE_REENTER:
- /* A probe has been hit in the codepath leading up to, or just
- * after, single-stepping of a probed instruction. This entire
- * codepath should strictly reside in .kprobes.text section.
- * Raise a BUG or we'll continue in an endless reentering loop
- * and eventually a stack overflow.
- */
- printk(KERN_WARNING "Unrecoverable kprobe detected at %p.\n",
- p->addr);
- dump_kprobe(p);
- BUG();
- default:
- /* impossible cases */
- WARN_ON(1);
- return 0;
- }
- return 1;
- }
- NOKPROBE_SYMBOL(reenter_kprobe);
2、p存在但curent_kprobe不存在
- } else {
- set_current_kprobe(p, regs, kcb);
- kcb->kprobe_status = KPROBE_HIT_ACTIVE;
- /*
- * If we have no pre-handler or it returned 0, we
- * continue with normal processing. If we have a
- * pre-handler and it returned non-zero, it prepped
- * for calling the break_handler below on re-entry
- * for jprobe processing, so get out doing nothing
- * more here.
- */
- if (!p->pre_handler || !p->pre_handler(p, regs))
- setup_singlestep(p, regs, kcb, 0);
- return 1;
- }
這是一般最通用的kprobe執行流程,首先調用set_current_kprobe綁定p爲當前正在處理的kprobe:
- static nokprobe_inline void
- set_current_kprobe(struct kprobe *p, struct pt_regs *regs,
- struct kprobe_ctlblk *kcb)
- {
- __this_cpu_write(current_kprobe, p);
- kcb->kprobe_saved_flags = kcb->kprobe_old_flags
- = (regs->flags & (X86_EFLAGS_TF | X86_EFLAGS_IF));
- if (p->ainsn.if_modifier)
- kcb->kprobe_saved_flags &= ~X86_EFLAGS_IF;
- }
3、p不存在且被探測地址的指令也不是BREAKPOINT_INSTRUCTION
- } else if (*addr != BREAKPOINT_INSTRUCTION) {
- /*
- * The breakpoint instruction was removed right
- * after we hit it. Another cpu has removed
- * either a probepoint or a debugger breakpoint
- * at this address. In either case, no further
- * handling of this interrupt is appropriate.
- * Back up over the (now missing) int3 and run
- * the original instruction.
- */
- regs->ip = (unsigned long)addr;
- preempt_enable_no_resched();
- return 1;
這種情況表示kprobe可能已經被其他CPU註銷了,則讓他執行原始指令即可,因此這裏設置regs->ip值爲addr並重新開啓內核搶佔返回1。
4、p不存在但curent_kprobe存在
- } else if (kprobe_running()) {
- p = __this_cpu_read(current_kprobe);
- if (p->break_handler && p->break_handler(p, regs)) {
- if (!skip_singlestep(p, regs, kcb))
- setup_singlestep(p, regs, kcb, 0);
- return 1;
- }
這種情況一般用於實現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架構實現
- static inline void __kprobes
- singlestep(struct kprobe *p, struct pt_regs *regs, struct kprobe_ctlblk *kcb)
- {
- p->ainsn.insn_singlestep(p->opcode, &p->ainsn, regs);
- }
- static void __kprobes arm_singlestep(probes_opcode_t insn,
- struct arch_probes_insn *asi, struct pt_regs *regs)
- {
- regs->ARM_pc += 4;
- asi->insn_handler(insn, asi, regs);
- }
- const union decode_action kprobes_arm_actions[NUM_PROBES_ARM_ACTIONS] = {
- [PROBES_PRELOAD_IMM] = {.handler = probes_simulate_nop},
- [PROBES_PRELOAD_REG] = {.handler = probes_simulate_nop},
- [PROBES_BRANCH_IMM] = {.handler = simulate_blx1},
- [PROBES_MRS] = {.handler = simulate_mrs},
- [PROBES_BRANCH_REG] = {.handler = simulate_blx2bx},
- [PROBES_CLZ] = {.handler = emulate_rd12rm0_noflags_nopc},
- [PROBES_SATURATING_ARITHMETIC] = {
- .handler = emulate_rd12rn16rm0_rwflags_nopc},
- [PROBES_MUL1] = {.handler = emulate_rdlo12rdhi16rn0rm8_rwflags_nopc},
- [PROBES_MUL2] = {.handler = emulate_rd16rn12rm0rs8_rwflags_nopc},
- [PROBES_SWP] = {.handler = emulate_rd12rn16rm0_rwflags_nopc},
- [PROBES_LDRSTRD] = {.handler = emulate_ldrdstrd},
- ......
- }
- void __kprobes simulate_mov_ipsp(probes_opcode_t insn,
- struct arch_probes_insn *asi, struct pt_regs *regs)
- {
- regs->uregs[12] = regs->uregs[13];
- }
以上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()。下面來簡單看一下:
- static void setup_singlestep(struct kprobe *p, struct pt_regs *regs,
- struct kprobe_ctlblk *kcb, int reenter)
- {
- if (setup_detour_execution(p, regs, reenter))
- return;
- ......
- if (reenter) {
- save_previous_kprobe(kcb);
- set_current_kprobe(p, regs, kcb);
- kcb->kprobe_status = KPROBE_REENTER;
- } else
- kcb->kprobe_status = KPROBE_HIT_SS;
- /* Prepare real single stepping */
- clear_btf();
- regs->flags |= X86_EFLAGS_TF;
- regs->flags &= ~X86_EFLAGS_IF;
- /* single step inline if the instruction is an int3 */
- if (p->opcode == BREAKPOINT_INSTRUCTION)
- regs->ip = (unsigned long)p->addr;
- else
- regs->ip = (unsigned long)p->ainsn.insn;
- }
接下來考試準備單步執行,首先設置regs->flags中的TF位並清空IF位,同時把int3異常返回的指令寄存器地址改爲前面保存的被探測指令,當int3異常返回時這些設置就會生效,即立即執行保存的原始指令(注意這裏是在觸發int3之前原來的上下文中執行,因此直接執行原始指令即可,無需特別的模擬操作)。該函數返回後do_int3函數立即返回,由於cpu的標識寄存器被設置,在單步執行完被探測指令後立即觸發debug異常,進入debug異常處理函數do_debug。
- dotraplinkage void do_debug(struct pt_regs *regs, long error_code)
- {
- ......
- #ifdef CONFIG_KPROBES
- if (kprobe_debug_handler(regs))
- goto exit;
- #endif
- ......
- exit:
- ist_exit(regs, prev_state);
- }
- /*
- * Interrupts are disabled on entry as trap1 is an interrupt gate and they
- * remain disabled throughout this function.
- */
- int kprobe_debug_handler(struct pt_regs *regs)
- {
- struct kprobe *cur = kprobe_running();
- struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();
- if (!cur)
- return 0;
- resume_execution(cur, regs, kcb);
- regs->flags |= kcb->kprobe_saved_flags;
- if ((kcb->kprobe_status != KPROBE_REENTER) && cur->post_handler) {
- kcb->kprobe_status = KPROBE_HIT_SSDONE;
- cur->post_handler(cur, regs, 0);
- }
- /* Restore back the original saved kprobes variables and continue. */
- if (kcb->kprobe_status == KPROBE_REENTER) {
- restore_previous_kprobe(kcb);
- goto out;
- }
- reset_current_kprobe();
- out:
- preempt_enable_no_resched();
- /*
- * if somebody else is singlestepping across a probe point, flags
- * will have TF set, in which case, continue the remaining processing
- * of do_debug, as if this is not a probe hit.
- */
- if (regs->flags & X86_EFLAGS_TF)
- return 0;
- return 1;
- }
- NOKPROBE_SYMBOL(kprobe_debug_handler);
以上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
- static inline int notify_page_fault(struct pt_regs *regs, unsigned int fsr)
- {
- int ret = 0;
- if (!user_mode(regs)) {
- /* kprobe_running() needs smp_processor_id() */
- preempt_disable();
- if (kprobe_running() && kprobe_fault_handler(regs, fsr))
- ret = 1;
- preempt_enable();
- }
- return ret;
- }
- int __kprobes kprobe_fault_handler(struct pt_regs *regs, unsigned int fsr)
- {
- struct kprobe *cur = kprobe_running();
- struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();
- switch (kcb->kprobe_status) {
- case KPROBE_HIT_SS:
- case KPROBE_REENTER:
- /*
- * We are here because the instruction being single
- * stepped caused a page fault. We reset the current
- * kprobe and the PC to point back to the probe address
- * and allow the page fault handler to continue as a
- * normal page fault.
- */
- regs->ARM_pc = (long)cur->addr;
- if (kcb->kprobe_status == KPROBE_REENTER) {
- restore_previous_kprobe(kcb);
- } else {
- reset_current_kprobe();
- }
- break;
- case KPROBE_HIT_ACTIVE:
- case KPROBE_HIT_SSDONE:
- /*
- * We increment the nmissed count for accounting,
- * we can also use npre/npostfault count for accounting
- * these specific fault cases.
- */
- kprobes_inc_nmissed_count(cur);
- /*
- * We come here because instructions in the pre/post
- * handler caused the page_fault, this could happen
- * if handler tries to access user space by
- * copy_from_user(), get_user() etc. Let the
- * user-specified handler try to fix it.
- */
- if (cur->fault_handler && cur->fault_handler(cur, regs, fsr))
- return 1;
- break;
- default:
- break;
- }
- return 0;
- }
5.2、x86_64調用流程
1、do_page_fault->__do_page_fault->kprobes_fault
- static nokprobe_inline int kprobes_fault(struct pt_regs *regs)
- {
- int ret = 0;
- /* kprobe_running() needs smp_processor_id() */
- if (kprobes_built_in() && !user_mode(regs)) {
- preempt_disable();
- if (kprobe_running() && kprobe_fault_handler(regs, 14))
- ret = 1;
- preempt_enable();
- }
- return ret;
- }
2、do_general_protection->notify_die->kprobe_exceptions_notify
- int kprobe_exceptions_notify(struct notifier_block *self, unsigned long val,
- void *data)
- {
- struct die_args *args = data;
- int ret = NOTIFY_DONE;
- if (args->regs && user_mode(args->regs))
- return ret;
- if (val == DIE_GPF) {
- /*
- * To be potentially processing a kprobe fault and to
- * trust the result from kprobe_running(), we have
- * be non-preemptible.
- */
- if (!preemptible() && kprobe_running() &&
- kprobe_fault_handler(args->regs, args->trapnr))
- ret = NOTIFY_STOP;
- }
- return ret;
- }
- int kprobe_fault_handler(struct pt_regs *regs, int trapnr)
- {
- struct kprobe *cur = kprobe_running();
- struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();
- if (unlikely(regs->ip == (unsigned long)cur->ainsn.insn)) {
- /* This must happen on single-stepping */
- WARN_ON(kcb->kprobe_status != KPROBE_HIT_SS &&
- kcb->kprobe_status != KPROBE_REENTER);
- /*
- * We are here because the instruction being single
- * stepped caused a page fault. We reset the current
- * kprobe and the ip points back to the probe address
- * and allow the page fault handler to continue as a
- * normal page fault.
- */
- regs->ip = (unsigned long)cur->addr;
- regs->flags |= kcb->kprobe_old_flags;
- if (kcb->kprobe_status == KPROBE_REENTER)
- restore_previous_kprobe(kcb);
- else
- reset_current_kprobe();
- preempt_enable_no_resched();
- } else if (kcb->kprobe_status == KPROBE_HIT_ACTIVE ||
- kcb->kprobe_status == KPROBE_HIT_SSDONE) {
- /*
- * We increment the nmissed count for accounting,
- * we can also use npre/npostfault count for accounting
- * these specific fault cases.
- */
- kprobes_inc_nmissed_count(cur);
- /*
- * We come here because instructions in the pre/post
- * handler caused the page_fault, this could happen
- * if handler tries to access user space by
- * copy_from_user(), get_user() etc. Let the
- * user-specified handler try to fix it first.
- */
- if (cur->fault_handler && cur->fault_handler(cur, regs, trapnr))
- return 1;
- /*
- * In case the user-specified fault handler returned
- * zero, try to fix up.
- */
- if (fixup_exception(regs))
- return 1;
- /*
- * fixup routine could not handle it,
- * Let do_page_fault() fix it.
- */
- }
- return 0;
- }
以上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