Linux のデバッグ手法をマスターする

Linux のバグを検出し、鎮圧するための主な戦略

実行中のユーザー・スペース・プログラムは、いろいろな方法で監視することができます。プログラムをデバッガーにかけ、ステップ実行し、〔実行状況を示す〕文を表示させるという方法もありますし、プログラムを分析するためのツールを追加するという手もあります。本稿では、Linux 上で実行されるプログラムをデバッグするための手法をいろいろと紹介します。ここでは、セグメンテーション障害、メモリーのオーバーランとリーク、およびハングアップなどの問題をデバッグするための 4つのシナリオについて説明します。

Steve Best, JFS core team member, IBM

Steve Best は、テキサス州オースチンにある IBM Linux テクノロジー・センターに勤務しています。現在、Linux プロジェクトの Journaled File System (JFS) の開発に携わっています。Steve は、オペレーティング・システム開発のさまざまな側面に関ってきており、ファイル・システム、国際化対応、セキュリティーの分野を専門としてます。



2002年 8月 01日

本稿では、Linux プログラムをデバッグするための 4 通りのシナリオを紹介します。シナリオ 1 では、メモリー割り当ての問題を扱うサンプル・プログラムを 2つ使用します。MEMWATCH と Yet Another Malloc Debugger (YAMD) というツールを使ってデバッグを行います。シナリオ 2 では、Linux の strace ユーティリティーを使用します。このユーティリティーでは、システム・コールとシグナルをトレースすることで、プログラムの問題箇所を検出することができます。シナリオ 3 では、Linux カーネルの Oops 機能を使って、セグメンテーション障害を解決するとともに、同じ問題を GNU デバッガー (gdb) で解決するために、カーネル・ソース・レベル・デバッガー (kgdb) をセットアップする方法を紹介します。kgdb プログラムは、シリアル接続経由での Linux カーネル・リモート gdb です。シナリオ 4 では、Linux で利用できる magic key sequence を使って、ハングアップを起こしているコンポーネントについての情報を表示します。

一般的なデバッグ方針

プログラムにバグがあるというのは、コードのなかのどこかに、自分では正しいと思っていたものが実際には間違っていたということでしょう。バグを見つけ出す作業は、どこか間違っている箇所を発見するまで、自分で正しいと思っていることを確認する過程だと言えます。

自分では正しいと思っていることの例としては、以下のような類のことがあります。

  • ソース・コードのある箇所で、変数に何らかの値が割り当てられている。
  • ある構造体が、ある箇所で、すでに正しくセットアップされている。
  • ある if-then-else 文で、if 以下が実際に実行された個所である。
  • サブルーチンが呼び出される際、そのルーチンはパラメーターを正しく受けとっている。

バグの発見は、これらのことをすべて確認する作業からなります。あるサブルーチンが呼び出されるときに、ある変数は、ある値をとっているはずだと考えるのであれば、そのことを確認する必要があります。ある if 構文が実行されるものと考えるのであれば、そのことを確認する必要があります。通常、プログラマーは自分の想定していたことを確認していくうちに、ついには、その考えが間違っている箇所を発見することになります。その結果、バグの在処〔ありか〕がわかることになります。

デバッグ作業は、決して避けて通ることはできません。デバッグには、いろいろな方法があります。画面にメッセージを表示させるとか、デバッガーを使うとか、あるいはプログラムの実行を頭の中で想像し、経験的に原因を推測するといった方法もあります。

バグを修正するには、まず、その在処〔ありか〕を発見する必要があります。たとえば、セグメンテーション障害の場合、コード中の何行目がセグメンテーション障害の原因となっているのかを知る必要があります。何行目が問題なのかがわかれば、次に、そのメソッド中の変数の値、そのメソッドの呼び出し方、および、なぜエラーが発生したのかを突き止めます。デバッガーを使えば、これらすべての情報を簡単に調べることができます。デバッガーがない場合、他のツールを使う手もあります。(実稼働環境にデバッガーが用意されていない場合もあるでしょう。Linux カーネルには、デバッガーは組み込まれていません。)

メモリーとカーネルに関する便利なデバッグ・ツール

