1. 程式人生 > >Linux核心Kprobes除錯技術

Linux核心Kprobes除錯技術

核心開發者一直在試圖尋找一種快捷高效的核心除錯手段,用於核心開發之中。高效的除錯技術有利於提高核心開發效率,縮短核心開發週期。    本文研究了一種新型的核心除錯技術―KprobesKprobes是一個輕量級的核心除錯工具,利用Kprobes技術可以在執行的核心中動態的插入探測點,在探測點處執行使用者預定義的操作。本文首先根據KprobesLinux核心中的原始碼實現,針對Linux CPU異常技術,single-step技術,Loadable Kernel Module技術以及RCU同步技術在Kprobes中的應用進行了研究。其次,針對Kprobes目前所支援的kprobe,jprobe,kretprobe等三種除錯手段的實現進行了詳細的分析研究。


    一直以來,核心開發者一直在試圖尋找一種快捷高效的核心除錯手段,用於核心開發之中。從 2.6版本的Linux開始,一種新的核心除錯技術出現了,這就是Kprobes技術。
    Kprobes最早是源於IBM的Dprobe專案發展起來的,Dprobe是一個IBM公司開發的核心除錯工具。從2.6.9 Linux核心開始,Kprobes被加入核心原始碼,並處於不斷完善之中,越來越多的功能被新增到 Kprobes 核心除錯技術中來。Kprobes 目前已經能在 i386,x86_64, ppc64, ia64,sparc64等CPU平臺上正常工作。

    目前,大多發行版本都包含一個核心除錯工具 SystemTap。SystemTap可以通過編寫指令碼除錯核心,該工具正是依託 

Kprobes 來實現的。Kprobes 是一個輕量級的核心除錯工具,也就是說,Kprobes的執行基本不會影響到正常核心執行的流程。
    利用Kprobes技術可以在執行的核心中動態的插入探測點,當核心執行到該探測點後可以執行使用者預定義的回撥函式。當執行完使用者函式後又會回到正常的核心執行流程,開始新一輪的除錯工作。 

    Kprobes支援三種探測方式:第一種是最基本的探測方式稱為 kprobe,該探測方式支援在核心的任意位置放置探測點(除了與 Kprobes實 現相關的程式碼)。第二種除錯方式稱之為 jprobe,該探測方式主要用於除錯函式傳入的引數。第三種除錯方式稱之為 kretprobe,該除錯方式是在函式返回時執行使用者回撥函式,利用該方式可以除錯核心函式返回值。以上討論的後兩種除錯方式都是基於第一種 kprobe除錯方式實現的。

Kprobes還支援三種回撥函式型別:第一種叫做 pre_handler,該回調函式用於在執行被探測指令前執行。第二種叫 post_handler,該回調函式用於在執行完被探測指令後執行。第三種叫fault_handler,此函式用於在出現記憶體訪問錯誤時進行處理。
    目前Kprobes核心除錯技術已經被很多核心開發者所採用,並使用在核心開發過程的各個階段。
    Kprobes的開發還在不斷的進行之中,目前的SystemTap社群負責對該除錯技術的維護以及新功能的開發,主要由IBM,Intel,Redhat等公司維護。

    使用 Kprobes 進行核心除錯前,先要對被除錯核心進行相關配置。以 Linux 2.6.20.3 版本Linux核心為例:
    首先,需要把 Kprobes 相關程式碼編譯進核心,進入核心目錄執行 make menuconfig 命令,在Instrumentation Support專案中選擇Kprobes
    其次,選擇 Configure standard kernel features 中的 Include all symbols in kallsyms項,該項用於啟用kallsyms_lookup_name()函式,這個函式用於檢索核心函式的地址。
    在最新版本的Kprobes實現中已經支援直接利用函式名來進行註冊。
    第三,選擇Loadable module support中的Enable Loadable module support項,該項用於啟用核心的可插入模組功能。因為Kprobes的除錯是通過模組插入實現的,除錯者需要編寫除錯模組並插入核心方式進行核心除錯,因此必須選擇該選項。


    Kprobes不只是純軟體的實現方案,該技術與具體硬體緊密相連,用到了一些硬體的特性。因此Kprobes的實現框架分為兩個部分實現:第一部分是Kprobes的 管理,這部分是與體系結構無關的程式碼。第二部分是與具體CPU體系結構相關的實現程式碼,比如準備單步執行環境等。這些程式碼與具體 CPU體系結構緊密相連,因此在不同 CPU 上各有不同的實現方式。本文以下所有的討論都是基於 Intel IA32 CPU架構,2.6.20.3核心。下文討論用於支援Kprobes實現的四種關鍵技術。

