Linux內核kprobe機制

一、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使用實例

本文附帶的包包含了三個示例模塊,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提供了三種形式的探測點,一種是最基本的kprobe,能夠在指定代碼執行前、執行後進行探測,但此時不能訪問被探測函數內的相關變量信息;一種是jprobe,用於探測某一函數的入口,並且能夠訪問對應的函數參數;一種是kretprobe,用於完成指定函數返回值的探測功能。其中最基本的就是kprobe機制,jprobe以及kretprobe的實現都依賴於kprobe,但其代碼的實現都很巧妙,強烈建議每一個內核愛好者閱讀。
    
    好了,閒話少敘,開始上代碼:
  首先是struct kprobe結構,每一個探測點的基本結構

點擊(此處)摺疊或打開

  1. struct kprobe {
  2.     /*用於保存kprobe的全局hash表,以被探測的addr爲key*/
  3.     struct hlist_node hlist;

  4.     /* list of kprobes for multi-handler support */
  5.     /*當對同一個探測點存在多個探測函數時,所有的函數掛在這條鏈上*/
  6.     struct list_head list;

  7.     /*count the number of times this probe was temporarily disarmed */
  8.     unsigned long nmissed;

  9.     /* location of the probe point */
  10.     /*被探測的目標地址*/
  11.     kprobe_opcode_t *addr;

  12.     /* Allow user to indicate symbol name of the probe point */
  13.     /*symblo_name的存在,允許用戶指定函數名而非確定的地址*/
  14.     const char *symbol_name;

  15.     /* Offset into the symbol */
  16.     /*如果被探測點爲函數內部某個指令,需要使用addr + offset的方式*/
  17.     unsigned int offset;

  18.     /* Called before addr is executed. */
  19.     /*探測函數,在目標探測點執行之前調用*/
  20.     kprobe_pre_handler_t pre_handler;

  21.     /* Called after addr is executed, unless... */
  22.     /*探測函數,在目標探測點執行之後調用*/
  23.     kprobe_post_handler_t post_handler;

  24.     /*
  25.      * ... called if executing addr causes a fault (eg. page fault).
  26.      * Return 1 if it handled fault, otherwise kernel will see it.
  27.      */
  28.     kprobe_fault_handler_t fault_handler;

  29.     /*
  30.      * ... called if breakpoint trap occurs in probe handler.
  31.      * Return 1 if it handled break, otherwise kernel will see it.
  32.      */
  33.     kprobe_break_handler_t break_handler;

  34.     /*opcode 以及 ainsn 用於保存被替換的指令碼*/
  35.     
  36.     /* Saved opcode (which has been replaced with breakpoint) */
  37.     kprobe_opcode_t opcode;

  38.     /* copy of the original instruction */
  39.     struct arch_specific_insn ainsn;

  40.     /*
  41.      * Indicates various status flags.
  42.      * Protected by kprobe_mutex after this kprobe is registered.
  43.      */
  44.     u32 flags;
  45. };

    對於kprobe功能的實現主要利用了內核中的兩個功能特性:異常(尤其是int 3),單步執行(EFLAGS中的TF標誌)。

    大概的流程:
 1)在註冊探測點的時候,對被探測函數的指令碼進行替換,替換爲int 3的指令碼;
 2)在執行int 3的異常執行中,通過通知鏈的方式調用kprobe的異常處理函數;
 3)在kprobe的異常出來函數中,判斷是否存在pre_handler鉤子,存在則執行;
 4)執行完後,準備進入單步調試,通過設置EFLAGS中的TF標誌位,並且把異常返回的地址修改爲保存的原指令碼;
 5)代碼返回,執行原有指令,執行結束後觸發單步異常;
 6)在單步異常的處理中,清除單步標誌,執行post_handler流程,並最終返回;

    下面又進入代碼時間,首先看一下kprobe模塊的初始化代碼,初始化代碼主要做了兩件事:標記出哪些代碼是不能被探測的,這些代碼屬於kprobe實現的關鍵代碼;註冊通知鏈到die_notifier,用於接收異常通知。