Linux でデバッグ・ツールを使ってユーザー・スペースやカーネルの問題を追跡するには、いろいろな方法があります。以下に列挙したツールや手法を使ってソース・コードをビルドし、デバッグします。

ユーザー・スペース用のツール:

  • メモリー・ツール: MEMWATCH、YAMD
  • strace
  • GNU デバッガー (gdb)
  • Magic key sequence

カーネル用のツール:

  • カーネル・ソース・レベル・デバッガー (kgdb)
  • 内蔵のカーネル・デバッガー (kdb)
  • Oops

本稿では、コードを目で見て検査することによっては発見の難しい問題を扱うことにします。この種の問題は、まれな状況でしか起こらないかもしれません。多くの場合、メモリー・エラーは、いくつかの状況が重なった場合にのみ起こります。また、プログラムを配備してからでないとメモリー関係のバグを発見できない場合もあります。

シナリオ 1: メモリー・デバッグ・ツール

C 言語は、Linux システムでの標準的なプログラミング言語として、動的メモリー割り当てを、さまざまな形で制御することができます。しかし、このように自由自在に制御できるということは、メモリー管理に重大な問題をもたらし得るということでもあり、そうした問題が、プログラムをクラッシュさせたり、時間の経過とともにプログラムの性能を低下させたりすることがあります。

よく起こるのは、メモリー・リーク (malloc() で獲得されたメモリーが、対応する free() 呼び出しで解放されないという問題) やバッファー・オーバーラン (たとえば、配列として割り当てられたメモリー領域を越えて書き込みを行うという問題) で、これらの問題は、検出し難い面があります。以下では、メモリー関係の問題を非常に簡単に検出、分離するためのデバッグ・ツールをいくつか取り上げたいと思います。


MEMWATCH

MEMWATCH は、Johan Lindh が作成した C 用のオープン・ソースのメモリー・エラー検出ツールで、ダウンロードが可能です (稿末の参考文献参照)。コードにヘッダー・ファイルを追加し、gcc のコマンド・ラインで MEMWATCH を定義するだけで、プログラム中のメモリー・リークやメモリーのデータ破壊 (corruptions) を追跡することができます。MEMWATCH は、ANSI C をサポートし、結果をログとして残します。二重解放、間違った解放、未解放メモリー、オーバーフロー、アンダーフローなどを検出することができます。

リスト 1. メモリー・サンプル (test1.c)
#include <stdlib.h>
#include <stdio.h>
#include "memwatch.h"

int main(void)
{
  char *ptr1;
  char *ptr2;

  ptr1 = malloc(512);
  ptr2 = malloc(512);

  ptr2 = ptr1;
  free(ptr2);
  free(ptr1);
}

リスト 1 のコードでは、512 バイトのメモリー・ブロックを 2つ割り当てた後、2個目のブロックへのポインターに 1個目のブロックへのポインターをセットしています。その結果、2個目のブロックのアドレスは失われてしまい、メモリー・リークが発生しています。

そこで、リスト 1 を memwatch.c といっしょにコンパイルしてみます。MAKE ファイルは以下のようなものになります。

test1
gcc -DMEMWATCH -DMW_STDIO test1.c memwatch
c -o test1

test1 のプログラムを実行すると、メモリー・リークの報告が作成されます。出力ファイル memwatch.log は、リスト 2 のようなものになります。

リスト 2. test1 の memwatch.log ファイル
MEMWATCH 2.67 Copyright (C) 1992-1999 Johan Lindh

...
double-free: <4> test1.c(15), 0x80517b4 was freed from test1.c(14)
...
unfreed: <2> test1.c(11), 512 bytes at 0x80519e4
{FE FE FE FE FE FE FE FE FE FE FE FE ..............}

Memory usage statistics (global):
  N)umber of allocations made: 	2
  L)argest memory usage : 	1024
  T)otal of all alloc() calls: 	1024
  U)nfreed bytes totals : 	512

MEMWATCH は、問題のある具体的な行を示しています。解放済みのポインターを解放すると、MEMWATCH は、そのことを指摘します。同様に、未解放のメモリーについても指摘します。ログの最後の部分には、メモリー・リークの量、メモリーの使用量、割り当ての総量など、統計が示されます。


YAMD