Linux CPU異常處理技術以及在Kprobes中的應用
    CPU異常是在CPU執行期間,由於外圍硬體發出中斷訊號或是執行CPU異常指令等情況所引發的。
    CPU異常可分為硬體異常和軟體異常。
    硬體異常也稱為硬體中斷,一般是有外圍硬體裝置發出中斷訊號引起。當外圍裝置發出一箇中斷訊號,該訊號被髮往中斷處理器仲裁,比較有名的是 8259中斷處理晶片,目前Intel的CPU一般採用APIC(Advanced Programmable Interrupt Controllers)來實現。中斷處理器仲裁後發往 CPU的中斷引腳,這時CPU會開始一次中斷處理流程。
    軟體異常不是由外圍裝置發出的,而是由程式設計師寫入程式裡的一些 CPU 異常指令引起的,比如int,int3這類指令就會引起一次軟體異常。在早期的 CPU上Linux系統呼叫的實現就是利用 int指令。當CPU執行到這些異常指令時,同樣會開始一次異常處理流程。
    作業系統CPU異常處理的實現是和特定CPU體系結構緊密相關的。Intel IA32體系的CPU的每個中斷或是異常都有一個向量號,這些向量號從 0到255。Linux核心中,每一箇中斷向量號都對應中斷描述符表(IDT)中的一項,因此中斷描述符表一共有 256項。IDT每一項包含8個位元組,這8個位元組的其中一部分是中斷處理函式的地址。表項的其他欄位的含義這裡不再介紹,可以從 Intel IA32程式設計師手冊中查到。Linux核心在初始化階段用set_trap_gate()巨集初始中斷描述符表。
    Linux核心中,異常處理是通過兩級跳轉實現的。比如,如果當CPU執行過程中接收到一次異常訊號,此時CPU會到根據中斷向量號到中斷描述符表中找到對應項,再跳轉到表項中所指的地址去執行。
    這裡跳轉到的地址一般情況下對應原始檔 linux/arch/i386/kernel/entry.S 中的某個彙編函式入口。彙編函式經過一定的初始化工作後再跳轉到C函式中去執行。這裡的C函式才是真正的異常處理函式,當從C函式返回到原來的彙編函式 後,彙編函式還會做一部分後續工作。最後彙編函式執行 iret指令從中斷上下文中返回到被中斷的程式碼中繼續執行。這是 Linux核心處理異常一般過程,因此稱Linux的異常處理過程是兩次跳轉實現的。
    在Kprobes的實現中同樣也用到了CPU異常,當插入一個探測點的時候,Kprobes處理例程會把插入點處的指令儲存起來,然後用int3指令代替。當CPU執行到插入的int3指令時,Linux核心就進入了異常處理流程,之後再執行除錯者預定義的回撥函式。利用 CPU異常來實現探測點的觸發並處理是Kprobes實現的關鍵。

single-step技術及在Kprobes中應用

    偵錯程式的單獨執行功能是非常有用的,程式設計師可以通過單步執行程式碼來確定程式執行的流程,隨時掌握變數變化情況,從而精確定位程式錯誤發生的位置。可以說單步執行功能是一個偵錯程式必須具備的功能。single-step技術就是為了偵錯程式的單步執行而設計的。
    single-step技術的主要思想是,當程式執行到某條想要單獨執行 CPU指令時,在執行之前產生一次CPU異常,此時把異常返回時的CPU的EFLAGS暫存器的TF(除錯位)位置為1,把IF(中斷遮蔽位)標誌位置為 0,然後把EIP指向單步執行的指令。當單步指令執行完成後,CPU會自動產生一次除錯異常(由於TF被置位)。此時,偵錯程式一般都會把控制權又交回除錯 器,回到互動模式。
    在Kprobes實現中同樣也用到了single-step技術,但目的不是為了回到互動模式,而是把控制權交回Kprobes控制流程。當Kprobes完成pre_handler()處理後,就會利用single-step技術執行被除錯指令。此時,Kprobes會利用debug異常,執行post_handler()。這是single-step
