一文吃透Kprobes:從源碼探秘到調(diào)試實(shí)戰(zhàn)
曾經(jīng),有一位開(kāi)發(fā)者在維護(hù)一個(gè)基于 Linux 內(nèi)核的服務(wù)器系統(tǒng)時(shí),遇到了一個(gè)棘手的問(wèn)題:系統(tǒng)時(shí)不時(shí)會(huì)出現(xiàn)短暫的卡頓,排查日志卻找不到明顯線索。經(jīng)過(guò)一番分析,他懷疑是某個(gè)內(nèi)核函數(shù)在特定條件下執(zhí)行異常。但傳統(tǒng)的調(diào)試方法,如添加打印語(yǔ)句,需要重新編譯內(nèi)核,不僅繁瑣,還可能影響生產(chǎn)環(huán)境。
這時(shí),Kprobes 技術(shù)進(jìn)入了他的視野。通過(guò) Kprobes,他在懷疑的內(nèi)核函數(shù)入口處設(shè)置了探測(cè)點(diǎn),收集函數(shù)的輸入?yún)?shù)和執(zhí)行時(shí)間等信息。經(jīng)過(guò)一段時(shí)間的監(jiān)測(cè)和分析,終于發(fā)現(xiàn)是一個(gè)資源競(jìng)爭(zhēng)問(wèn)題導(dǎo)致了卡頓。通過(guò)修改代碼,成功解決了這個(gè)困擾已久的難題。這個(gè)故事充分展現(xiàn)了 Kprobes 在調(diào)試內(nèi)核問(wèn)題時(shí)的強(qiáng)大作用。
那么,Kprobes 究竟是什么呢?簡(jiǎn)單來(lái)說(shuō),Kprobes 是 Linux 內(nèi)核提供的一種動(dòng)態(tài)調(diào)試機(jī)制,它允許開(kāi)發(fā)者在不修改內(nèi)核源碼、不重啟系統(tǒng)的情況下,對(duì)內(nèi)核函數(shù)進(jìn)行探測(cè)。無(wú)論是內(nèi)核開(kāi)發(fā)者排查性能瓶頸,還是驅(qū)動(dòng)開(kāi)發(fā)者調(diào)試設(shè)備驅(qū)動(dòng),Kprobes 都能提供極大的便利,讓你深入內(nèi)核執(zhí)行的 “幕后”,一探究竟。
一、Kprobes 概述
Kprobes 是 Linux 內(nèi)核中一種強(qiáng)大的動(dòng)態(tài)探測(cè)機(jī)制,猶如一把 “萬(wàn)能鑰匙”,能打開(kāi)內(nèi)核函數(shù)的 “神秘大門” 。它允許開(kāi)發(fā)者在不重新編譯內(nèi)核、不重啟系統(tǒng)的情況下,對(duì)內(nèi)核函數(shù)進(jìn)行實(shí)時(shí)監(jiān)測(cè)和分析,極大地提高了調(diào)試和性能優(yōu)化的效率。這對(duì)于內(nèi)核開(kāi)發(fā)者來(lái)說(shuō),無(wú)疑是一項(xiàng) “神器”,讓他們能夠在不中斷系統(tǒng)運(yùn)行的情況下,深入了解內(nèi)核的運(yùn)行狀態(tài),快速定位和解決問(wèn)題。
Kprobes 的出現(xiàn),解決了傳統(tǒng)內(nèi)核調(diào)試方法的諸多痛點(diǎn)。在 Kprobes 之前,開(kāi)發(fā)者若要調(diào)試內(nèi)核函數(shù),往往需要在函數(shù)中添加打印語(yǔ)句,然后重新編譯內(nèi)核并重啟系統(tǒng)。這個(gè)過(guò)程不僅繁瑣耗時(shí),還可能影響生產(chǎn)環(huán)境的穩(wěn)定性。而 Kprobes 打破了這些限制,它就像一個(gè) “隱形的觀察者”,可以隨時(shí)在運(yùn)行中的內(nèi)核函數(shù)中插入探測(cè)點(diǎn),收集函數(shù)的執(zhí)行信息,如函數(shù)參數(shù)、返回值、執(zhí)行時(shí)間等,卻不會(huì)對(duì)內(nèi)核的正常運(yùn)行造成干擾。
二、Kprobes 如何運(yùn)作
2.1關(guān)鍵數(shù)據(jù)結(jié)構(gòu)
在 Kprobes 的源碼世界里,struct kprobe是一個(gè)核心結(jié)構(gòu)體,它就像是一個(gè) “探測(cè)點(diǎn)管理器”,承載著 Kprobes 運(yùn)作的關(guān)鍵信息。下面是struct kprobe的簡(jiǎn)化定義:
struct kprobe {
kprobe_opcode_t *addr; // 被探測(cè)點(diǎn)的地址
const char *symbol_name; // 被探測(cè)函數(shù)的名稱
unsigned int offset; // 被探測(cè)點(diǎn)在函數(shù)內(nèi)部的偏移,若為0則表示函數(shù)入口
kprobe_pre_handler_t pre_handler; // 該回調(diào)函數(shù)用于在執(zhí)行被探測(cè)指令前執(zhí)行
kprobe_post_handler_t post_handler; // 該回調(diào)函數(shù)用于在執(zhí)行完被探測(cè)指令后執(zhí)行
kprobe_fault_handler_t fault_handler; // 此函數(shù)用于在出現(xiàn)內(nèi)存訪問(wèn)錯(cuò)誤時(shí)進(jìn)行處理
kprobe_opcode_t opcode; // 保存被替換的原始指令
struct arch_specific_insn ainsn; // 架構(gòu)相關(guān)的指令信息
u32 flags; // 各種狀態(tài)標(biāo)志
};
其中,addr成員指明了探測(cè)點(diǎn)的具體位置,它是 Kprobes 定位內(nèi)核函數(shù)中特定指令的 “導(dǎo)航儀” 。symbol_name則以函數(shù)名的形式,為開(kāi)發(fā)者提供了一種更直觀的方式來(lái)指定探測(cè)目標(biāo),就像是給探測(cè)點(diǎn)貼上了一個(gè)清晰的 “標(biāo)簽” 。offset用于精確到函數(shù)內(nèi)部的具體指令,讓探測(cè)更加細(xì)致入微,如同在精密儀器中調(diào)整刻度,實(shí)現(xiàn)精準(zhǔn)探測(cè)。
pre_handler、post_handler和fault_handler這三個(gè)回調(diào)函數(shù),是 Kprobes 與內(nèi)核交互的 “橋梁” 。pre_handler在被探測(cè)指令執(zhí)行前被調(diào)用,就像是比賽前的熱身,讓開(kāi)發(fā)者有機(jī)會(huì)提前獲取信息、設(shè)置環(huán)境;post_handler在指令執(zhí)行后登場(chǎng),如同賽后的復(fù)盤,用于收集指令執(zhí)行后的結(jié)果和狀態(tài);fault_handler則在內(nèi)存訪問(wèn)出錯(cuò)時(shí)發(fā)揮作用,是處理異常情況的 “救火隊(duì)員” 。
opcode保存了被斷點(diǎn)指令替換的原始指令,確保在探測(cè)完成后,內(nèi)核能夠恢復(fù)到原本的執(zhí)行狀態(tài),就像在書本中夾了一張書簽,方便后續(xù)繼續(xù)閱讀。ainsn和flags則分別負(fù)責(zé)存儲(chǔ)架構(gòu)相關(guān)的指令信息和各種狀態(tài)標(biāo)志,為 Kprobes 在不同硬件架構(gòu)上的穩(wěn)定運(yùn)行提供支持,以及記錄探測(cè)點(diǎn)的各種狀態(tài),如是否啟用、是否出錯(cuò)等。
2.2注冊(cè)與卸載流程
當(dāng)我們想要使用 Kprobes 對(duì)某個(gè)內(nèi)核函數(shù)進(jìn)行探測(cè)時(shí),就需要將探測(cè)點(diǎn)注冊(cè)到內(nèi)核中。這個(gè)過(guò)程就像是在圖書館的書架上添加一本新書,需要遵循一定的流程。下面是注冊(cè) Kprobes 探測(cè)點(diǎn)的關(guān)鍵代碼示例:
#include <linux/module.h>
#include <linux/kprobes.h>
// 定義pre_handler回調(diào)函數(shù)
static int handler_pre(struct kprobe *p, struct pt_regs *regs) {
pr_info("< %s > pre_handler: p->addr = 0x%p, ip = %lx, flags = 0x%lx\n",
p->symbol_name, p->addr, regs->ip, regs->flags);
return 0;
}
// 定義post_handler回調(diào)函數(shù)
static void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {
pr_info("< %s > post_handler: p->addr = 0x%p, flags = 0x%lx\n",
p->symbol_name, p->addr, flags);
}
// 定義fault_handler回調(diào)函數(shù)
int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr) {
pr_info("fault_handler: p->addr = 0x%p, trap #%d\n", p->addr, trapnr);
return 0;
}
// 定義kprobe結(jié)構(gòu)
static struct kprobe kp = {
.symbol_name = "do_fork", // 要追蹤的內(nèi)核函數(shù)為do_fork
.pre_handler = handler_pre, // pre_handler回調(diào)函數(shù)
.post_handler = handler_post, // post_handler回調(diào)函數(shù)
.fault_handler = handler_fault // fault_handler回調(diào)函數(shù)
};
// 模塊初始化函數(shù),用于注冊(cè)kprobe
static int __init kprobe_init(void) {
int ret;
ret = register_kprobe(&kp);
if (ret < 0) {
pr_err("register_kprobe failed, returned %d\n", ret);
return ret;
}
pr_info("Planted kprobe at %p\n", kp.addr);
return 0;
}
// 模塊退出函數(shù),用于卸載kprobe
static void __exit kprobe_exit(void) {
unregister_kprobe(&kp);
pr_info("kprobe at %p unregistered\n", kp.addr);
}
module_init(kprobe_init);
module_exit(kprobe_exit);
MODULE_LICENSE("GPL");
在上述代碼中,首先定義了三個(gè)回調(diào)函數(shù)handler_pre、handler_post和handler_fault,分別用于在被探測(cè)指令執(zhí)行前、執(zhí)行后和出現(xiàn)內(nèi)存訪問(wèn)錯(cuò)誤時(shí)執(zhí)行。然后,創(chuàng)建了一個(gè)struct kprobe結(jié)構(gòu)體實(shí)例kp,并指定要探測(cè)的內(nèi)核函數(shù)為do_fork,同時(shí)將三個(gè)回調(diào)函數(shù)與kp關(guān)聯(lián)起來(lái)。
在kprobe_init函數(shù)中,通過(guò)調(diào)用register_kprobe函數(shù)將kp注冊(cè)到內(nèi)核中。如果注冊(cè)成功,會(huì)打印出探測(cè)點(diǎn)的地址;如果失敗,則打印錯(cuò)誤信息并返回錯(cuò)誤碼。
2.3回調(diào)函數(shù)機(jī)制
Kprobes 的回調(diào)函數(shù)機(jī)制是其實(shí)現(xiàn)動(dòng)態(tài)探測(cè)的核心,它就像是一個(gè)精心編排的 “演出”,每個(gè)回調(diào)函數(shù)在不同的時(shí)刻登場(chǎng),為開(kāi)發(fā)者提供內(nèi)核運(yùn)行時(shí)的關(guān)鍵信息。
pre_handler回調(diào)函數(shù)在被探測(cè)指令執(zhí)行前被觸發(fā),此時(shí),內(nèi)核的執(zhí)行流程就像行駛到一個(gè)岔路口,暫時(shí)停下,先進(jìn)入pre_handler函數(shù)。在這個(gè)函數(shù)中,開(kāi)發(fā)者可以獲取當(dāng)前的寄存器狀態(tài)、被探測(cè)函數(shù)的參數(shù)等信息。例如,在前面的代碼中,handler_pre函數(shù)通過(guò)pr_info函數(shù)打印出了被探測(cè)函數(shù)的名稱、探測(cè)點(diǎn)地址、指令指針ip和標(biāo)志寄存器flags的值,這些信息就像是內(nèi)核運(yùn)行時(shí)的 “快照”,為開(kāi)發(fā)者分析問(wèn)題提供了重要線索。
post_handler回調(diào)函數(shù)則在被探測(cè)指令執(zhí)行完畢后閃亮登場(chǎng)。它就像是在一場(chǎng)比賽結(jié)束后,對(duì)比賽結(jié)果進(jìn)行總結(jié)和分析。在這個(gè)函數(shù)中,開(kāi)發(fā)者可以獲取指令執(zhí)行后的返回值、內(nèi)核狀態(tài)的變化等信息。同樣以之前的代碼為例,handler_post函數(shù)打印出了被探測(cè)函數(shù)的名稱、探測(cè)點(diǎn)地址和標(biāo)志寄存器的值,幫助開(kāi)發(fā)者了解指令執(zhí)行后的情況。
fault_handler回調(diào)函數(shù)是在執(zhí)行pre_handler、post_handler或單步執(zhí)行被探測(cè)指令時(shí)出現(xiàn)內(nèi)存訪問(wèn)異常時(shí)被調(diào)用的。它就像是一個(gè)緊急救援小組,在出現(xiàn)問(wèn)題時(shí)迅速響應(yīng)。當(dāng)內(nèi)核在執(zhí)行探測(cè)相關(guān)操作時(shí)發(fā)生內(nèi)存錯(cuò)誤,fault_handler函數(shù)會(huì)被觸發(fā),開(kāi)發(fā)者可以在這個(gè)函數(shù)中進(jìn)行錯(cuò)誤處理,如打印錯(cuò)誤信息、記錄故障現(xiàn)場(chǎng)等,以便后續(xù)排查問(wèn)題。
通過(guò)這三個(gè)回調(diào)函數(shù)的協(xié)同工作,Kprobes 為開(kāi)發(fā)者打造了一個(gè)全方位、多層次的內(nèi)核探測(cè)體系,讓開(kāi)發(fā)者能夠深入了解內(nèi)核的運(yùn)行細(xì)節(jié),快速定位和解決問(wèn)題。
三、Kprobe實(shí)現(xiàn)原理
當(dāng)安裝一個(gè)kprobes探測(cè)點(diǎn)時(shí)。kprobe首先備份被探測(cè)的指令,然后使用斷點(diǎn)指令(即在i386和x86_64的int3指令)來(lái)代替被探測(cè)指令的頭一個(gè)或幾個(gè)字節(jié)。當(dāng)CPU運(yùn)行到探測(cè)點(diǎn)時(shí),將因運(yùn)行斷點(diǎn)指令而運(yùn)行trap操作,那將導(dǎo)致保存CPU的寄存器,調(diào)用對(duì)應(yīng)的trap處理函數(shù)。而trap處理函數(shù)將調(diào)用對(duì)應(yīng)的notifier_call_chain(內(nèi)核中一種異步工作機(jī)制)中注冊(cè)的全部notifier函數(shù)。kprobe正是通過(guò)向trap對(duì)應(yīng)的notifier_call_chain注冊(cè)關(guān)聯(lián)到探測(cè)點(diǎn)的處理函數(shù)來(lái)實(shí)現(xiàn)探測(cè)處理的。
當(dāng)kprobe注冊(cè)的notifier被運(yùn)行時(shí),它首先運(yùn)行關(guān)聯(lián)到探測(cè)點(diǎn)的pre_handler函數(shù),并把對(duì)應(yīng)的kprobe struct和保存的寄存器作為該函數(shù)的參數(shù),接著,kprobe單步運(yùn)行被探測(cè)指令的備份。最后,kprobe運(yùn)行post_handler。等全部這些運(yùn)行完成后。緊跟在被探測(cè)指令后的指令流將被正常運(yùn)行。
kretprobe也使用了kprobes來(lái)實(shí)現(xiàn),當(dāng)用戶調(diào)用register_kretprobe()時(shí),kprobe在被探測(cè)函數(shù)的入口建立了一個(gè)探測(cè)點(diǎn)。當(dāng)運(yùn)行到探測(cè)點(diǎn)時(shí),kprobe保存了被探測(cè)函數(shù)的返回地址并代替返回地址為一個(gè)trampoline的地址,kprobe在初始化時(shí)定義了該trampoline而且為該trampoline注冊(cè)了一個(gè)kprobe,當(dāng)被探測(cè)函數(shù)運(yùn)行它的返回指令時(shí)??刂苽鬟f到該trampoline,因此kprobe已經(jīng)注冊(cè)的相應(yīng)于trampoline的處理函數(shù)將被運(yùn)行。而該處理函數(shù)會(huì)調(diào)用用戶關(guān)聯(lián)到該kretprobe上的處理函數(shù)。處理完成后,設(shè)置指令寄存器指向已經(jīng)備份的函數(shù)返回地址。因而原來(lái)的函數(shù)返回被正常運(yùn)行。
被探測(cè)函數(shù)的返回地址保存在類型為kretprobe_instance的變量中。結(jié)構(gòu)kretprobe的maxactive字段指定了被探測(cè)函數(shù)能夠被同一時(shí)候探測(cè)的實(shí)例數(shù),函數(shù)register_kretprobe()將預(yù)分配指定數(shù)量的kretprobe_instance。假設(shè)被探測(cè)函數(shù)是非遞歸的而且調(diào)用時(shí)已經(jīng)保持了自旋鎖(spinlock),那么maxactive為1就足夠了;假設(shè)被探測(cè)函數(shù)是非遞歸的且執(zhí)行時(shí)是搶占失效的,那么maxactive為NR_CPUS就能夠了;假設(shè)maxactive被設(shè)置為小于等于0, 它被設(shè)置到缺省值(假設(shè)搶占使能, 即配置了 CONFIG_PREEMPT,缺省值為10和2*NR_CPUS中的最大值,否則缺省值為NR_CPUS)。
假設(shè)maxactive被設(shè)置的太小了,一些探測(cè)點(diǎn)的運(yùn)行可能被丟失,可是不影響系統(tǒng)的正常運(yùn)行,在結(jié)構(gòu)kretprobe中nmissed字段將記錄被丟失的探測(cè)點(diǎn)運(yùn)行數(shù),它在返回探測(cè)點(diǎn)被注冊(cè)時(shí)設(shè)置為0,每次當(dāng)運(yùn)行探測(cè)函數(shù)而沒(méi)有kretprobe_instance可用時(shí),它就加1。
四 、Kprobe限制
kprobe同意在同一地址注冊(cè)多個(gè)kprobes,可是不能同一時(shí)候在該地址上有多個(gè)jprobes。通常,用戶能夠在內(nèi)核的不論什么位置注冊(cè)探測(cè)點(diǎn),特別是能夠?qū)χ袛嗵幚砗瘮?shù)注冊(cè)探測(cè)點(diǎn),可是也有一些例外。假設(shè)用戶嘗試在實(shí)現(xiàn)kprobe的代碼(包含kernel/kprobes.c和arch/*/kernel/kprobes.c以及do_page_fault和notifier_call_chain)中注冊(cè)探測(cè)點(diǎn)。register_*probe將返回-EINVAL。
假設(shè)為一個(gè)內(nèi)聯(lián)(inline)函數(shù)注冊(cè)探測(cè)點(diǎn),kprobe無(wú)法保證對(duì)該函數(shù)的全部實(shí)例都注冊(cè)探測(cè)點(diǎn),由于gcc可能隱式地內(nèi)聯(lián)一個(gè)函數(shù)。因此,要記住,用戶可能看不到預(yù)期的探測(cè)點(diǎn)的運(yùn)行。一個(gè)探測(cè)點(diǎn)處理函數(shù)可以改動(dòng)被探測(cè)函數(shù)的上下文,如改動(dòng)內(nèi)核數(shù)據(jù)結(jié)構(gòu),寄存器等。因此,kprobe可以用來(lái)安裝bug解決代碼或注入一些錯(cuò)誤或測(cè)試代碼。
假設(shè)一個(gè)探測(cè)處理函數(shù)調(diào)用了還有一個(gè)探測(cè)點(diǎn),該探測(cè)點(diǎn)的處理函數(shù)不將執(zhí)行,可是它的nmissed數(shù)將加1。多個(gè)探測(cè)點(diǎn)處理函數(shù)或同一處理函數(shù)的多個(gè)實(shí)例可以在不同的CPU上同一時(shí)候執(zhí)行。除了注冊(cè)和卸載,kprobe不會(huì)使用mutexe或分配內(nèi)存。探測(cè)點(diǎn)處理函數(shù)在執(zhí)行時(shí)是失效搶占的。依賴于特定的架構(gòu),探測(cè)點(diǎn)處理函數(shù)執(zhí)行時(shí)也可能是中斷失效的。因此,對(duì)于不論什么探測(cè)點(diǎn)處理函數(shù),不要使用導(dǎo)致睡眠或進(jìn)程調(diào)度的不論什么內(nèi)核函數(shù)(如嘗試獲得semaphore)。
kretprobe是通過(guò)代替返回地址為提前定義的trampoline的地址來(lái)實(shí)現(xiàn)的。因此?;厮莺蚲cc內(nèi)嵌函數(shù)__builtin_return_address()調(diào)用將返回trampoline的地址而不是真正的被探測(cè)函數(shù)的返回地址。
假設(shè)一個(gè)函數(shù)的調(diào)用次數(shù)與它的返回次數(shù)不同樣,那么在該函數(shù)上注冊(cè)的kretprobe探測(cè)點(diǎn)可能產(chǎn)生無(wú)法預(yù)料的結(jié)果(do_exit()就是一個(gè)典型的樣例,但do_execve() 和 do_fork()沒(méi)有問(wèn)題)。
五、怎樣在內(nèi)核中引入Kprobe
probe已經(jīng)被包括在2.6內(nèi)核中??墒莾H僅有最新的內(nèi)核才提供了上面描寫敘述的所有功能,因此假設(shè)讀者想實(shí)驗(yàn)本文附帶的內(nèi)核模塊,須要最新的內(nèi)核,作者在2.6.18內(nèi)核上測(cè)試的這些代碼。內(nèi)核缺省時(shí)并沒(méi)有使能kprobe,因此用戶需使能它。
為了使能kprobe。用戶必須在編譯內(nèi)核時(shí)設(shè)置CONFIG_KPROBES,即選擇在“Instrumentation Support“中的“Kprobes”項(xiàng)。假設(shè)用戶希望動(dòng)態(tài)載入和卸載使用kprobe的模塊,還必須確?!癓oadable module support” (CONFIG_MODULES)和“Module unloading” (CONFIG_MODULE_UNLOAD)設(shè)置為y。假設(shè)用戶還想使用kallsyms_lookup_name()來(lái)得到被探測(cè)函數(shù)的地址,也要確保CONFIG_KALLSYMS設(shè)置為y,當(dāng)然設(shè)置CONFIG_KALLSYMS_ALL為y將更好。
內(nèi)核中引入Kprobe需要進(jìn)行以下步驟:
- 首先需要確認(rèn)內(nèi)核版本是否支持Kprobe,可以通過(guò)查詢文檔或者源代碼來(lái)確定。
- 在內(nèi)核配置文件中開(kāi)啟CONFIG_KPROBES選項(xiàng)。
- 編譯內(nèi)核,并安裝新的內(nèi)核。
- 寫一個(gè) Kprobe 監(jiān)聽(tīng)函數(shù),在該函數(shù)中可以添加相應(yīng)的邏輯,例如日志輸出、性能統(tǒng)計(jì)等等。Kprobe 監(jiān)聽(tīng)函數(shù)需要使用 Kprobe API 來(lái)注冊(cè)到系統(tǒng)中。
- 使用 insmod 命令加載編寫好的模塊,即可開(kāi)始監(jiān)聽(tīng)指定的內(nèi)核函數(shù)并執(zhí)行相應(yīng)操作。
六、Kprobe使用實(shí)例
6.1編寫簡(jiǎn)單的 Kprobes 探測(cè)模塊
接下來(lái),讓我們通過(guò)一個(gè)具體的例子,來(lái)深入了解如何編寫一個(gè)簡(jiǎn)單的 Kprobes 探測(cè)模塊。假設(shè)我們要探測(cè)do_sys_open函數(shù),這個(gè)函數(shù)負(fù)責(zé)處理系統(tǒng)的文件打開(kāi)操作,在實(shí)際的系統(tǒng)調(diào)試中,了解文件打開(kāi)的具體情況,如文件名、打開(kāi)標(biāo)志等信息,對(duì)于排查文件相關(guān)的問(wèn)題非常有幫助。以下是詳細(xì)的代碼實(shí)現(xiàn):
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/sched.h>
// 定義一個(gè)計(jì)數(shù)器,用于統(tǒng)計(jì)函數(shù)被調(diào)用的次數(shù)
static int count = 0;
// pre_handler回調(diào)函數(shù),在被探測(cè)指令執(zhí)行前被調(diào)用
static int handler_pre(struct kprobe *p, struct pt_regs *regs) {
// 從寄存器中獲取文件名和標(biāo)志信息
char *filename = (char *)regs->di;
int flags = (int)regs->si;
// 打印函數(shù)調(diào)用信息,包括文件名和標(biāo)志
printk(KERN_INFO "do_sys_open called with filename=%s, flags=%x\n", filename, flags);
// 計(jì)數(shù)器加一
count++;
return 0;
}
// 定義kprobe結(jié)構(gòu),指定要探測(cè)的函數(shù)為do_sys_open,并關(guān)聯(lián)pre_handler回調(diào)函數(shù)
static struct kprobe kp = {
.symbol_name = "do_sys_open",
.pre_handler = handler_pre,
};
// 模塊初始化函數(shù),用于注冊(cè)kprobe
static int __init mymodule_init(void) {
int ret;
// 調(diào)用register_kprobe函數(shù)注冊(cè)kprobe
ret = register_kprobe(&kp);
if (ret < 0) {
// 如果注冊(cè)失敗,打印錯(cuò)誤信息
printk(KERN_INFO "register_kprobe failed\n");
return ret;
}
// 如果注冊(cè)成功,打印成功信息
printk(KERN_INFO "kprobe registered\n");
return 0;
}
// 模塊退出函數(shù),用于卸載kprobe
static void __exit mymodule_exit(void) {
// 調(diào)用unregister_kprobe函數(shù)卸載kprobe
unregister_kprobe(&kp);
// 打印卸載信息,包括函數(shù)被調(diào)用的次數(shù)
printk(KERN_INFO "kprobe unregistered\n");
printk(KERN_INFO "do_sys_open called %d times\n", count);
}
// 聲明模塊初始化和退出函數(shù)
module_init(mymodule_init);
module_exit(mymodule_exit);
// 指定模塊許可證為GPL
MODULE_LICENSE("GPL");
在上述代碼中,首先定義了一個(gè)count變量,用于統(tǒng)計(jì)do_sys_open函數(shù)被調(diào)用的次數(shù)。handler_pre函數(shù)是pre_handler回調(diào)函數(shù),它從寄存器中獲取do_sys_open函數(shù)的參數(shù)filename和flags,并通過(guò)printk函數(shù)打印出來(lái),同時(shí)將count加一。
然后,創(chuàng)建了一個(gè)struct kprobe結(jié)構(gòu)體實(shí)例kp,指定要探測(cè)的函數(shù)為do_sys_open,并將handler_pre函數(shù)關(guān)聯(lián)到kp的pre_handler成員。
在mymodule_init函數(shù)中,通過(guò)register_kprobe函數(shù)將kp注冊(cè)到內(nèi)核中,如果注冊(cè)失敗,打印錯(cuò)誤信息并返回錯(cuò)誤碼;如果注冊(cè)成功,打印成功信息。
在mymodule_exit函數(shù)中,通過(guò)unregister_kprobe函數(shù)將kp從內(nèi)核中卸載,并打印卸載信息和do_sys_open函數(shù)被調(diào)用的次數(shù)。
6.2基于ftrace使用kprobe
kprobe和內(nèi)核的ftrac結(jié)合使用,需要對(duì)內(nèi)核進(jìn)行配置,然后添加探測(cè)點(diǎn)、進(jìn)行探測(cè)、查看結(jié)果。
(1)kprobe配置
打開(kāi)"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)kprobe trace events使用
kprobe事件相關(guān)的節(jié)點(diǎn)有如下:
/sys/kernel/debug/tracing/kprobe_events-----------------------配置kprobe事件屬性,增加事件之后會(huì)在kprobes下面生成對(duì)應(yīng)目錄。
/sys/kernel/debug/tracing/kprobe_profile----------------------kprobe事件統(tǒng)計(jì)屬性文件。
/sys/kernel/debug/tracing/kprobes/<GRP>/<EVENT>/enabled-------使能kprobe事件
/sys/kernel/debug/tracing/kprobes/<GRP>/<EVENT>/filter--------過(guò)濾kprobe事件
/sys/kernel/debug/tracing/kprobes/<GRP>/<EVENT>/format--------查詢kprobe事件顯示格式
下面就結(jié)合實(shí)例,看一下如何使用kprobe事件。
(3)kprobe事件配置
新增一個(gè)kprobe事件,通過(guò)寫kprobe_events來(lái)設(shè)置。
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----------------------------------------------------------刪除一個(gè)探測(cè)點(diǎn)
細(xì)節(jié)解釋如下:
GRP : Group name. If omitted, use "kprobes" for it.------------設(shè)置后會(huì)在events/kprobes下創(chuàng)建<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.--------------------------模塊名,一般不設(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 : Fetch Nth entry of stack (N >= 0)----------------------------------獲取指定??臻g值,即sp寄存器+N后的位置值
$stack : Fetch stack address.-----------------------------------------------獲取sp寄存器值
$retval : Fetch return value.(*)--------------------------------------------獲取返回值,用戶return kprobe
$comm : Fetch current task comm.----------------------------------------獲取對(duì)應(yīng)進(jìn)程名稱。
+|-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.----------------設(shè)置參數(shù)的類型,可以支持字符串和比特類型
(*) only for return probe.
(**) this is useful for fetching a field of data structures.
執(zhí)行如下兩條命令就會(huì)生成目錄/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-----------------------------------------------------這里面一定要用">>",不然就會(huì)覆蓋前面的設(shè)置。
echo '-:myprobe' >> /sys/kernel/debug/tracing/kprobe_events
echo '-:myretprobe' >> /sys/kernel/debug/tracing/kprobe_events
參數(shù)后面的寄存器是跟架構(gòu)相關(guān)的,%ax、%dx、%cx表示第1/2/3個(gè)參數(shù),超出部分使用$stack來(lái)存儲(chǔ)參數(shù)。
函數(shù)返回值保存在$retval中。
(4)kprobe使能
對(duì)kprobe事件的是能通過(guò)往對(duì)應(yīng)事件的enable寫1開(kāi)啟探測(cè);寫0暫停探測(cè)。
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中可以看到結(jié)果。
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
(5)kprobe事件過(guò)濾
跟蹤函數(shù)需要通過(guò)filter進(jìn)行過(guò)濾,可以有效過(guò)濾掉冗余信息。filter文件用于設(shè)置過(guò)濾條件,可以減少trace中輸出的信息,它支持的格式和c語(yǔ)言的表達(dá)式類似,支持 ==,!=,>,<,>=,<=判斷,并且支持與&&,或||,還有()。
echo 'filename==0x8241' > /sys/kernel/debug/tracing/events/kprobes/myprobe/filter
(6)kprobe和棧配合使用
如果要在顯示函數(shù)的同時(shí)顯示其棧信息,可以通過(guò)配置trace_options來(lái)達(dá)到。
echo stacktrace > /sys/kernel/debug/tracing/trace_options
(7)kprobe_profile統(tǒng)計(jì)信息
獲取一段kprobe時(shí)間之后,可以再kprobe_profile中查看統(tǒng)計(jì)信息。
后面兩列分別表示命中和未命中的次數(shù)。
cat /sys/kernel/debug/tracing/kprobe_profile myprobe
6.3調(diào)試工具搭配使用
在使用 Kprobes 進(jìn)行調(diào)試時(shí),搭配其他工具可以更高效地分析和解決問(wèn)題,就像一場(chǎng)精彩的交響樂(lè),不同的樂(lè)器相互配合,才能演奏出美妙的旋律。
查看內(nèi)核日志是一個(gè)非常重要的輔助手段。在前面的代碼中,我們使用了printk函數(shù)來(lái)輸出調(diào)試信息,這些信息會(huì)被記錄到內(nèi)核日志中。通過(guò)查看內(nèi)核日志,我們可以了解 Kprobes 探測(cè)模塊的運(yùn)行情況,如探測(cè)點(diǎn)是否成功注冊(cè)、回調(diào)函數(shù)是否被正確調(diào)用、函數(shù)的參數(shù)和執(zhí)行結(jié)果等。在 Linux 系統(tǒng)中,可以使用dmesg命令來(lái)查看內(nèi)核日志,例如:dmesg | grep "do_sys_open",這個(gè)命令會(huì)過(guò)濾出內(nèi)核日志中與do_sys_open相關(guān)的信息,方便我們快速定位問(wèn)題。
gdb調(diào)試器也能與 Kprobes 配合使用,為調(diào)試工作提供更多便利。雖然 Kprobes 主要用于動(dòng)態(tài)調(diào)試運(yùn)行中的內(nèi)核,但在某些情況下,結(jié)合gdb可以更深入地分析問(wèn)題。比如,當(dāng) Kprobes 探測(cè)到某個(gè)函數(shù)出現(xiàn)異常,但通過(guò)printk輸出的信息不足以定位問(wèn)題時(shí),可以使用gdb來(lái)調(diào)試內(nèi)核模塊。首先,需要在內(nèi)核編譯時(shí)開(kāi)啟調(diào)試信息,然后使用gdb加載內(nèi)核和內(nèi)核模塊,通過(guò)設(shè)置斷點(diǎn)、單步執(zhí)行等操作,詳細(xì)分析函數(shù)的執(zhí)行過(guò)程,找出問(wèn)題的根源。
6.4常見(jiàn)問(wèn)題與解決方法
在使用 Kprobes 的過(guò)程中,可能會(huì)遇到一些常見(jiàn)問(wèn)題,這些問(wèn)題就像是前進(jìn)道路上的絆腳石,但只要我們掌握了解決方法,就能輕松跨越。
探測(cè)點(diǎn)無(wú)法注冊(cè)是一個(gè)常見(jiàn)的問(wèn)題。這可能是由于目標(biāo)函數(shù)不存在、符號(hào)未導(dǎo)出或內(nèi)核保護(hù)等原因?qū)е碌?。?dāng)遇到這種情況時(shí),首先要確認(rèn)目標(biāo)函數(shù)是否存在,可以通過(guò)查看內(nèi)核源碼或使用nm命令查看內(nèi)核符號(hào)表來(lái)確認(rèn)。如果函數(shù)存在,再檢查符號(hào)是否導(dǎo)出,可以查看/proc/kallsyms文件,看目標(biāo)函數(shù)的符號(hào)是否在其中。如果是內(nèi)核保護(hù)導(dǎo)致的問(wèn)題,例如內(nèi)核處于寫保護(hù)狀態(tài),可能需要臨時(shí)關(guān)閉相關(guān)保護(hù)機(jī)制,但這需要謹(jǐn)慎操作,因?yàn)殛P(guān)閉保護(hù)機(jī)制可能會(huì)影響系統(tǒng)的穩(wěn)定性和安全性。
回調(diào)函數(shù)未按預(yù)期執(zhí)行也是一個(gè)需要關(guān)注的問(wèn)題。這可能是由于回調(diào)函數(shù)中存在錯(cuò)誤,如內(nèi)存訪問(wèn)越界、空指針引用等,導(dǎo)致回調(diào)函數(shù)執(zhí)行異常。在編寫回調(diào)函數(shù)時(shí),要確保代碼的正確性和健壯性,避免出現(xiàn)這些常見(jiàn)的錯(cuò)誤。同時(shí),要注意回調(diào)函數(shù)的執(zhí)行環(huán)境,因?yàn)榛卣{(diào)函數(shù)運(yùn)行在中斷上下文中,所以不能執(zhí)行可能會(huì)導(dǎo)致阻塞的操作,如睡眠、等待信號(hào)量等。如果需要進(jìn)行一些復(fù)雜的操作,可以將這些操作放到工作隊(duì)列或內(nèi)核線程中執(zhí)行,以避免影響回調(diào)函數(shù)的正常執(zhí)行。