歡迎光臨
每天分享高質量文章

kprobe原理解析

參考 

http://www.cnblogs.com/honpey/p/4575928.html

kprobe 是linux內核的一個重要特性,4.0版本的核心中,強大的eBPF特性也寄生於kprobe之上,所以kprobe在核心中的地位就可見一斑了。

kprobe是什麼?

        如何高效的除錯核心?printk是一種方法,但是printk終歸是毫無選擇的全量輸出,某些場景下不適用,於是你可以試一下tracepoint,我使能tracepoint機制的時候才輸出。對於傻傻的放置printk來輸出資訊的方式,tracepoint是個進步,但是tracepoint只是核心某些特定行為(比如行程切換)上部署的一些靜態錨點,這些錨點並不一定是你需要的,所以你仍需要自己部署tracepoint,重現編譯核心。那麼kprobe的出現就很有必要了,它可以在執行的核心中動態插入探測點,執行你預定義的操作。

kprobe怎麼使用?

        kprobe主要有兩種使用方法,一是透過模組載入,二是透過debugfs介面。

        模組載入的方式:核心原始碼下有目錄下sample/kprobes,該目錄下有許多kprobes的例子,可以仿照這些例子寫自己的kprobe模組。以kprobe_example.c為例,首先宣告一個kprobe結構體,然後定義其中幾個關鍵成員變數,包括symbol_name,pre_handler,post_handler。其中,symbol_name是函式名(kprobe_example.c中該項為do_fork),告訴核心我的探測點放置在了函式do_fork處,pre_handler和post_handler分別表示在執行探測點之前和之後執行的鉤子函式。然後透過register_kprobe函式註冊kprobe即可。將kprobe_example.ko inmod進核心之後,每當系統新啟動一個行程,比如執行ls,cat等,都會輸出:

pre_handler: p->addr = 0x***, ip = ****.

post_handler: p->addr = 0x***, pc = ****.

        第一行是執行pre_handler鉤子函式的輸出,第二行是執行post_handler鉤子函式的輸出,當然這些都是核心中案例的寫法,你可以寫自己的鉤子函式。

        透過debugfs介面註冊kprobe:模組載入的終究不是很方便,尤其對於一些不帶gcc的嵌入式系統,需要交叉編譯ko,將ko複製到單板,然後insmod,不便。debugfs下(確切地說,應該是ftrace)提供了一套註冊、使能、登出kprobe的介面,可以很方便的操作kprobe。

用法如下:

        1)cd /sys/kernel/debug/tracing[有些系統沒有掛載debugfs,需要先掛載下mount -t debugfs nodev /sys/kernel/debug]

        2)進入tracing目錄,這裡就是傳說中ftrace的天下了,執行:

echo “p:sys_write_event sys_write” > kprobe_events

        向kprobe_events寫入“p:sys_write_event sys_write”,註冊kprobe事件。你會發現,當前目錄下的events下,新增一個kprobes目錄,該目錄下:

root@station:/sys/kernle/debug/tracing/events/kprobes# ls

enable filter sys_write_event

        即,我們註冊的kprobe時間生效了。那麼“p:sys_write_event sys_write”是什麼意思呢?首先p表示我們要註冊一個kprobe,如果要註冊retprobe,此處應為r;sys_write_event表示這個kprobe叫什麼名字;sys_write表示我們的插入點在哪裡。那麼,“p:sys_write_event sys_write”的語意就很明顯了:在函式sys_write處插入了一個kprobe點,這個點的名字叫做sys_write_event。

        3)使能kprobe。執行:

cd  /sys/kernel/debug/tracing/events/kprobes/events/sys_write_event

echo 1 > enable

cd ../../..[退回到/sys/kernel/debug/tracing,檢視trace檔案的輸出]

cat trace

trace檔案的輸出是如下的:

……

bash-808 [003] d… 42715.347565:sys_write_event:(SyS_write+0x0/0xb0)

……

解釋下置紅的這條輸出:pid為808的行程bash,在自本次開機42715.347565秒的時候,呼叫了一次函式sys_write。

        4)撤銷kprobe。執行

cd /sys/krenel/debug/tracing/events/sys_write_event

echo 0 > enable[首先先關閉kprobe]

cd ../../..

echo “-:kprobes/sys_write_event” >> kprobe_events[登出kprobe]

  

        以上就是kprobe的兩種序號產生器使用方式:透過模組載入以及透過debugfs註冊。

        使用模組載入的方式,是kprobe的一種原始用法:在kprobe結構體裡定義插入點、鉤子函式,然後透過register_kprobe註冊上這個kprobe即可。ftrace介面是kprobe的一種應用,它是一套trace的框架,下麵的trace機制包括tracepoint、function trace等,kprobe僅僅是這些trace機制中的一員。上面的講述我們也已經看出來了,透過ftrace註冊的kprobe的輸出是在ftrace的輸出:trace檔案。模組載入樣式中我們可以定義kprobe的鉤子函式pre_handler和post_handler,但是在ftrace下註冊的kprobe的鉤子是ftrace介面預設的,我們設定不了,但是具體輸出什麼,我們可以在echo “p:sys_write_event sys_write”時指定,比如指定x1暫存器的內容等,所以ftrace下註冊的kprobe功能同樣很強大。同時,由於ftrace下kprobe的輸出基於ftrace的輸出框架,所以輸出資訊包含當前行程、CPU、時間戳等資訊,對於trace來說非常有用。

        上面和大家簡要說明瞭下kprobe到底應該怎樣用,那麼現在我們就揭開kprobe神秘的面紗。

