Kprobesによるカーネルのデバッグ

Linuxカーネルに動的にprintkを挿入する

printkを使ってLinux™カーネルからデバッグ情報を収集するのはよく知られた方法ですが、Kprobesを使うと、何度もリブートしたりカーネルをビルドし直したりせずに情報収集できるようになります。Kprobesを2.6カーネルと組み合わせることによって軽量で影響を与えない強力な機構ができ、printkを動的に挿入できるようになります。そのためカーネルのスタック・トレースやカーネルのデータ構造、レジスターなどといったデバッグ情報のログが、今までにないほど簡単になります。

Prasanna Panchamukhi, Developer, Linux Technology Center, IBM India Software Labs

Prasanna S. PanchamukhiはインドのバンガロールにあるIBM's Linux Technology Centerで開発者として働いています。現在はLinux用の様々なデバッグ・ツールの改善に関わる業務に従事しています。以前はファイバー・チャネルのデバイス・ドライバーやネットワーク・プロセッサー・アプリケーションの開発、Unixオペレーティング・システムの維持管理などに従事してきました。



2004年 8月 19日

Kprobesは単純で軽量な機構ですが、Kprobesを使うことによって実行中のLinuxカーネルにブレークポイントを設定できるようになります。Kprobesには一つのインターフェースがあり、影響を与えることなく、どのようなカーネル・ルーチンにも入り込んでインタラプト・ハンドラーから情報を収集することができます。Kprobesを使用すると、プロセッサーのレジスターやグローバルなデータ構造などのようなデバッグ情報を容易に収集することができます。またKprobesを使って、レジスター値やグローバルなデータ構造値を変更することさえできるのです。

Kprobesはこれを実現するために、実行中のカーネルの指定されたアドレスで動的にブレークポイント命令を書くことによってプローブを挿入します。プローブ対象の命令を実行するとブレークポイントとして終わります。Kprobesはブレークポイント・ハンドラーにフックし、デバッグ情報を収集します。またKprobesはプローブ対象の命令を1ステップごとに命令することさえできるのです。

インストール

Kprobesをインストールするには、Kprobesのホームページ(参考文献)から最新のパッチをダウンロードします。tarファイルにkprobes-2.6.8-rc1.tar.gzのような名前を付けます。パッチのtarを解凍し、Linuxカーネルにそのパッチを当てます。

$tar -xvzf kprobes-2.6.8-rc1.tar.gz
$cd /usr/src/linux-2.6.8-rc1
$patch -p1 < ../kprobes-2.6.8-rc1-base.patch

KprobesはSysRqキーを利用しています。これはDOS時代からの成果物ですが、Linuxで数多くの新しい使い方をされています(参考文献)。SysRqキーはScroll Lockキーの左側にあります。一般的にPrint Screenとも書かれています。Kprobes用にSysRqキーを使用可能にするには、kprobes-2.6.8-rc1-sysrq.patchのパッチを当てます。

$patch -p1 < ../kprobes-2.6.8-rc1-sysrq.patch

カーネルをmake xconfig/ make menuconfig/ make oldconfigで設定し、CONFIG_KPROBESCONFIG_MAGIC_SYSRQフラグを使用可能にします。ビルドして新しいカーネルをブートします。これによって簡単なKprobesモジュールを書きさえすればprintkを挿入できるようになり、デバッグ情報を動的、かつ影響を与えずに収集できるようになります。

Kprobesモジュールを書く

各プローブに対して、構造struct kprobe kp;を割り当てる必要があります(これに関する詳しい情報についてはinclude/linux/kprobes.hを見て下さい)。

リスト1. プリ・ハンドラー、ポスト・ハンドラー、そしてフォールト・ハンドラーを定義する
 /* pre_handler: this is called just before the probed instruction is
  *	executed.
  */
int handler_pre(struct kprobe *p, struct pt_regs *regs) {
	printk("pre_handler: p->addr=0x%p, eflags=0x%lx\n",p->addr,
		regs->eflags);
	return 0;
}
 /* post_handler: this is called after the probed instruction is executed
  * 	(provided no exception is generated).
  */
void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {
	printk("post_handler: p->addr=0x%p, eflags=0x%lx \n", p->addr,
		regs->eflags);
}
 /* fault_handler: this is called if an exception is generated for any
  *	instruction within the fault-handler, or when Kprobes
  *	single-steps the probed instruction.
  */
int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr) {
	printk("fault_handler:p->addr=0x%p, eflags=0x%lx\n", p->addr,
		regs->eflags);
	return 0;
}

カーネル・ルーチンのアドレスを取得する

