Linux下 kprobe工具的使用

weixin_34162629發表於2017-07-26

此處轉載:

一、Kprobe簡單介紹

kprobe是一個動態地收集除錯和效能資訊的工具,它從Dprobe專案派生而來,是一種非破壞性工具,使用者用它差點兒能夠跟蹤不論什麼函式或被執行的指令以及一些非同步事件(如timer)。

它的基本工作機制是:使用者指定一個探測點。並把一個使用者定義的處理函式關聯到該探測點。當核心執行到該探測點時,對應的關聯函式被執行。然後繼續執行正常的程式碼路徑。


kprobe實現了三種型別的探測點: kprobes, jprobes和kretprobes (也叫返回探測點)。

kprobes是能夠被插入到核心的不論什麼指令位置的探測點,jprobes則僅僅能被插入到一個核心函式的入口,而kretprobes則是在指定的核心函式返回時才被執行。

一般。使用kprobe的程式實現作一個核心模組。模組的初始化函式來負責安裝探測點。退出函式解除安裝那些被安裝的探測點。kprobe提供了介面函式(APIs)來安裝或解除安裝探測點。

眼下kprobe支援例如以下架構:i386、x86_64、ppc64、ia64(不支援對slot1指令的探測)、sparc64 (返回探測還沒有實現)。

二、Kprobe實現原理

當安裝一個kprobes探測點時。kprobe首先備份被探測的指令,然後使用斷點指令(即在i386和x86_64的int3指令)來代替被探測指令的頭一個或幾個位元組。當CPU執行到探測點時,將因執行斷點指令而執行trap操作,那將導致儲存CPU的暫存器,呼叫對應的trap處理函式。而trap處理函式將呼叫對應的notifier_call_chain(核心中一種非同步工作機制)中註冊的全部notifier函式。kprobe正是通過向trap對應的notifier_call_chain註冊關聯到探測點的處理函式來實現探測處理的。

當kprobe註冊的notifier被執行時,它首先執行關聯到探測點的pre_handler函式,並把對應的kprobe struct和儲存的暫存器作為該函式的引數,接著,kprobe單步執行被探測指令的備份。最後,kprobe執行post_handler。等全部這些執行完成後。緊跟在被探測指令後的指令流將被正常執行。

kretprobe也使用了kprobes來實現,當使用者呼叫register_kretprobe()時,kprobe在被探測函式的入口建立了一個探測點。當執行到探測點時,kprobe儲存了被探測函式的返回地址並代替返回地址為一個trampoline的地址,kprobe在初始化時定義了該trampoline而且為該trampoline註冊了一個kprobe,當被探測函式執行它的返回指令時。控制傳遞到該trampoline,因此kprobe已經註冊的相應於trampoline的處理函式將被執行。而該處理函式會呼叫使用者關聯到該kretprobe上的處理函式。處理完成後,設定指令暫存器指向已經備份的函式返回地址。因而原來的函式返回被正常執行。

被探測函式的返回地址儲存在型別為kretprobe_instance的變數中。結構kretprobe的maxactive欄位指定了被探測函式能夠被同一時候探測的例項數,函式register_kretprobe()將預分配指定數量的kretprobe_instance。假設被探測函式是非遞迴的而且呼叫時已經保持了自旋鎖(spinlock),那麼maxactive為1就足夠了; 假設被探測函式是非遞迴的且執行時是搶佔失效的,那麼maxactive為NR_CPUS就能夠了;假設maxactive被設定為小於等於0, 它被設定到預設值(假設搶佔使能, 即配置了 CONFIG_PREEMPT,預設值為10和2*NR_CPUS中的最大值,否則預設值為NR_CPUS)。

假設maxactive被設定的太小了,一些探測點的執行可能被丟失,可是不影響系統的正常執行,在結構kretprobe中nmissed欄位將記錄被丟失的探測點執行數,它在返回探測點被註冊時設定為0,每次當執行探測函式而沒有kretprobe_instance可用時,它就加1。

三、Kprobe註冊函式