技術在Kprobes的主要應用。

oadable kernel module技術及在Kprobes中的應用

    loadable kernel module(LKM)技術是Linux核心的又一強大特性。
    在LKM技術出現以前,新增核心程式碼只能通過修改核心原始碼,然後重新編譯核心再重啟後才能生效。
    當LKM技術出現後,核心程式設計變的簡單許多,核心開發者只需要把程式碼寫成核心模組形式,再插入核心就可以作為核心的一部分執行。確切來講,LKM技術為內 核開發者提供了在核心執行過程中動態插入和解除安裝核心模組的功能。LKM技術在不用重新編譯核心的情況下,可以擴充套件核心的功能。目前,大量的裝置驅動程式利 用LKM技術實現。驅動開發者把驅動程式寫成模組的形式,並在核心啟動時自動載入入核心,當然這也可以通過手動載入方式實現。
    LKM技術的出現帶來了兩個好處:第一,LKM技術使得驅動程式的開發和除錯變得異常簡單,很大程度上提高了驅動開發的效率。第二,LKM技術的出現使得 核心映象不至於過於龐大,因為大部分的核心程式碼可以寫成模組形式,使用時才載入。這樣不會使得核心在系統記憶體中佔用太多空間而影響系統性能。
    在利用Kprobes進行核心除錯時同樣用到了LKM技術。除錯者需要把除錯程式碼寫成模組形式並插入核心。當除錯模組被插入核心後就進入除錯階段,而當除錯模組從核心中解除安裝時,也就意味著除錯過程的結束。可以說LKM技術是Kprobes技術進行核心除錯基礎。

RCU技術及在Kprobes中的應用

    Linux內 核有時會訪問到一些全域性的資料結構,如果此時核心被搶佔並且資料被修改,又或者在SMP系統(即多處理器系統)上執行,可能會有多個 CPU同時訪問同一塊記憶體的情況,此時核心資料就可能產生不一致性。為了避免這種情況,核心使用了一些同步機制,比如利用訊號量同步,利用自旋鎖同步等方 法。在2.6版本的核心中又引入了一種新的核心同步機制RCU,這是Read Copy Update的縮寫。RCU的主要思想是分為兩個部分,第一部分是防止其他訪問者對被保護物件寫入,第二部分是真正展開寫入行為。讀者可以隨時訪問被 RCU保護的物件而不用獲得任何鎖。寫者先對物件的副本修改,在所有讀者都退出時再執行寫入行為,不同的寫者之間需要同步。RCU同步機制適用於存在大量 讀操作而很少寫操作的情況。因為這種情況下,讀操作不用獲得任何鎖就可以對共享物件進行讀操作,極大提高了效率。
    在Kprobes對探測點資料結構的操作中也是大量存在讀操作而只有很少部分寫操作。為了提高效率,Kprobes的 實現中也引入了RCU機制。當發生探測點註冊或是登出時,或是在執行探測點的回撥函式時,探測點資料結構struct kprobe就會被RCU機制保護起來。根據RCU的原理,寫者的操作是被延遲的,而如果讀者發生了阻塞,那寫者的操作直到讀者被喚醒後才進行,這會大大 降低 RCU的效率。因此在執行Kprobes回撥函式時核心的搶佔以及CPU中斷都被禁用了。對於Kprobes來說,用RCU機制實現回撥函式訪問也極大的提高了SMP系統上多探測點探測的效率,因為可以並行的執行回撥函式。但在這樣的機制下,回撥函式必須被設計成可重入的函式。

    根據Kprobes的原始碼實現,從邏輯上基本可以把 Kprobes分為3個部分:第一部分是註冊探測點部分,第二部分是除錯處理部分,第三部分是登出探測點部分。第一部分主要功能是進行一些安全性檢查,並根據要求安裝探測點等工作。第二部分是 Kprobes實現的關鍵,該部分根據探測型別執行預定的操作。這裡的探測型別主要有三種:kprobe,jprobe和kretprobe。這部分還會完成被探測指令單步執行等操作。第三部分是當除錯者撤銷探測要求時,對探測點的登出操作,主要是恢復被探測指令等工作。

    下面分別介紹kprobe,jprobe和kretprobe的實現機制。