Nate Eldredge 作の YAMD パッケージは、C および C++ での動的なメモリー割り当てに関係する問題を検出します。本稿執筆時点での YAMD の最新バージョンは 0.32 で、yamd-0.32.tar.gz がダウンロード可能です (参考文献参照)。YAMD を使用するには、make コマンドを実行してプログラムをビルドし、さらに make install コマンドを実行して、プログラムをインストールして、ツールのセットアップを行います。

YAMD をダウンロードしたら、test1.c で YAMD を試してみてください。#include memwatch.h を削除し、MAKE ファイルも、以下のように、少し変更します。

test1 を YAMD といっしょにコンパイルする
gcc -g test1.c -o test1

リスト 3 は、test1 に対する YAMD の出力結果を示したものです。

リスト 3. test1 に対する YAMD の出力
YAMD version 0.32
Executable: /usr/src/test/yamd-0.32/test1
...
INFO: Normal allocation of this block
Address 0x40025e00, size 512
...
INFO: Normal allocation of this block
Address 0x40028e00, size 512
...
INFO: Normal deallocation of this block
Address 0x40025e00, size 512
...
ERROR: Multiple freeing At
free of pointer already freed
Address 0x40025e00, size 512
...
WARNING: Memory leak
Address 0x40028e00, size 512
WARNING: Total memory leaks:
1 unfreed allocations totaling 512 bytes

*** Finished at Tue ... 10:07:15 2002
Allocated a grand total of 1024 bytes 2 allocations
Average of 512 bytes per allocation
Max bytes allocated at one time: 1024
24 K alloced internally / 12 K mapped now / 8 K max
Virtual program size is 1416 K
End.

YAMD は、メモリーが解放済みであること、およびメモリー・リークがあることを示しています。次に、リスト 4 の別のサンプル・プログラムで YAMD を試してみることにします。

リスト 4. メモリー関係のコード (test2.c)
#include <stdlib.h>
#include <stdio.h>

int main(void)
{
  char *ptr1;
  char *ptr2;
  char *chptr;
  int i = 1;
  ptr1 = malloc(512);
  ptr2 = malloc(512);
  chptr = (char *)malloc(512);
  for (i; i <= 512; i++) {
    chptr[i] = 'S';
  }	
  ptr2 = ptr1;
  free(ptr2);
  free(ptr1);
  free(chptr);
}

次のコマンドで YAMD を始動することができます。

./run-yamd /usr/src/test/test2/test2

リスト 5 は、サンプル・プログラム test2 に対して YAMD を使用したときの出力結果を示しています。YAMD は、for ループで境界を越えてのアクセスがあることを示しています。

リスト 5. test2 に対する YAMD の出力
Running /usr/src/test/test2/test2
Temp output to /tmp/yamd-out.1243
*********
./run-yamd: line 101: 1248 Segmentation fault (core dumped)
YAMD version 0.32
Starting run: /usr/src/test/test2/test2
Executable: /usr/src/test/test2/test2
Virtual program size is 1380 K
...
INFO: Normal allocation of this block
Address 0x40025e00, size 512
...
INFO: Normal allocation of this block
Address 0x40028e00, size 512
...
INFO: Normal allocation of this block
Address 0x4002be00, size 512
ERROR: Crash
...
Tried to write address 0x4002c000
Seems to be part of this block:
Address 0x4002be00, size 512
...
Address in question is at offset 512 (out of bounds)
Will dump core after checking heap.
Done.

MEMWATCH と YAMD は、いずれも、便利なデバッグ・ツールですが、それぞれ別々の使い方をする必要があります。MEMWATCH の場合、インクルード・ファイル memwatch.h を付加し、コンパイル時のフラグを 2つオンにする必要があります。YAMD は、リンク・コマンドに -g オプションを指定するだけで済みます。


Electric Fence

Electric Fence のパッケージは、Linux のほとんどのディストリビューションに含まれていますが、ダウンロードすることもできます。Electric Fence は、Bruce Perens 作の malloc() デバッグ用ライブラリーです。Electric Fence は、プログラムで割り当てたメモリーの直後にプロテクト・メモリーを割り当てます。保護柵 (fencepost) エラー (配列境界を越えてのアクセス) が検出されると、そのプログラムは、保護エラーとして、即座に終了させられることになります。Electric Fence と gdb を組み合わせて利用すると、プロテクト・メモリーにアクセスしようとした行を正確に検出することができます。Electric Fence は、メモリー・リークを検出することもできます。