kprobe為每一型別的探測點提供了註冊和解除安裝函式。



1.register_kprobe

它用於註冊一個kprobes型別的探測點,其函式原型為:

int register_kprobe(struct kprobe *kp);

為了使用該函式。使用者須要在原始檔裡包括標頭檔案linux/kprobes.h。

該函式的引數是struct kprobe型別的指標。struct kprobe包括了欄位addr、pre_handler、post_handler和fault_handler,addr指定探測點的位置,pre_handler指定執行到探測點時執行的處理函式,post_handler指定執行完探測點後執行的處理函式。fault_handler指定錯誤處理函式,當在執行pre_handler、post_handler以及被探測函式期間錯誤發生時。它會被呼叫。在呼叫該註冊函式前。使用者必須先設定好struct kprobe的這些欄位,使用者能夠指定不論什麼處理函式為NULL。

該註冊函式會在kp->addr地址處註冊一個kprobes型別的探測點,當執行到該探測點時,將呼叫函式kp->pre_handler。執行完被探測函式後,將呼叫kp->post_handler。假設在執行kp->pre_handler或kp->post_handler時或在單步跟蹤被探測函式期間錯誤發生,將呼叫kp->fault_handler。



該函式成功時返回0,否則返回負的錯誤碼。

探測點處理函式pre_handler的原型例如以下:

int pre_handler(struct kprobe *p, struct pt_regs *regs);

使用者必須依照該原型引數格式定義自己的pre_handler,當然函式名取決於使用者自己。

引數p就是指向該處理函式關聯到的kprobes探測點的指標,能夠在該函式內部引用該結構的不論什麼欄位。就如同在使用呼叫register_kprobe時傳遞的那個引數。引數regs指向執行到探測點時儲存的暫存器內容。kprobe負責在呼叫pre_handler時傳遞這些引數,使用者不必關心,僅僅是要知道在該函式內你能訪問這些內容。

一般地,它應當始終返回0。除非使用者知道自己在做什麼。



探測點處理函式post_handler的原型例如以下:

void post_handler(struct kprobe *p, struct pt_regs *regs,
	unsigned long flags);

前兩個引數與pre_handler同樣。最後一個引數flags總是0。

錯誤處理函式fault_handler的原刑例如以下:

int fault_handler(struct kprobe *p, struct pt_regs *regs, int trapnr);
前兩個引數與pre_handler同樣,第三個引數trapnr是與錯誤處理相關的架構依賴的trap號(比如,對於i386,通常的保護錯誤是13。而頁失效錯誤是14)。

假設成功地處理了異常。它應當返回1。
2 . register_kretprobe

該函式用於註冊型別為kretprobes的探測點。它的原型例如以下:
int register_kretprobe(struct kretprobe *rp);

為了使用該函式,使用者須要在原始檔裡包括標頭檔案linux/kprobes.h。

該註冊函式的引數為struct kretprobe型別的指標,使用者在呼叫該函式前必須定義一個struct kretprobe的變數並設定它的kp.addr、handler以及maxactive欄位。kp.addr指定探測點的位置,handler指定探測點的處理函式。maxactive指定能夠同一時候執行的最大處理函式例項數,它應當被恰當設定。否則可能丟失探測點的某些執行。

該註冊函式在地址rp->kp.addr註冊一個kretprobe型別的探測點。當被探測函式返回時。rp->handler會被呼叫。

假設成功,它返回0,否則返回負的錯誤碼。



kretprobe處理函式的原型例如以下:

int kretprobe_handler(struct kretprobe_instance *ri, struct pt_regs *regs);
引數regs指向儲存的暫存器。ri指向型別為struct kretprobe_instance的變數,該結構的ret_addr欄位表示返回地址,rp指向對應的kretprobe_instance變數,task欄位指向對應的task_struct。結構struct kretprobe_instance是註冊函式register_kretprobe依據使用者指定的maxactive值來分配的。kprobe負責在呼叫kretprobe處理函式時傳遞對應的kretprobe_instance。

3. 相應於每個註冊函式。有相應的解除安裝函式。

