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_KPROBESとCONFIG_MAGIC_SYSRQフラグを使用可能にします。ビルドして新しいカーネルをブートします。これによって簡単なKprobesモジュールを書きさえすればprintkを挿入できるようになり、デバッグ情報を動的、かつ影響を与えずに収集できるようになります。
各プローブに対して、構造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;
}
|
登録の際にプローブを挿入したい場所の、カーネル・ルーチン・アドレスも指定する必要があります。カーネル・ルーチンのアドレスを取得するには下記のどれかのメソッドを使います。
-
System.mapファイルから直接アドレスを取得する
例えばdo_forkのアドレスを取得するには、コマンドラインで$grep do_fork /usr/src/linux/System.mapを実行します。 -
nmコマンドを使う
$nm vmlinuz |grep do_fork -
/proc/kallsymsファイルからアドレスを得る
$cat /proc/kallsyms |grep do_fork -
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 = 0x14をkallsyms_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キーのサポートのために既にコンパイルはしました。次でそれを使用可能にします。
$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
|
プローブ・イベント・ハンドラーはシステムのブレークポイント・インタラプト・ハンドラーに対する拡張として実行するため、システム機能にはほとんど、あるいは全く依存しません。従って最も困難な環境にも置くことができ、例えばインタラプト時やタスク時から、使用不可となったコンテキスト間スイッチや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が入手できます。もっと手早くしたければ、ハウ・ツー記事や技術サポートが製品毎に集められていますので、ご自由に入手して下さい。