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でカーネルスレッドの場合、何故かスキップしている。自分自身を自分自身で測るようなもので、タイマ更新にかかるスレッドを考慮してか?
#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 execute
returned 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 execute
returned 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;
}

補足

entry_handlerの処理で、if (!current->mm)とし、カーネルスレッドをスキップしていた。その理由として、タイマ更新にかかるうんうん。と、訳の分からん事を書いている。trampoline_handlerの処理で該当するstruct kretprobe_instanceの取得で、if (ri->task != current)としている。ri->taskは、pre_handler_kretprobe()で設定されている。という事であった。


最終更新 2012/07/20 18:12:14 - north
(2012/07/19 19:44:42 作成)


検索

アクセス数
3686183
最近のコメント
コアダンプファイル - sakaia
list_head構造体 - yocto_no_yomikata
勧告ロックと強制ロック - wataash
LKMからのファイル出力 - 重松 宏昌
kprobe - ななし
ksetの実装 - スーパーコピー
カーネルスレッドとは - ノース
カーネルスレッドとは - nbyst
asmlinkageってなに? - ノース
asmlinkageってなに? - よろしく
Adsense
広告情報が設定されていません。