kretprobe
kretprobeはkprobeで実装されており(kprobeの応用例と言った方が正しいか。)、名称からしてカーネル関数の終了時に呼ばれるが、実は関数の開始時にもコールされる。従って以下のようにftraceのような事ができる。
サンプルはカーネルソースにあった物を、エラー/コメント等を端折ったもので、関数の実行時間と関数の戻り値を取得する。ftraceみたいなもの。
ソースの概略は説明する程のものでなく、いたってシンプル。struct kretprobe my_kretprobeに、関数開始時/終了時のコールバックを設定っするだけ。必要ならコールバック関数で使うデータを.data_sizeとして設定する。.maxactiveはstruct my_dataのインスタンスの数(kmalocで.maxactive数確保)。ネスティング度合いみたいなものか。kretprobe_exitのmy_kretprobe.nmissedは、ネスティングによるインスタンスが足りなかった場合の、ヒットミス数となる。
entry_handlerでカーネルスレッドの場合、何故かスキップしている。自分自身を自分自身で測るようなもので、タイマ更新にかかるスレッドを考慮してか?
これは、post_handlerで実装すると、オーバヘッドの故だろう。と漠然と思っていた。で、ひらめいて、post_handlerでその処理を行うには、トラップするアドレスはcallしているアドレスでなければならない。そうなると実用上使い物にならない。トラップする箇所は、callして呼ばれた最初のアドレスとなる(通常push bp たぶん)。従ってpost_handlerしても、pshu bpが実行された後の結果にすぎないわけだ。
kprobes構造体のrp->kpのpre_handlerに、コールバックpre_handler_kretprobeを設定し、他のコールバックにNULLを設定し、rp->maxactive個のstruct kretprobeのインスタンスを作成する。struct kretprobe *rpそのものを使わないのは、対象関数がスリープしたりSMP等の、複数処理を考慮してか・・・。
そのインスタンスをhlistにリストして、register_kprobeをコールする。
entry_handlerの帰り値が0なら、arch_prepare_kretprobe()でトランポリンの設定を行う。
そして、そのアドレスにkretprobe_trampolineのアドレスを設定する。これでターゲットとなる関数がretすると、その処理はkretprobe_trampolineへと行く。
kretprobe_trampolineのインライン良くわからん。雰囲気、trampoline_handlerを呼び出して、その結果をspに設定してるっぽい。
サンプルはカーネルソースにあった物を、エラー/コメント等を端折ったもので、関数の実行時間と関数の戻り値を取得する。ftraceみたいなもの。
ソースの概略は説明する程のものでなく、いたってシンプル。struct kretprobe my_kretprobeに、関数開始時/終了時のコールバックを設定っするだけ。必要ならコールバック関数で使うデータを.data_sizeとして設定する。.maxactiveはstruct my_dataのインスタンスの数(kmalocで.maxactive数確保)。ネスティング度合いみたいなものか。kretprobe_exitのmy_kretprobe.nmissedは、ネスティングによるインスタンスが足りなかった場合の、ヒットミス数となる。
entry_handlerでカーネルスレッドの場合、何故かスキップしている。自分自身を自分自身で測るようなもので、タイマ更新にかかるスレッドを考慮してか?
#include <linux/kernel.h> #include <linux/module.h> #include <linux/kprobes.h> #include <linux/ktime.h> #include <linux/limits.h> #include <linux/sched.h> 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"); struct my_data { ktime_t entry_stamp; }; 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; } 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; } static struct kretprobe my_kretprobe = { .handler = ret_handler, .entry_handler = entry_handler, .data_size = sizeof(struct my_data), .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) { return -1; } return 0; } static void __exit kretprobe_exit(void) { unregister_kretprobe(&my_kretprobe); printk(KERN_INFO "Missed probing %d instances of %s\n", my_kretprobe.nmissed, my_kretprobe.kp.symbol_name); } module_init(kretprobe_init) module_exit(kretprobe_exit) MODULE_LICENSE("GPL");実行結果
[root@localhost lkm]# insmod kretprobe_test.ko [root@localhost lkm]# dmesg : [87702.796993] do_fork returned 6539 and took 309105 ns to executereturned 6539はプロセスIDとなる。
[root@localhost lkm]# insmod kretprobe_test.ko func="do_sys_open" [root@localhost lkm]# dmesg : [87601.986395] do_sys_open returned 3 and took 19732 ns to executereturned 3はファイルIDとなる。
実装
kretprobeはkprobesのpre_handler/post_handlerに、その処理を記述すればいいだけでは?と思っていたが、実際はpre_handlerのみで実装している。復帰時の処理は?と言うと、Cのプログラム技法のトランポリン(関数コールのスタック上の復帰アドレスを、別の処理を行うアドレスに書き換え、その処理のスタック上の復帰アドレスを、オリジナルのアドレスに書き換える手法)で行っている。これは、post_handlerで実装すると、オーバヘッドの故だろう。と漠然と思っていた。で、ひらめいて、post_handlerでその処理を行うには、トラップするアドレスはcallしているアドレスでなければならない。そうなると実用上使い物にならない。トラップする箇所は、callして呼ばれた最初のアドレスとなる(通常push bp たぶん)。従ってpost_handlerしても、pshu bpが実行された後の結果にすぎないわけだ。
kprobes構造体のrp->kpのpre_handlerに、コールバックpre_handler_kretprobeを設定し、他のコールバックにNULLを設定し、rp->maxactive個のstruct kretprobeのインスタンスを作成する。struct kretprobe *rpそのものを使わないのは、対象関数がスリープしたりSMP等の、複数処理を考慮してか・・・。
そのインスタンスをhlistにリストして、register_kprobeをコールする。
int __kprobes register_kretprobe(struct kretprobe *rp) { int ret = 0; struct kretprobe_instance *inst; int i; void *addr; : : rp->kp.pre_handler = pre_handler_kretprobe; rp->kp.post_handler = NULL; rp->kp.fault_handler = NULL; rp->kp.break_handler = NULL; : : 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; ret = register_kprobe(&rp->kp); if (ret != 0) free_rp_inst(rp); return ret; }pre_handler_kretprobeはトラップされた時に、kprobeからコールされる。トラップされたstruct kretprobeをhlistから取得し、entry_handlerが定義されていたら、それをコールする。なお帰り値が0でないと、処理は終了する。
entry_handlerの帰り値が0なら、arch_prepare_kretprobe()でトランポリンの設定を行う。
static int __kprobes 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; 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); 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; }arch_prepare_kretprobeでトランポリンの設定を行う。ri->ret_addrにオリジナルの帰りアドレスを設定する。これは、ターゲットとしてる関数がコールされたアドレスである。
そして、そのアドレスにkretprobe_trampolineのアドレスを設定する。これでターゲットとなる関数がretすると、その処理はkretprobe_trampolineへと行く。
kretprobe_trampolineのインライン良くわからん。雰囲気、trampoline_handlerを呼び出して、その結果をspに設定してるっぽい。
void __kprobes arch_prepare_kretprobe(struct kretprobe_instance *ri, struct pt_regs *regs) { unsigned long *sara = stack_addr(regs); ri->ret_addr = (kprobe_opcode_t *) *sara; *sara = (unsigned long) &kretprobe_trampoline; } 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"); }trampoline_handler()は、ターゲットとなる関数がretした時の処理となる。hlistから該当するstruct kretprobe_instance riを取得し、ターゲット関数がretした時の処理となるri->rp->handlerをコールする。そして、orig_ret_address = (unsigned long)ri->ret_addrで、本来の復帰アドレスを返している。
static __used __kprobes void *trampoline_handler(struct pt_regs *regs) { struct kretprobe_instance *ri = NULL; struct hlist_head *head, empty_rp; struct hlist_node *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); hlist_for_each_entry_safe(ri, node, tmp, head, hlist) { if (ri->task != current) /* another task is sharing our hash bucket */ continue; if (ri->rp && ri->rp->handler) { __get_cpu_var(current_kprobe) = &ri->rp->kp; get_kprobe_ctlblk()->kprobe_status = KPROBE_HIT_ACTIVE; ri->rp->handler(ri, regs); __get_cpu_var(current_kprobe) = NULL; } orig_ret_address = (unsigned long)ri->ret_addr; recycle_rp_inst(ri, &empty_rp); if (orig_ret_address != trampoline_address) break; } kretprobe_assert(ri, orig_ret_address, trampoline_address); kretprobe_hash_unlock(current, &flags); hlist_for_each_entry_safe(ri, node, tmp, &empty_rp, hlist) { hlist_del(&ri->hlist); kfree(ri); } return (void *)orig_ret_address; }