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是如何工作的。具體流程見下圖:

image

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");

程序中定義了一個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探測點。

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;
}

handler_pre回調函數的第一個入參是注冊的struct kprobe探測實例,第二個參數是保存的觸發斷點前的寄存器狀態,它在do_fork函數被調用之前被調用,該函數僅僅是打印了被探測點的地址,保存的個別寄存器參數。由於受CPU架構影響,這里對不同的架構進行了宏區分(雖然沒有實現arm架構的,但是支持的,可以自行添加);

/* 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
}

handler_post回調函數的前兩個入參同handler_pre,第三個參數目前尚未使用,全部為0;該函數在do_fork函數調用之后被調用,這里打印的內容同handler_pre類似。

/*
 * 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;
}

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模塊:

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*

加載到內核中后,隨便在終端上敲一個命令,可以看到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/ / /enabled
4、過濾屬性文件:events/kprobes/ / /filter
5、格式查詢屬性文件:events/kprobes/ / /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/ 目錄下生成對應名字的目錄
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/ / /enabled

開啟探測:echo 1 > events/kprobes/ / /enabled
暫停探測:echo 0 > events/kprobes/ / /enabled

3、events/kprobes/ / /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規則,函數入參14通過r0r3寄存器傳遞,多余的參數通過棧傳遞),由於入參為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初始化

image

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

接下來調用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()						\
......

例如其中的get_kprobe函數:

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;
}

由於沒有啟用THUMB2模式,這里arm_probes_decode_init主要是獲取PC和當前執行地址偏移值(ARM的流水線機制一般為8)以及設置相關寄存器值獲取方式等代碼;而register_undef_hook函數向全局undef_hook鏈表注冊了一個未定義指令異常處理的鈎子,相關的結構體如下:

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,
};

這樣在觸發未定義指令KPROBE_ARM_BREAKPOINT_INSTRUCTION(機器碼0x07f001f8)時將會調用到這里的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
};

其中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

image

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);

函數首先調用kprobe_addr函數初始化被探測點的地址p->addr。因為一般的探測模塊並不會指定想要探測的addr地址,同kprobe_example例程一樣通過傳入函數名來指定要探測的函數,kprobe_addr函數的作用就是將函數名轉換為最終的被探測地址:

再來看一下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;
}

首先檢測被探測地址不能在異常代碼段中並且地址必須是4字節對齊的,隨后取出被探測點的指令保存在kprobe->opcode中,並調用arm_probes_decode_insn函數來判斷被探測的指令是什么類型的:

/* 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);
}

這里假設不適用ftrace和optimize kprobe特性,將直接調用架構相關的函數arch_arm_kprobe,其中x86的實現如下:

void arch_arm_kprobe(struct kprobe *p)
{
	text_poke(p->addr, ((unsigned char []){BREAKPOINT_INSTRUCTION}), 1);
}

直接調用text_poke函數將addr地址處的指令替換為BREAKPOINT_INSTRUCTION指令(機器碼是0xCC),當正常執行流程執行到這條指令后就會觸發int3中斷,進而進入探測回調流程。再看一下arm的實現流程:

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;
}

在前文中看到,該函數會在對同一個被探測地址注冊多個kprobe實例時會被調用到,該函數會引入一個kprobe aggregator的概念,即由一個統一的kprobe實例接管所有注冊到該地址的kprobe。這個函數的注釋非常詳細,並不難理解,來簡單分析一下:

函數的第一個入參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;
}

它通過kprobe的pre_handler回調判斷,如果是aggregator則它的pre_handler回調函數會被替換成aggr_pre_handler函數。一般對於第二次注冊kprobe的情況顯然是不會滿足條件的,會調用alloc_aggr_kprobe函數創建一個,對於沒有開啟CONFIG_OPTPROBES選項的情況,alloc_aggr_kprobe僅僅是分配了一塊內存空間,然后調用init_aggr_kprobe函數初始化這個aggr kprobe。

/*
 * 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);
}

可以看到,這個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。

/* 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;
}

注意最主要的就是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():

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;
}

call_undef_hook()調用未定義指令的回調函數,對於KPROBE_ARM_BREAKPOINT_INSTRUCTION指令即調用到kprobe_trap_handler函數,其中入參struct pt_regs *regs保存的時執行異常指令時的寄存器信息,同時該函數在處理kprobe的流程時會禁用掉本地CPU的中斷。

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

注釋中說明了當前arm架構的kprobe實現不支持在中斷中多層kprobe重入,因此為了防止在處理一個kprobe期間由於中斷可能會導致多次觸發kprobe的情況,所以需要禁用中斷。函數首先調用get_kprobe_ctlblk函數獲取本cpu的per_cpu結構體變量kprobe_ctlblk,該結構體是架構相關的,arm的定義如下:

/* 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];
};

其中保存了kprobe的一些狀態信息以及jpboe用到的字段,目前需要關注的是其中的kprobe_status和prev_kprobe字段,其中kprobe_status代表了當前kprobe的處理狀態,一共包括以下幾種:

#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));
}

這里的current_kprobe也是一個per_cpu變量,其中保存了當前cpu正在處理的kprobe實例,若沒有正在處理的則為NULL。下面調用get_kprobe函數獲取本次要處理的kprobe,入參是regs->ARM_pc,即觸發異常指令所在的地址,也就是被探測點的地址,利用它就可以在全局hash表中找到注冊的kprobe實例了。接下來根據cur和p的存在情況進行多分支處理:

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();
			}

這種情況屬於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字段中去;

static void __kprobes save_previous_kprobe(struct kprobe_ctlblk *kcb)
{
	kcb->prev_kprobe.kp = kprobe_running();
	kcb->prev_kprobe.status = kcb->kprobe_status;
}

然后調用set_current_kprobe函數將本次需要處理的kprobe(p)設置到current_kprobe的per_cpu變量中去,並且更新kprobe_status狀態為KPROBE_REENTER,表示存在重入情況;接下來調用singlestep函數啟動單步執行,這個函數稍后再看;最后調用restore_previous_kprobe函數恢復前面所保存的kprobe。

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);

該函數的功能很直觀,即遍歷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。

/* 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);
}

回到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函數跳過當前的指令,繼續執行后面的指令,就像什么都沒有發生過一樣

static void __kprobes
singlestep_skip(struct kprobe *p, struct pt_regs *regs)
{
#ifdef CONFIG_THUMB2_KERNEL
	......
#else
	regs->ARM_pc += 4;
#endif
}

該函數僅僅修改了regs結構中的PC值,在kprobe處理結束后將從被探測指令之后的指令繼續執行。 這里就有一個疑問,如果不執行被探測點的原始指令,直接執行之后的指令難道不會出問題嗎?

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();

這種情況一般用於jprobe實現,函數調用cur kprobe的break_handler回調函數且在break_handler返回非0的情況下啟動單步執行和執行post_handler回調,最后一樣調用reset_current_kprobe函數解除cur kprobe綁定。該流程先不做詳細推演分析,后面分析jprobe實現時再細細分析。

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.
		 */
	}