kprobe的工作過程大致如下:

    1)註冊kprobe。註冊的每個kprobe對應一個kprobe結構體,該結構體記錄著插入點(位置),以及該插入點本來對應的指令original_opcode;

    2)替換原有指令。使能kprobe的時候,將插入點位置的指令替換為一條異常(BRK)指令,這樣當CPU執行到插入點位置時會陷入到異常態;

    3)執行pre_handler。進入異常態後,首先執行pre_handler,然後利用CPU提供的單步除錯(single-step)功能,設定好相應的暫存器,將下一條指令設定為插入點處本來的指令,從異常態傳回;

    4)再次陷入異常態。上一步驟中設定了single-step相關的暫存器,所以original_opcode剛一執行,便會二進宮:再次陷入異常態,此時將signle-step清楚,並且執行post_handler,然後從異常態安全傳回。

        步驟2),3),4)便是一次kprobe工作的過程,它的一個基本思路就是將本來執行一條指令擴充套件成執行kprobe->pre_handler—>指令—>kprobe–>post_handler這樣三個過程。下麵詳細解釋每個過程:

        指令替換過程:上圖中藍色區域表示記憶體,紅色標明瞭地址,綠色部分代表一條指令,上圖的意思是,記憶體0xffffffc000162914處存放一條指令是0xa9bd7bfd。那麼,現在我註冊了一個kprobe,探測點是sys_write函式,該函式的起始位置就是0xffffffc000162914,現在我要使能kprobe了,那麼我要做的就是把0xffffffc000162914處原來的指令0xa9bd7bfd替換成一條BRK指令,即上圖所表示的一個一花節目過程。你可能會好奇原來的指令0xa9bd7bfd存在哪裡?存在kprobe結構體的opcode域!這樣當不再使能kprobe的時候,我再恢復回去。

    觸發BRK指令:

        上面把人家指令給改了,那麼CPU執行到BRK必然會引發核心陷入BRK異常狀態:

        藍色部分依舊表示記憶體,綠色部分表示指令,紅色表示CPU,上圖表示CPU執行到0xffffffc000162914(sys_write)處,該處指令為BRK,於是核心陷入異常態。在異常態中,核心透過BRK指令的錯誤碼判斷這是一個kprobe異常,於是進入了kprobe處理函式。kprobe異常處理函式會根據發生異常的地址來找到對應的kprobe(kprobe的addr域記錄著地址),執行kprobe的pre_handler函式,然後設定single-step相關的暫存器,為下一步執行原指令時發生single-step異常做準備。那麼緊接著就是設定元之靈的地址了,我們知道0xffffffc000162914處已經被替換成了BRK指令,原指令儲存在kprobe結構體中,怎麼保證下一步執行到原指令呢?最簡單的做法是申請一塊記憶體,然後將原指令複製到這塊記憶體開始處,設定PC暫存器位該記憶體的首地址,這樣當程式碼從異常態傳回時,執行的第一條指令便是原指令了!

    

    原指令得到執行,二進宮

        經過上面一個步驟,pre_handler得到了執行,從異常態傳回之後,原指令也得到了執行,但是由於設定了single-step樣式,所以執行完原指令,馬上又陷入了異常態,二進宮:

        這次進入異常態後,先清一下single-step相關的暫存器,確保下次從異常傳回時的指令不會由於single-step發生三進宮,然後執行post_handler,最後將地址0xffffffc000162918寫入到PC暫存器,為什麼是這個數值呢?它正是緊接著0xffffffc000162914的下一條指令的地址,有沒有發現,至此我們已經完成了pre_handler->原指令->post_handler這樣三個階段,也就是說kprobe要做的事情都做完了,此時的工作就是收拾下殘局,傳回到正常的指令流程,我們的探測點在0xffffffc000162914處,下一條指令應該就是0xffffffc000162918了,所以把此值寫入PC暫存器,讓一切恢復正軌!

        kprobe工作結束,走上正軌

上面把PC設定成了0xffffffc000162918,所以從異常態傳回時,CPU就走上了正軌接著朝下麵執行了,一個BRK指令引發的反映再次就搞一段落了。

        但是每次當CPU執行到0xffffffc000162914處,都會觸發上面的一連串操作,kprobe的機制也就是從一個BRK指令開始了。

贊(0)

分享創造快樂