3.1 kprobe的實現
3.1.1 相關資料結構與函式分析 
1)  struct kprobe結構
    該資料結構是整個 Kprobes 體系的基礎,所有 Kprobes 的行為都是圍繞該結構展開。以下是
struct kprobe結構的主要成員:

2)  struct notifier_block 結構
    該結構用於註冊異常發生時呼叫的回撥函式。 int (*notifier_call)(struct notifier_block *, unsigned long, void *)成員是回撥函式指標,int priority成員用於設定呼叫優先順序。Kprobes中定義的優先順序為最高優先順序,確保註冊的回撥函式被首先呼叫到。

3)  register_kprobe()函式
    該函式用於完成struct kprobe結構的註冊,插入探測點等操作。

4)  kprobe_handler()函式
    該函式用於處理由kprobe引發的int3異常。

5)  post_kprobe_handler ()函式
    該函式用於處理由kprobe引發的debug異常。

kprobe處理流程分析

kprobe是Kprobes實現體系中最底層也是最基本的一種探測方式,jprobe和kretprobe探測方式都是通過kprobe來實現的。kprobe的主要處理流程如下圖所示:

1)kprobe的註冊過程
    當除錯者向核心插入一個 kprobe 模組時,首先會執行註冊探測點操作。該操作主要由register_kprobe()函式完成,在下文中都稱該函式為註冊器。註冊器的引數中包含一個 struct kprobe結構,該結構由除錯者在除錯模組中建立。
    首先,註冊器會進行一些正確性檢查工作,判斷傳入的 struct kprobe結構中symbol_name和addr 是否同時存在,如果是則返回錯誤。之後還會判斷探測地址是否在核心程式碼段中並且不在Kprobes實現相關的程式碼中。這樣的檢查是很必要的,如果探測地址出現在Kprobes實現相關程式碼中就會造成遞迴現象。如果被探測的地址已經被註冊過,則會在kprobe_table中以連結串列形式組織。
    其次,註冊器會儲存被探測地址的指令碼到struct kprobe結構的ainsn.ainsn中去,以便以後進行single-step操作。對kprobe初始化完成後,註冊器會把傳入的struct kprobe結構指標插入雜湊表中。最後,註冊器把被探測的指令的第一個位元組替換成 int3指令。到這裡,kprobe的註冊工作就完成了。

2)kprobe int3異常處理過程
    完成註冊後kprobe的準備工作就完成了,一旦核心執行到被探測的指令,也就是註冊時被替換成的int3指令時,就會引發一次軟體異常。CPU會根據中斷描述符表執行中斷處理函式,int3的中斷處理函式在/linux/arch/i386/kernel/entry.S中實現,KPROBE_ENTRY(int3)就是該中斷處理函式的入口。彙編中斷處理函式會呼叫 do_int3()函式,作為 int3 中斷處理的 C 語言處理函式。
    do_int3()函式一開始就會去呼叫notify_die()函式,該函式的主要作用是呼叫核心程式碼註冊的異常的 回 調 函 數 。 在 Kprobes 的初始化程式碼(init_Kprobes()函式)中呼叫了register_die_notifier() 用於註冊異常回調函式 。 Kprobes註冊的異常回調函式為probe_exceptions_notify()。
    此時執行權交到Kprobes之 中,kprobe_exception_notify()函式開始執行。該函式的引數中有一個引數val,該引數可以用於判斷當前回撥函式由有什麼異常產 生的。這裡異常由 int3指令產生,因此接收到的引數應該為“DIE_INT3”。此時,又會呼叫 kprobe_handler()函式,該函式是Kprobes處理int3異常的主要實現函式。該函式首先會把發生異常的地址記錄下來,因為該地址就是註冊探測點的地址。為了防止核心被搶佔,該函式禁止核心搶佔功能。在 i386 CPU上,進入int3中斷處理時已經關閉CPU中斷,目前Kprobes的實現中只有i386體系上會關閉CPU中斷,在其他體系上的實現都沒有這樣做。
    接著,開始檢查此次 int3 異常是否是由前一次 Kprobes 處理流程引發的,如果是由前一次Kprobes處理流程引發,則有兩種可能性。第一是該次Kprobes處理由於前一次回撥函式執行了被探測程式碼造成的,第二種可能性是由於 jprobe造成的,這部分將在 jprobe的實現一節中詳細討論。如果int3異常不是由前一次Kprobes處理流程引發的,根據先前記錄下來的探測點地址到雜湊表中找到已註冊的struct kprobe結構。如果該結構中包含了pre_handler函式指標,則執行該預定的函式。

    執行完使用者定義的 pre_handler函式時,已經完成了一部分的除錯工作。接下來,就開始準備single-step步驟,該步驟用 prepare_singlestep()函式完成。這個函式與體系結構相關,下面是prepare_singlestep()函式在i386體系CPU 上的主要實現程式碼:
