轉自:https://www.cnblogs.com/arnoldlu/p/9752061.html
kprobe調試技術是為了便於跟蹤內核函數執行狀態所設計的一種輕量級內核調試技術。
利用kprobe技術,可以在內核絕大多數函數中動態插入探測點,收集調試狀態所需信息而基本不影響原有執行流程。
kprobe提供三種探測手段:kprobe、jprobe和kretprobe,其中jprobe和kretprobe基於kprobe實現,分別應用於不同探測場景中。
可以通過兩種方式使用kprobe:第一種是編寫內核模塊,向內核注冊探測點,探測函數根據需要自行定制,但是使用不方便;
第二種是使用kprobes in ftrace,這種方式結合kprobe和ftrace,可以通過kprobe來優化ftrace跟蹤函數。
1. kprobe技術背景
如果需要知道內核函數是否被調用、被調用上下文、入參以及返回值,比較簡單的方法是加printk,但是效率低。
利用kprobe技術,用戶可以自定義自己的回調函數,可以再幾乎所有的函數中動態插入探測點。
當內核執行流程執行到指定的探測函數時,會調用該回調函數,用戶即可收集所需的信息了,同時內核最后還會回到原本的正常執行流程。如果用戶已經收集足夠的信息,不再需要繼續探測,則同樣可以動態的移除探測點。
kprobes技術包括的3種探測手段分別時kprobe、jprobe和kretprobe。
首先kprobe是最基本的探測方式,是實現后兩種的基礎,它可以在任意的位置放置探測點(就連函數內部的某條指令處也可以),它提供了探測點的調用前、調用后和內存訪問出錯3種回調方式,分別是pre_handler、post_handler和fault_handler,其中pre_handler函數將在被探測指令被執行前回調,post_handler會在被探測指令執行完畢后回調(注意不是被探測函數),fault_handler會在內存訪問出錯時被調用;jprobe基於kprobe實現,它用於獲取被探測函數的入參值;最后kretprobe從名字種就可以看出其用途了,它同樣基於kprobe實現,用於獲取被探測函數的返回值。
1.1 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。
1.2 kprobe原理
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、最后,執行流程回到被探測指令之后的正常流程繼續執行。
2. kprobe使用實例
2.1 編寫kprobe探測模塊
2.1.1 struct kprobe結構體
內核提供了struct kprobe表示一個探測點,以及一系列API接口,用戶可以通過這些接口實現回調函數並實現struct kprobe結構,然后將它注冊到內核的kprobe子系統中來達到探測的目的。
struct kprobe {
struct hlist_node hlist;-----------------------------------------------被用於kprobe全局hash,索引值為被探測點的地址。
/* list of kprobes for multi-handler support */
struct list_head list;-------------------------------------------------用於鏈接同一被探測點的不同探測kprobe。
/*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;---------------------------------------------------被探測點在函數內部的偏移,用於探測函數內核的指令,如果該值為0表示函數的入口。
/* Called before addr is executed. */
kprobe_pre_handler_t pre_handler;--------------------------------------被探測點指令執行之前調用的回調函數。
/* Called after addr is executed, unless... */
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;-------------------------------------------------------------狀態標記。
};
2.1.2 kprobe API函數
內核使用kprobe,可以使用register_kprobe()/unregister_kprobe()進行注冊/卸載,還可以臨時關閉/使能探測點。
int register_kprobe(struct kprobe *p);--------------------------注冊kprobe探測點 void unregister_kprobe(struct kprobe *p);-----------------------卸載kprobe探測點 int register_kprobes(struct kprobe **kps, int num);-------------注冊多個kprobe探測點 void unregister_kprobes(struct kprobe **kps, int num);----------卸載多個kprobe探測點 int disable_kprobe(struct kprobe *kp);--------------------------暫停指定定kprobe探測點 int enable_kprobe(struct kprobe *kp);---------------------------回復指定kprobe探測點 void dump_kprobe(struct kprobe *kp);----------------------------打印指定kprobe探測點的名稱、地址、偏移
2.1.3 kprobe_example.c解讀
下面以內核中samples/kprobes/kprobe_example.c為例,介紹如何使用kprobe進行內核函數探測。
該kprobe實例實現了_do_fork的探測,該函數會在fork系統調用或者kernel_kthread創建內核線程時被調用。
對原%p修改為%pF后,可讀性更強。可以顯示函數名稱以及偏移量。
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#define MAX_SYMBOL_LEN 64
static char symbol[MAX_SYMBOL_LEN] = "_do_fork";
module_param_string(symbol, symbol, sizeof(symbol), 0644);
/* For each probe you need to allocate a kprobe structure */
static struct kprobe kp = {---------------------------------------------------------定義一個實例kp並初始化symbol_name為"_do_fork",將探測_do_fork函數。
.symbol_name = symbol,
};
/* kprobe pre_handler: called just before the probed instruction is executed */
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
#ifdef CONFIG_X86
pr_info("<%s> pre_handler: p->addr = %pF, ip = %lx, flags = 0x%lx\n",
p->symbol_name, p->addr, regs->ip, regs->flags);
#endif
#ifdef CONFIG_ARM64
pr_info("<%s> pre_handler: p->addr = %pF, pc = 0x%lx,"
" pstate = 0x%lx\n",
p->symbol_name, p->addr, (long)regs->pc, (long)regs->pstate);
#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
pr_info("<%s> post_handler: p->addr = %pF, flags = 0x%lx\n",
p->symbol_name, p->addr, regs->flags);
#endif
#ifdef CONFIG_ARM64
pr_info("<%s> post_handler: p->addr = %pF, pstate = 0x%lx\n",
p->symbol_name, p->addr, (long)regs->pstate);
#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)
{
pr_info("fault_handler: p->addr = %pF, trap #%dn", p->addr, trapnr);
/* Return 0 because we don't handle the fault. */
return 0;
}
static int __init kprobe_init(void)
{
int ret;
kp.pre_handler = handler_pre;---------------------------------------------------初始化kp的三個回調函數。
kp.post_handler = handler_post;
kp.fault_handler = handler_fault;
ret = register_kprobe(&kp);-----------------------------------------------------注冊kp探測點到內核。
if (ret < 0) {
pr_err("register_kprobe failed, returned %d\n", ret);
return ret;
}
pr_info("Planted kprobe at %pF\n", kp.addr);
return 0;
}
static void __exit kprobe_exit(void)
{
unregister_kprobe(&kp);
pr_info("kprobe at %pF unregistered\n", kp.addr);
}
module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");
模塊的編譯Makefile如下:
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*
執行結果如下:
[ 9363.905687] Planted kprobe at _do_fork+0x0/0x3f0 [ 9366.924852] <_do_fork> pre_handler: p->addr = _do_fork+0x0/0x3f0, ip = ffffffff88a86a61, flags = 0x246 [ 9366.924858] <_do_fork> post_handler: p->addr = _do_fork+0x0/0x3f0, flags = 0x246 [ 9366.932935] <_do_fork> pre_handler: p->addr = _do_fork+0x0/0x3f0, ip = ffffffff88a86a61, flags = 0x246 [ 9366.932938] <_do_fork> post_handler: p->addr = _do_fork+0x0/0x3f0, flags = 0x246 [ 9366.957594] kprobe at _do_fork+0x0/0x3f0 unregistered
可以通過sudo cat /proc/kallsyms | grep l_do_fork來驗證地址和符號是否對應。
若沒有sudo,看不到真實的地址。
2.2 基於ftrace使用kprobe
kprobe和內核的ftrac結合使用,需要對內核進行配置,然后添加探測點、進行探測、查看結果。
2.2.1 kprobe配置
打開"General setup"->"Kprobes",以及"Kernel hacking"->"Tracers"->"Enable kprobes-based dynamic events"。
CONFIG_KPROBES=y CONFIG_OPTPROBES=y CONFIG_KPROBES_ON_FTRACE=y CONFIG_UPROBES=y CONFIG_KRETPROBES=y CONFIG_HAVE_KPROBES=y CONFIG_HAVE_KRETPROBES=y CONFIG_HAVE_OPTPROBES=y CONFIG_HAVE_KPROBES_ON_FTRACE=y CONFIG_KPROBE_EVENT=y
2.2.2 kprobe trace events使用
kprobe事件相關的節點有如下:
/sys/kernel/debug/tracing/kprobe_events-----------------------配置kprobe事件屬性,增加事件之后會在kprobes下面生成對應目錄。 /sys/kernel/debug/tracing/kprobe_profile----------------------kprobe事件統計屬性文件。 /sys/kernel/debug/tracing/kprobes/<GRP>/<EVENT>/enabled-------使能kprobe事件 /sys/kernel/debug/tracing/kprobes/<GRP>/<EVENT>/filter--------過濾kprobe事件 /sys/kernel/debug/tracing/kprobes/<GRP>/<EVENT>/format--------查詢kprobe事件顯示格式
下面就結合實例,看一下如何使用kprobe事件。
2.2.2.1 kprobe事件配置
新增一個kprobe事件,通過寫kprobe_events來設置。
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下創建<GRP>目錄。 EVENT : Event name. If omitted, the event name is generated based on SYM+offs or MEMADDR.---指定后在events/kprobes/<GRP>生成<EVENT>目錄。
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 kprobe $comm : Fetch current task comm.----------------------------------------獲取對應進程名稱。 +|-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), hexadecimal types (x8/x16/x32/x64), "string" and bitfield are supported.----------------設置參數的類型,可以支持字符串和比特類型 (*) only for return probe. (**) this is useful for fetching a field of data structures.
執行如下兩條命令就會生成目錄/sys/kernel/debug/tracing/events/kprobes/myprobe;第三條命令則可以刪除指定kprobe事件,如果要全部刪除則echo > /sys/kernel/debug/tracing/kprobe_events。
echo 'p:myprobe do_sys_open dfd=%ax filename=%dx flags=%cx mode=+4($stack)' > /sys/kernel/debug/tracing/kprobe_events echo 'r:myretprobe do_sys_open ret=$retval' >> /sys/kernel/debug/tracing/kprobe_events-----------------------------------------------------這里面一定要用">>",不然就會覆蓋前面的設置。 echo '-:myprobe' >> /sys/kernel/debug/tracing/kprobe_events
echo '-:myretprobe' >> /sys/kernel/debug/tracing/kprobe_events
參數后面的寄存器是跟架構相關的,%ax、%dx、%cx表示第1/2/3個參數,超出部分使用$stack來存儲參數。
函數返回值保存在$retval中。
2.2.2.2 kprobe使能
對kprobe事件的是能通過往對應事件的enable寫1開啟探測;寫0暫停探測。
echo > /sys/kernel/debug/tracing/trace echo 'p:myprobe do_sys_open dfd=%ax filename=%dx flags=%cx mode=+4($stack)' > /sys/kernel/debug/tracing/kprobe_events echo 'r:myretprobe do_sys_open ret=$retval' >> /sys/kernel/debug/tracing/kprobe_events echo 1 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable echo 1 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable ls echo 0 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable echo 0 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable cat /sys/kernel/debug/tracing/trace
然后在/sys/kernel/debug/tracing/trace中可以看到結果。
sourceinsight4.-3356 [000] .... 3542865.754536: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd6764a0 filename=0x8000 flags=0x1b6 mode=0xe3afff48ffffffff
bash-26041 [001] .... 3542865.757014: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x8241 flags=0x1b6 mode=0xe0c0ff48ffffffff
ls-18078 [005] .... 3542865.757950: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x1 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.757953: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.757966: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x6168 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.757969: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.758001: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x6168 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.758004: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.758030: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x1000 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.758033: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.758055: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x1000 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.758057: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.758080: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x19d0 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.758082: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.758289: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x8000 flags=0x1b6 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.758297: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.758339: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x88000 flags=0x0 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.758343: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
ls-18078 [005] .... 3542865.758444: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x98800 flags=0x2 mode=0xc1b7bf48ffffffff
ls-18078 [005] d... 3542865.758446: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
bash-26041 [001] .... 3542865.760416: myprobe: (do_sys_open+0x0/0x290) dfd=0xffffffffbd676460 filename=0x8241 flags=0x1b6 mode=0xe0c0ff48ffffffff
bash-26041 [001] d... 3542865.760426: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
bash-26041 [001] d... 3542865.793477: myretprobe: (SyS_open+0x1e/0x20 <- do_sys_open) ret=0x3
2.2.2.3 kprobe事件過濾
跟蹤函數需要通過filter進行過濾,可以有效過濾掉冗余信息。
filter文件用於設置過濾條件,可以減少trace中輸出的信息,它支持的格式和c語言的表達式類似,支持 ==,!=,>,<,>=,<=判斷,並且支持與&&,或||,還有()。
echo 'filename==0x8241' > /sys/kernel/debug/tracing/events/kprobes/myprobe/filter
2.2.2.4 kprobe和棧配合使用
如果要在顯示函數的同時顯示其棧信息,可以通過配置trace_options來達到。
echo stacktrace > /sys/kernel/debug/tracing/trace_options
2.2.2.5 kprobe_profile統計信息
獲取一段kprobe時間之后,可以再kprobe_profile中查看統計信息。
后面兩列分別表示命中和未命中的次數。
cat /sys/kernel/debug/tracing/kprobe_profile myprobe 11 0 myretprobe 11 0
3. kprobe相關源碼分析
kprobe在ARM和X86_64架構下的源碼分析,可以參考《Linux內核調試技術——kprobe使用與實現》 第四節《四、kprobe實現源碼分析》。
參考文檔:

