kprobは、実行カーネル内を再コンパイルすることなく、任意のアドレスにブレイクを設定できる。そしてそのブレイクした時のデバック処理を記述することで、カーネルデバッグを行うというもの。
以下の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=0xd930ffa0
do_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;
}