天天看點

linux 核心技術内幕_Linux核心調試技術——kprobe使用與實作(一)

Linux kprobes調試技術是核心開發者們專門為了便于跟蹤核心函數執行狀态所設計的一種輕量級核心調試技術。利用kprobes技術,核心開發人員可以在核心的絕大多數指定函數中動态的插入探測點來收集所需的調試狀态資訊而基本不影響核心原有的執行流程。kprobes技術目前提供了3種探測手段:kprobe、jprobe和kretprobe,其中jprobe和kretprobe是基于kprobe實作的,他們分别應用于不同的探測場景中。

一、kprobes技術背景

開發人員在核心或者子產品的調試過程中,往往會需要要知道其中的一些函數有無被調用、何時被調用、執行是否正确以及函數的入參和傳回值是什麼等等。比較簡單的做法是在核心代碼對應的函數中添加日志列印資訊,但這種方式往往需要重新編譯核心或子產品,重新啟動裝置之類的,操作較為複雜甚至可能會破壞原有的代碼執行過程。

而利用kprobes技術,使用者可以定義自己的回調函數,然後在核心或者子產品中幾乎所有的函數中(有些函數是不可探測的,例如kprobes自身的相關實作函數,後文會有詳細說明)動态的插入探測點,當核心執行流程執行到指定的探測函數時,會調用該回調函數,使用者即可收集所需的資訊了,同時核心最後還會回到原本的正常執行流程。如果使用者已經收集足夠的資訊,不再需要繼續探測,則同樣可以動态的移除探測點。是以kprobes技術具有對核心執行流程影響小和操作友善的優點。

kprobes技術包括的3種探測手段分别時kprobe、jprobe和kretprobe。首先kprobe是最基本的探測方式,是實作後兩種的基礎,它可以在任意的位置放置探測點(就連函數内部的某條指令處也可以),它提供了探測點的調用前、調用後和記憶體通路出錯3種回調方式,分别是pre_handler、post_handler和fault_handler,其中pre_handler函數将在被探測指令被執行前回調,post_handler會在被探測指令執行完畢後回調(注意不是被探測函數),fault_handler會在記憶體通路出錯時被調用;jprobe基于kprobe實作,它用于擷取被探測函數的入參值;最後kretprobe從名字種就可以看出其用途了,它同樣基于kprobe實作,用于擷取被探測函數的傳回值。

kprobes的技術原理并不僅僅包含存軟體的實作方案,它也需要硬體架構提供支援。其中涉及硬體架構相關的是CPU的異常處理和單步調試技術,前者用于讓程式的執行流程陷入到使用者注冊的回調函數中去,而後者則用于單步執行被探測點指令,是以并不是所有的架構均支援,目前kprobes技術已經支援多種架構,包括i386、x86_64、ppc64、ia64、sparc64、arm、ppc和mips(有些架構實作可能并不完全,具體可參考核心的Documentation/kprobes.txt)。

kprobes的特點與使用限制:

1、kprobes允許在同一個被被探測位置注冊多個kprobe,但是目前jprobe卻不可以;同時也不允許以其他的jprobe回掉函數和kprobe的post_handler回調函數作為被探測點。