點擊(此處)摺疊或打開

  1. 初始化代碼位於kernel/kprobes.c中
  2. static int __init init_kprobes(void)
  3. {
  4.     int i, err = 0;
  5.         ....

  6.      /*kprobe_blacklist中保存的是kprobe實現的關鍵代碼路徑,這些函數不應該被kprobe探測*/
  7.     /*
  8.      * Lookup and populate the kprobe_blacklist.
  9.      *
  10.      * Unlike the kretprobe blacklist, we'll need to determine
  11.      * the range of addresses that belong to the said functions,
  12.      * since a kprobe need not necessarily be at the beginning
  13.      * of a function.
  14.      */
  15.     for (kb = kprobe_blacklist; kb->name != NULL; kb++) {
  16.         kprobe_lookup_name(kb->name, addr);
  17.         if (!addr)
  18.             continue;

  19.         kb->start_addr = (unsigned long)addr;
  20.         symbol_name = kallsyms_lookup(kb->start_addr,
  21.                 &size, &offset, &modname, namebuf);
  22.         if (!symbol_name)
  23.             kb->range = 0;
  24.         else
  25.             kb->range = size;
  26.     }
  27.         ....
  28.     if (!err)
  29.         /*註冊通知鏈到die_notifier,用於接收int 3的異常信息*/
  30.         err = register_die_notifier(&kprobe_exceptions_nb);
  31.          ....
  32. }
  33. 其中的通知鏈:
  34. static struct notifier_block kprobe_exceptions_nb = {
  35.     .notifier_call = kprobe_exceptions_notify,
  36.     /*優先級最高,保證最先執行*/
  37.     .priority = 0x7fffffff /* we need to be notified first */
  38. };
    kprobe的註冊流程register_kprobe。

點擊(此處)摺疊或打開

  1. int __kprobes register_kprobe(struct kprobe *p)
  2. {
  3.     int ret = 0;
  4.     struct kprobe *old_p;
  5.     struct module *probed_mod;
  6.     kprobe_opcode_t *addr;

  7.     /*獲取被探測點的地址,指定了symbol_name,則從kallsyms中獲取;指定了offset,則返回addr + offset*/
  8.     addr = kprobe_addr(p);
  9.     if (!addr)
  10.         return -EINVAL;
  11.     p->addr = addr;

  12.     /*判斷同一個kprobe是否被重複註冊*/
  13.     ret = check_kprobe_rereg(p);
  14.     if (ret)
  15.         return ret;

  16.     jump_label_lock();
  17.     preempt_disable();
  18.     /*判斷被註冊的函數是否位於內核的代碼段內,或位於不能探測的kprobe實現路徑中*/
  19.     if (!kernel_text_address((unsigned long) p->addr) ||
  20.      in_kprobes_functions((unsigned long) p->addr) ||
  21.      ftrace_text_reserved(p->addr, p->addr) ||
  22.      jump_label_text_reserved(p->addr, p->addr))
  23.         goto fail_with_jump_label;

  24.     /* User can pass only KPROBE_FLAG_DISABLED to register_kprobe */
  25.     p->flags &= KPROBE_FLAG_DISABLED;

  26.     /*
  27.      * Check if are we probing a module.
  28.      */
  29.     /*判斷被探測的地址是否屬於某一個模塊,並且位於模塊的text section內*/
  30.     probed_mod = __module_text_address((unsigned long) p->addr);
  31.     if (probed_mod) {
  32.         /*如果被探測的爲模塊地址,首先要增加模塊的引用計數*/
  33.         /*
  34.          * We must hold a refcount of the probed module while updating
  35.          * its code to prohibit unexpected unloading.
  36.          */
  37.         if (unlikely(!try_module_get(probed_mod)))
  38.             goto fail_with_jump_label;

  39.         /*
  40.          * If the module freed .init.text, we couldn't insert
  41.          * kprobes in there.
  42.          */
  43.         /*如果被探測的地址位於模塊的init地址段內,但該段代碼區間已被釋放,則直接退出*/
  44.         if (within_module_init((unsigned long)p->addr, probed_mod) &&
  45.          probed_mod->state != MODULE_STATE_COMING) {
  46.             module_put(probed_mod);
  47.             goto fail_with_jump_label;
  48.         }
  49.     }
  50.     preempt_enable();
  51.     jump_label_unlock();

  52.     p->nmissed = 0;
  53.     INIT_LIST_HEAD(&p->list);
  54.     mutex_lock(&kprobe_mutex);

  55.     jump_label_lock(); /* needed to call jump_label_text_reserved() */

  56.     get_online_cpus();    /* For avoiding text_mutex deadlock. */
  57.     mutex_lock(&text_mutex);

  58.     /*判斷在同一個探測點是否已經註冊了其他的探測函數*/
  59.     old_p = get_kprobe(p->addr);
  60.     if (old_p) {
  61.         /* Since this may unoptimize old_p, locking text_mutex. */
  62.         /*如果已經存在註冊過的kprobe,則將探測點的函數修改爲aggr_pre_handler,並將所有的handler掛載到其鏈表上,由其負責所有handler函數的執行*/
  63.         ret = register_aggr_kprobe(old_p, p);
  64.         goto out;
  65.     }

  66.     /* 分配特定的內存地址用於保存原有的指令
  67.      * 按照內核註釋,被分配的地址必須must be on special executable page on x86.
  68.      * 該地址被保存在kprobe->ainsn.insn
  69.      */
  70.     ret = arch_prepare_kprobe(p);
  71.     if (ret)
  72.         goto out;

  73.     /*將kprobe加入到相應的hash表內*/
  74.     INIT_HLIST_NODE(&p->hlist);
  75.     hlist_add_head_rcu(&p->hlist,
  76.          &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]);

  77.     if (!kprobes_all_disarmed && !kprobe_disabled(p))
  78. /*將探測點的指令碼修改爲int 3指令*/
  79.         __arm_kprobe(p);

  80.     /* Try to optimize kprobe */
  81.     try_to_optimize_kprobe(p);

  82. out:
  83.     mutex_unlock(&text_mutex);
  84.     put_online_cpus();
  85.     jump_label_unlock();
  86.     mutex_unlock(&kprobe_mutex);

  87.     if (probed_mod)
  88.         module_put(probed_mod);

  89.     return ret;

  90. fail_with_jump_label:
  91.     preempt_enable();
  92.     jump_label_unlock();
  93.     return -EINVAL;
    註冊完畢,就開始kprobe的執行流程了。對於該探測點,由於其起始指令已經被修改爲int3,因此在執行到該地址時,必然會觸發3號中斷向量的處理流程do_int3.

