目次

ログを取っている環境が i386だったり x86_64だったりするので、stackframeの addressの出方もまちまちだったりするが、 その辺は爽やかにスルーする方向でお願いしますw

動機

Solaris10上のコマンドDTraceにある機能 ustack() みたいなことを Linux上でもやってみたいということで、なんとかならんか調べてみた。 具体的にはこんな感じ。
lynx@root[119] dtrace -n 'syscall::read:entry { ustack(); exit(0)}' -c 'ls /' 
dtrace: description 'syscall::read:entry ' matched 1 probe
TT_DB       devices     lib         opt         tmp
archives    etc         lost+found  platform    usr
bin         export      mnt         proc        var
boot        home        mnt2        sbin        vol
dev         kernel      net         system      zfs
dtrace: pid 18116 has exited
CPU     ID                    FUNCTION:NAME
  3  45642                       read:entry
              libc.so.1`_read+0xa
              libproc.so.1`Preadauxvec+0xd7
              libproc.so.1`Pupdate_maps+0x47
              libproc.so.1`Pupdate_syms+0x1f
              libdtrace.so.1`dt_proc_rdevent+0xe4
              libdtrace.so.1`dt_proc_bpmatch+0xc6
              libdtrace.so.1`dt_proc_control+0x4a9
              libc.so.1`_thr_setup+0x5b
              libc.so.1`_lwp_start

First Step

まずは呼ばれたら、stack frameを順に追って、自分の手前までの分をdumpする Library関数 int mydump() を作るところから始めてみる。glibc 2.1以降で実装されているbacktraceがそのまま使えるか試してみた。

まず素で使ってみる

$ cat test.c
#include        <stdio.h>
#include        <string.h>
#include        <stdlib.h>
#include        <unistd.h>
#include        <execinfo.h>

void foo() {
    void *trace[BUFSIZ];
    size_t size = backtrace(trace, sizeof(trace) / sizeof(trace[0]));
        char **strings = backtrace_symbols(trace, size);
        for ( int i = 0; i < size; i++ )
                printf("%s\n", strings[i]);

        free(strings);
}

int main() {
    foo();
    return 0;
}
$ ./test
./test(foo+0x1c) [0x400834]
./test(main+0xe) [0x4008c5]
/lib64/tls/libc.so.6(__libc_start_main+0xdb) [0x34be11c4bb]
./test [0x40078a]
backtrace()は表示用のバッファサイズ(trace)とnestの深さ(n)の分だけ表示できる

2nd Step

毎回コードに直書きするのはどうかと思うので、foo()の部分だけ libraryに入れて、 コードからはその関数を呼び出す形に変えてみた。ついでにもう1枚、libを挟んでみた。

まずはsoに入れる部分

$ cat tracehere.c
#include        <stdio.h>
#include        <stdlib.h>
#include        <execinfo.h>

int tracehere() {
        void *trace[BUFSIZ];
        size_t size;
        char **strings;
        size_t i;

        size = backtrace(trace, sizeof(trace));
        strings = backtrace_symbols(trace, size);
        printf("Obtained %zd stack frames.\n", size);
        for ( i = 0; i < size; i++ ) {
                printf("%s\n", strings[i]);
        }

        free(strings);// the array 'strings' is malloced by backtrace_symbols().

        return 0;
}
次に上記のsoに入れた関数を呼び出す方と実行結果。
$ cat mock.c
#include <stdio.h> 
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int tracehere();

void foo() {
	tracehere();
}
$ cat backtrace.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int foo();

int main(int argc, char *argv[]) {
        foo();
        return (0);
}
$ make run
export LD_LIBRARY_PATH=.; ./backtrace
Obtained 5 stack frames.
./libmito.so(tracehere+0x1c) [0x2a956827d8]
./libmock.so(foo+0xe) [0x2a955576ba]
./backtrace(main+0x19) [0x400721]
/lib64/tls/libc.so.6(__libc_start_main+0xdb) [0x34be11c4bb]
./backtrace [0x40067a]
backtrace()を呼び出したバイナリに含まれてる所(=libmito.so)はちゃんとsymbol名が表示されているが、本体(たぶん libmito.so以外?)はsymbol名が解決されていない。 単にstaticの関数にしてただけだった orz

上記のman pageには

シンボル名は特別なリンカ・オプションを使用しないと利用できない場合がある。 
GNU リンカを使用するシステムでは、 -rdynamic リンカ・オプションを使う必要がある。
"static" な関数のシンボル名は公開されず、バックトレースでは利用できない点に注意すること。  
と書いてあるので、-rdynamicを GCCに渡して buildしてみたが、有無に関わらず出力されなかった。通りでしたw

3rd Step

backtrace()は glibc(2.1以上)に依存するので、他OSへの portingも考えると http://sourceware.org/binutils/docs/bfd/index.html?とかを使って頑張ってみる。とりあえず objdump --syms optionでSTDOUTに出力してるのは、関数ポインタabfd->xvec->_bfd_print_symbolのさしてる先で、elf 32bitバイナリの場合、elf.cにある bfd_elf_print_symbol()でやってる。

TBD

4th Step

ここまでは tracehere()をチェック箇所に埋めこんで rebuildする必要があったが、live debugしたい対象を rebuildするのはむずかしい。なので、以下のような感じで動くようにしてみる。
  • 1. 対象symbolの処理に入った所で ustack()を出力する
    • 対象の設定は固定位置($HOME/.xxx/xxx.confみたいな?)のファイルに書く。
  • 2. 対象binaryのrebuildは無し。とりあえず ELF限定でいいだろう。
  • 3. .debug section無しで動作する。stripされてても動く。

作り方はこんな感じで想定中。

  • LD_PRELOADで対象 libを指定しておく
  • 設定ファイルに書いてある対象のsymbolを lib内で動的に生成して、後からloadされるものを wrapする
  • backtrace箇所は glibc版、libbfd版、readelf同等品(=自力構成)をつくってみる

TBD

参考文献

参考文献からのメモ

  • 1. 環境変数 LD_PRELOAD に /lib/libSegFault.so を指定するだけで異常終了時にバックトレースを表示できる。/usr/bin/catchsegvというwrapper scriptもある。
$ catchsegv --help
Usage: catchsegv PROGRAM ARGS...
  --help      print this help, then exit
  --version   print version number, then exit
For bug reporting instructions, please see:
<http://www.gnu.org/software/libc/bugs.html>.

ということで、試してみた。テストコードと実行結果はこんな感じ。

$ cat segvtest.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int foo() {
        char *a = NULL;
        printf("%s\n", a);
        return 0;
}

int main(int argc, char *argv[]) {
        foo();
        return (0);
}
$ ./segvtest
Segmentation fault

/lib/libSegFault.soをLD_PRELOADに刺して実行した結果はこんな感じ。wrapper scriptの catchsegvから実行した結果も同じだった。(そりゃそうだ。)

$ setenv LD_PRELOAD /lib/libSegFault.so
$ ./segvtest
*** Segmentation fault
Register dump:

 EAX: 00000000   EBX: 00254ff4   ECX: 00000000   EDX: 00000001
 ESI: 00377ca0   EDI: 00000000   EBP: bfb9fbe8   ESP: bfb9fbc8

 EIP: 001700d3   EFLAGS: 00210246

 CS: 0073   DS: 007b   ES: 007b   FS: 0000   GS: 0033   SS: 007b

 Trap: 0000000e   Error: 00000004   OldMask: 00000000
 ESP/signal: bfb9fbc8   CR2: 00000000

Backtrace:
/lib/i686/nosegneg/libc.so.6(strlen+0x33)[0x1700d3]
/lib/i686/nosegneg/libc.so.6(_IO_puts+0x25)[0x15a405]
./segvtest(foo+0x18)[0x804855c]
./segvtest(main+0x16)[0x8048579]
/lib/i686/nosegneg/libc.so.6(__libc_start_main+0xe0)[0x116f70]
./segvtest[0x8048491]

Memory map:

00101000-00253000 r-xp 00000000 fd:00 4124746    /lib/i686/nosegneg/libc-2.6.so
00253000-00255000 r--p 00151000 fd:00 4124746    /lib/i686/nosegneg/libc-2.6.so
00255000-00256000 rw-p 00153000 fd:00 4124746    /lib/i686/nosegneg/libc-2.6.so
00256000-00259000 rw-p 00256000 00:00 0
00348000-00353000 r-xp 00000000 fd:00 4124787    /lib/libgcc_s-4.1.2-20070503.so.1
00353000-00354000 rw-p 0000a000 fd:00 4124787    /lib/libgcc_s-4.1.2-20070503.so.1
0035c000-00377000 r-xp 00000000 fd:00 4124744    /lib/ld-2.6.so
00377000-00378000 r--p 0001a000 fd:00 4124744    /lib/ld-2.6.so
00378000-00379000 rw-p 0001b000 fd:00 4124744    /lib/ld-2.6.so
0077c000-0077d000 r-xp 0077c000 00:00 0          [vdso]
00918000-0091b000 r-xp 00000000 fd:00 4124753    /lib/libSegFault.so
0091b000-0091c000 r--p 00002000 fd:00 4124753    /lib/libSegFault.so
0091c000-0091d000 rw-p 00003000 fd:00 4124753    /lib/libSegFault.so
08048000-08049000 r-xp 00000000 fd:00 9466779    /home/mage/sym-works/backtrace/tests/segvtest/segvtest
08049000-0804a000 rw-p 00000000 fd:00 9466779    /home/mage/sym-works/backtrace/tests/segvtest/segvtest
088df000-08900000 rw-p 088df000 00:00 0
b7f4f000-b7f50000 rw-p b7f4f000 00:00 0
b7f6b000-b7f6c000 rw-p b7f6b000 00:00 0
bfb8c000-bfba1000 rw-p bfb8c000 00:00 0          [stack]
Segmentation fault

残課題

  • 1. binutilを --enable-sharedでbuildすると libbfd-2.1.x.soとsymlinkの libbfd.soができる。使う側のbuild時に -lbfdだと未解決シンボルが残るが、-lbfd-2.1.xだと残らない。なんでだろ。

Last-modified: 2009-01-27 13:39:20