程式1  prepare_singlestep()函式部分程式碼
01 regs->eflags |= TF_MASK;
02 regs->eflags &= ~IF_MASK;
03 regs->eip = (unsigned long)p->ainsn.insn;
上面的程式碼中設定了EFLAGS中的TF位並清空IF位,同時把異常返回的指令暫存器地址改為儲存起來的原探測指令處,當異常返回時這些設定就會生效。 single-step技術已經在上文中討論過,這裡不再贅述。執行完被探測的指令後,由於 CPU的標誌暫存器被置位,此時又會引發一次CPU異常,該異
常在Linux核心中被稱為DEBUG異常。

3)Kprobe DEBUG異常處理
    Linux核心中對DEBUG異常的處理方式與處理int3異常很類似。DEGUG異常的中斷處理函式也是在/linux/arch/i386/kernel/entry.S 中實現,KPROBE_ENTRY(debug)就是該異常的中斷處理函式的入口。該函式會呼叫do_debug()函式進一步處理DEBUG異常,同樣 的notify_die()函式被呼叫。與int3異常不同的是此時傳入notify_die()函式的第一個引數是“DIE_DEBUG”。
    最終,notify_die() 函式會呼叫Kprobes初始化時註冊的回撥函式kprobe_exceptions_notify() 。此時,控制權又一次交回Kprobes 。
    kprobe_exceptions_notify() 判斷傳入的型別為DIE_DEBUG,這時會去呼叫post_kprobe_handler ()函式。post_kprobe_handler ()首先判斷使用者定義的post_handler回撥函式是否存在,如果存在則執行之。
    之後,會呼叫 resume_execution()函式做一些會做恢復工作,該函式會把 EFLAGES暫存器的TF 為清空,並根據被探測指令型別的不同,做不同的處理。在 resume_execution()返回後,post_kprobe_handler ()函式就會啟用在 int3 異常處理中被禁止的核心搶佔功能。到這裡,Kprobes對DEBUG異常的處理基本完成了,又把控制權交回核心。
    以上是kprobe執行的主要流程,可以看出kprobe利用了兩次CPU異常的方式執行了使用者定義的pre_handler 和 post_handler 回撥函式。並通過 single-step 技術執行了被探測指令。當一次kprobe 執行週期完成後,又開始等待新一輪執行週期的到來。只有當除錯者解除安裝了除錯模組後,kprobe的生命週期才算結束。

jprobe的實現
    相關資料結構與函式分析
1) struct jprobe結構
    該結構在註冊jprobe探測點時使用,它包含兩個成員:
    struct kprobe kp;//這是jprobe一個內嵌的struct kprobe結構成員,因為jprobe是基於kprobe實現的。
    kprobe_opcode_t *entry;//這是被探測函式的代理函式。

2) setjmp_pre_handler()函式
    該函式作為jprobe內嵌kprobe的pre_handler,在探測點被觸發時首先會被呼叫到。

3) longjmp_break_handler()函式
    該函式作為jprobe內嵌kprobe的break_handler,當再次進入int3異常時被呼叫。

