2005年10月25日
普通のやつらの下を行け: C でバックトレース表示
普通のやつらの下を行けの第2回として、今回は glibc の関数を使って C でバックトレース (スタックトレース) の表示を行ってみます。
バックトレースとは
バックトレースとは、大ざっぱに言うと、現在の関数に至るまでの道筋です。たとえば、次の Ruby プログラムを実行すると、 1 / 0 の行で例外が発生して、バックトレースの表示とともにプログラムは異常終了します。
def foo 1 / 0 end def main foo end main
この例では main から foo を呼び foo の中の 1 / 0 の部分で例外が発生しています。
% ruby divide-by-zero.rb divide-by-zero.rb:2:in `/': divided by 0 (ZeroDivisionError) from divide-by-zero.rb:2:in `foo' from divide-by-zero.rb:6:in `main' from divide-by-zero.rb:9
バックトレースは、スタックフレームと呼ばれる一連のデータから復元されます。スタックフレームとは関数呼び出しのたびにスタックに積み上げられる、リターンアドレスや引数のデータをまとめたものです。Cのスタックフレームについては Schemeの実装におけるスタックフレームの前半の解説が参考になります。
同様のプログラムを今度は Cで書いて実行してみます。
int foo() { return 1 / 0; } int main() { foo(); return 0; }
すると、エラーメッセージは表示されるものの、バックトレースは表示されません。
% ./a.out zsh: 6392 floating point exception (core dumped) ./a.out
Cのプログラムの場合、gdb を使えばバックトレースを表示できます。
% gdb a.out core (gdb) bt #0 0x08048369 in foo () #1 0x08048389 in main ()
コンパイル時に gcc に -g オプションをつけた場合はファイル名と行番号も表示されます。ELFバイナリに埋め込まれたデバッグ情報が用いられるためです。
(gdb) bt #0 0x08048369 in foo () at divide-by-zero.c:2 #1 0x08048389 in main () at divide-by-zero.c:6
C でバックトレースを表示
glibc に含まれる backtrace() と backtrace_symbols_fd() を使うと実行中の C プログラムのバックトレースを表示できます。これらの関数の説明は glibc のマニュアルに載っています。以下に簡単な使用例を紹介します。
#includevoid foo() { void *trace[128]; int n = backtrace(trace, sizeof(trace) / sizeof(trace[0])); backtrace_symbols_fd(trace, n, 1); } int main() { foo(); return 0; }
このプログラムを gcc -g -rdynamic でコンパイルして実行すると次のようなバックトレースが表示されます。i386 上では backtrace_symbols_fd() は .dynsym セクション内の情報を利用する (内部的に dladdr を使っている) ため -rdynamic が必要です。
% ./a.out ./a.out(foo+0x1f)[0x8048693] ./a.out(main+0x15)[0x80486d0] /lib/libc.so.6(__libc_start_main+0xc6)[0x40032e36] ./a.out[0x80485d1]
あまり見やすくありませんが、 main から foo が呼ばれていることがわかります。また、 GNU binutils に含まれる addr2line を使うと ELF バイナリに含まれるデバッグ情報を用いてソースコードのファイル名と行番号を表示できます。
% ./a.out | egrep -o '0x[0-9a-f]{7}' | addr2line -f foo /home/tmp/c/backtrace.c:5 main /home/tmp/c/backtrace.c:11 ?? ??:0 _start ../sysdeps/i386/elf/start.S:105
異常終了時のバックトレース
sigaction を使って Ruby と同様にプログラムの異常終了時にバックトレースを表示するように試みたところ、シグナルハンドラに飛んだ時点でスタックの内容が崩れているため、正確なバックトレースにはならないようでした。
#include <stdlib.h> #include <execinfo.h> #include <signal.h> void stacktrace(int signal) { void *trace[128]; int n = backtrace(trace, sizeof(trace) / sizeof(trace[0])); backtrace_symbols_fd(trace, n, 1); } int foo() { return 1 /0; } void bar() { foo(); } int main() { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = stacktrace; sa.sa_flags = SA_ONESHOT; sigaction(SIGFPE, &sa, NULL); bar(); return 0; }
上のプログラムを手元の環境 (Debian GNU/Linux sarge) で実行すると次のようなバックトレースが表示されました。stacktrace() にたどりつくまでに foo のスタックフレームは消えています。
% ./a.out ./a.out(stacktrace+0x1f)[0x8048743] /lib/libc.so.6[0x400466f8] ./a.out(bar+0xb)[0x8048796] ./a.out(main+0x65)[0x80487fd] /lib/libc.so.6(__libc_start_main+0xc6)[0x40032e36] ./a.out[0x8048681] zsh: 7442 floating point exception (core dumped) ./a.out
/lib/libSegFault.so
何となく glibc のコードを漁っていると sysdeps/generic/segfault.c というファイルが見つかりました。これは /lib/libSegFault.so に使われています。/lib/libSegFault.so を使うと、わざわざ自分でシグナルハンドラをセットしなくても、 環境変数 LD_PRELOAD をセットするだけで、異常終了時のバックトレースを表示できます。
試しに最初の最も単純な C のプログラムを LD_PRELOAD=/lib/libSegFault.so SEGFAULT_SIGNALS=all をセットした上で実行すると次のようなメッセージが表示されました。
% export LD_PRELOAD=/lib/libSegFault.so % export SEGFAULT_SIGNALS=all % ./a.out *** Floating point exception Register dump: EAX: 00000001 EBX: 40150880 ECX: 00000001 EDX: 00000000 ESI: 40016540 EDI: bfffe894 EBP: bfffe828 ESP: bfffe824 EIP: 080485f9 EFLAGS: 00010286 CS: 0023 DS: 002b ES: 002b FS: 0000 GS: 0000 SS: 002b Trap: 00000000 Error: 00000000 OldMask: 00000000 ESP/signal: bfffe824 CR2: 00000000 Backtrace: ./a.out(foo+0x15)[0x80485f9] ./a.out(main+0x15)[0x8048619] /lib/libc.so.6(__libc_start_main+0xc6)[0x40036e36] ./a.out[0x8048541] Memory map: 08048000-08049000 r-xp 00000000 09:00 5538441 /home/satoru/tmp/a.out 08049000-0804a000 rw-p 00000000 09:00 5538441 /home/satoru/tmp/a.out 40000000-40016000 r-xp 00000000 08:01 700392 /lib/ld-2.3.2.so 40016000-40017000 rw-p 00015000 08:01 700392 /lib/ld-2.3.2.so 40017000-40018000 rw-p 00000000 00:00 0 40018000-4001b000 r-xp 00000000 08:01 700650 /lib/libSegFault.so 4001b000-4001c000 rw-p 00002000 08:01 700650 /lib/libSegFault.so 40021000-40149000 r-xp 00000000 08:01 700628 /lib/libc-2.3.2.so 40149000-40151000 rw-p 00127000 08:01 700628 /lib/libc-2.3.2.so 40151000-40154000 rw-p 00000000 00:00 0 bfffd000-c0000000 rwxp ffffe000 00:00 0 zsh: 11875 floating point exception (core dumped) ./a.out
こちらのバックトレースでは、 0 による除算が発生している foo() を含めてスタックトレースが表示されています。
/lib/libSegFault.so のラッパーとして、セグメンテーションフォルト (segmentation fault) 専用の catchsegv コマンドがあります。catchsegv はただのシェルスクリプトです。
まとめ
glibc の関数を使えば Cでも簡単にバックトレースを表示できることがわかりました。デバッグ用の情報として使うと便利なのではないかと思います。今回のポイントは以下の通りです。
- glibc には backtrace(), backtrace_symbols(), backtrace_symbols_fd() が含まれている
- 環境変数 LD_PRELOAD に /lib/libSegFault.so を指定するだけで異常終了時にバックトレースを表示できる
- セグメンテーションフォルトを捕まえるだけなら catchsegv コマンドが使える