點擊(此處)摺疊或打開

  1. /* May run on IST stack. */
  2. dotraplinkage void __kprobes do_int3(struct pt_regs *regs, long error_code)
  3. {
  4. #ifdef CONFIG_KGDB_LOW_LEVEL_TRAP
  5.     if (kgdb_ll_trap(DIE_INT3, "int3", regs, error_code, 3, SIGTRAP)
  6.             == NOTIFY_STOP)
  7.         return;
  8. #endif /* CONFIG_KGDB_LOW_LEVEL_TRAP */
  9. #ifdef CONFIG_KPROBES
  10.     /*在這裏以DIE_INT3,通知kprobe註冊的通知鏈*/
  11.     if (notify_die(DIE_INT3, "int3", regs, error_code, 3, SIGTRAP)
  12.             == NOTIFY_STOP)
  13.         return;
  14. #else
  15.     if (notify_die(DIE_TRAP, "int3", regs, error_code, 3, SIGTRAP)
  16.             == NOTIFY_STOP)
  17.         return;
  18. #endif

  19.     preempt_conditional_sti(regs);
  20.     do_trap(3, SIGTRAP, "int3", regs, error_code, NULL);
  21.     preempt_conditional_cli(regs);
  22. }
    在do_int3中觸發kprobe註冊的通知鏈函數,kprobe_exceptions_notify。由於kprobe以及jprobe等機制的處理核心都在此函數內,這裏只針對kprobe的流程進行分析:進入函數的原因是DIE_INT3,並且是第一次進入該函數。

