kprobe


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

最終更新 2012/07/18 16:59:49 - north
(2012/07/18 16:59:49 作成)


検索

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