シナリオ 2: strace の利用

Strace コマンドは、ユーザー・スペース・プログラムが発行したすべてのシステム・コールを表示してくれる強力なツールです。Strace は、システム・コールへの引数と戻り値をシンボリック形式で表示します。Strace は、カーネルから情報を受け取りますが、カーネルを特別な方法でビルドしなければならないわけではありません。トレース情報は、アプリケーション開発者にとってもカーネル開発者にとっても有益な情報として利用することができます。リスト 6 は、パーティションのフォーマットに失敗している例で、このリストは、make file system (mkfs) の呼び出しに対する strace の最初の部分を示したものです。Strace は、どの呼び出しが問題を起こしているのかを示しています。

リスト 6. mkfs に対する strace の最初の部分
execve("/sbin/mkfs.jfs", ["mkfs.jfs", "-f", "/dev/test1"], &
 ...
 open("/dev/test1", O_RDWR|O_LARGEFILE) = 4
 stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0
 ioctl(4, 0x40041271, 0xbfffe128) = -1 EINVAL (Invalid argument)
 write(2, "mkfs.jfs: warning - cannot setb" ..., 98mkfs.jfs: warning -
 cannot set blocksize on block device /dev/test1: Invalid argument )
  = 98
 stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0
 open("/dev/test1", O_RDONLY|O_LARGEFILE) = 5
 ioctl(5, 0x80041272, 0xbfffe124) = -1 EINVAL (Invalid argument)
 write(2, "mkfs.jfs: can\'t determine device"..., ..._exit(1)
  = ?

リスト 6 は、パーティションをフォーマットするための mkfs プログラムが、ioctl 呼び出しでエラーになったことを示しています。ioctl BLKGETSIZE64 がエラーを起こしています。(BLKGET-SIZE64 は、ioctl を呼び出しているソース・コードの中で定義されています。) BLKGETSIZE64 ioctl は、Linux のすべてのデバイスに追加されつつありますが、上の例では、論理ボリューム・マネージャーが、これをまだサポートしていないというわけです。したがって、mkfs のコードは、BLKGETSIZE64 ioctl 呼び出しがエラーになった場合、代わりに、古い ioctl 呼び出しを呼び出すようにします。それによって、mkfs は、この論理ボリューム・マネージャーを利用できることになります。


シナリオ 3: gdb および Oops の利用

Free Software Foundation 作のデバッガーである gdb プログラムを、コマンド・ラインまたは Data Display Debugger (DDD) などのグラフィカル・ツールから実行することで、エラーを探り出すこともできます。gdb は、ユーザー・スペース・プログラムのデバッグにも Linux カーネルのデバッグにも使用することができます。ここでは、gdb をコマンド・ラインから利用する方法のみを紹介します。

<gdb プログラム名> というコマンドで gdb を始動します。すると、gdb は、その実行ファイルのシンボルをロードした後、入力プロンプトを表示して、デバッガーを使用できる状態にします。gdb でプロセスを表示させるには、以下の 3 通りの方法があります。

  • attach コマンドを使って、すでに実行中のプロセスの表示を開始する。 attach は、そのプロセスを停止します。
  • run コマンドを使ってプログラムを実行し、そのプログラムを最初からデバッグする。
  • 既存のコア・ファイルで、プロセスが終了したときのそのプロセスの状態を調べる。コア・ファイルを参照するには、以下のようにして gdb を始動します。

    gdb programname corefilename

    コア・ファイルを使ってデバッグを行うには、コア・ファイルの他に、プログラムの実行ファイルとソース・ファイルが必要です。コア・ファイルを添えて gdb を始動するには、-c オプションを指定します。

    gdb -c core programname

    gdb は、コード中の何行目がコア・ダンプを起こさせたのかを表示します。

プログラムを実行する場合、あるいは、すでに実行中のプログラムに attach を行う場合、先にソース・コード中のバグの潜んでいそうな部分を表示させ、ブレークポイントを設定しておいてから、プログラムのデバッグを開始します。gdb には、幅広いオンライン・ヘルプおよび詳細なチュートリアルが用意されており、help コマンドを使って表示させることができます。


kgdb

kgdb プログラム (gdb 経由のリモート・ホスト Linux カーネル・デバッガー) は、gdb を使って Linux のカーネルをデバッグする仕掛けを提供します。kgdb プログラムはカーネルの拡張で、拡張されたカーネルが稼動しているマシンにリモートホストで実行している gdb から接続出来るようにします。kgdb でカーネルを拡張することで、カーネルにブレークをかけたり、ブレーク・ポイントを設定したり、データを確認したりすることができます (アプリケーション・プログラムに対して gdb を使用するのと同様のことができます)。このパッチの主な機能の 1つは、gdb を実行しているリモート・ホストが、(デバッグすべきカーネルを実行している) ターゲット・マシンに、ブート処理中に接続することです。このため、できるだけ早い段階でデバッグを開始できます。ちなみに、このパッチは Linux のカーネルに機能を追加しますので、gdb を使って Linux のカーネルをデバッグすることもできます。

kgdb を使うにはマシンが 2 台必要です。1 台は開発用のマシン、もう 1 台はテスト・マシンです。それぞれのシリアル・ポートを使って、シリアル・ケーブル (ヌル・モデム・ケーブル) で 2 台のマシンを接続します。デバッグしたいカーネルはテスト・マシンで実行し、gdb は開発マシンで実行します。gdb は、シリアル・ケーブルを介して、デバッグ対象のカーネルと通信を行います。

kgdb のデバッグ環境は、以下の手順でセットアップします。

  1. Linux カーネルのバージョンに合わせて、適当なパッチをダウンロードする。
  2. そのコンポーネントをカーネルに組み込む形でビルドする。 kgdb を使う場合、これが最も簡単な方法です。(カーネルのほとんどのコンポーネントは、ビルドの方法に 2 通りがあります。モジュールとしてビルドするか、直接カーネルにビルドするかです。たとえば、Journaled File System (JFS) は、モジュールとしてビルドすることもできれば、カーネルに直接ビルドすることもできます。gdb のパッチを使う場合、JFS を直接カーネルにビルドすることも可能です。)
  3. カーネルにパッチを当て、カーネルを再ビルドする。
  4. .gdbinit というファイルを作成し、それをカーネル・ソースのサブディレクトリー (/usr/src/linux) に入れる。.gdbinit ファイルには、以下の 4 行を書き込んでおきます。
    • set remotebaud 115200
    • symbol-file vmlinux
    • target remote /dev/ttyS0
    • set output-radix 16
  5. カーネルのブート時にどのカーネルを使うかを選択するためのブート・ロードである lilo に、append=gdb の行を追加する。
    • image=/boot/bzImage-2.4.17
    • label=gdb2417
    • read-only
    • root=/dev/sda8
    • append="gdb gdbttyS=1 gdb-baud=115200 nmi_watchdog=0"

リスト 7 は、開発マシン上にビルドしたカーネルやモジュールを、テスト・マシンに引き出してくるためのスクリプトの例です。以下の項目は、変更する必要があります。

  • best@sfb: ユーザー ID とマシン名。
  • /usr/src/linux-2.4.17: カーネル・ソース・ツリーのディレクトリー。
  • bzImage-2.4.17: テスト・マシンでブートしたいカーネルの名前。
  • rcprsync: カーネルがビルドされたマシンで、実行できるようにする必要がある。
リスト 7. カーネルと各種モジュールをテスト・マシンに引き出してくるためのスクリプト
set -x
rcp best@sfb: /usr/src/linux-2.4.17/arch/i386/boot/bzImage /boot/bzImage-2.4.17
rcp best@sfb:/usr/src/linux-2.4.17/System.map /boot/System.map-2.4.17
rm -rf /lib/modules/2.4.17
rsync -a best@sfb:/lib/modules/2.4.17 /lib/modules
chown -R root /lib/modules/2.4.17
lilo

これで、開発マシンのディレクトリーをカーネル・ソース・ツリーの開始ディレクトリーに変更すれば、gdb プログラムを始動できるようになりました。上の例では、カーネル・ソース・ツリーは /usr/src/linux-2.4.17 にあります。gdb とタイプしてプログラムを始動します。

すべて問題なく進めば、テスト・マシンはブート処理で停止するはずです。そこで、gdb コマンドの cont を入力して、ブート処理を続行します。このとき、ヌル・モデム・ケーブルを間違ったシリアル・ポートに接続してしまうという問題がよく起こります。gdb が始動しない場合は、ポートを 2 番目のシリアルに切り換えると、gdb が始動するようになるはずです。


kgdb を使ってカーネルの問題をデバッグする

リスト 8 は、109 行目でヌル・ポインター例外を起こさせ、セグメンテーション障害を発生させるために、jfs_mount.c ファイルのソースに手を加えたコードです。

リスト 8. jfs_mount.c に手を加えたコード
int jfs_mount(struct super_block *sb)
{
...
int ptr;                        /* line 1 added */
jFYI(1, ("\nMount JFS\n"));
/ *
* read/validate superblock
* (initialize mount inode from the superblock)
* /
if ((rc = chkSuper(sb))) {
                goto errout20;
        }
108     ptr=0;                  /* line 2 added */
109     printk("%d\n",*ptr);    /* line 3 added */

リスト 9 は、ファイル・システムに対してマウント・コマンドを発行したときの gdb の例外を示したものです。kgdb で利用できるコマンドには、データ構造や変数の値を表示したり、システム中のすべてのタスクの状態や、タスクがどこでスリープし、どこで CPU を消費しているのかを確認するためのコマンドなど、いろいろなものがあります。リスト 9 には、上の問題に対してバック・トレースで得られた情報が示されています。where コマンドは、バック・トレースを行うためのもので、コード中の停止位置に達するまでに実行された一連の呼び出しを教えてくれます。

リスト 9. gdb の例外とバック・トレース
mount -t jfs /dev/sdb /jfs

Program received signal SIGSEGV, Segmentation fault.
jfs_mount (sb=0xf78a3800) at jfs_mount.c:109
109             printk("%d\n",*ptr);
(gdb)where
#0 jfs_mount (sb=0xf78a3800) at jfs_mount.c:109
#1 0xc01a0dbb in jfs_read_super ... at super.c:280
#2 0xc0149ff5 in get_sb_bdev ... at super.c:620
#3 0xc014a89f in do_kern_mount ... at super.c:849
#4 0xc0160e66 in do_add_mount ... at namespace.c:569
#5 0xc01610f4 in do_mount ... at namespace.c:683
#6 0xc01611ea in sys_mount ... at namespace.c:716
#7 0xc01074a7 in system_call () at af_packet.c:1891
#8 0x0 in ?? ()
(gdb)

今度は、デバッガーのセットアップなしで、同じ JFS のセグメンテーション障害の問題を分析することにします。kgdb 拡張していないカーネル環境でリスト 8 のコードを実行したときにカーネルが生成する Oops を使用します。


Oops 分析

Oops (つまり、パニック) メッセージには、CPU レジスターの内容など、システム障害の詳細が示されます。従来 Linux でシステム・クラッシュをデバッグするには、クラッシュ時にシステム・コンソールに表示される Oops メッセージの詳細を分析するという手法がとられてきました。そうした詳細が得られれば、そのメッセージを ksymoops ユーティリティーに渡してやることができます。そうすれば ksymoops がコードを命令に変換し、スタックの値をカーネルのシンボルに対応付けてくれます。多くの場合、これで、障害の原因を調べるのに充分な情報が得られます。ちなみに、Oops メッセージにコア・ファイルは含まれません。

いま、システムが Oops メッセージを 1個作成したとします。みなさんがそのコードの開発者なら、問題を解決したいと考え、その Oops メッセージの原因を突き止めたいでしょうし、開発した人が別の人なら、その Oops メッセージを表示したコードを開発した人に、その問題に関するほとんどの情報を提供し、折よく問題を解決してもらいたいと思うでしょう。Oops メッセージは、方程式の一部ではありますが、ksymoops プログラムにかけなければ、それは役には立ちません。以下の図は、Oops メッセージのフォーマット処理の過程を示したものです。

図 1. Oops メッセージのフォーマット処理
Oops メッセージのフォーマット処理

ksymoops は、いろいろな情報を必要とします。Oops メッセージ出力、実行中のカーネルの System.map ファイル、/proc/ksyms、vmlinux、/proc/modules といった情報です。カーネル・ソースの /usr/src/linux/Documentation/oops-tracing.txt あるいは ksymoops の man ページには、ksymoops の使い方についての詳細な説明があります。Ksymoops は、コード部を逆アセンブルし、エラーを起こしている命令を指示するとともに、そのコードがどのように呼び出されたのかを示すトレース・セクションを表示します。

まずは、Oops メッセージをファイルに取り込み、ksymoops ユーティリティーで実行できるようにします。リスト 10 は、JFS のマウント・コードにリスト 8 の 3 行を追加したことで問題を起こすようになっている JFS ファイル・システムに対してマウントを行うことで作成された Oops を示しています。

リスト 10. ksymoops で処理された Oops
ksymoops 2.4.0 on i686 2.4.17. Options used
... 15:59:37 sfb1 kernel: Unable to handle kernel NULL pointer dereference at
virtual address 0000000
... 15:59:37 sfb1 kernel: c01588fc
... 15:59:37 sfb1 kernel: *pde = 0000000
... 15:59:37 sfb1 kernel: Oops: 0000
... 15:59:37 sfb1 kernel: CPU:    0
... 15:59:37 sfb1 kernel: EIP:    0010:[jfs_mount+60/704]

... 15:59:37 sfb1 kernel: Call Trace: [jfs_read_super+287/688] 
[get_sb_bdev+563/736] [do_kern_mount+189/336] [do_add_mount+35/208]
[do_page_fault+0/1264]
... 15:59:37 sfb1 kernel: Call Trace: [<c0155d4f>]...
... 15:59:37 sfb1 kernel: [<c0106e04 ...
... 15:59:37 sfb1 kernel: Code: 8b 2d 00 00 00 00 55 ...

>>EIP; c01588fc <jfs_mount+3c/2c0> <=====
...
Trace; c0106cf3 <system_call+33/40>
Code; c01588fc <jfs_mount+3c/2c0>
00000000 <_EIP>:
Code; c01588fc <jfs_mount+3c/2c0>  <=====
   0: 8b 2d 00 00 00 00 	mov 	0x0,%ebp    <=====
Code; c0158902 <jfs_mount+42/2c0>
   6:  55 			push 	%ebp

次に、jfs_mount で問題を起こしている行がどこなのかを調べる必要があります。Oops メッセージから、オフセット 3c の命令が問題を起こしていることがわかります。1つのやり方として、jfs_mount.o ファイルを objdump ユーティリティーにかけ、オフセット 3c を調べることで、このことがわかります。Objdump は、モジュール関数を逆アセンブルし、C のソース・コードからどんなアセンブラー命令が作成されたのかを調べるために使用されます。リスト 11 は、objdump の出力を示したもので、そこで jfs_mount の C のコードを調べれば、(3c における)ヌルは 109 行目で発生していることがわかります。オフセット 3c は、そこが問題の原因であると Oops メッセージが指示している箇所であり、重要な箇所です。

リスト 11. jfs_mount のアセンブラー・リスト
109     printk("%d\n",*ptr);

objdump jfs_mount.o

jfs_mount.o:    file format elf32-i386

Disassembly of section .text:

00000000 <jfs_mount>:
   0:55                         push %ebp
  ...
  2c:   e8 cf 03 00 00     call    400 <chkSuper>
  31:   89 c3                   mov     %eax,%ebx
  33:   58                      pop     %eax
  34:   85 db                   test    %ebx,%ebx
  36:   0f 85 55 02 00 00 jne   291 <jfs_mount+0x291>
  3c:   8b 2d 00 00 00 00 mov   0x0,%ebp << problem line above
  42:   55                      push    %ebp

kdb

Linux カーネル・デバッガー (kdb) は、Linux カーネルに対するパッチであり、システムが動作しているときのカーネルのメモリーやデータ構造を確認するのに利用できます。kdb は、マシンを 2 台必要としませんが、kgdb のようなソース・レベルのデバッグを行うことはできません。kdb では、システムの基本的なデータ構造の名前かアドレスを指定することで、そうしたデータ構造をフォーマットしたり表示したりするためのコマンドを追加することができます。現在のコマンド・セットでは、カーネルの動作を、以下のような方法で制御することができます。

  • プロセッサーのシングル・ステップ動作
  • 指定した命令を実行する時点での停止
  • 指定した仮想メモリー・アドレスのアクセス (または書き換え) の時点での停止
  • 入出力アドレス空間のレジスターへのアクセス時点での停止
  • 現在アクティブなタスクおよび他のすべてのタスク (プロセス ID 単位) に対するスタックのバック・トレース
  • 命令の逆アセンブル

シナリオ 4: magic key sequence を使ってバック・トレースを行う

メモリー・オーバーランの検出

何千回か呼び出しを行うと割り当てオーバーランが起こるというのは、嫌なバグです。

私のチームも、ある奇妙なメモリーのデータ化けの原因を究明するのに、すごく長い時間を費やしたことがありました。そのアプリケーションは、われわれの開発ワークステーションでは問題なく動いていたのですが、新しい製品版のワークステーションで malloc() を 2 百万回呼び出すとエラーが発生しました。原因は、呼び出しを百万回行ったときのオーバーランの回帰でした。新しいシステムでは、malloc() 用の予約領域の配置が異なり、エラーを起こすメモリーが異なる場所に位置しており、オーバーランを起こしたときに違うところを破壊することで、問題が発生しているのでした。

われわれは、この問題を解決するために、デバッガーを使ったり、ソース・コードにトレースをかけたするなど、いろいろな手法を用いました。私の経歴の中では、この頃から、メモリー・デバッグ・ツールを使ってこの種の問題を速くかつ効率よく解決することを考えるようになりました。新しいプロジェクトを開始するときに私が最初に行うことの 1つは、MEMWATCH と YAMD の両方を実行してみて、メモリー管理の問題を検出できるかどうかを確かめることです。

メモリー・リークは、アプリケーションによく起こる問題ですが、本稿で紹介したツールを使えば、そうした問題も解決することができます。

キーボードの機能は生きているが、Linux はハングアップしているという場合は、以下の方法が、ハングアップの解決に役立ちます。以下の手順に従うことで、magic key sequence を利用して、現在実行中のプロセスの他すべてのプロセスのバック・トレースを表示することができます。

  1. 実行中のカーネルは、CONFIG_MAGIC_SYS-REQ を有効にしてビルドしたものでなければなりません。また、テキスト・モードにしておく必要もあります。CLTR+ALT+F1 でテキスト・モードにすることができ、CLTR+ALT+F7 で X Windows に戻すことができます。
  2. テキスト・モードで、<ALT+ScrollLock> を押し、さらに <Ctrl+ScrollLock> を押します。すると、magic keystrokes は、現在実行中のプロセス他すべてのプロセスのそれぞれのスタック・トレースを表示します。
  3. /var/log/messages の内容を確認します。すべてが正しくセットアップされていれば、システムがシンボリックなカーネル・アドレスに変換してくれるはずです。バック・トレースが /var/log/messages ファイルに書き出されているはずです。

結論

Linux でプログラムをデバッグする場合、いろいろなツールが利用できます。本稿で紹介したツールは、さまざまなコーディングの問題を解決するのに役立てることができます。メモリー・リーク、オーバーランなどのの発生箇所を検出するツールを利用すれば、メモリー管理の問題を解決することができます。そのようなツールとして、MEMWATCH と YAMD は役に立ちます。

カーネルにパッチを当てて gdb で扱えるようにすることで、私が開発に携わっている Linux のファイル・システムのいろいろな問題を解決することができました。また、strace ユーティリティーは、ファイル・システムがどのシステム・コールでエラーを起こすのかを調べるのに役に立ちました。みなさんも、Linux 内のバグ潰しを行うときには、これらのツールを試してみてはいかがでしょうか。

参考文献

コメント

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
ArticleID=228111
ArticleTitle=Linux のデバッグ手法をマスターする
publish-date=08012002