jprobe處理流程分析

    jprobe是Kprobes中實現的另一種除錯方式,該除錯方式主要為了滿足除錯核心函式傳遞的引數的情況。jprobe是基於kprobe實現的,是kprobe除錯的一種擴充套件形式。
    jprobe的基本原理是利用了一個探測代理函式來接收傳入引數,做相應處理後再把控制權交回被除錯函式。下圖為jprobe的執行流程圖:

1)jprobe的註冊過程
    由於jprobe用於除錯傳入引數情況,使用者在編寫jprobe探測模組時與編寫kprobe模組有所不同。jprobe結構定義時只要給entry成員 賦值成除錯代理函式,該函式的引數型別必須與被探測函式完全相同。當一個 jprobe 探測模組插入核心後,jprobe 的註冊過程會被啟動。首先,jp->kp.pre_handler 會 被 設 置 成 setjmp_pre_handler , jp->kp.break_handler 被設定成longjmp_break_handler,這兩個函式會在以後討論。之後會呼叫kprobe的註冊函式進行探測點的插入。在之前的章節中已經 詳細分析了kprobe的註冊過程,這裡不再重複。

2)jprobe探測點觸發過程
    與 kprobe 相同,當核心執行到由 jprobe 插入的探測點時同樣會產生 int3 CPU 異常。在kprobe實現一節,已經詳細的分析了 kprobe對int3 CPU異常的處理方式。最終pre_handler函式會被呼叫。在 jprobe 中,pre_handler 不是由除錯者定義的,而是在註冊時被賦值成了setjmp_pre_handler函式。該函式會把異常發生時的堆疊內容儲存起來,並且把異常返回時的 EIP值設為除錯代理函式的地址。
    經過以上處理,int3中斷返回時就會去執行除錯代理函式而不是被探測的函式。代理函式執行完使用者定義操作後必須呼叫 jprobe_return()函式,目的是為了確保執行流程能回到被探測的函式中。
    jprobe_return()函式利用內嵌彙編再次執行int3指令,此時又會發生一次int3 CPU異常處理。
    當異常處理執行到kprobe_handler()函式進行判斷是否有kprobe正在執行時,發現目前有kprobe正在執行,而此時產生異常的地址並 沒有被註冊過。這種情況下,struct kprobe 結構中的break_handler函式會被呼叫,也就是在jprobe註冊階段註冊的longjmp_break_handler()函式開始執行。       該函式主要作用是把在setjmp_pre_handler函式中儲存起來的堆疊內容以及暫存器進行恢復,當異常返回時其環境與jprobe探測點異常發 生時完全相同。
    接著kprobe_handler()又會準備好單步執行的環境,並單步執行被探測指令,同時產生 Debug異常。在 kprobe 實現一節已經詳細分析了 Debug 異常處理過程。當 Debug 異常返回時也就回到了jprobe探測指令的下一條指令的位置繼續執行。
    通過以上分析可以看出,jprobe是基於kprobe除錯方式實現的,jprobe利用了三次CPU異常,產生的前兩次CPU異常都是int3異常,第 三次產生了Debug異常。jprobe主要通過代理函式的方式來實現傳入引數的除錯,並利用修改異常返回地址的方式來控制執行的流程。

kretprobe的實現
    相關資料結構與函式分析
1) struct kretprobe結構
    該結構是kretprobe實現的基礎資料結構,以下是該結構的成員:
    struct kprobe kp; //該成員是kretprobe內嵌的struct kprobe結構。
    kretprobe_handler_t handler;//該成員是除錯者定義的回撥函式。
    int maxactive;//該成員是最多支援的返回地址例項數。
    int nmissed;//該成員記錄有多少次該函式返回沒有被回撥函式處理。
    struct hlist_head free_instances;
    用於連結未使用的返回地址例項,在註冊時初始化。
    struct hlist_head used_instances;//該成員是正在被使用的返回地址例項連結串列。

2) struct kretprobe_instance結構
    該結構表示一個返回地址例項。因為函式每次被呼叫的地方不同,這造成了返回地址不同,因此需要為每一次發生的呼叫記錄在這樣一個結構裡面。以下是該結構的成員:
    struct hlist_node uflist;
    該成員被連結入kretprobe的used_instances或是free_instances連結串列。
    struct kretprobe *rp;//該成員指向所屬的kretprobe結構。
    kprobe_opcode_t *ret_addr;//該成員用於記錄被探測函式正真的返回地址。
    struct task_struct *task;//該成員記錄當時執行的程序。

