Linux內(nèi)核調(diào)試技術(shù)——kprobe使用與實(shí)現(xiàn)(一)
Linux kprobes調(diào)試技術(shù)是內(nèi)核開(kāi)發(fā)者們專(zhuān)門(mén)為了便于跟蹤內(nèi)核函數(shù)執(zhí)行狀態(tài)所設(shè)計(jì)的一種輕量級(jí)內(nèi)核調(diào)試技術(shù)。利用kprobes技術(shù),內(nèi)核開(kāi)發(fā)人員可以在內(nèi)核的絕大多數(shù)指定函數(shù)中動(dòng)態(tài)的插入探測(cè)點(diǎn)來(lái)收集所需的調(diào)試狀態(tài)信息而基本不影響內(nèi)核原有的執(zhí)行流程。kprobes技術(shù)目前提供了3種探測(cè)手段:kprobe、jprobe和kretprobe,其中jprobe和kretprobe是基于kprobe實(shí)現(xiàn)的,他們分別應(yīng)用于不同的探測(cè)場(chǎng)景中。本文首先簡(jiǎn)單描述這3種探測(cè)技術(shù)的原理與區(qū)別,然后主要圍繞其中的kprobe技術(shù)進(jìn)行分析并給出一個(gè)簡(jiǎn)單的實(shí)例介紹如何利用kprobe進(jìn)行內(nèi)核函數(shù)探測(cè),最后分析kprobe的實(shí)現(xiàn)過(guò)程(jprobe和kretprobe會(huì)在后續(xù)的博文中進(jìn)行分析)。
內(nèi)核源碼:Linux-4.1.15
實(shí)驗(yàn)環(huán)境:CentOS(x86_64)、樹(shù)莓派1b
一、kprobes技術(shù)背景
開(kāi)發(fā)人員在內(nèi)核或者模塊的調(diào)試過(guò)程中,往往會(huì)需要要知道其中的一些函數(shù)有無(wú)被調(diào)用、何時(shí)被調(diào)用、執(zhí)行是否正確以及函數(shù)的入?yún)⒑头祷刂凳鞘裁吹鹊取1容^簡(jiǎn)單的做法是在內(nèi)核代碼對(duì)應(yīng)的函數(shù)中添加日志打印信息,但這種方式往往需要重新編譯內(nèi)核或模塊,重新啟動(dòng)設(shè)備之類(lèi)的,操作較為復(fù)雜甚至可能會(huì)破壞原有的代碼執(zhí)行過(guò)程。
而利用kprobes技術(shù),用戶可以定義自己的回調(diào)函數(shù),然后在內(nèi)核或者模塊中幾乎所有的函數(shù)中(有些函數(shù)是不可探測(cè)的,例如kprobes自身的相關(guān)實(shí)現(xiàn)函數(shù),后文會(huì)有詳細(xì)說(shuō)明)動(dòng)態(tài)的插入探測(cè)點(diǎn),當(dāng)內(nèi)核執(zhí)行流程執(zhí)行到指定的探測(cè)函數(shù)時(shí),會(huì)調(diào)用該回調(diào)函數(shù),用戶即可收集所需的信息了,同時(shí)內(nèi)核最后還會(huì)回到原本的正常執(zhí)行流程。如果用戶已經(jīng)收集足夠的信息,不再需要繼續(xù)探測(cè),則同樣可以動(dòng)態(tài)地移除探測(cè)點(diǎn)。因此kprobes技術(shù)具有對(duì)內(nèi)核執(zhí)行流程影響小和操作方便的優(yōu)點(diǎn)。
kprobes技術(shù)包括的3種探測(cè)手段分別時(shí)kprobe、jprobe和kretprobe。首先kprobe是最基本的探測(cè)方式,是實(shí)現(xiàn)后兩種的基礎(chǔ),它可以在任意的位置放置探測(cè)點(diǎn)(就連函數(shù)內(nèi)部的某條指令處也可以),它提供了探測(cè)點(diǎn)的調(diào)用前、調(diào)用后和內(nèi)存訪問(wèn)出錯(cuò)3種回調(diào)方式,分別是pre_handler、post_handler和fault_handler,其中pre_handler函數(shù)將在被探測(cè)指令被執(zhí)行前回調(diào),post_handler會(huì)在被探測(cè)指令執(zhí)行完畢后回調(diào)(注意不是被探測(cè)函數(shù)),fault_handler會(huì)在內(nèi)存訪問(wèn)出錯(cuò)時(shí)被調(diào)用;jprobe基于kprobe實(shí)現(xiàn),它用于獲取被探測(cè)函數(shù)的入?yún)⒅担蛔詈髃retprobe從名字中就可以看出其用途了,它同樣基于kprobe實(shí)現(xiàn),用于獲取被探測(cè)函數(shù)的返回值。
kprobes的技術(shù)原理并不僅僅包含存軟件的實(shí)現(xiàn)方案,它也需要硬件架構(gòu)提供支持。其中涉及硬件架構(gòu)相關(guān)的是CPU的異常處理和單步調(diào)試技術(shù),前者用于讓程序的執(zhí)行流程陷入到用戶注冊(cè)的回調(diào)函數(shù)中去,而后者則用于單步執(zhí)行被探測(cè)點(diǎn)指令,因此并不是所有的架構(gòu)均支持,目前kprobes技術(shù)已經(jīng)支持多種架構(gòu),包括i386、x86_64、ppc64、ia64、sparc64、arm、ppc和mips(有些架構(gòu)實(shí)現(xiàn)可能并不完全,具體可參考內(nèi)核的Documentation/kprobes.txt)。
kprobes的特點(diǎn)與使用限制: 1、kprobes允許在同一個(gè)被被探測(cè)位置注冊(cè)多個(gè)kprobe,但是目前jprobe卻不可以;同時(shí)也不允許以其他的jprobe回調(diào)函數(shù)和kprobe的post_handler回調(diào)函數(shù)作為被探測(cè)點(diǎn)。
2、一般情況下,可以探測(cè)內(nèi)核中的任何函數(shù),包括中斷處理函數(shù)。不過(guò)在kernel/kprobes.c和arch/*/kernel/kprobes.c程序中用于實(shí)現(xiàn)kprobes自身的函數(shù)是不允許被探測(cè)的,另外還有do_page_fault和notifier_call_chain;
3、如果以一個(gè)內(nèi)聯(lián)函數(shù)為探測(cè)點(diǎn),則kprobes可能無(wú)法保證對(duì)該函數(shù)的所有實(shí)例都注冊(cè)探測(cè)點(diǎn)。由于gcc可能會(huì)自動(dòng)將某些函數(shù)優(yōu)化為內(nèi)聯(lián)函數(shù),因此可能無(wú)法達(dá)到用戶預(yù)期的探測(cè)效果;
4、一個(gè)探測(cè)點(diǎn)的回調(diào)函數(shù)可能會(huì)修改被探測(cè)函數(shù)運(yùn)行的上下文,例如通過(guò)修改內(nèi)核的數(shù)據(jù)結(jié)構(gòu)或者保存與struct pt_regs結(jié)構(gòu)體中的觸發(fā)探測(cè)器之前寄存器信息。因此kprobes可以被用來(lái)安裝bug修復(fù)代碼或者注入故障測(cè)試代碼;
5、kprobes會(huì)避免在處理探測(cè)點(diǎn)函數(shù)時(shí)再次調(diào)用另一個(gè)探測(cè)點(diǎn)的回調(diào)函數(shù),例如在printk()函數(shù)上注冊(cè)了探測(cè)點(diǎn),則在它的回調(diào)函數(shù)中可能再次調(diào)用printk函數(shù),此時(shí)將不再觸發(fā)printk探測(cè)點(diǎn)的回調(diào),僅僅時(shí)增加了kprobe結(jié)構(gòu)體中nmissed字段的數(shù)值;
6、在kprobes的注冊(cè)和注銷(xiāo)過(guò)程中不會(huì)使用mutex鎖和動(dòng)態(tài)的申請(qǐng)內(nèi)存;
7、kprobes回調(diào)函數(shù)的運(yùn)行期間是關(guān)閉內(nèi)核搶占的,同時(shí)也可能在關(guān)閉中斷的情況下執(zhí)行,具體要視CPU架構(gòu)而定。因此不論在何種情況下,在回調(diào)函數(shù)中不要調(diào)用會(huì)放棄CPU的函數(shù)(如信號(hào)量、mutex鎖等);
8、kretprobe通過(guò)替換返回地址為預(yù)定義的trampoline的地址來(lái)實(shí)現(xiàn),因此?;厮莺蚲cc內(nèi)嵌函數(shù)__builtin_return_address()調(diào)用將返回trampoline的地址而不是真正的被探測(cè)函數(shù)的返回地址;
9、如果一個(gè)函數(shù)的調(diào)用次數(shù)和返回次數(shù)不相等,則在類(lèi)似這樣的函數(shù)上注冊(cè)kretprobe將可能不會(huì)達(dá)到預(yù)期的效果,例如do_exit()函數(shù)會(huì)存在問(wèn)題,而do_execve()函數(shù)和do_fork()函數(shù)不會(huì);
10、如果當(dāng)在進(jìn)入和退出一個(gè)函數(shù)時(shí),CPU運(yùn)行在非當(dāng)前任務(wù)所有的棧上,那么往該函數(shù)上注冊(cè)kretprobe可能會(huì)導(dǎo)致不可預(yù)料的后果,因此,kprobes不支持在X86_64的結(jié)構(gòu)下為_(kāi)_switch_to()函數(shù)注冊(cè)kretprobe,將直接返回-EINVAL。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【865977150】整理了一些個(gè)人覺(jué)得比較好的學(xué)習(xí)書(shū)籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!?。?/p>
內(nèi)核學(xué)習(xí)網(wǎng)站:
Linux內(nèi)核源碼/內(nèi)存調(diào)優(yōu)/文件系統(tǒng)/進(jìn)程管理/設(shè)備驅(qū)動(dòng)/網(wǎng)絡(luò)協(xié)議棧-學(xué)習(xí)視頻教程-騰訊課堂ke.qq.com/course/4032547?flowToken=1040236
二、kprobe原理
下面來(lái)介紹一下kprobe是如何工作的。具體流程見(jiàn)下圖:

1、當(dāng)用戶注冊(cè)一個(gè)探測(cè)點(diǎn)后,kprobe首先備份被探測(cè)點(diǎn)的對(duì)應(yīng)指令,然后將原始指令的入口點(diǎn)替換為斷點(diǎn)指令,該指令是CPU架構(gòu)相關(guān)的,如i386和x86_64是int3,arm是設(shè)置一個(gè)未定義指令(目前的x86_64架構(gòu)支持一種跳轉(zhuǎn)優(yōu)化方案Jump Optimization,內(nèi)核需開(kāi)啟CONFIG_OPTPROBES選項(xiàng),該種方案使用跳轉(zhuǎn)指令來(lái)代替斷點(diǎn)指令);
2、當(dāng)CPU流程執(zhí)行到探測(cè)點(diǎn)的斷點(diǎn)指令時(shí),就觸發(fā)了一個(gè)trap,在trap處理流程中會(huì)保存當(dāng)前CPU的寄存器信息并調(diào)用對(duì)應(yīng)的trap處理函數(shù),該處理函數(shù)會(huì)設(shè)置kprobe的調(diào)用狀態(tài)并調(diào)用用戶注冊(cè)的pre_handler回調(diào)函數(shù),kprobe會(huì)向該函數(shù)傳遞注冊(cè)的struct kprobe結(jié)構(gòu)地址以及保存的CPU寄存器信息;
3、隨后kprobe單步執(zhí)行前面所拷貝的被探測(cè)指令,具體執(zhí)行方式各個(gè)架構(gòu)不盡相同,arm會(huì)在異常處理流程中使用模擬函數(shù)執(zhí)行,而x86_64架構(gòu)則會(huì)設(shè)置單步調(diào)試flag并回到異常觸發(fā)前的流程中執(zhí)行;
4、在單步執(zhí)行完成后,kprobe執(zhí)行用戶注冊(cè)的post_handler回調(diào)函數(shù);
5、最后,執(zhí)行流程回到被探測(cè)指令之后的正常流程繼續(xù)執(zhí)行。
三、kprobe使用實(shí)例
在分析kprobe的實(shí)現(xiàn)之前先來(lái)看一下如何利用kprobe對(duì)函數(shù)進(jìn)行探測(cè),以便于讓我們對(duì)kprobre所完成功能有一個(gè)比較清晰的認(rèn)識(shí)。目前,使用kprobe可以通過(guò)兩種方式,第一種是開(kāi)發(fā)人員自行編寫(xiě)內(nèi)核模塊,向內(nèi)核注冊(cè)探測(cè)點(diǎn),探測(cè)函數(shù)可根據(jù)需要自行定制,使用靈活方便;第二種方式是使用kprobes on ftrace,這種方式是kprobe和ftrace結(jié)合使用,即可以通過(guò)kprobe來(lái)優(yōu)化ftrace來(lái)跟蹤函數(shù)的調(diào)用。下面來(lái)分別介紹:
1、編寫(xiě)kprobe探測(cè)模塊
內(nèi)核提供了一個(gè)struct kprobe結(jié)構(gòu)體以及一系列的內(nèi)核API函數(shù)接口,用戶可以通過(guò)這些接口自行實(shí)現(xiàn)探測(cè)回調(diào)函數(shù)并實(shí)現(xiàn)struct kprobe結(jié)構(gòu),然后將它注冊(cè)到內(nèi)核的kprobes子系統(tǒng)中來(lái)達(dá)到探測(cè)的目的。同時(shí)在內(nèi)核的samples/kprobes目錄下有一個(gè)例程kprobe_example.c描述了kprobe模塊最簡(jiǎn)單的編寫(xiě)方式,開(kāi)發(fā)者可以以此為模板編寫(xiě)自己的探測(cè)模塊。
1.1、kprobe結(jié)構(gòu)體與API介紹
struct kprobe結(jié)構(gòu)體定義如下:
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;
};
其中各個(gè)字段的含義如下:
struct hlist_node hlist:被用于kprobe全局hash,索引值為被探測(cè)點(diǎn)的地址;
struct list_head list:用于鏈接同一被探測(cè)點(diǎn)的不同探測(cè)kprobe;
kprobe_opcode_t *addr:被探測(cè)點(diǎn)的地址;
const char *symbol_name:被探測(cè)函數(shù)的名字;
unsigned int offset:被探測(cè)點(diǎn)在函數(shù)內(nèi)部的偏移,用于探測(cè)函數(shù)內(nèi)部的指令,如果該值為0表示函數(shù)的入口;
kprobe_pre_handler_t pre_handler:在被探測(cè)點(diǎn)指令執(zhí)行之前調(diào)用的回調(diào)函數(shù);
kprobe_post_handler_t post_handler:在被探測(cè)指令執(zhí)行之后調(diào)用的回調(diào)函數(shù);
kprobe_fault_handler_t fault_handler:在執(zhí)行pre_handler、post_handler或單步執(zhí)行被探測(cè)指令時(shí)出現(xiàn)內(nèi)存異常則會(huì)調(diào)用該回調(diào)函數(shù);
kprobe_break_handler_t break_handler:在執(zhí)行某一kprobe過(guò)程中觸發(fā)了斷點(diǎn)指令后會(huì)調(diào)用該函數(shù),用于實(shí)現(xiàn)jprobe;
kprobe_opcode_t opcode:保存的被探測(cè)點(diǎn)原始指令;
struct arch_specific_insn ainsn:被復(fù)制的被探測(cè)點(diǎn)的原始指令,用于單步執(zhí)行,架構(gòu)強(qiáng)相關(guān)(可能包含指令模擬函數(shù));
u32 flags:狀態(tài)標(biāo)記。
涉及的API函數(shù)接口如下:
int register_kprobe(struct kprobe *kp) ? ? ?//向內(nèi)核注冊(cè)kprobe探測(cè)點(diǎn)
void unregister_kprobe(struct kprobe *kp) ? //卸載kprobe探測(cè)點(diǎn)
int register_kprobes(struct kprobe **kps, int num) ? ? //注冊(cè)探測(cè)函數(shù)向量,包含多個(gè)探測(cè)點(diǎn)
void unregister_kprobes(struct kprobe **kps, int num) ?//卸載探測(cè)函數(shù)向量,包含多個(gè)探測(cè)點(diǎn)
int disable_kprobe(struct kprobe *kp) ? ? ? //臨時(shí)暫停指定探測(cè)點(diǎn)的探測(cè)
int enable_kprobe(struct kprobe *kp) ? ? ? ?//恢復(fù)指定探測(cè)點(diǎn)的探測(cè)
1.2、用例kprobe_example.c分析與演示
該用例函數(shù)非常簡(jiǎn)單,它實(shí)現(xiàn)了內(nèi)核函數(shù)do_fork的探測(cè),該函數(shù)會(huì)在fork系統(tǒng)調(diào)用或者內(nèi)核kernel_thread函數(shù)創(chuàng)建進(jìn)程時(shí)被調(diào)用,觸發(fā)也十分的頻繁。下面來(lái)分析一下用例代碼:
/* 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");
程序中定義了一個(gè)struct kprobe結(jié)構(gòu)實(shí)例kp并初始化其中的symbol_name字段為“do_fork”,表明它將要探測(cè)do_fork函數(shù)。在模塊的初始化函數(shù)中,注冊(cè)了pre_handler、post_handler和fault_handler這3個(gè)回調(diào)函數(shù)分別為handler_pre、handler_post和handler_fault,最后調(diào)用register_kprobe注冊(cè)。在模塊的卸載函數(shù)中調(diào)用unregister_kprobe函數(shù)卸載kp探測(cè)點(diǎn)。
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回調(diào)函數(shù)的第一個(gè)入口是注冊(cè)的struct kprobe探測(cè)實(shí)例,第二個(gè)參數(shù)是保存的觸發(fā)斷點(diǎn)前的寄存器狀態(tài),它在do_fork函數(shù)被調(diào)用之前被調(diào)用,該函數(shù)僅僅是打印了被探測(cè)點(diǎn)的地址,保存的個(gè)別寄存器參數(shù)。由于受CPU架構(gòu)影響,這里對(duì)不同的架構(gòu)進(jìn)行了宏區(qū)分(雖然沒(méi)有實(shí)現(xiàn)arm架構(gòu)的,但是支持的,可以自行添加);
/* 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回調(diào)函數(shù)的前兩個(gè)入?yún)⑼琱andler_pre,第三個(gè)參數(shù)目前尚未使用,全部為0;該函數(shù)在do_fork函數(shù)調(diào)用之后被調(diào)用,這里打印的內(nèi)容同handler_pre類(lèi)似。
/*
* 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回調(diào)函數(shù)會(huì)在執(zhí)行handler_pre、handler_post或單步執(zhí)行do_fork時(shí)出現(xiàn)錯(cuò)誤時(shí)調(diào)用,這里第三個(gè)參數(shù)時(shí)具體發(fā)生錯(cuò)誤的trap number,與架構(gòu)相關(guān),例如i386的page fault為14。
下面將它編譯成模塊在我的x86(CentOS 3.10)環(huán)境下進(jìn)行演示,首先確保架構(gòu)和內(nèi)核已經(jīng)支持kprobes,開(kāi)啟以下選項(xiàng)(一般都是默認(rèn)開(kāi)啟的):
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單獨(dú)編譯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*
加載到內(nèi)核中后,隨便在終端上敲一個(gè)命令,可以看到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
可以看到被探測(cè)點(diǎn)的地址為0xc0439cc0,用以下命令確定這個(gè)地址就是do_fork的入口地址。 [root@apple kprobes]# cat /proc/kallsyms | grep do_fork c0439cc0 T do_fork
2、使用kprobe on ftrace來(lái)跟蹤函數(shù)和調(diào)用棧
這種方式用戶通過(guò)
/sys/kernel/debug/tracing/目錄下的trace等屬性文件來(lái)探測(cè)用戶指定的函數(shù),用戶可添加kprobe支持的任意函數(shù)并設(shè)置探測(cè)格式與過(guò)濾條件,無(wú)需再編寫(xiě)內(nèi)核模塊,使用更為簡(jiǎn)便,但需要內(nèi)核的debugfs和ftrace功能的支持。
首先,在使用前需要保證開(kāi)啟以下內(nèi)核選項(xiàng):
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文件系統(tǒng)掛在到/sys/kernel/debug/目錄下:
# mount -t debugfs nodev /sys/kernel/debug/
此時(shí)/sys/kernel/debug/tracing目錄下就出現(xiàn)了若干個(gè)文件和目錄用于用戶設(shè)置要跟蹤的函數(shù)以及過(guò)濾條件等等,這里我主要關(guān)注以下幾個(gè)文件:
1、配置屬性文件:kprobe_events 2、查詢(xún)屬性文件:trace和trace_pipe 3、使能屬性文件:events/kprobes///enabled 4、過(guò)濾屬性文件:events/kprobes///filter 5、格式查詢(xún)屬性文件:events/kprobes///format 6、事件統(tǒng)計(jì)屬性文件:kprobe_profile
其中配置屬性文件用于用戶配置要探測(cè)的函數(shù)以及探測(cè)的方式與參數(shù),在配置完成后,會(huì)在events/kprobes/目錄下生成對(duì)應(yīng)的目錄;其中會(huì)生成enabled、format、filter和id這4個(gè)文件,其中的enable屬性文件用于控制探測(cè)的開(kāi)啟或關(guān)閉,filter用于設(shè)置過(guò)濾條件,format可以查看當(dāng)前的輸出格式,最后id可以查看當(dāng)前probe event的ID號(hào)。然后若被探測(cè)函數(shù)被執(zhí)行流程觸發(fā)調(diào)用,用戶可以通過(guò)trace屬性文件進(jìn)行查看。最后通過(guò)kprobe_profile屬性文件可以查看探測(cè)命中次數(shù)和丟失次數(shù)(probe hits and probe miss-hits)。
下面來(lái)看看各個(gè)屬性文件的常用操作方式(其中具體格式和參數(shù)方面的細(xì)節(jié)可以查看內(nèi)核的
Documentation/trace/kprobetrace.txt文件,描述非常詳細(xì)):
1、kprobe_events
該屬性文件支持3中格式的輸入:
p[:[GRP/]EVENT] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS]——設(shè)置一個(gè)probe探測(cè)點(diǎn) r[:[GRP/]EVENT] [MOD:]SYM[+0] [FETCHARGS] ——設(shè)置一個(gè)return probe探測(cè)點(diǎn) -:[GRP/]EVENT ——?jiǎng)h除一個(gè)探測(cè)點(diǎn) 各個(gè)字段的含義如下:
GRP : Group name. If omitted, use "kprobes" for it. ——指定后會(huì)在events/kprobes目錄下生成對(duì)應(yīng)名字的目錄,一般不設(shè) EVENT : Event name. If omitted, the event name is generated based on SYM+offs or MEMADDR. ——指定后會(huì)在events/kprobes/目錄下生成對(duì)應(yīng)名字的目錄 MOD : Module name which has given SYM. ——模塊名,一般不設(shè) SYM[+offs] : Symbol+offset where the probe is inserted. ——指定被探測(cè)函數(shù)和偏移 MEMADDR : Address where the probe is inserted. ——指定被探測(cè)的內(nèi)存絕對(duì)地址
FETCHARGS : Arguments. Each probe can have up to 128 args. ——指定要獲取的參數(shù)信息 %REG : Fetch register REG ——獲取指定寄存器值 @ADDR : Fetch memory at ADDR (ADDR should be in kernel) ——獲取指定內(nèi)存地址的值 @SYM[+|-offs] : Fetch memory at SYM +|- offs (SYM should be a data symbol) ——獲取全局變量的值
stackN:FetchNthentryofstack(N>=0)——獲取指定??臻g值,即sp寄存器+N后的位置值
2、events/kprobes///enabled
開(kāi)啟探測(cè):echo 1 > events/kprobes///enabled 暫停探測(cè):echo 0 > events/kprobes///enabled
3、events/kprobes///filter
該屬性文件用于設(shè)置過(guò)濾條件,可以減少trace中輸出的信息,它支持的格式和c語(yǔ)言的表達(dá)式類(lèi)似,支持 ==,!=,>,<,>=,<=判斷,并且支持與&&,或||,還有()。
下面還是以do_fork()函數(shù)為例來(lái)舉例看一下具體如何使用(實(shí)驗(yàn)環(huán)境:樹(shù)莓派1b):
1、設(shè)置配置屬性
首先添加配置探測(cè)點(diǎn):
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
這里注冊(cè)probe和retprobe,其中probe中設(shè)定了獲取do_fork()函數(shù)的入?yún)⒅担ㄗ⒁膺@里的參數(shù)信息根據(jù)不同CPU架構(gòu)的函數(shù)參數(shù)傳遞規(guī)則強(qiáng)相關(guān),根據(jù)ARM遵守的ATPCS規(guī)則,函數(shù)入?yún)?4通過(guò)r0r3寄存器傳遞,多余的參數(shù)通過(guò)棧傳遞),由于入?yún)?個(gè),所以前4個(gè)通過(guò)寄存器獲取,最后一個(gè)通過(guò)棧獲取。
現(xiàn)可通過(guò)format文件查看探測(cè)的輸出格式:
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、開(kāi)啟探測(cè)并觸發(fā)函數(shù)調(diào)用
往對(duì)應(yīng)的enable函數(shù)中寫(xiě)入1用以開(kāi)啟探測(cè)功能:
root@apple:/sys/kernel/debug/tracing# echo 1 > events/kprobes/myprobe/enable
root@apple:/sys/kernel/debug/tracing# echo 1 > events/kprobes/myretprobe/enable
然后在終端上敲幾條命令和建立一個(gè)ssh鏈接觸發(fā)進(jìn)程創(chuàng)建do_fork函數(shù)調(diào)用,并通過(guò)trace屬性文件獲取函數(shù)調(diào)用時(shí)的探測(cè)情況
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函數(shù)由bash(PID=513) 和sshd(PID=520)進(jìn)程調(diào)用,同時(shí)執(zhí)行的CPU為0,調(diào)用do_fork函數(shù)是入?yún)⒅捣謩e是stack_start=0x0 stack_size=0x0 parent_tidptr=0x0 child_tidptr=0xbxxxxxxx,同時(shí)輸出函數(shù)返回上層SyS_clone系統(tǒng)調(diào)用的nr值。
如果輸出太多了,想要清除就向trace中寫(xiě)0即可
root@apple:/sys/kernel/debug/tracing# echo 0 > trace
3、使用filter進(jìn)行過(guò)濾
例如想要把前面列出的PID為513調(diào)用信息的給過(guò)濾掉,則向filter中寫(xiě)入如下的命令即可:
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
如此就不會(huì)在打印PID為513的進(jìn)程調(diào)用信息了,這里的參數(shù)可以參考前面的format中輸出的,例如想指定輸出特定clone_flags值,則可以輸入clone_flags=xxx即可。
最后補(bǔ)充一點(diǎn),若此時(shí)需要查看函數(shù)調(diào)用的棧信息(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
