Linux下的一個全新的性能測量和調試診斷工具Systemtap[1]kprobe

本系列文章詳細地介紹了一個Linux下的全新的調式、診斷和性能測量工具Systemtap和它所依賴的基礎kprobe以及促使開發該工具的先驅DTrace並給出實際使用例子使讀者更進一步瞭解和認識這些工具。本文是該系列文章之一,它講解了kprobe的原理、編程接口、侷限性和使用注意事項並給出實際使用示例幫助讀者理解和認識kprobe。本系列文章之二講解了DTrace以及Systemtap與DTrace比較。本系列文章之三講解了Systemtap的原理,並通過一個例子向讀者展示Systemtap的工作機理。

一、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。等所有這些運行完畢後,緊跟在被探測指令後的指令流將被正常執行。
jprobe通過註冊kprobes在被探測函數入口的來實現,它能無縫地訪問被探測函數的參數。jprobe處理函數應當和被探測函數有同樣的原型,而且該處理函數在函數末必須調用kprobe提供的函數jprobe_return()。當執行到該探測點時,kprobe備份CPU寄存器和棧的一些部分,然後修改指令寄存器指向jprobe處理函數,當執行該jprobe處理函數時,寄存器和棧內容與執行真正的被探測函數一模一樣,因此它不需要任何特別的處理就能訪問函數參數, 在該處理函數執行到最後時,它調用jprobe_return(),那導致寄存器和棧恢復到執行探測點時的狀態,因此被探測函數能被正常運行。需要注意,被探測函數的參數可能通過棧傳遞,也可能通過寄存器傳遞,但是jprobe對於兩種情況都能工作,因爲它既備份了棧,又備份了寄存器,當然,前提是jprobe處理函數原型必須與被探測函數完全一樣。
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_jprobe
該函數用於註冊jprobes類型的探測點,它的原型如下:
int register_jprobe(struct jprobe *jp);
爲了使用該函數,用戶需要在源文件中包含頭文件linux/kprobes.h。
用戶在調用該註冊函數前需要定義一個struct jprobe類型的變量並設置它的kp.addr和entry字段,kp.addr指定探測點的位置,它必須是被探測函數的第一條指令的地址,entry指定探測點的處理函數,該處理函數的參數表和返回類型應當與被探測函數完全相同,而且它必須正好在返回前調用jprobe_return()。如果被探測函數被聲明爲asmlinkage、fastcall或影響參數傳遞的任何其他形式,那麼相應的處理函數也必須聲明爲相應的形式。
該註冊函數在jp->kp.addr註冊一個jprobes類型的探測點,當內核運行到該探測點時,jp->entry指定的函數會被執行。
如果成功,該函數返回0,否則返回負的錯誤碼。
3.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。
4.unregister_*probe
對應於每一個註冊函數,有相應的卸載函數。
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使用實例

本文附帶的包,kprobes-examples.tar.bz2
包含了三個示例模塊,kprobe-exam.c是kprobes使用示例,jprobe-exam.c是jprobes使用示例,kretprobe-exam.c是kretprobes使用示例,讀者可以下載該包並執行如下指令來實驗這些模塊:

$ tar -jxvf kprobes-examples.tar.bz2
$ cd kprobes-examples
$ make

$ su -

$ insmod kprobe-example.ko
$ dmesg

$ rmmod kprobe-example
$ dmesg

$ insmod jprobe-example.ko
$ cat kprobe-example.c
$dmesg

$ rmmod jprobe-example
$ dmesg

$ insmod kretprobe-example.ko
$ dmesg

$ ls -Rla / > /dev/null &
$ dmesg

$ rmmod kretprobe-example
$ dmesg

$
示例模塊kprobe-exame.c探測schedule()函數,在探測點執行前後分別輸出當前正在運行的進程、所在的CPU以及preempt_count(),當卸載該模塊時將輸出該模塊運行時間以及發生的調度次數。這是該模塊在作者系統上的輸出:
kprobe registered
current task on CPU#1: swapper (before scheduling), preempt_count = 0
current task on CPU#1: swapper (after scheduling), preempt_count = 0
current task on CPU#0: insmod (before scheduling), preempt_count = 0
current task on CPU#0: insmod (after scheduling), preempt_count = 0
current task on CPU#1: klogd (before scheduling), preempt_count = 0
current task on CPU#1: klogd (after scheduling), preempt_count = 0
current task on CPU#1: klogd (before scheduling), preempt_count = 0
current task on CPU#1: klogd (after scheduling), preempt_count = 0
current task on CPU#1: klogd (before scheduling), preempt_count = 0

Scheduling times is 5918 during of 7655 milliseconds.
kprobe unregistered
示例模塊jprobe-exam.c是一個jprobes探測例子,它示例了獲取系統調用open的參數,但讀者不要試圖在實際的應用中這麼使用,因爲copy_from_user可能導致睡眠,而kprobe並不允許在探測點處理函數中這麼做(請參看前面內容瞭解詳細描述)。
這是該模塊在作者系統上的輸出:
Registered a jprobe.
process 'cat' call open('/etc/ld.so.cache', 0, 0)
process 'cat' call open('/lib/libc.so.6', 0, -524289)
process 'cat' call open('/usr/lib/locale/locale-archive', 32768, 1)
process 'cat' call open('/usr/share/locale/locale.alias', 0, 438)
process 'cat' call open('/usr/lib/locale/en_US.UTF-8/LC_CTYPE', 0, 0)
process 'cat' call open('/usr/lib/locale/en_US.utf8/LC_CTYPE', 0, 0)
process 'cat' call open('/usr/lib/gconv/gconv-modules.cache', 0, 0)
process 'cat' call open('kprobe-exam.c', 32768, 0)

process 'rmmod' call open('/etc/ld.so.cache', 0, 0)
process 'rmmod' call open('/lib/libc.so.6', 0, -524289)
process 'rmmod' call open('/proc/modules', 0, 438)
jprobe unregistered
示例模塊kretprobe-exam.c是一個返回探測例子,它探測系統調用open並輸出返回值小於0的情況。它也有意設置maxactive爲1,以便示例丟失探測運行的情況,當然,只有系統併發運行多個sys_open纔可能導致這種情況,因此,讀者需要有SMP的系統或者有超線程支持才能看到這種情況。如果讀者比較仔細,會看到在前面的命令有”ls -Rla / > /dev/null & ,那是專門爲了導致出現丟失探測運行的。
這是該模塊在作者系統上的輸出:
Registered a return probe.
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2

kretprobe unregistered
Missed 11 sys_open probe instances.

小結

本文詳細地講解了kprobe的方方面面並給出實際的例子代碼幫助讀者學習和使用kprobe。本文是系列文章“Linux下的一個全新的性能測量和調式診斷工具 -- Systemtap”之一,有興趣的讀者可以閱讀該系列文章之二和三。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章