3) pre_handler_kretprobe()函式
    該函式在kretprobe探測點被執行到後,用於修改被探測函式的返回地址。

4) trampoline_handler()函式
    該函式用於執行除錯者定義的回撥函式以及把被探測函式的返回地址修改回原來的返回地址。

kretprobe處理流程分析
    kretprobe探測方式是基於kprobe實現的又一種核心探測方式,該探測方式主要用於在函式返回時進行探測,獲得核心函式的返回值,還可以用於計算函式執行時間等方面。

1) kretprobe的註冊過程
    除錯者要進行kretprobe除錯首先要註冊處理,這需要在除錯模組中呼叫register_kretprobe(),下文中稱該函式為 kretprobe 註冊器。kretprobe 註冊器對傳入的kretprobe結構的中kprobe.pre_handler賦值為pre_handler_kretprobe()函式,用於在探測 點被觸發時被呼叫。接著,kretprobe註冊器還會初始化kretprobe的一些成員,比如分配返回地址例項的空間等操作。最後, kretprobe註冊器會利用 kretprobe內嵌的struct kprobe結構進行kropbe的註冊。自此,kretprobe註冊過程就完成了。

2) kretprobe探測點的觸發過程
    kretprobe觸發是在剛進入被探測函式的第一條彙編指令時發生的,因為 kretprobe註冊時把該地址修改位int3指令。
    此時發生了一次CPU異常處理,這與kprobe探測點被觸發相同。但與kprobe處理不同的是,這裡並不是執行使用者定義的 pre_handler函式,而是執行pre_handler_kretprobe()函式,該函式又會呼叫 arch_prepare_kretprobe()函式。arch_prepare_kretprobe()函式的主要功能是把被探測函式的返回地址變換 為&kretprobe_trampoline所在的地址,這是一個彙編地址標籤。這個標籤的地址在 kretprobe_trampoline_holder()中用匯編偽指令定義。替換函式返回地址是kretprobe實現的關鍵。當被探測函式返回 時,返回到&kretprobe_trampoline地址處開始執行。
    接著,在一些保護現場的處理後,又去呼叫trampoline_handler()函式。該函式的主要有兩個作用,一是根據當前的例項去執行使用者定義的調 試函式,也就是 kretprobe結構中的handler所指向的函式,二是把返回值設成被探測函式正真的返回地址。最後,在進行一些堆疊的處理後,被探測函式又返回到 了正常執行流程中去。
    以上討論的就是kretprobe的執行過程,可以看出,該除錯方式的關鍵點在於修改被探測函式的返回地址到kprobes的控制流程中,之後再把返回地址修改到原來的返回地址並使得該函式繼續正常執行。

結束語
    Kprobes核心除錯技術的優勢是顯而易見的:除錯者可以通過 Kprobes提供的簡單介面對Kprobes探測點進行註冊,通過編寫回調函式就可以實現除錯,使用起來非常簡便。利用 Kprobes除錯時,可以以核心模組的形式編寫除錯程式碼。這樣做可以在不重新編譯核心的情況下實現核心除錯,大大提高了除錯的效率。目前Kprobes提供了三種除錯手段:kprobe,jprobe,kretprobe,這三種手段可以滿足不同的除錯目的。同時Kprobes支援除了與Kprobes實現相關程式碼外的任意核心程式碼的除錯,能滿足大多數除錯需要。Kprobes的分為體系結構無關和體系結構相關兩部分程式碼實現,這樣做增加了Kprobes的可擴充套件性。雖然優勢明顯,但Kprobes還是存在一些不足。比如,Kprobes雖然提供了豐富的除錯手段,但除錯者無法對插入的除錯模組有

效的控制和管理。另外,Kprobes是一個彙編級的除錯技術,只能對暫存器以及記憶體地址級別進行除錯,目前還無法對核心函式中的區域性變數等,也就是原始碼級別進行有效的除錯。目前Kprobes還不能支援所有的CPU架構。這些不足在一定程度上限制了Kprobes的使用。

轉自:http://blog.chinaunix.net/uid-387104-id-1744082.html