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 のマニュアルに載っています。以下に簡単な使用例を紹介します。

#include 

void 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 コマンドが使える