kprobe
kprobは、実行カーネル内を再コンパイルすることなく、任意のアドレスにブレイクを設定できる。そしてそのブレイクした時のデバック処理を記述することで、カーネルデバッグを行うというもの。
以下のstatic struct kprobe kpにブレイクする位置(シンボル又はアドレスの指定およびオフセット)、ブレイクした時の処理、ブレイクしたコードを実行した後の処理、必要なら、上記の処理で例外が発生した時の処理を記述して、register_kprobe()をコールするだけである。
必要なら/sys/kernel/debug/kprobes/enabledを1に設定する。(fedora16ではデフォルトで設定されていた。)
ブレイク時の各レジスターを表示している(あんまし意味ないけど)が、その時チェックしたいカーネル内各種シンボル(タスク構造体等)を表示させたりする。
ここでのブレイクは、call do_forkでなく、そこから呼ばれたfo_forkその物にブレイクが設定される。たぶん先頭コードはpush bpではないかな(bpレジスターも表示させればよかったか?)。で、pre kprobe/post kprobeを見ると、ax,bx,cx,dxは同じだが、spが大きく異なる。pre kprobe時にスタックを消費。
pre kprobe/post kprobeのブレイクアドレスを見ると、p->addr = 0xc0436270。これはlkmで設定したアドレス(.symbol_name = "do_fork")。で、ip = c0436271となっている。kprobeの実装はブレイクコード0x33を設定する。この時int 3の割り込みが発生する。で、pre kprobeは割り込みが発生した後のスタックポインタ。post kprobeはこの割り込みから復帰した時の、スタックポインタと言う事でないのかな?
CONFIG_OPTPROBESのカーネル下で、/proc/sys/debug/kprobes-optimization=1の時に採用されみた。ブレイクコードは、インストラクションコードで0xcc。相対ジャンプコードのそれも0xe9で1バイト。しかし相対ジャンプコードはジャンプアドレスが付加して、そのサイズは5バイトとなる。kprobeはカーネルコード内のどこに設定されるか分からない。相対ジャンプコードに書き換え中に、そのコードが実行されることもありえる。その回避として、optimize kprobeでは直接書き換えないで、ワークスレッドを通して書き換えるようになっている。確かワークスレッドは排他的に実行され、そのコード以外は実行されないことが保障されているからか。(たぶん・・・)
とりあえず以下に、register_kprobe()の概略。
kprobe_addr()で、シンボルで指定されている場合を考慮して、オフセットを加算したプロブするアドレスを取得すし、そのkprobe構造体そのものが、登録されているかcheck_kprobe_rereg()でチェックする。ここでのダブルチェックは、プロブするアドレスでない。
まず、プロブアドレスの以下の正当性をチェック。
・カーネルのアドレス
・kprobe内のアドレスでない
・ftrace内のアドレスでない
・テーブルデータ部のアドレスでない
次にlkm内にプロブする場合。
・unloadされないため、try_module_getモジュールが参照カウンタをtry_module_get()でインクリメントする。
・モジュールの初期化関数内のプロブなら、モジュールステイタスがMODULE_STATE_COMINGでないこと。
get_kprobe()でプロブするアドレスが、すでに設定されていたなら、register_aggr_kprobe()で登録済みのプロブを新規プロブで更新してるっぽい。
arch_prepare_kprobe()からkprobの実態的処理となる。struct kprobe->ainsn.insnに executable pageを割り当てて、そのページにプロブされるアドレスのコードを複写して、kprobe_tableのハッシュリストに登録する。
kprobes_all_disarmedは/sys/kernel/debug/kprobes/enabledを、kprobe_disabled()は kprobe->flagをチェックして、 __arm_kprobe()でブレイクコードをカーネルコード内に設定する。
optimize kprobeについては、以下の様に設定される。これはワークスレッドから呼ばれている。
以下のstatic struct kprobe kpにブレイクする位置(シンボル又はアドレスの指定およびオフセット)、ブレイクした時の処理、ブレイクしたコードを実行した後の処理、必要なら、上記の処理で例外が発生した時の処理を記述して、register_kprobe()をコールするだけである。
struct kprobe { /* location of the probe point */ kprobe_opcode_t *addr; /* Allow user to indicate symbol name of the probe point */ const char *symbol_name; /* Offset into the symbol */ unsigned int offset; /* Called before addr is executed. */ kprobe_pre_handler_t pre_handler; /* Called after addr is executed, unless... */ kprobe_post_handler_t post_handler; /* * ... called if executing addr causes a fault (eg. page fault). * Return 1 if it handled fault, otherwise kernel will see it. */ kprobe_fault_handler_t fault_handler; /* * ... called if breakpoint trap occurs in probe handler. * Return 1 if it handled break, otherwise kernel will see it. */ kprobe_break_handler_t break_handler; };以下はdo_forkのアドレスにブレイクを設定し、その時の各レジスタを表示したもの。
#include <linux/kernel.h> #include <linux/module.h> #include <linux/kprobes.h> MODULE_DESCRIPTION("kprobe test"); MODULE_AUTHOR("y.kitamura"); MODULE_LICENSE("GPL"); static void printk_regs(const char* msg, struct kprobe *p, struct pt_regs *regs); static int pre_kprobe(struct kprobe *p, struct pt_regs *regs) { printk_regs("pre kprobe", p, regs); return 0; } static void post_kprobe(struct kprobe *p, struct pt_regs *regs, unsigned long flags) { printk_regs("post kprobe", p, regs); } static void printk_regs(const char* msg, struct kprobe *p, struct pt_regs *regs) { printk(KERN_INFO "%s:" "p->addr = 0x%p, ip = %lx\n" " ax=0x%lx,bx=0x%lx\n" " cx=0x%lx,dx=0x%lx\n" " sp=0x%lx\n\n", msg, p->addr, regs->ip, regs->ax, regs->bx, regs->cx, regs->dx, regs->sp); } static struct kprobe kp = { .symbol_name = "do_fork", .pre_handler = pre_kprobe, .post_handler = post_kprobe, }; static int __init kprobe_init(void) { int ret; ret = register_kprobe(&kp); if (ret < 0) { return -1; } return 0; } static void __exit kprobe_exit(void) { unregister_kprobe(&kp); } module_init(kprobe_init) module_exit(kprobe_exit)実行結果
必要なら/sys/kernel/debug/kprobes/enabledを1に設定する。(fedora16ではデフォルトで設定されていた。)
[root@localhost sys]# cat /sys/kernel/debug/kprobes/enabled 1 [root@localhost lkm]# insmod kprobe_test.ko [root@localhost lkm]# dmesg : [70316.964916] pre kprobe:p->addr = 0xc0436270, ip = c0436271 [70316.964922] ax=0x1200011,bx=0xd930ffb4 [70316.964926] cx=0xd930ffb4,dx=0xbf857290 [70316.964929] sp=0xc040ae94 [70316.964932] [70316.964967] post kprobe:p->addr = 0xc0436270, ip = c0436271 [70316.964971] ax=0x1200011,bx=0xd930ffb4 [70316.964974] cx=0xd930ffb4,dx=0xbf857290 [70316.964977] sp=0xd930ffa0do_forkがコールされ、do_fork処理の先頭コード毎にブレイクする。ちなみに上記検証は、dmesgプロセスがフォークされた結果となる。たぶん。
ブレイク時の各レジスターを表示している(あんまし意味ないけど)が、その時チェックしたいカーネル内各種シンボル(タスク構造体等)を表示させたりする。
ここでのブレイクは、call do_forkでなく、そこから呼ばれたfo_forkその物にブレイクが設定される。たぶん先頭コードはpush bpではないかな(bpレジスターも表示させればよかったか?)。で、pre kprobe/post kprobeを見ると、ax,bx,cx,dxは同じだが、spが大きく異なる。pre kprobe時にスタックを消費。
pre kprobe/post kprobeのブレイクアドレスを見ると、p->addr = 0xc0436270。これはlkmで設定したアドレス(.symbol_name = "do_fork")。で、ip = c0436271となっている。kprobeの実装はブレイクコード0x33を設定する。この時int 3の割り込みが発生する。で、pre kprobeは割り込みが発生した後のスタックポインタ。post kprobeはこの割り込みから復帰した時の、スタックポインタと言う事でないのかな?
実装
kprobeは、ブレイクコードと相対ジャンプコードの2つの実装がある。後者はoptimize kprobeと称して、CONFIG_OPTPROBESのカーネル下で、/proc/sys/debug/kprobes-optimization=1の時に採用されみた。ブレイクコードは、インストラクションコードで0xcc。相対ジャンプコードのそれも0xe9で1バイト。しかし相対ジャンプコードはジャンプアドレスが付加して、そのサイズは5バイトとなる。kprobeはカーネルコード内のどこに設定されるか分からない。相対ジャンプコードに書き換え中に、そのコードが実行されることもありえる。その回避として、optimize kprobeでは直接書き換えないで、ワークスレッドを通して書き換えるようになっている。確かワークスレッドは排他的に実行され、そのコード以外は実行されないことが保障されているからか。(たぶん・・・)
とりあえず以下に、register_kprobe()の概略。
kprobe_addr()で、シンボルで指定されている場合を考慮して、オフセットを加算したプロブするアドレスを取得すし、そのkprobe構造体そのものが、登録されているかcheck_kprobe_rereg()でチェックする。ここでのダブルチェックは、プロブするアドレスでない。
まず、プロブアドレスの以下の正当性をチェック。
・カーネルのアドレス
・kprobe内のアドレスでない
・ftrace内のアドレスでない
・テーブルデータ部のアドレスでない
次にlkm内にプロブする場合。
・unloadされないため、try_module_getモジュールが参照カウンタをtry_module_get()でインクリメントする。
・モジュールの初期化関数内のプロブなら、モジュールステイタスがMODULE_STATE_COMINGでないこと。
get_kprobe()でプロブするアドレスが、すでに設定されていたなら、register_aggr_kprobe()で登録済みのプロブを新規プロブで更新してるっぽい。
arch_prepare_kprobe()からkprobの実態的処理となる。struct kprobe->ainsn.insnに executable pageを割り当てて、そのページにプロブされるアドレスのコードを複写して、kprobe_tableのハッシュリストに登録する。
kprobes_all_disarmedは/sys/kernel/debug/kprobes/enabledを、kprobe_disabled()は kprobe->flagをチェックして、 __arm_kprobe()でブレイクコードをカーネルコード内に設定する。
int __kprobes register_kprobe(struct kprobe *p) { int ret = 0; struct kprobe *old_p; struct module *probed_mod; kprobe_opcode_t *addr; addr = kprobe_addr(p); if (IS_ERR(addr)) return PTR_ERR(addr); p->addr = addr; ret = check_kprobe_rereg(p); if (ret) return ret; jump_label_lock(); preempt_disable(); if (!kernel_text_address((unsigned long) p->addr) || in_kprobes_functions((unsigned long) p->addr) || ftrace_text_reserved(p->addr, p->addr) || jump_label_text_reserved(p->addr, p->addr)) { ret = -EINVAL; goto cannot_probe; } p->flags &= KPROBE_FLAG_DISABLED; probed_mod = __module_text_address((unsigned long) p->addr); if (probed_mod) { ret = -ENOENT; if (unlikely(!try_module_get(probed_mod))) goto cannot_probe; if (within_module_init((unsigned long)p->addr, probed_mod) && probed_mod->state != MODULE_STATE_COMING) { module_put(probed_mod); goto cannot_probe; } } preempt_enable(); jump_label_unlock(); p->nmissed = 0; INIT_LIST_HEAD(&p->list); mutex_lock(&kprobe_mutex); jump_label_lock(); /* needed to call jump_label_text_reserved() */ get_online_cpus(); /* For avoiding text_mutex deadlock. */ mutex_lock(&text_mutex); old_p = get_kprobe(p->addr); if (old_p) { ret = register_aggr_kprobe(old_p, p); goto out; } ret = arch_prepare_kprobe(p); if (ret) goto out; INIT_HLIST_NODE(&p->hlist); hlist_add_head_rcu(&p->hlist, &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]); if (!kprobes_all_disarmed && !kprobe_disabled(p)) __arm_kprobe(p); try_to_optimize_kprobe(p); out: mutex_unlock(&text_mutex); put_online_cpus(); jump_label_unlock(); mutex_unlock(&kprobe_mutex); if (probed_mod) module_put(probed_mod); return ret; cannot_probe: preempt_enable(); jump_label_unlock(); return ret; } void __kprobes arch_arm_kprobe(struct kprobe *p) { text_poke(p->addr, ((unsigned char []){BREAKPOINT_INSTRUCTION}), 1); }int 3が発生すると設定されている割り込みアドレスにジャンプする。そこで割り込みが発生したアドレスから、ハッシュリストkprobe_tableを操作することで、kprobe構造体を取得し、該当するハンドラを呼び出すものと思われる。
optimize kprobeについては、以下の様に設定される。これはワークスレッドから呼ばれている。
static void __kprobes setup_optimize_kprobe(struct text_poke_param *tprm, u8 *insn_buf, struct optimized_kprobe *op) { s32 rel = (s32)((long)op->optinsn.insn - ((long)op->kp.addr + RELATIVEJUMP_SIZE)); /* Backup instructions which will be replaced by jump address */ memcpy(op->optinsn.copied_insn, op->kp.addr + INT3_SIZE, RELATIVE_ADDR_SIZE); insn_buf[0] = RELATIVEJUMP_OPCODE; *(s32 *)(&insn_buf[1]) = rel; tprm->addr = op->kp.addr; tprm->opcode = insn_buf; tprm->len = RELATIVEJUMP_SIZE; }