日経Linux 2003年8月号掲載
※掲載記事の内容とは若干異なります。
これまで Linux でできなかったこと
様々なプログラムを開発する際、どうしても問題となるのがバグである。
プログラムの開発規模が大きくなればなるほど、また、開発者の人数が多くなればなるほど、プログラム中にバグの混入する可能性は高くなる。
何らかのソフトウェア開発を行う際には、工程として、設計、開発、そして品質を確保するための評価。という工程があるはずである。
この評価工程である程度のバグは取り除かれるが、中には取りこぼされたまま世の中にでてくるバグも存在する。
開発者としては、この取りこぼされたバグ(障害)ほど厄介なものはない。評価工程で発見できなかったため、その原因も非常に根深いものが多く。再現性に乏しかったり、果てはユーザ環境に依存するものまである。
再現性の高い障害ならともかく、ほとんどの場合、開発者はこのようなバグの原因を究明するために、様々な情報を収集しなければならない。coreファイルには、その中でも非常に有効な情報が含まれている。
coreファイルはプログラム自身がハンドリングしていないシグナルを受け取った際に生成される。代表的なものとして、SIGSEGV※1が挙げられる。プロセスはこのようなシグナルを受信すると、許可される※2範囲でのcoreファイル生成を試みる。coreファイル生成後、プロセスは異常終了することとなる。
ただし、障害は必ずしもcoreファイルを生成するような問題であるとは限らない。メモリリークやストール、期待した出力が得られないといった、プロセス的には生きた状態である場合もある。
このような障害が発生した時、稼働システムがLinuxであれば、システムやアプリケーションのログファイル等の情報の収集、strace、ltrace、あるいはgdbのプロセスアタッチングによるトレース収集等を行うことになる。
たしかにこれまでのLinuxシステムにおいても、gdbによるプロセスアタッチによるスタックトレース取得は可能であった。しかし、ユーザ先で障害が発生したような場合、ユーザにgdbの操作を強いることは難しく、かと言って技術者が現地に到着するまで、システムを異常状態にしておく時間も許容されない場合が多い。
Linux以外の他のプラットホームでは、このようなシチュエーションで有効なコマンドとして、gcoreというコマンドが提供されている。
gcoreは動作中プロセスにアタッチし、そのプロセスのメモリイメージをcoreファイルとして生成する。coreファイル生成後、gcoreはプロセスからデタッチし、プロセスへの処理再開を促す。
つまり、プロセスはgcoreによりアタッチされている間はその処理を中断させられるが、coreファイルを生成し終えると何事も無かったかのように処理を継続し続けることが可能となるわけである。
gcoreによるcoreファイル生成は実にシンプルである。調査者は、
$ ps | grep mule
408 p0 T 0:15.84 mule -nw (mule-19.34)
$ gcore `which mule` 408
$ file core.408
core.408: ELF 32-bit LSB core file of 'On?' (signal 4477762),
Intel 80386, version 1 (FreeBSD), from 'mule-19.34'
|
のようにgcoreに対して実行中プロセスのプログラムファイル名とpidを指定することによりcoreファイルを得ることができる。ただし、gcoreによりcoreファイルを生成が許されるのは、スーパーユーザーあるいはプロセスのオーナー※3である必要があることに注意して欲しい。
※1 |
Segmentation Violation が発生した時にカーネルからプロセスへ送信されるシグナル。0番地アドレスの参照の際等に発生する。 |
※2 |
プログラムを起動したシェルのリソース値(coredumpsize)設定やプログラム自身によるリソース値の制御による抑止がないこと。 |
※3 |
プログラムを実行したアカウント。 |
GDB での動的 core ファイル生成
前述した通り、gcoreの提供されていないLinuxシステムでは、これまで動的にcoreファイルを生成することはできなかった。
たしかに、gcoreを必要とするような人間は限られており、一般的に恩恵を受ける人は少なかったと思われる。しかしながら、Linux上で何らかのソフトウェア開発を行っているコアな技術者にとっては、ニーズが高かった機能ではないだろうか※4。
かねてから、gdbならばgcoreの様な動作をさせることが可能なのではないだろうかと思っていたが、遂にgdb-5.2.1からこの機能が実装された。
gdbのgenerate-core-fileコマンドがこれを補ってくれる。generate-core-fileの使用法については
(gdb) help generate-core-file
Save a core file with the current state of the debugged process.
Argument is optional filename. Default filename is 'core.<process_id>'.
|
を参照するとよいだろう。
generate-core-fileコマンドを利用したgdbによる動的coreファイル生成は以下のステップで行われる。
- 対象プロセスのプロセスIDを調べる。
$ ps aux | grep コマンド名
- gdbにて、対象プロセスにアタッチ
する。
$ gdb コマンドファイル プロセスID
- (gdb)プロンプト上で、generate-core-file
コマンドを実行する。
(gdb) generate-core-file
- (gdb)プロンプト上で、detachコマ
ンドを実行し、プロセスからデタッ
チする。
(gdb) detach
- gdbを終了する。
(gdb) quit
上記一連の手順により、容易にcoreファイルを得ることができる。
※4 |
インターネット上を検索してみれば、Linux上でgcoreを必要としている技術者がいるということがお分かりいただけると思う。 |
gcore.sh の使用方法
今回、MIRACLE LINUXで、gdbのバージョンをgenerate-core-fileのサポートされている5.2.1へアップグレードした。
また、ユーザ先でも容易にcoreを採取してもらえるよう、generate-core-fileを自動で発行するシェルスクリプトgcore.shをパッケージ内に同梱しておいた。gcore.shは前述した手順の1〜5をすべて行う。
gcore.shは/usr/binにインストールされ、そのオプションは、他プラットホームでのgcoreとほぼ同様の引数を渡せる※5よう記述されている。
表はgcore.shへの引数とその意味である。
gcore.shのオプション
引数 | 意味 |
-c core |
生成するcoreファイルのファイル名をcoreに指定する。省略時は、gcore.shを実行したカレントディレクトリ配下に、core.プロセスIDという名前のファイルが生成される。省略可。 |
executable |
対象プロセスのプログラムファイル名を指定する。省略可。 |
pid |
対象プロセスのプロセスIDを指定する。必須。 |
-h: |
ヘルプメッセージの出力。 |
実際には、前述したgcoreの実行と同様にgcore.shへ対象プロセスのpidを指定するだけでよい。
$ ps aux | grep xemacs
3983 pts/2 00:00:00 xemacs
$ gcore.sh 3983
Saved corefile core.3983
$ file core.3983
core.3983: ELF 32-bit LSB core file Intel 80386, version 1, from 'xemacs-21.1.14
|
coreファイル生成時に注意すべき点は、ファイルの出力先である。
coreファイルは、プロセスのメモリイメージをディスク上へそのまま出力するため、プロセスの占有しているメモリサイズ分の空き領域が必要である。例えば、プロセスサイズ2GBのcoreイメージは2GBとなる。
gcore.shを実行するカレントディレクトリを有するパーティションに、十分な空きが無い場合は、-c coreオプションにて、空きのあるパーティション上のディレクトリにcoreファイルを生成しなければならない。
※5 |
FreeBSDのgcoreにはcoreファイルの一貫性を保つためのオプション-s(SIGSTOPを発行する)が実装されている。gcore.shにはこのオプションは、実装されていない。 |
core ファイルの解析
では、実際に生成されたcoreファイルをどのようにして解析するのかを簡単に説明しておく。説明を進めていく上で簡単なC言語で記述されたサンプルプログラムcoretest※6を作成してみた。coretestは後述するstripにより、シンボルテーブルを削除しているが、strip実行前のcoretest.unstripも用意しておくことにする。
coreファイルの解析は、
$ gdb /path/to/coretest core.3993
|
のようにgdbを利用することにより行える。gdbへの引数は、基本的に以下のような書式である。
gdb プログラムファイル coreファイル
あとは通常通り、whereコマンド等で関数のスタックトレースを得ることができる。
しかしながら、ここでスタックトレースを正常に得られるのは、プログラムがシンボルテーブルを持っている場合に限られ、非常に稀であると言える。
なぜなら、通常、開発したソフトをリリースする場合、ロードモジュール中のデバッグ情報の削除、およびstripを実行しシンボルとそのアドレスをマッピングしているシンボルテーブルの除去をする場合がほとんどである。特にrpmを使ってパッケージングを行うと、rpmが自動的にstripを実行してしまう※7。
このような場合、coreファイルを得ることができても、そこから得られるスタックトレースは
$ gdb /path/to/coretest core.4939
(略)
(gdb) where
#0 0x4004fa90 in msort_with_tmp (b=0x809a384, n=327, s=4,
cmp=0x80486e0 <strcpy+260>,
t=0x8086e98 ".........") at msort.c:59
(略)
#6 0x4004fc96 in qsort (b=0x8099430, n=10464, s=4, cmp=0x80486e0 <strcpy+260>)
at msort.c:150
#7 0x0804883e in strcpy () at strcpy:-1
#8 0x080488b6 in strcpy () at strcpy:-1
#9 0x08048913 in strcpy () at strcpy:-1
#10 0x08048913 in strcpy () at strcpy:-1
#11 0x08048913 in strcpy () at strcpy:-1
#12 0x08048913 in strcpy () at strcpy:-1
#13 0x080489af in strcpy () at strcpy:-1
#14 0x4003d4e1 in __libc_start_main (main=0x804895c <strcpy+896>, argc=2,
ubp_av=0xbffed614, init=0x80484b4, fini=0x8048a30 <strcpy+1108>,
rtld_fini=0x4000cce4 <_dl_fini>, stack_end=0xbffed60c)
at ../sysdeps/generic/libc-start.c:129
|
のように正確さを欠いた結果が得られる。デバッガは、トレースを出力時、シンボルテーブルから得られたアドレスににマッチするシンボルを取得するためである。
__libc_start_main()からmain()関数ではなく、strcpy()が呼び出されていたり、strcpy()からstrcpy()が呼び出されている時点で、正確ではないと判断できるだろう。
こういったケースを想定し、ソフトウェアをリリースする際には、strip前のロードモジュールを保持しておくべきである。同じcoreファイルを、シンボルテーブルを保持しているcoretest.unstripを利用しトレースをとると、
$ gdb /path/to/coretest.unstrip core.4939
(略)
(gdb) where
(略)
(gdb) where
#0 0x4004fa90 in msort_with_tmp (b=0x809a384, n=327, s=4,
cmp=0x80486e0 <comp>, t=0x8086e98 ".........") at msort.c:59
(略)
#6 0x4004fc96 in qsort (b=0x8099430, n=10464, s=4, cmp=0x80486e0 <comp>)
at msort.c:150
#7 0x0804883e in insert_file ()
#8 0x080488b6 in path_walk ()
#9 0x08048913 in path_walk ()
#10 0x08048913 in path_walk ()
#11 0x08048913 in path_walk ()
#12 0x08048913 in path_walk ()
#13 0x080489af in main ()
#14 0x4003d4e1 in __libc_start_main (main=0x804895c <main>, argc=2,
ubp_av=0xbffed614, init=0x80484b4 <_init>, fini=0x8048a30 <_fini>,
rtld_fini=0x4000cce4 <_dl_fini>, stack_end=0xbffed60c)
at ../sysdeps/generic/libc-start.c:129
|
のようになる。
この結果から、それらしいスタックトレースが得られていることがわかると思う。
スタックトレースが得られれば、解析の大きなヒントになる。また、各関数への引数も
(gdb) up 8
#8 0x080488b6 in path_walk ()
(gdb) p $ebp+8
$1 = (void *) 0xbffe9084
(gdb) x 0xbffe9084
0xbffe9084: 0xbffe90b0
(gdb) p (char*)0xbffe90b0
$2 = 0xbffe90b0 "/usr/lib/python1.5/test/output"
|
の様にレジスタからたどることができる※8※9。
また、アセンブラの知識があるならば、
(gdb) disassemble
(略)
0x80488aa <path_walk+94> : add $0xfffffff4,%esp
0x80488ad <path_walk+97> : add $0xb,%eax
0x80488b0 <path_walk+100>: push %eax
0x80488b1 <path_walk+101>: call 0x8048700 <insert_file>
0x80488b6 <path_walk+106>: add $0x10,%esp
0x80488b9 <path_walk+109>: test %eax,%eax
0x80488bb <path_walk+111>: je 0x804891a <path_walk+206>
0x80488bd <path_walk+113>: add $0xfffffff4,%esp
0x80488c0 <path_walk+116>: push %ebx
※ #8 フレームが0x80488b6番地にあることがわかるので、その付近のアセンブラ出力
をチェックしてみる。
|
のようにgdbのdisassembleコマンドを使うと良いだろう。
※6 |
gcore.shによるトーレス出力を目的としたサンプルプログラムであり、それ以上のものではないため、プログラムの仕様、ソースについては本稿では割愛する。 |
※7 |
rpmのマクロの設定で自動stripを抑止することも可能である。 |
※8 |
第一引数はebpレジスタの指すアドレスから+8バイトのところにそのアドレスが格納されている。第二引数以降はさらに+4バイトずつ加算していくことになる。つまり、第二引数は、$ebp+12。 |
※9 |
もちろん各関数の各引数の方は把握しおく必要があり、参照時にはその型でキャストする必要がある。 |
まとめ
障害解析を行う上で、如何に必要な情報を収集できるかは非常に重要なことである。
プログラムの動作を記録するログファイルもそれなりに有効かもしれないが、プログラム自身のメモリイメージを後から参照できるcoreファイルに勝るものはない。
地味ではあるが、今回Linuxにおいてこのような障害解析手法が新たに加わった意義は大きい。これは、Linuxのソフトウェア開発プラットホームとしての基盤がより堅固なものとなったと言えるのではないだろうか。