Linux內(nèi)核調(diào)試利器kprobe的使用,從這幾點入手!
軟件調(diào)試 是軟件開發(fā)中一個必不可少的過程,通過軟件調(diào)試可以排查系統(tǒng)中存在的 BUG。我們在開發(fā)應(yīng)用層程序時,可以使用 GDB 對程序進行調(diào)試。但由于 GDB 只能調(diào)試應(yīng)用層程序,并不能用于調(diào)試內(nèi)核代碼。
那么,如何調(diào)試內(nèi)核代碼呢?與調(diào)試應(yīng)用層程序的 GDB 類似,調(diào)試內(nèi)核代碼也有個名叫 KGDB 的工具,但是使用起來比較繁瑣。所以,本文將會介紹一個使用起來比較簡單的內(nèi)核調(diào)試工具:kprobe。
kprobe 簡介
回憶一下我們在開發(fā)應(yīng)用程序時是怎樣調(diào)試代碼的?最原始的方法就是,在代碼中使用 printf 這類打印函數(shù)把結(jié)果輸出到屏幕或者日志中。當然在內(nèi)核中有類似的打印函數(shù):printk,但使用 printk 函數(shù)調(diào)試內(nèi)核代碼的話,必須要重新編譯 Linux 內(nèi)核代碼,代價非常高。
所以,內(nèi)核開發(fā)者們開發(fā)出一種不需要重新編譯內(nèi)核代碼的調(diào)試工具:kprobe。
kprobe 可以讓用戶在內(nèi)核幾乎所有的地址空間或函數(shù)(某些函數(shù)是被能被探測的)中插入探測點,用戶可以在這些探測點上通過定義自定義函數(shù)來調(diào)試內(nèi)核代碼。
用戶可以對一個探測點進行執(zhí)行前和執(zhí)行后調(diào)試,在介紹 kprobe 的使用方式前,我們先來了解一下 struct kprobe 結(jié)構(gòu),其定義如下:
一個 struct kprobe 結(jié)構(gòu)表示一個探測點,下面介紹一下其各個字段的作用:
addr:要探測的指令所在的內(nèi)存地址(由于需要知道指令的內(nèi)存地址,所以比較少使用)。
symbol_name:要探測的內(nèi)核函數(shù),symbol_name 與 addr 只能選擇一個進行探測。
offset:探測點在內(nèi)核函數(shù)內(nèi)的偏移量,用于探測內(nèi)核函數(shù)內(nèi)部的指令,如果該值為0表示函數(shù)的入口。
pre_handler:在探測點處的指令執(zhí)行前,被調(diào)用的調(diào)試函數(shù)。
post_handler:在探測點處的指令執(zhí)行后,被調(diào)用的調(diào)試函數(shù)。
fault_handler:在執(zhí)行 pre_handler、post_handler 或單步執(zhí)行被探測指令時出現(xiàn)內(nèi)存異常,則會調(diào)用這個回調(diào)函數(shù)。
一個 kprobe 探測點的執(zhí)行過程如下圖所示:

從上面的介紹可知,kprobe 一般用于調(diào)試內(nèi)核函數(shù)。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。∏?00名進群領(lǐng)取,額外贈送一份價值699的內(nèi)核資料包(含視頻教程、電子書、實戰(zhàn)項目及代碼)?