void unregister_kprobe(struct kprobe *kp);
void unregister_jprobe(struct jprobe *jp);
void unregister_kretprobe(struct kretprobe *rp);

上面是相應與三種探測點型別的解除安裝函式。當使用探測點的模組解除安裝或須要解除安裝已經註冊的探測點時,須要使用相應的解除安裝函式來解除安裝已經註冊的探測點。kp,jp和rp分別為指向結構struct kprobe,struct jprobe和struct kretprobe的指標。它們應當指向呼叫相應的註冊函式時使用的那個結構。也就說註冊和解除安裝必須針對相同的探測點。否則會導致系統崩潰。這些解除安裝函式能夠在註冊後的不論什麼時刻呼叫。

四 、Kprobe限制

kprobe同意在同一地址註冊多個kprobes,可是不能同一時候在該地址上有多個jprobes。



通常,使用者能夠在核心的不論什麼位置註冊探測點,特別是能夠對中斷處理函式註冊探測點,可是也有一些例外。

假設使用者嘗試在實現kprobe的程式碼(包含kernel/kprobes.c和arch/*/kernel/kprobes.c以及do_page_fault和notifier_call_chain)中註冊探測點。register_*probe將返回-EINVAL.

假設為一個內聯(inline)函式註冊探測點,kprobe無法保證對該函式的全部例項都註冊探測點,由於gcc可能隱式地內聯一個函式。因此,要記住,使用者可能看不到預期的探測點的執行。



一個探測點處理函式可以改動被探測函式的上下文,如改動核心資料結構,暫存器等。因此,kprobe可以用來安裝bug解決程式碼或注入一些錯誤或測試程式碼。



假設一個探測處理函式呼叫了還有一個探測點,該探測點的處理函式不將執行,可是它的nmissed數將加1。

多個探測點處理函式或同一處理函式的多個例項可以在不同的CPU上同一時候執行。

除了註冊和解除安裝,kprobe不會使用mutexe或分配記憶體。



探測點處理函式在執行時是失效搶佔的。依賴於特定的架構,探測點處理函式執行時也可能是中斷失效的。因此,對於不論什麼探測點處理函式,不要使用導致睡眠或程式排程的不論什麼核心函式(如嘗試獲得semaphore)。



kretprobe是通過代替返回地址為提前定義的trampoline的地址來實現的。因此棧回溯和gcc內嵌函式__builtin_return_address()呼叫將返回trampoline的地址而不是真正的被探測函式的返回地址。



假設一個函式的呼叫次數與它的返回次數不同樣,那麼在該函式上註冊的kretprobe探測點可能產生無法預料的結果(do_exit()就是一個典型的樣例,但do_execve() 和 do_fork()沒有問題)。



當進入或退出一個函式時,假設CPU正執行在一個非當前任務全部的棧上,那麼該函式的kretprobe探測可能產生無法預料的結果,因此kprobe並不支援在x86_64上對__switch_to()的返回探測。假設使用者對它註冊探測點,註冊函式將返回-EINVAL。

五、怎樣在核心中引入Kprobe

kprobe已經被包括在2.6核心中。可是僅僅有最新的核心才提供了上面描寫敘述的所有功能,因此假設讀者想實驗本文附帶的核心模組,須要最新的核心,作者在2.6.18核心上測試的這些程式碼。核心預設時並沒有使能kprobe,因此使用者需使能它。

為了使能kprobe。使用者必須在編譯核心時設定CONFIG_KPROBES,即選擇在“Instrumentation Support“中的“Kprobes”項。假設使用者希望動態載入和解除安裝使用kprobe的模組,還必須確保“Loadable module support” (CONFIG_MODULES)和“Module unloading” (CONFIG_MODULE_UNLOAD)設定為y。假設使用者還想使用kallsyms_lookup_name()來得到被探測函式的地址,也要確保CONFIG_KALLSYMS設定為y,當然設定CONFIG_KALLSYMS_ALL為y將更好。

六、Kprobe使用例項

演示樣例見核心原碼: samples/kprobes/

相關文章