點擊(此處)摺疊或打開

  1. int __kprobes kprobe_exceptions_notify(struct notifier_block *self,
  2.                  unsigned long val, void *data)
  3. {
  4.     struct die_args *args = data;
  5.     int ret = NOTIFY_DONE;

  6.     if (args->regs && user_mode_vm(args->regs))
  7.         return ret;

  8.     switch (val) {
  9.     case DIE_INT3:
  10. /*對於kprobe,進入kprobe_handle*/
  11.         if (kprobe_handler(args->regs))
  12.             ret = NOTIFY_STOP;
  13.         break;
  14.     case DIE_DEBUG:
  15.         if (post_kprobe_handler(args->regs)) {
  16.             /*
  17.              * Reset the BS bit in dr6 (pointed by args->err) to
  18.              * denote completion of processing
  19.              */
  20.             (*(unsigned long *)ERR_PTR(args->err)) &= ~DR_STEP;
  21.             ret = NOTIFY_STOP;
  22.         }
  23.         break;
  24.     case DIE_GPF:
  25.         /*
  26.          * To be potentially processing a kprobe fault and to
  27.          * trust the result from kprobe_running(), we have
  28.          * be non-preemptible.
  29.          */
  30.         if (!preemptible() && kprobe_running() &&
  31.          kprobe_fault_handler(args->regs, args->trapnr))
  32.             ret = NOTIFY_STOP;
  33.         break;
  34.     default:
  35.         break;
  36.     }
  37.     return ret;
  38. }

點擊(此處)摺疊或打開

  1. static int __kprobes kprobe_handler(struct pt_regs *regs)
  2. {
  3.     kprobe_opcode_t *addr;
  4.     struct kprobe *p;
  5.     struct kprobe_ctlblk *kcb;

  6.     /*對於int 3中斷,其被Intel定義爲Trap,那麼異常發生時EIP寄存器內指向的爲異常指令的後一條指令*/
  7.     addr = (kprobe_opcode_t *)(regs->ip - sizeof(kprobe_opcode_t));
  8.     /*
  9.      * We don't want to be preempted for the entire
  10.      * duration of kprobe processing. We conditionally
  11.      * re-enable preemption at the end of this function,
  12.      * and also in reenter_kprobe() and setup_singlestep().
  13.      */
  14.     preempt_disable();

  15.     kcb = get_kprobe_ctlblk();
  16.     /*獲取addr對應的kprobe*/
  17.     p = get_kprobe(addr);

  18.     if (p) {
  19. /*如果異常的進入是由kprobe導致,則進入reenter_kprobe(jprobe需要,到時候分析)*/
  20.         if (kprobe_running()) {
  21.             if (reenter_kprobe(p, regs, kcb))
  22.                 return 1;
  23.         } else {
  24.             set_current_kprobe(p, regs, kcb);
  25.             kcb->kprobe_status = KPROBE_HIT_ACTIVE;

  26.             /*
  27.              * If we have no pre-handler or it returned 0, we
  28.              * continue with normal processing. If we have a
  29.              * pre-handler and it returned non-zero, it prepped
  30.              * for calling the break_handler below on re-entry
  31.              * for jprobe processing, so get out doing nothing
  32.              * more here.
  33.              */
  34.     /*執行在此地址上掛載的pre_handle函數*/
  35.             if (!p->pre_handler || !p->pre_handler(p, regs))
  36. /*設置單步調試模式,爲post_handle函數的執行做準備*/
  37.                 setup_singlestep(p, regs, kcb, 0);
  38.             return 1;
  39.         }
  40.     } else if (*addr != BREAKPOINT_INSTRUCTION) {
  41.         /*
  42.          * The breakpoint instruction was removed right
  43.          * after we hit it. Another cpu has removed
  44.          * either a probepoint or a debugger breakpoint
  45.          * at this address. In either case, no further
  46.          * handling of this interrupt is appropriate.
  47.          * Back up over the (now missing) int3 and run
  48.          * the original instruction.
  49.          */
  50.         regs->ip = (unsigned long)addr;
  51.         preempt_enable_no_resched();
  52.         return 1;
  53.     } else if (kprobe_running()) {
  54.         p = __this_cpu_read(current_kprobe);
  55.         if (p->break_handler && p->break_handler(p, regs)) {
  56.             setup_singlestep(p, regs, kcb, 0);
  57.             return 1;
  58.         }
  59.     } /* else: not a kprobe fault; let the kernel handle it */

  60.     preempt_enable_no_resched();
  61.     return 0;
  62. }