kprobe 使用
接下來,我們介紹一下怎么使用 kprobe 來調(diào)試內(nèi)核函數(shù)。
使用 kprobe 來進行內(nèi)核調(diào)試的方式有兩種:
第一種是通過編寫內(nèi)核模塊,向內(nèi)核注冊探測點。探測函數(shù)可根據(jù)需要自行定制,使用靈活方便;
第二種方式是使用 kprobes on ftrace,這種方式是 kprobe 和 ftrace 結(jié)合使用,即可以通過 kprobe 來優(yōu)化 ftrace 來跟蹤函數(shù)的調(diào)用。
由于第一種方式靈活而且功能更為強大,所以本文主要介紹第一種使用方式。
要編寫一個 kprobe 內(nèi)核模塊,可以按照以下步驟完成:
第一步:根據(jù)需要來編寫探測函數(shù),如 pre_handler 和 post_handler 回調(diào)函數(shù)。
第二步:定義 struct kprobe 結(jié)構(gòu)并且填充其各個字段,如要探測的內(nèi)核函數(shù)名和各個探測回調(diào)函數(shù)。
第三步:通過調(diào)用 register_kprobe 函數(shù)注冊一個探測點。
第四步:編寫 Makefile 文件。
第五步:編譯并安裝內(nèi)核模塊。
接下來就按照上面的步驟來完成一個 kprobe 的內(nèi)核模塊。
1. 定義回調(diào)函數(shù)
第一步就是編寫追蹤的回調(diào)函數(shù),一般來說只需要編寫 pre_handler、post_handler 和 fault_handler 這三個回調(diào)函數(shù),當然也可以只編寫你想追蹤的其中某一個回調(diào)函數(shù)。下面我們將會完成這三個追蹤回調(diào)函數(shù)的編寫:
pre_handler 回調(diào)函數(shù)
我們首先編寫要追蹤的內(nèi)核函數(shù)被調(diào)用前的回調(diào)函數(shù) pre_handler,代碼如下:
static int pre_handler(struct kprobe *p, struct pt_regs *regs) { ? ?printk(KERN_INFO "pre_handler: p->addr = 0x%p, ip = %lx, flags = 0x%lx\n", ? ? ? ? ? p->addr, regs->ip, regs->flags); ? ?return 0; }
上面的函數(shù)只是簡單的打印了要追蹤的內(nèi)核函數(shù)的內(nèi)存地址、ip 寄存器和 flags 寄存器的值,在函數(shù)的定義中可以發(fā)現(xiàn)有個類型為 pt_regs 結(jié)構(gòu)的參數(shù) ,其主要保存了 CPU 各個寄存器的值,不同 CPU 架構(gòu)的定義不一樣,例如 x86 CPU 架構(gòu)的定義如下:
所以我們可以通過這個結(jié)構(gòu)來獲取 CPU 各個寄存器的值。
post_handler 回調(diào)函數(shù)
接著我們來編寫要追蹤的內(nèi)核函數(shù)被調(diào)用后的回調(diào)函數(shù) post_handler,其代碼如下:
post_handler 回調(diào)函數(shù)也只是簡單的打印了要追蹤的內(nèi)核函數(shù)的內(nèi)存地址和 flags 寄存器的值。
fault_handler 回調(diào)函數(shù)
最后我們來編寫當發(fā)生內(nèi)存異常時的回調(diào)函數(shù) fault_handler,其代碼如下:
fault_handler 回調(diào)函數(shù)打印了要追蹤的內(nèi)核函數(shù)的內(nèi)存地址和發(fā)生異常時的異常編號。
2. 定義 kprobe 結(jié)構(gòu)
接下來我們定義一個 struct kprobe 結(jié)構(gòu)并且填充其各個字段值,代碼如下:
由于我們要追蹤 do_fork 內(nèi)核函數(shù),所以在 kprobe 結(jié)構(gòu)的 symbol_name 設(shè)置為 do_fork 字符串,然后設(shè)置各個回調(diào)函數(shù)即可。
3. 注冊追蹤點
最后通過調(diào)用 register_kprobe 函數(shù)來注冊追蹤點,代碼如下:
編寫 Makefile 文件
Makefile 文件用于編譯內(nèi)核模塊時使用,一般來說編譯內(nèi)核模塊的 Makefile 格式相對固定,如下:
5. 編譯并安裝內(nèi)核模塊
最后,我們編譯并且安裝這個內(nèi)核模塊,命令如下:
安裝完成后,隨便敲入一個命令(如 ls),然后通過調(diào)用 dmesg 命令查看內(nèi)核模塊輸出的結(jié)果,如下所示:
可以看出,我們的調(diào)試模塊已經(jīng)正常工作,并且輸出我們需要的信息。
總結(jié)
本文主要介紹了 kprobe 的使用方式,kprobe 的功能非常強大,可以幫助我們發(fā)現(xiàn)內(nèi)核的一些 BUG。當然,本文也只是非常簡單的介紹其使用,但有了這些基礎(chǔ)就可以完成很多復雜的調(diào)試。
