摘自:https://blog.csdn.net/luckyapple1028/article/details/54782659
前兩篇博文介紹了kprobes探測技術中kprobe和jprobe的使用與實現。本文介紹kprobes中的最后一種探測技術kretprobe,它同樣基於kprobe實現,可用於探測函數的返回值以及計算函數執行的耗時。本文首先通過一個簡單的示例程序介紹kretprobe的使用方式,然后通過源碼分析它是如何實現的。
內核源碼:Linux-4.1.x
實驗環境:Fedora25(x86_64)、樹莓派1b
1、kretprobe使用示例
使用kretprobe探測函數的返回值同jprobe一樣需要編寫內核模塊,當然內核也提供了一個簡單的示例程序kretprobe_example.c(位於sample/kprobes目錄),該程序的實現更為通用,用戶可以在使用時直接通過模塊參數指定需要探測的函數,它在探測函數返回值的同時還統計了被探測函數的執行用時。在分析kretprobe_example.c之前先熟悉一下kretprobe的結構體定義和API接口。
1.1、kretprobe結構體與API介紹
/*
* Function-return probe -
* Note:
* User needs to provide a handler function, and initialize maxactive.
* maxactive - The maximum number of instances of the probed function that
* can be active concurrently.
* nmissed - tracks the number of times the probed function's return was
* ignored, due to maxactive being too low.
*
*/
struct kretprobe {
struct kprobe kp;
kretprobe_handler_t handler;
kretprobe_handler_t entry_handler;
int maxactive;
int nmissed;
size_t data_size;
struct hlist_head free_instances;
raw_spinlock_t lock;
};
struct kretprobe結構體用於定義一個kretprobe。由於它的實現基於kprobe,結構體中自然也少不了kprobe字段;然后handler和entry_handler分別表示兩個回調函數,用戶自行定義,entry_handler會在被探測函數執行之前被調用,handler在被探測函數返回后被調用(一般在這個函數中打印被探測函數的返回值);maxactive表示同時支持並行探測的上限,因為kretprobe會跟蹤一個函數從開始到結束,因此對於一些調用比較頻繁的被探測函數,在探測的時間段內重入的概率比較高,這個maxactive字段值表示在重入情況發生時,支持同時檢測的進程數(執行流數)的上限,若並行觸發的數量超過了這個上限,則kretprobe不會進行跟蹤探測,僅僅增加nmissed字段的值以作提示;data_size字段表示kretprobe私有數據的大小,在注冊kretprobe時會根據該大小預留空間;最后free_instances表示空閑的kretprobe運行實例鏈表,它鏈接了本kretprobe的空閑實例struct kretprobe_instance結構體表示。
struct kretprobe_instance {
struct hlist_node hlist;
struct kretprobe *rp;
kprobe_opcode_t *ret_addr;
struct task_struct *task;
char data[0];
};
這個結構體表示kretprobe的運行實例,前文說過被探測函數在跟蹤期間可能存在並發執行的現象,因此kretprobe使用一個kretprobe_instance來跟蹤一個執行流,支持的上限為maxactive。在沒有觸發探測時,所有的kretprobe_instance實例都保存在free_instances表中,每當有執行流觸發一次kretprobe探測,都會從該表中取出一個空閑的kretprobe_instance實例用來跟蹤。
kretprobe_instance結構提中的rp指針指向所屬的kretprobe;ret_addr用於保存原始被探測函數的返回地址(后文會看到被探測函數返回地址會被暫時替換);task用於綁定其跟蹤的進程;最后data保存用戶使用的kretprobe私有數據,它會在整個kretprobe探測運行期間在entry_handler和handler回調函數之間進行傳遞(一般用於實現統計被探測函數的執行耗時)。
1.2、示例kretprobe_example分析與演示
內核提供的kretprobe_example.c示例程序默認探測do_fork函數的執行耗時和返回值,支持通過模塊參數指定被探測函數,用戶若需要探測其他函數,只需要在加載內核模塊時傳入自己需要探測的函數名即可,無需修改模塊代碼。
static char func_name[NAME_MAX] = "do_fork";
module_param_string(func, func_name, NAME_MAX, S_IRUGO);
MODULE_PARM_DESC(func, "Function to kretprobe; this module will report the"
" function's execution time");
下面詳細分析:
/* per-instance private data */
struct my_data {
ktime_t entry_stamp;
};
static struct kretprobe my_kretprobe = {
.handler = ret_handler,
.entry_handler = entry_handler,
.data_size = sizeof(struct my_data),
/* Probe up to 20 instances concurrently. */
.maxactive = 20,
};
static int __init kretprobe_init(void)
{
int ret;
my_kretprobe.kp.symbol_name = func_name;
ret = register_kretprobe(&my_kretprobe);
if (ret < 0) {
printk(KERN_INFO "register_kretprobe failed, returned %d\n",
ret);
return -1;
}
printk(KERN_INFO "Planted return probe at %s: %p\n",
my_kretprobe.kp.symbol_name, my_kretprobe.kp.addr);
return 0;
}
static void __exit kretprobe_exit(void)
{
unregister_kretprobe(&my_kretprobe);
printk(KERN_INFO "kretprobe at %p unregistered\n",
my_kretprobe.kp.addr);
/* nmissed > 0 suggests that maxactive was set too low. */
printk(KERN_INFO "Missed probing %d instances of %s\n",
my_kretprobe.nmissed, my_kretprobe.kp.symbol_name);
}
程序定義了一個結構體struct my_data,其中唯一的參數entry_stamp用於計算函數執行的時間;同時程序定義了一個struct kretprobe實例,注意其中的私有數據長度為my_data的長度,最大支持的並行探測數為20(即若在某一時刻,do_fork函數同時調用的執行流數量超過20那將不會再進行探測跟蹤)。最后在模塊的init和exit函數中僅僅調用register_kretprobe和unregister_kretprobe函數對my_kretprobe進行注冊和注銷,在kretprobe注冊完成后就默認啟動探測了。
/* Here we use the entry_hanlder to timestamp function entry */
static int entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
struct my_data *data;
if (!current->mm)
return 1; /* Skip kernel threads */
data = (struct my_data *)ri->data;
data->entry_stamp = ktime_get();
return 0;
}
/*
* Return-probe handler: Log the return value and duration. Duration may turn
* out to be zero consistently, depending upon the granularity of time
* accounting on the platform.
*/
static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
int retval = regs_return_value(regs);
struct my_data *data = (struct my_data *)ri->data;
s64 delta;
ktime_t now;
now = ktime_get();
delta = ktime_to_ns(ktime_sub(now, data->entry_stamp));
printk(KERN_INFO "%s returned %d and took %lld ns to execute\n",
func_name, retval, (long long)delta);
return 0;
}
函數entry_handler在do_fork函數被調用時觸發調用,注意第一個入參不是struct kretprobe結構,而是代表一個探測實例的struct kretprobe_instance結構,它從kretprobe的free_instances鏈表中分配,在跟蹤完本次觸發流程后回收。entry_handler函數利用了kretprobe_instance中私有數據,保存do_fork函數執行的開始時間。
函數ret_handler在do_fork函數執行完成返回后被調用,它根據當前的時間減去kretprobe_instance中私有數據保存的起始時間,即可計算出do_fork函數執行的耗時。同時它調用regs_return_value函數獲取do_fork函數的返回值,該函數是架構相關的:
static inline long regs_return_value(struct pt_regs *regs)
{
return regs->ARM_r0;
}
例如arm環境是通過r0寄存器傳遞返回值的,因此該函數的實現僅僅是返回r0寄存器的值(需要注意的是regs指針傳遞的是do_fork函數返回時所保存的寄存器信息,這一點后面會分析)。ret_handler函數最后打印出do_fork函數的返回值和執行耗時(單位ns)。
下面在x86_64環境下演示該示例程序的實際效果(環境配置請參考《Linux內核調試技術——kprobe使用與實現》):
<6>[ 1217.859349] _do_fork returned 1838 and took 518081 ns to execute
<6>[ 1217.863880] _do_fork returned 1839 and took 223701 ns to execute
<6>[ 1217.865731] _do_fork returned 1840 and took 221746 ns to execute
<6>[ 1220.077508] _do_fork returned 1841 and took 433573 ns to execute
<6>[ 1220.081512] _do_fork returned 1842 and took 362684 ns to execute
<6>[ 1220.083767] _do_fork returned 1843 and took 284184 ns to execute
<6>[ 1220.995537] _do_fork returned 1844 and took 503414 ns to execute
<6>[ 1221.000363] _do_fork returned 1845 and took 427427 ns to execute
加載kretprobe_example.ko,不指定探測函數,默認探測do_fork函數,內核輸出以上messages。可見do_fork函數的返回值(新創建進程的pid)是依次遞增的,同時函數執行用時也呈現的非常直觀。因此,使用kretprobe可以簡單的獲取一個函數在執行時的返回值,在內核調試時非常有用。探測其他函數方法類似,不再贅述。
2、kretprobe實現分析
kretprobe的實現基於kprobe,因此這里將在前一篇博文《Linux內核調試技術——kprobe使用與實現》的基礎之上分析它的實現,主要包括kretprobe注冊流程和觸發探測流程,涉及kprobe的部分不再詳細描。
2.1、kretprobe實現原理
同jprobe類似,kretprobe也是一種特殊形式的kprobe,它有自己私有的pre_handler,並不支持用戶定義pre_handler和post_handler等回調函數。其中它的pre_handler回調函數會為kretprobe探測函數執行的返回值做准備工作,其中最主要的就是替換掉正常流程的返回地址,讓被探測函數在執行之后能夠跳轉到kretprobe所精心設計的函數中去,它會獲取函數返回值,然后調用kretprobe->handler回調函數(被探測函數的返回地址此刻得到輸出),最后恢復正常執行流程。
2.2、注冊一個kretprobe
kretprobe探測模塊調用register_kretprobe函數向內核注冊一個kretprobe實例,代碼路徑為kernel/kprobes.c,其主要流程如下圖:
圖1 kretprobe注冊流程
int register_kretprobe(struct kretprobe *rp)
{
int ret = 0;
struct kretprobe_instance *inst;
int i;
void *addr;
if (kretprobe_blacklist_size) {
addr = kprobe_addr(&rp->kp);
if (IS_ERR(addr))
return PTR_ERR(addr);
for (i = 0; kretprobe_blacklist[i].name != NULL; i++) {
if (kretprobe_blacklist[i].addr == addr)
return -EINVAL;
}
}
函數的開頭首先處理kretprobe所特有的blacklist,如果指定的被探測函數在這個blacklist中就直接返回EINVAL,表示不支持探測。其中kretprobe_blacklist_size表示隊列的長度,kretprobe_blacklist是一個全局結構體數組,每一項都是一個struct kretprobe_blackpoint結構體:
struct kretprobe_blackpoint {
const char *name;
void *addr;
};
其中name字段表示函數名,addr表示函數的運行地址。該kretprobe_blacklist是架構相關的,用於申明該架構哪些函數是不支持使用kretprobe探測的,其中arm架構並沒有被定義,而x86_64架構的定義如下:
struct kretprobe_blackpoint kretprobe_blacklist[] = {
{"__switch_to", }, /* This function switches only current task, but
doesn't switch kernel stack.*/
{NULL, NULL} /* Terminator */
};
這表明在x86_64架構下的__switch_to函數不可以被kretprobe所探測(這一點在內核的kprobes.txt中已經有說明)。回到register_kretprobe函數中,在blacklist檢測時比較的是函數運行地址addr字段,該字段在kprobes子系統初始化函數init_kprobes中初始化:
if (kretprobe_blacklist_size) {
/* lookup the function address from its name */
for (i = 0; kretprobe_blacklist[i].name != NULL; i++) {
kprobe_lookup_name(kretprobe_blacklist[i].name,
kretprobe_blacklist[i].addr);
if (!kretprobe_blacklist[i].addr)
printk("kretprobe: lookup failed: %s\n",
kretprobe_blacklist[i].name);
}
}
繼續往下分析register_kretprobe注冊函數:
rp->kp.pre_handler = pre_handler_kretprobe;
rp->kp.post_handler = NULL;
rp->kp.fault_handler = NULL;
rp->kp.break_handler = NULL;
/* Pre-allocate memory for max kretprobe instances */
if (rp->maxactive <= 0) {
#ifdef CONFIG_PREEMPT
rp->maxactive = max_t(unsigned int, 10, 2*num_possible_cpus());
#else
rp->maxactive = num_possible_cpus();
#endif
}
此處只指定了kprobe的pre_handler回調函數為pre_handler_kretprobe;然后若用戶沒有指定最大並行探測數maxactive,這里會計算並設置一個默認的值。
raw_spin_lock_init(&rp->lock);
INIT_HLIST_HEAD(&rp->free_instances);
for (i = 0; i < rp->maxactive; i++) {
inst = kmalloc(sizeof(struct kretprobe_instance) +
rp->data_size, GFP_KERNEL);
if (inst == NULL) {
free_rp_inst(rp);
return -ENOMEM;
}
INIT_HLIST_NODE(&inst->hlist);
hlist_add_head(&inst->hlist, &rp->free_instances);
}
rp->nmissed = 0;
/* Establish function entry probe point */
ret = register_kprobe(&rp->kp);
if (ret != 0)
free_rp_inst(rp);
return ret;
}
接下來根據maxactive的值,為各個kretprobe_instance實例分配內存並將它們鏈接到kretprobe的free_instances鏈表中,最后調用register_kprobe函數注冊內嵌的kprobe。
以上就是kretprobe的注冊流程,可見它同jprobe一樣也非常的簡單,最終依賴的依然是kprobe機制。
2.3、觸發kretprobe探測
基於kprobe機制,在執行到指定的被探測函數后,會觸發CPU異常,進入kprobe探測流程。首先由kprobe_handler函數調用pre_handler回調函數,此處為pre_handler_kretprobe函數,該函數首先找到一個空閑的kretprobe_instance探測實例並將它和當前進程綁定,然后調用entry_handler回調函數,接着保存並替換被探測函數的返回地址,最后kprobe探測流程結束並回到正常的執行流程執行被探測函數,在函數返回后將跳轉到被替換的kretprobe_trampoline,該函數會獲取被探測函數的寄存器信息並調用用戶定義的回調函數輸出其中的返回值,最后函數返回正常的執行流程。
圖2 kretprobe觸發流程
/*
* This kprobe pre_handler is registered with every kretprobe. When probe
* hits it will set up the return probe.
*/
static int pre_handler_kretprobe(struct kprobe *p, struct pt_regs *regs)
{
struct kretprobe *rp = container_of(p, struct kretprobe, kp);
unsigned long hash, flags = 0;
struct kretprobe_instance *ri;
/*
* To avoid deadlocks, prohibit return probing in NMI contexts,
* just skip the probe and increase the (inexact) 'nmissed'
* statistical counter, so that the user is informed that
* something happened:
*/
if (unlikely(in_nmi())) {
rp->nmissed++;
return 0;
}
/* TODO: consider to only swap the RA after the last pre_handler fired */
hash = hash_ptr(current, KPROBE_HASH_BITS);
raw_spin_lock_irqsave(&rp->lock, flags);
if (!hlist_empty(&rp->free_instances)) {
ri = hlist_entry(rp->free_instances.first,
struct kretprobe_instance, hlist);
hlist_del(&ri->hlist);
raw_spin_unlock_irqrestore(&rp->lock, flags);
ri->rp = rp;
ri->task = current;
if (rp->entry_handler && rp->entry_handler(ri, regs)) {
raw_spin_lock_irqsave(&rp->lock, flags);
hlist_add_head(&ri->hlist, &rp->free_instances);
raw_spin_unlock_irqrestore(&rp->lock, flags);
return 0;
}
arch_prepare_kretprobe(ri, regs);
/* XXX(hch): why is there no hlist_move_head? */
INIT_HLIST_NODE(&ri->hlist);
kretprobe_table_lock(hash, &flags);
hlist_add_head(&ri->hlist, &kretprobe_inst_table[hash]);
kretprobe_table_unlock(hash, &flags);
} else {
rp->nmissed++;
raw_spin_unlock_irqrestore(&rp->lock, flags);
}
return 0;
}
首先根據當前的進程描述符地址以及KPROBE_HASH_BITS值計算出hash索引值,如果kretprobe的free_instances鏈表不為空,則從中找到一個空閑的kretprobe_instance實例,然后對其中的rp和task字段賦值,表示將該探測實例和當前進程綁定;然后調用entry_handler回調函數(前文kretprobe_example示例程序中的entry_handler函數在此被調用);接下來調用arch_prepare_kretprobe函數,該函數架構相關,用於保存並替換regs中的返回地址,其中arm架構的實現如下:
void __kprobes arch_prepare_kretprobe(struct kretprobe_instance *ri,
struct pt_regs *regs)
{
ri->ret_addr = (kprobe_opcode_t *)regs->ARM_lr;
/* Replace the return addr with trampoline addr. */
regs->ARM_lr = (unsigned long)&kretprobe_trampoline;
}
這里將regs->ARM_lr保存到了ri->ret_addr中,然后原始值被替換成了kretprobe_trampoline函數的地址。注意regs->ARM_lr值的含義是原始代碼流程調用被探測函數后的下一條指令的地址(由於regs中指向的是執行被探測函數入口指令時所保存的寄存器值,因此lr寄存器中的內容為執行被探測函數的返回地址),經過這一替換,原始執行流程在執行完整個被探測函數后將跳轉到kretprobe_trampoline函數執行,整個函數稍后分析。在來看x86_64架構的函數實現:
void arch_prepare_kretprobe(struct kretprobe_instance *ri, struct pt_regs *regs)
{
unsigned long *sara = stack_addr(regs);
ri->ret_addr = (kprobe_opcode_t *) *sara;
/* Replace the return addr with trampoline addr */
*sara = (unsigned long) &kretprobe_trampoline;
}
整體大同小異,x86_64架構的函數調用棧同arm的不同,它將返回地址保存在棧頂空間中(即sp指向的位置),因此保存和替換的方式同arm架構略有不同。
繼續回到pre_handler_kretprobe函數中,接下來將本此使用的kretprobe_instance鏈接到全局kretprobe_inst_table哈希表中,該哈希表在init_kprobes中初始化。最后如果kretprobe的free_instances鏈表為空,則說明被探測函數的並行觸發流程超過了指定的maxactive上限,僅增加nmissed值不進行探測跟蹤。
pre_handler_kretprobe函數返回后,kprobe流程接着執行singlestep流程並返回到正常的執行流程,被探測函數(do_fork)繼續執行,直到它執行完畢並返回。由於返回地址被替換為kretprobe_trampoline,所以跳轉到kretprobe_trampoline執行,該函數架構相關且有嵌入匯編實現,具體分析一下。
1)arm架構實現:
/*
* When a retprobed function returns, trampoline_handler() is called,
* calling the kretprobe's handler. We construct a struct pt_regs to
* give a view of registers r0-r11 to the user return-handler. This is
* not a complete pt_regs structure, but that should be plenty sufficient
* for kretprobe handlers which should normally be interested in r0 only
* anyway.
*/
void __naked __kprobes kretprobe_trampoline(void)
{
__asm__ __volatile__ (
"stmdb sp!, {r0 - r11} \n\t"
"mov r0, sp \n\t"
"bl trampoline_handler \n\t"
"mov lr, r0 \n\t"
"ldmia sp!, {r0 - r11} \n\t"
#ifdef CONFIG_THUMB2_KERNEL
"bx lr \n\t"
#else
"mov pc, lr \n\t"
#endif
: : : "memory");
}
該函數在棧空間構造出一個不完整的pt_regs結構體變量,僅僅填充了r0~r11寄存器(由於kretprobe所關注的僅是函數返回值r0,這已經足夠了),然后跳轉到trampoline_handler函數執行:
/* Called from kretprobe_trampoline */
static __used __kprobes void *trampoline_handler(struct pt_regs *regs)
{
struct kretprobe_instance *ri = NULL;
struct hlist_head *head, empty_rp;
struct hlist_node *tmp;
unsigned long flags, orig_ret_address = 0;
unsigned long trampoline_address = (unsigned long)&kretprobe_trampoline;
INIT_HLIST_HEAD(&empty_rp);
kretprobe_hash_lock(current, &head, &flags);
/*
* It is possible to have multiple instances associated with a given
* task either because multiple functions in the call path have
* a return probe installed on them, and/or more than one return
* probe was registered for a target function.
*
* We can handle this because:
* - instances are always inserted at the head of the list
* - when multiple return probes are registered for the same
* function, the first instance's ret_addr will point to the
* real return address, and all the rest will point to
* kretprobe_trampoline
*/
hlist_for_each_entry_safe(ri, tmp, head, hlist) {
if (ri->task != current)
/* another task is sharing our hash bucket */
continue;
if (ri->rp && ri->rp->handler) {
__this_cpu_write(current_kprobe, &ri->rp->kp);
get_kprobe_ctlblk()->kprobe_status = KPROBE_HIT_ACTIVE;
ri->rp->handler(ri, regs);
__this_cpu_write(current_kprobe, NULL);
}
orig_ret_address = (unsigned long)ri->ret_addr;
recycle_rp_inst(ri, &empty_rp);
if (orig_ret_address != trampoline_address)
/*
* This is the real return address. Any other
* instances associated with this task are for
* other calls deeper on the call stack
*/
break;
}
kretprobe_assert(ri, orig_ret_address, trampoline_address);
kretprobe_hash_unlock(current, &flags);
hlist_for_each_entry_safe(ri, tmp, &empty_rp, hlist) {
hlist_del(&ri->hlist);
kfree(ri);
}
return (void *)orig_ret_address;
}
由於前面的kprobe執行流程已經完全退出了,因此這里無法通過傳參的手段來獲取所觸發的到底是哪一個kretprobe_instance,所以只能通過前面的全局kretprobe_inst_table哈希表和current進程描述符指針來確定kretprobe_instance實例。所以函數首先遍歷kretprobe_inst_table哈希表,找到和當前進程綁定的kretprobe_instance。找到了以后會臨時修改current_kprobe的值和kprobe的狀態值,表明又進入了kprobe的處理流程,防止沖突。接着調用handler回調函數,傳入的第二個入參就是前面kretprobe_trampoline函數構造出來的pt_regs,注意其中的r0寄存器保存的是函數的返回值。
handler回調函數執行完畢以后,調用recycle_rp_inst函數將當前的kretprobe_instance實例從kretprobe_inst_table哈希表釋放,重新鏈入free_instances中,以備后面kretprobe觸發時使用,另外如果kretprobe已經被注銷則將它添加到銷毀表中待銷毀:
void recycle_rp_inst(struct kretprobe_instance *ri,
struct hlist_head *head)
{
struct kretprobe *rp = ri->rp;
/* remove rp inst off the rprobe_inst_table */
hlist_del(&ri->hlist);
INIT_HLIST_NODE(&ri->hlist);
if (likely(rp)) {
raw_spin_lock(&rp->lock);
hlist_add_head(&ri->hlist, &rp->free_instances);
raw_spin_unlock(&rp->lock);
} else
/* Unregistering */
hlist_add_head(&ri->hlist, head);
}
回到trampoline_handler函數中,接下來有一種情況需要注意,由於此處在查找kretprobe_instance時采用的時遍歷全局哈希表的方法,同時可能會存在多個kretprobe實例同當前進程綁定的情況,因為在一個被探測函數的調用流程中是可能會調用到其他的被探測函數的,例如下面這種情況:
int b(void)
{
int ret;
...
return ret;
}
int a(void)
{
int ret;
ret = b();
...
return ret;
}
如果對a函數和b函數同時注冊了kretprobe,就會出現多kretprobe_instance綁定同一進程的情況。對於這種多綁定的情況,在處理b函數返回值時可能會錯誤的找到綁定到a函數的kretprobe_instance實例,導致探測出現錯誤。那如何避免這種錯誤?其實在注釋中已經給出說明。這里采用了一種非常巧妙的方法,首先每次插入kretprobe_inst_table表時都是從頭插入的,在取出的時候也是從頭獲取,類似一個堆棧,其次在循環的最后給出了一個break條件,那就是如果函數的原始返回地址不等於kretprobe_trampoline函數的地址,那就break,不再循環查找下一個kretprobe_instance實例。我們知道在一般的情況下這break條件必然滿足,所以這里找到的必然是流程上最后一次觸發kretprobe探測的實例。
回到trampoline_handler函數最后遍歷empty_rp銷毀需要釋放的kretprobe_instance實例。最后返回被探測函數的原始返回地址,執行流程再次回到kretprobe_trampoline函數中:
"mov lr, r0 \n\t"
"ldmia sp!, {r0 - r11} \n\t"
#ifdef CONFIG_THUMB2_KERNEL
"bx lr \n\t"
#else
"mov pc, lr
接下來從r0寄存器中取出原始的返回地址,然后恢復原始函數調用棧空間,最后跳轉到原始返回地址執行,至此函數調用的流程就回歸正常流程了,整個kretprobe探測結束。
2)x86_64架構實現
/*
* When a retprobed function returns, this code saves registers and
* calls trampoline_handler() runs, which calls the kretprobe's handler.
*/
static void __used kretprobe_trampoline_holder(void)
{
asm volatile (
".global kretprobe_trampoline\n"
"kretprobe_trampoline: \n"
#ifdef CONFIG_X86_64
/* We don't bother saving the ss register */
" pushq %rsp\n"
" pushfq\n"
SAVE_REGS_STRING
" movq %rsp, %rdi\n"
" call trampoline_handler\n"
/* Replace saved sp with true return address. */
" movq %rax, 152(%rsp)\n"
RESTORE_REGS_STRING
" popfq\n"
#else
" pushf\n"
SAVE_REGS_STRING
" movl %esp, %eax\n"
" call trampoline_handler\n"
/* Move flags to cs */
" movl 56(%esp), %edx\n"
" movl %edx, 52(%esp)\n"
/* Replace saved flags with true return address. */
" movl %eax, 56(%esp)\n"
RESTORE_REGS_STRING
" popf\n"
#endif
" ret\n");
}
實現的原理同arm是一致的,這里會調用SAVE_REGS_STRING把寄存器壓棧,構造出pt_regs變量,然后調用trampoline_handler函數,這個函數基本同arm的一模一樣,就不貼了,最后kretprobe_trampoline_holder恢復棧空間和原始返回地址,跳轉到正常的執行流程中繼續執行。
3、總結
kretprobe探測技術基於kprobe實現,是kprobes探測技術中的最后一種,內核開發人員可以通過它來動態的探測函數執行的返回值,並且也可以做一些定制話的動作,例如檢測函數的執行時間等,使用非常方便。本文介紹了kretprobe探測工具的使用方式及其原理,並通過源碼分析了arm架構和x86_64架構下它的實現方式。
最后,本文連同前兩篇博文較為詳細的分析了kprobes的三種函數探測技術,靈活的使用這三種調試技術能夠大大的提高內核開發與問題定位的效率。
————————————————
版權聲明:本文為CSDN博主「luckyapple1028」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/luckyapple1028/java/article/details/54782659