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;
}