這種情況表示當前kprobe已經被注銷了,但是可能在注銷的過程中(注銷的過程並不是原子操作)可能被其他執行流程搶占進而觸發該kprobe,對於這種情況什么都不需要做,直接返回即可。

至此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);

x86_64架構下,執行到前文中替換的BREAKPOINT_INSTRUCTION指令后將觸發INT3中斷,進而調用到do_int3函數。do_init3函數做的事情比較多,但是和kprobe相關的僅代碼中列出的這1處,下面來看kprobe_int3_handler函數,這個函數同arm結構的kprobe_handler函數很像,依然分段來分析:

/*
 * 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;
};

該定義比arm結構的多一些字段,其中kprobe_status字段不變,kprobe_old_flags和kprobe_saved_flags字段用於保存寄存器pt_regs的flag標識。
下面回到函數中根據不同的情況分別分析:

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);

這個流程同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不存在

		} 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;
}

這里在設置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

	} 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架構實現

arm架構單步執行的原理並不非常復雜(但是實現非常復雜),它本質上所做的就是執行被探測點的被替換前的“原始指令”,但是當前的上下文已經是kprobe的執行上下文了,不再是原始指令所處的上下文,所以單步執行流程無法直接執行原始指令,而是會調用其他函數來模擬實現原始指令以達到相同的效果,因此涉及的函數很多,基本每條不同的匯編指令都有不同的模擬函數。

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);
}

singlestep函數直接調用保存在arch_probes_insn結構中的insn_singlestep函數指針(該指針在注冊kprobe時由arm_probes_decode_insn函數負責初始化),即arm_singlestep函數。入參為保存的被探測點指令、arch_probes_insn結構地址及寄存器參數。

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);
}

首先讓寄存器參數中的PC加4,表示kprobe處理完成后將跳過觸發kprobe時的KPROBE_ARM_BREAKPOINT_INSTRUCTION指令繼續執行。然后調用insn_handler函數指針中設置的注冊函數,該函數指針由probes_decode_insn函數根據不同的原始指令被設置為不同的處理函數,它們被定義在kprobes_arm_actions數組中:

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},
	......
}

這里的函數眾多就不一一分析了,現仍然以do_fork函數的入口指令“mov ip, sp”為例,調用的函數為simulate_mov_ipsp:

void __kprobes simulate_mov_ipsp(probes_opcode_t insn,
		struct arch_probes_insn *asi, struct pt_regs *regs)
{
	regs->uregs[12] = regs->uregs[13];
}

這里的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()。下面來簡單看一下:

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;
}

首先在前文中已經介紹了,函數的最后一個入參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。

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);
首先調用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
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;
}

可見在觸發缺頁異常之后,若當前正在處理kprobe流程期間,會調用kprobe_fault_handler進行處理。

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;
}

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

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;
}

這個缺頁異常的調用流程同arm實現的幾乎完全一樣,就不贅述了。
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;
}

前文中init_kprobes初始化時會注冊die內核通知鏈kprobe_exceptions_nb,它的回調函數為kprobe_exceptions_notify,在內核觸發DIE_GPF類型的notify_die時,該函數會調用kprobe_fault_handler進行處理。下面來簡單看一下x86_64架構的kprobe_fault_handler函數實現:

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;
}

流程基本同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


免責聲明!

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



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