點擊(此處)摺疊或打開

  1. static void __kprobes setup_singlestep(struct kprobe *p, struct pt_regs *regs,
  2.                  struct kprobe_ctlblk *kcb, int reenter)
  3. {
  4.     if (setup_detour_execution(p, regs, reenter))
  5.         return;

  6. #if !defined(CONFIG_PREEMPT)
  7.     if (p->ainsn.boostable == 1 && !p->post_handler) {
  8.         /* Boost up -- we can execute copied instructions directly */
  9.         if (!reenter)
  10.             reset_current_kprobe();
  11.         /*
  12.          * Reentering boosted probe doesn't reset current_kprobe,
  13.          * nor set current_kprobe, because it doesn't use single
  14.          * stepping.
  15.          */
  16.         regs->ip = (unsigned long)p->ainsn.insn;
  17.         preempt_enable_no_resched();
  18.         return;
  19.     }
  20. #endif
  21.     /*jprobe*/
  22.     if (reenter) {
  23.         save_previous_kprobe(kcb);
  24.         set_current_kprobe(p, regs, kcb);
  25.         kcb->kprobe_status = KPROBE_REENTER;
  26.     } else
  27.         kcb->kprobe_status = KPROBE_HIT_SS;
  28.     /* Prepare real single stepping */
  29.     /*準備單步模式,設置EFLAGS的TF標誌位,清楚IF標誌位(禁止中斷)*/
  30.     clear_btf();
  31.     regs->flags |= X86_EFLAGS_TF;
  32.     regs->flags &= ~X86_EFLAGS_IF;
  33.     /* single step inline if the instruction is an int3 */
  34.     if (p->opcode == BREAKPOINT_INSTRUCTION)
  35.         regs->ip = (unsigned long)p->addr;
  36.     else
  37. /*設置異常返回的指令爲保存的被探測點的指令*/
  38.         regs->ip = (unsigned long)p->ainsn.insn;
  39. }
     對應kprobe,pre_handle的執行就結束了,按照代碼,程序開始執行保存的被探測點的指令,由於開啓了單步調試模式,執行完指令後會繼續觸發異常,這次的是do_debug異常處理流程。

點擊(此處)摺疊或打開

  1. dotraplinkage void __kprobes do_debug(struct pt_regs *regs, long error_code)
  2. {
  3.     ....

  4.     /*在do_debug中,以DIE_DEBUG再一次觸發kprobe的通知鏈*/
  5.     if (notify_die(DIE_DEBUG, "debug", regs, PTR_ERR(&dr6), error_code,
  6.                             SIGTRAP) == NOTIFY_STOP)
  7.         return;
  8.    
  9.     ....
  10.     return;
  11. }

點擊(此處)摺疊或打開

  1. /*對於kprobe_exceptions_notify,其DIE_DEBUG處理流程*/
  2. case DIE_DEBUG:
  3.         if (post_kprobe_handler(args->regs)) {
  4.             /*
  5.              * Reset the BS bit in dr6 (pointed by args->err) to
  6.              * denote completion of processing
  7.              */
  8.             (*(unsigned long *)ERR_PTR(args->err)) &= ~DR_STEP;
  9.             ret = NOTIFY_STOP;
  10.         }
  11.         break;

  12. static int __kprobes post_kprobe_handler(struct pt_regs *regs)
  13. {
  14.     struct kprobe *cur = kprobe_running();
  15.     struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();

  16.     if (!cur)
  17.         return 0;

  18.     /*設置異常返回的EIP爲下一條需要執行的指令*/
  19.     resume_execution(cur, regs, kcb);
  20.     /*恢復異常執行前的EFLAGS*/
  21.     regs->flags |= kcb->kprobe_saved_flags;

  22.     /*執行post_handler函數*/
  23.     if ((kcb->kprobe_status != KPROBE_REENTER) && cur->post_handler) {
  24.         kcb->kprobe_status = KPROBE_HIT_SSDONE;
  25.         cur->post_handler(cur, regs, 0);
  26.     }

  27.     /* Restore back the original saved kprobes variables and continue. */
  28.     if (kcb->kprobe_status == KPROBE_REENTER) {
  29.         restore_previous_kprobe(kcb);
  30.         goto out;
  31.     }
  32.     reset_current_kprobe();
  33. out:
  34.     preempt_enable_no_resched();

  35.     /*
  36.      * if somebody else is singlestepping across a probe point, flags
  37.      * will have TF set, in which case, continue the remaining processing
  38.      * of do_debug, as if this is not a probe hit.
  39.      */
  40.     if (regs->flags & X86_EFLAGS_TF)
  41.         return 0;

  42.     return 1;
  43. }
    至此,一個典型的kprobe的流程已經執行完畢了。

jprobe、kretprobe  to be continued...
    




參考鏈接:
4)2.6.38 linux kernel(RTFC)


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章