2、一般情況下,可以探測核心中的任何函數,包括中斷處理函數。不過在kernel/kprobes.c和arch/*/kernel/kprobes.c程式中用于實作kprobes自身的函數是不允許被探測的,另外還有do_page_fault和notifier_call_chain;

3、如果以一個内聯函數為探測點,則kprobes可能無法保證對該函數的所有執行個體都注冊探測點。由于gcc可能會自動将某些函數優化為内聯函數,是以可能無法達到使用者預期的探測效果;

4、一個探測點的回調函數可能會修改被探測函數運作的上下文,例如通過修改核心的資料結構或者儲存與struct pt_regs結構體中的觸發探測之前寄存器資訊。是以kprobes可以被用來安裝bug修複代碼或者注入故障測試代碼;

5、kprobes會避免在處理探測點函數時再次調用另一個探測點的回調函數,例如在printk()函數上注冊了探測點,則在它的回調函數中可能再次調用printk函數,此時将不再觸發printk探測點的回調,僅僅時增加了kprobe結構體中nmissed字段的數值;

6、在kprobes的注冊和登出過程中不會使用mutex鎖和動态的申請記憶體;

7、kprobes回調函數的運作期間是關閉核心搶占的,同時也可能在關閉中斷的情況下執行,具體要視CPU架構而定。是以不論在何種情況下,在回調函數中不要調用會放棄CPU的函數(如信号量、mutex鎖等);

8、kretprobe通過替換傳回位址為預定義的trampoline的位址來實作,是以棧回溯和gcc内嵌函數__builtin_return_address()調用将傳回trampoline的位址而不是真正的被探測函數的傳回位址;

9、如果一個函數的調用此處和傳回次數不相等,則在類似這樣的函數上注冊kretprobe将可能不會達到預期的效果,例如do_exit()函數會存在問題,而do_execve()函數和do_fork()函數不會;

10、如果當在進入和退出一個函數時,CPU運作在非目前任務所有的棧上,那麼往該函數上注冊kretprobe可能會導緻不可預料的後果,是以,kprobes不支援在X86_64的結構下為__switch_to()函數注冊kretprobe,将直接傳回-EINVAL。

二、kprobe原理

下面來介紹一下kprobe是如何工作的。具體流程見下圖:

linux 核心技術内幕_Linux核心調試技術——kprobe使用與實作(一)

圖1 kprobe的工作流程

1、當使用者注冊一個探測點後,kprobe首先備份被探測點的對應指令,然後将原始指令的入口點替換為斷點指令,該指令是CPU架構相關的,如i386和x86_64是int3,arm是設定一個未定義指令(目前的x86_64架構支援一種跳轉優化方案Jump Optimization,核心需開啟CONFIG_OPTPROBES選項,該種方案使用跳轉指令來代替斷點指令);

2、當CPU流程執行到探測點的斷點指令時,就觸發了一個trap,在trap處理流程中會儲存目前CPU的寄存器資訊并調用對應的trap處理函數,該處理函數會設定kprobe的調用狀态并調用使用者注冊的pre_handler回調函數,kprobe會向該函數傳遞注冊的struct kprobe結構位址以及儲存的CPU寄存器資訊;

3、随後kprobe單步執行前面所拷貝的被探測指令,具體執行方式各個架構不盡相同,arm會在異常處理流程中使用模拟函數執行,而x86_64架構則會設定單步調試flag并回到異常觸發前的流程中執行;

4、在單步執行完成後,kprobe執行使用者注冊的post_handler回調函數;

5、最後,執行流程回到被探測指令之後的正常流程繼續執行。

三、kprobe使用執行個體

在分析kprobe的實作之前先來看一下如何利用kprobe對函數進行探測,以便于讓我們對kprobre所完成功能有一個比較清晰的認識。目前,使用kprobe可以通過兩種方式,第一種是開發人員自行編寫核心子產品,向核心注冊探測點,探測函數可根據需要自行定制,使用靈活友善;第二種方式是使用kprobes on ftrace,這種方式是kprobe和ftrace結合使用,即可以通過kprobe來優化ftrace來跟蹤函數的調用。下面來分别介紹:

1、編寫kprobe探測子產品

核心提供了一個struct kprobe結構體以及一系列的核心API函數接口,使用者可以通過這些接口自行實作探測回調函數并實作struct kprobe結構,然後将它注冊到核心的kprobes子系統中來達到探測的目的。同時在核心的samples/kprobes目錄下有一個例程kprobe_example.c描述了kprobe子產品最簡單的編寫方式,開發者可以以此為模闆編寫自己的探測子產品。

1.1、kprobe結構體與API介紹

linux 核心技術内幕_Linux核心調試技術——kprobe使用與實作(一)
linux 核心技術内幕_Linux核心調試技術——kprobe使用與實作(一)

其中各個字段的含義如下:

struct hlist_node hlist:被用于kprobe全局hash,索引值為被探測點的位址;

struct list_head list:用于連結同一被探測點的不同探測kprobe;

kprobe_opcode_t *addr:被探測點的位址;

const char *symbol_name:被探測函數的名字;

unsigned int offset:被探測點在函數内部的偏移,用于探測函數内部的指令,如果該值為0表示函數的入口;

kprobe_pre_handler_t pre_handler:在被探測點指令執行之前調用的回調函數;

kprobe_post_handler_t post_handler:在被探測指令執行之後調用的回調函數;

kprobe_fault_handler_t fault_handler:在執行pre_handler、post_handler或單步執行被探測指令時出現記憶體異常則會調用該回調函數;

kprobe_break_handler_t break_handler:在執行某一kprobe過程中觸發了斷點指令後會調用該函數,用于實作jprobe;

kprobe_opcode_t opcode:儲存的被探測點原始指令;

struct arch_specific_insn ainsn:被複制的被探測點的原始指令,用于單步執行,架構強相關(可能包含指令模拟函數);

u32 flags:狀态标記。

涉及的API函數接口如下:

int register_kprobe(struct kprobe *kp)      //向核心注冊kprobe探測點

void unregister_kprobe(struct kprobe *kp)   //解除安裝kprobe探測點

int register_kprobes(struct kprobe **kps, int num)     //注冊探測函數向量,包含多個探測點

void unregister_kprobes(struct kprobe **kps, int num)  //解除安裝探測函數向量,包含多個探測點

int disable_kprobe(struct kprobe *kp)       //臨時暫停指定探測點的探測

int enable_kprobe(struct kprobe *kp)        //恢複指定探測點的探測

1.2、用例kprobe_example.c分析與示範

該用例函數非常簡單,它實作了核心函數do_fork的探測,該函數會在fork系統調用或者核心kernel_thread函數建立程序時被調用,觸發也十分的頻繁。下面來分析一下用例代碼:

linux 核心技術内幕_Linux核心調試技術——kprobe使用與實作(一)

程式中定義了一個struct kprobe結構執行個體kp并初始化其中的symbol_name字段為“do_fork”,表明它将要探測do_fork函數。在子產品的初始化函數中,注冊了

pre_handler、post_handler和fault_handler這3個回調函數分别為handler_pre、handler_post和handler_fault,最後調用register_kprobe注冊。在子產品的解除安裝函數中調用unregister_kprobe函數解除安裝kp探測點。

linux 核心技術内幕_Linux核心調試技術——kprobe使用與實作(一)

handler_pre回調函數的第一個入參是注冊的struct kprobe探測執行個體,第二個參數是儲存的觸發斷點前的寄存器狀态,它在do_fork函數被調用之前被調用,該函數僅僅是列印了被探測點的位址,儲存的個别寄存器參數。由于受CPU架構影響,這裡對不同的架構進行了宏區分

linux 核心技術内幕_Linux核心調試技術——kprobe使用與實作(一)

handler_post回調函數的前兩個入參同handler_pre,第三個參數目前尚未使用,全部為0;該函數在do_fork函數調用之後被調用,這裡列印的内容同handler_pre類似。

linux 核心技術内幕_Linux核心調試技術——kprobe使用與實作(一)

handler_fault回調函數會在執行handler_pre、handler_post或單步執行do_fork時出現錯誤時調用,這裡第三個參數時具體發生錯誤的trap number,與架構相關,例如i386的page fault為14。

下面将它編譯成子產品在我的x86-64環境下進行示範,首先確定架構和核心已經支援kprobes,開啟以下選項(一般都是預設開啟的):

Symbol: KPROBES [=y]                            

Type  : boolean                                 

Prompt: Kprobes                                 

  Location:                                     

(3) -> General setup                            

  Defined at arch/Kconfig:37                    

  Depends on: MODULES [=y] && HAVE_KPROBES [=y] 

  Selects: KALLSYMS [=y]                        

Symbol: HAVE_KPROBES [=y]                       

Type  : boolean                                 

  Defined at arch/Kconfig:174                   

  Selected by: X86 [=y]    

然後使用以下Makefile單獨編譯kprobe_example.ko子產品:

linux 核心技術内幕_Linux核心調試技術——kprobe使用與實作(一)

加載到核心中後,随便在終端上敲一個指令,可以看到dmesg中列印如下資訊:

linux 核心技術内幕_Linux核心調試技術——kprobe使用與實作(一)

可以看到被探測點的位址為ffffffff9407ee40,用以下指令确定這個位址就是do_fork的入口位址。

linux 核心技術内幕_Linux核心調試技術——kprobe使用與實作(一)

--------------------- 

作者:luckyapple1028 

來源:CSDN 

原文:https://blog.csdn.net/luckyapple1028/article/details/52972315 

版權聲明:本文為部落客原創文章,轉載請附上博文連結!