登録の際にプローブを挿入したい場所の、カーネル・ルーチン・アドレスも指定する必要があります。カーネル・ルーチンのアドレスを取得するには下記のどれかのメソッドを使います。

  1. System.mapファイルから直接アドレスを取得する
    例えばdo_forkのアドレスを取得するには、コマンドラインで$grep do_fork /usr/src/linux/System.mapを実行します。
  2. nmコマンドを使う
    $nm vmlinuz |grep do_fork
  3. /proc/kallsymsファイルからアドレスを得る
    $cat /proc/kallsyms |grep do_fork
  4. kallsyms_lookup_name() ルーチンを使う
    このルーチンはkernel/kallsyms.cファイルで定義されます。これを使うためには、CONFIG_KALLSYMSを使用可能にしてカーネルをコンパイルする必要があります。kallsyms_lookup_name()はストリングとしてカーネル・ルーチン名を受け付け、そのカーネル・ルーチンのアドレスを戻します。例えば次のようにします。kallsyms_lookup_name("do_fork");

それからinit_moduleにプローブを登録します。

リスト2. プローブを登録する
 /* specify pre_handler address
  */
	kp.pre_handler=handler_pre;
 /* specify post_handler address
  */
	kp.post_handler=handler_post;
 /* specify fault_handler address
  */
	kp.fault_handler=handler_fault;
 /* specify the address/offset where you want to insert probe.
  * You can get the address using one of the methods described above.
  */
	kp.addr = (kprobe_opcode_t *) kallsyms_lookup_name("do_fork");
 /* check if the kallsyms_lookup_name() returned the correct value.
  */
	if (kp.add == NULL) {
		printk("kallsyms_lookup_name could not find address
					for the specified symbol name\n");
		return 1;
	}
 /*	or specify address directly.
  * $grep "do_fork" /usr/src/linux/System.map
  * or
  * $cat /proc/kallsyms |grep do_fork
  * or
  * $nm vmlinuz |grep do_fork
  */
	kp.addr = (kprobe_opcode_t *) 0xc01441d0;
 /* All set to register with Kprobes
  */
        register_kprobe(&kp);

プローブが一旦登録されると、どのようなシェル・コマンドを実行してもdo_forkコールが行われ、コンソールでprintkを見ることができます。あるいはdmesgを実行することでも見ることができます。終了したら登録取り消しすることを忘れないで下さい。

unregister_kprobe(&kp);

次の出力はkprobeのアドレスとeflagsレジスターの内容を表しています。

$tail -5 /var/log/messages

Jun 14 18:21:18 llm05 kernel: pre_handler: p->addr=0xc01441d0, eflags=0x202
Jun 14 18:21:18 llm05 kernel: post_handler: p->addr=0xc01441d0, eflags=0x196

オフセットを取得する

printkはルーチンの最初に挿入することもできれば、ファンクション中のどのオフセットにも挿入することができます(オフセットは命令の境界にある必要があります)。次のコードサンプルは、オフセットの計算方法を示しています。最初にオブジェクト・ファイルからマシン命令を逆アセンブルし、ファイルとして保存します。

$objdump -D /usr/src/linux/kernel/fork.o > fork.dis

そうすると次が得られます。

リスト3. 逆アセンブルしたフォーク
000022b0 <do_fork>:
    22b0:       55                      push   %ebp
    22b1:       89 e5                   mov    %esp,%ebp
    22b3:       57                      push   %edi
    22b4:       89 c7                   mov    %eax,%edi
    22b6:       56                      push   %esi
    22b7:       89 d6                   mov    %edx,%esi
    22b9:       53                      push   %ebx
    22ba:       83 ec 38                sub    $0x38,%esp
    22bd:       c7 45 d0 00 00 00 00    movl   $0x0,0xffffffd0(%ebp)
    22c4:       89 cb                   mov    %ecx,%ebx
    22c6:       89 44 24 04             mov    %eax,0x4(%esp)
    22ca:       c7 04 24 0a 00 00 00    movl   $0xa,(%esp)
    22d1:       e8 fc ff ff ff          call   22d2 <do_fork+0x22>
    22d6:       b8 00 e0 ff ff          mov    $0xffffe000,%eax
    22db:       21 e0                   and    %esp,%eax
    22dd:       8b 00                   mov    (%eax),%eax

オフセット0x22c4にプローブを挿入するには、このルーチンの最初からの相対オフセット0x22c4 - 0x22b0 = 0x14を計算し、次にそのオフセットをdo_forkのアドレスに加えます0xc01441d0 + 0x14。(do_forkのアドレスを確かめるには$cat /proc/kallsyms | grep do_forkを実行します。)

do_forkの相対オフセット0x22c4 - 0x22b0 = 0x14kallsyms_lookup_name("do_fork");の出力に加えることもできます。ですから0x14 + kallsyms_lookup_name("do_fork");となります。

カーネルのデータ構造をダンプする

システムで実行中している全ジョブの要素の一部を、データ構造をダンプするために強化したKprobe post_handlerを使ってダンプしてみましょう。

リスト4. データ構造をダンプするために修正したKprope post_handler
void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {
	struct task_struct *task;
	read_lock(&tasklist_lock);
	for_each_process(task) {
		printk("pid =%x task-info_ptr=%lx\n", task->pid,
			task->thread_info);
		printk("thread-info element status=%lx,flags=%lx, cpu=%lx\n",
			task->thread_info->status, task->thread_info->flags,
			task->thread_info->cpu);
	}
	read_unlock(&tasklist_lock);
}

このモジュールはdo_forkのオフセットに挿入する必要があります。

リスト5. pid 1508と1509に対するstruct thread_infoの出力
$tail -10 /var/log/messages
Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=0, cpu=1
Jun 22 18:14:25 llm05 kernel: pid =5e4 task-info_ptr=f5948000
Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=8, cpu=0
Jun 22 18:14:25 llm05 kernel: pid =5e5 task-info_ptr=f5eca000

魔法のSysRqキーを使用可能にする

SysRqキーのサポートのために既にコンパイルはしました。次でそれを使用可能にします。

$echo 1 > /proc/sys/kernel/sysrq

これでAlt+SysRq+Wを使うと、挿入された全カーネル・プローブをコンソールで、または/var/log/messagesで見ることができるようになります。

リスト6. do_forkに挿入されたKprobeを/var/log/messagesで表示する
Jun 23 10:24:48 linux-udp4749545uds kernel: SysRq : Show kprobes
Jun 23 10:24:48 linux-udp4749545uds kernel:
Jun 23 10:24:48 linux-udp4749545uds kernel: [<c011ea60>] do_fork+0x0/0x1de

Kprobesで効率の良いデバッグを

プローブ・イベント・ハンドラーはシステムのブレークポイント・インタラプト・ハンドラーに対する拡張として実行するため、システム機能にはほとんど、あるいは全く依存しません。従って最も困難な環境にも置くことができ、例えばインタラプト時やタスク時から、使用不可となったコンテキスト間スイッチやSMPが動作するコード・パスなどに至るまで、どこに置いてもシステム・パフォーマンスを低下させることがありません。

Kprobesを使う利点は数多くあります。カーネルを再ビルドしたりリブートしたりせずにprintkを挿入することができます。デバッグ用にプロセッサーのレジスターをログすることもできれば、修正することさえでき、しかもシステムに影響を与えることがありません。Linuxのカーネル・データ構造も同じようにログができ、また影響を与えずに修正することまでできます。KprobesでSMPシステムのレース条件までデバッグできるのです。ですからKprobesを使うことで、再ビルドやリブートにまつわるトラブルから解放されるのです。Kprobesでカーネルのデバッグが今までになく早く、容易にできることが実感できるでしょう。

参考文献

  • Kprobes home pageには、さらに詳しい情報や最近のニュース、README、それにダウンロード情報などがあります。 READMEはKprobesのインターフェースの詳細を説明しています。
  • Kpobesは完全なDynamic Probesパッチから開発されました。Dynamic ProbesはKernel Hooksを使うことで、取得が困難な診断情報を収集しています。
  • Kprobesのサポートはv.2.5.26でカーネルに採り入れられました。その発表と簡単な評価がSupport for kernel probes (Kernel Traffic, 2002年7月25日)にありますので見て下さい。Kprobesは2.5.73でout-of-line single-stepping(行内での単一ステップ・トレース)を追加しています。
  • KprobesはBIOSインタラプトSysRqキーを利用しています。これをMagic SysRqキーに組み込むことができ、スパイウェアを無効にしたり、構わずリブートしたり、メモリ情報を表示したり、プロセスを中止したり、その他にも様々なことができるようになります。デバッグには便利ですが、セキュリティの脅威をさらすことになるため、実稼働のマシンにはあまり適切ではありません
  • objdumpは一つまたはそれ以上のオブジェクト・ファイルの情報を表示します。詳しい情報についてはLinux 2.6のobjdump manページを見て下さい。
  • Captain's UniverseがHOWTO compile kernel modules for the kernel 2.6にドキュメントを掲示しています。
  • カーネルのModule Utilities for 2.6をダウンロードして下さい。これは最近のカーネル用にmodutilsを置き換えるものです。これはRusty Russell's many useful patchesの中の一つです。
  • developerWorksのLinuxゾーンにはLinux開発者のための資料が豊富に用意されています。
  • Developer BookstoreのLinuxセクションではLinux関連の書籍が値引きして購入できますのでご利用下さい。
  • Linux上で実行する、より抜きのdeveloperWorks Subscription製品の無料の試用版をダウンロードして下さい。developerWorksのSpeed-start your Linux appセクションからWebSphere Studio Site DeveloperやWebSphere SDK for Web services、WebSphere Application Server、DB2 Universal Database Personal Developers Edition、Tivoli Access ManagerそれにLotus Domino Serverが入手できます。もっと手早くしたければ、ハウ・ツー記事や技術サポートが製品毎に集められていますので、ご自由に入手して下さい。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux, Open source
ArticleID=228933
ArticleTitle=Kprobesによるカーネルのデバッグ
publish-date=08192004