tracef - function call tracer

TOC

概要

tracefは、Linux向けの「関数コールトレーサ」です。 ディストリビューションに標準で含まれているltraceコマンドに似ていますが、次のような特徴・相違点があります。

これらの特徴により、

などに活用できるツールになっています。残念ながら、関数呼び出し際の引数の情報はltraceほど詳しくは表示されないので、デバッグ用途に用いるのは(まだ)厳しいかもしれません。 手元では、C++で書かれたやや大きめの実行ファイル(.textのサイズが5Mバイト程度、text/weakなsymbol数が2万程度、プロセス/スレッド数が数十)の解析が問題なく行えています。

解析対象のプログラムの再コンパイルは、解析対象がstripされていない限りは不要です。gdbでデバッグ可能な実行ファイルであれば、tracefでトレースすることができます。 解析対象の実行ファイルに含まれる「自作関数」の呼び出し状況の他、ltraceが表示するような、ライブラリ関数の呼び出し状況もある程度表示することが可能です(コマンドラインオプションで表示有無を選択できます)。

tracefは、ltraceやstrace、あるいはgdbと同じように、Linux kernelのptrace(2)システムコールを利用して、 別プロセスから解析対象のプログラムを観察し、観察状況を表示しています。

実行例

例えば次のようなプログラムをトレースすると、

#include <stdio.h>

void my_func_2() {
  puts("hello, world!");
}

void my_func_1() {
  my_func_2();
}

int main(int argc, char** argv) {
  my_func_1();
  fflush(stdout);
  return 0;
}

結果は次のようになります。

$ gcc -g -o hello hello.c 
$ tracef --plt --line-numbers --call-tree hello

[pid 30126] +++ process 30126 attached (ppid 30125) +++
[pid 30126] === symbols loaded: './hello' ===
[pid 30126] ==> _start() at 0x08048300
[pid 30126]    ==> __libc_start_main@plt() at 0x080482cc
[pid 30126]       ==> __libc_csu_init() at 0x08048440
[pid 30126]          ==> _init() at 0x08048294
[pid 30126]             ==> call_gmon_start() at 0x08048324
[pid 30126]             <== call_gmon_start() [eax = 0x0]
[pid 30126]             ==> frame_dummy() at 0x080483b0
[pid 30126]             <== frame_dummy() [eax = 0x0]
[pid 30126]             ==> __do_global_ctors_aux() at 0x080484b0
[pid 30126]             <== __do_global_ctors_aux() [eax = 0xffffffff]
[pid 30126]          <== _init() [eax = 0xffffffff]
[pid 30126]       <== __libc_csu_init() [eax = 0x8049514]
[pid 30126]       ==> main() at 0x080483f5 [/home/sato/tracef/sample/hello.c:14] 
[pid 30126]          ==> my_func_1() at 0x080483e8 [/home/sato/tracef/sample/hello.c:9] 
[pid 30126]             ==> my_func_2() at 0x080483d4 [/home/sato/tracef/sample/hello.c:4] 
[pid 30126]                ==> puts@plt() at 0x080482ec
[pid 30126]                <== puts@plt() [eax = 0xe]
[pid 30126]             <== my_func_2() [eax = 0xe]
[pid 30126]          <== my_func_1() [eax = 0xe]
[pid 30126]          ==> fflush@plt() at 0x080482dc
hello, world!
[pid 30126]          <== fflush@plt() [eax = 0x0]
[pid 30126]       <== main() [eax = 0x0]
[pid 30126]       ==> _fini() at 0x080484d8
[pid 30126]          ==> __do_global_dtors_aux() at 0x08048350
[pid 30126]          <== __do_global_dtors_aux() [eax = 0x0]
[pid 30126]       <== _fini() [eax = 0x0]
[pid 30126] +++ process 30126 detached (ppid 30125) +++
tracef: done

main()がmy_func_1()を呼び、my_func_1()がmy_func_2()を呼んでいる様子が表示されています。また、 これらの関数が定義されているソースファイル名も表示されています。

今回は --synthetic オプションを付けているため、puts@plt() などの、DSO内の関数呼び出し の様子もトレースされています。--synthetic オプションを外せば、純粋に自作関数だけのトレースが行われます。オプションによっては、関数の引数の簡単な情報を表示することもできます。

動作環境 (OS)

ダウンロード

ビルドに必要なライブラリ

Fedora Core 6 / Fedora 7

RHEL4 / CentOS4

Ubuntu 7.04

libdwarf

上記とは別にlibdwarfが要ります。

ビルド手順

$ tar xvzf tracef-0.1.tar.gz 
$ cd tracef-0.1
$ ./configure
$ cd src
$ make

makeの結果できあがった src/tracef がトレーサです。このバイナリ(tracef)は、/usr/local/bin などにインストールせずとも、お好きな場所に置いたままで、 それ単体ですぐに動かすことができます。動作を見るためのサンプルプログラムを sample/ に用意しました。

$ cd ../sample
$ ./sample.sh

sample.sh を実行すると、いくつかのテストプログラムがコンパイル、リンクされ、それぞれがいま作成したtracefで解析され、結果がsample/logs/に格納されます。 sample/logs/ ディレクトリには、mt-daapd-0.2.4 (iTunesサーバ) を私がtracefで解析した結果 (たとえばこんなの) も、参考として含めてあります。

解析可能なプログラム

再コンパイル要否

解析対象の再コンパイルは、解析対象がstripされていない場合は不要です。gdbで解析できるバイナリであれば再コンパイルは不要でしょう。

また、解析対象が -g 付きでコンパイルされていると、出力される情報が増えます。例えば行番号情報、引数情報が出力可能になります。解析対象は、-O0 でコンパイルされていることが望ましいですが、必須ではありません。最適化されている場合、一部の関数の呼び出しを検出できない場合があります。

stripされているバイナリをトレースしてもtracefが異常終了したりということはありませんが、何も解析結果が出力されません。

詳細

stripされているかどうかは、fileコマンドを使って "stripped" "not stipped" のどちらが表示されるかで確認可能です。あるいは readelf -S コマンドで.symtab セクションが存在するバイナリであることを直接確認する方法でもOKです。 下記の例ではa.outはstripされていません。

$ file a.out
a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.9, not stripped
$ readelf -S a.out | grep '\.symtab'
  [40] .symtab           SYMTAB          00000000 5d20b4 00d4d0 10     41 1056  4

なお、/lib/lib*.a や /usr/lib/lib*.a ファイルは .symtab を含んでいますので、こういった lib*.a ファイルを リンクするとその中に含まれる関数もトレースの対象になります。それらをトレースしたくない場合は、.a ではなく .so をリンクしてください。

プログラムが gcc -g でデバッグ情報ありでコンパイルされていると、トレース結果にその関数が定義された ソースファイル名、行番号を含めることができます。また、一部の関数の引数の情報を表示することも可能に なります。プログラムが -g (あるいは -ggdb や -g3 など) 付きでコンパイルされたかどうかは、readelf -S コマンドで確認可能です。.debug_* というセクションがあれば -g 付きでコンパイルされています。

$ readelf -S a.out | grep '\.debug_'
  [29] .debug_aranges    PROGBITS        00000000 0cb8a6 002aa8 00      0   0  1
  [30] .debug_pubnames   PROGBITS        00000000 0ce34e 02d72c 00      0   0  1
  [31] .debug_info       PROGBITS        00000000 0fba7a 1670e5 00      0   0  1
  (略)

その他細かい点:

スクリーンショット?

tracefによるプログラムの解析例を5つ載せておきます。

1. forkするプログラムの解析

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
  if (fork() == 0) {
    printf("hello world\n");  
    return 1;
  }
  return 0;
}

tracefで上のプログラムを解析すると次のような出力になります。forkされたプロセスを自動で解析開始(attach)しています。 --ff オプションで、プロセス/スレッド毎に別のログファイルに結果を出力することも可能です。

$ ../src/tracef --synthetic -flATu ./fork
[pid 30133] 13:56:14.041015 +++ process 30133 attached (ppid 30132) +++
[pid 30133] 13:56:14.065944 === symbols loaded: './fork' ===
[pid 30133] 13:56:14.086854 ==> _start() at 0x080482e0
[pid 30133] 13:56:14.103301    ==> __libc_start_main@plt() at 0x080482a4
[pid 30133] 13:56:14.120804       ==> __libc_csu_init() at 0x08048410
[pid 30133] 13:56:14.142981          ==> _init() at 0x0804826c
[pid 30133] 13:56:14.167027             ==> call_gmon_start() at 0x08048304
[pid 30133] 13:56:14.198099             <== call_gmon_start() [eax = 0x0]
[pid 30133] 13:56:14.228514             ==> frame_dummy() at 0x08048390
[pid 30133] 13:56:14.256405             <== frame_dummy() [eax = 0x0]
[pid 30133] 13:56:14.287810             ==> __do_global_ctors_aux() at 0x08048480
[pid 30133] 13:56:14.316187             <== __do_global_ctors_aux() [eax = 0xffffffff]
[pid 30133] 13:56:14.346243          <== _init() [eax = 0xffffffff]
[pid 30133] 13:56:14.373957       <== __libc_csu_init() [eax = 0x80494e0]
[pid 30133] 13:56:14.400881       ==> main() at 0x080483b4 [/home/sato/tracef/sample/fork.c:6] 
[pid 30133] 13:56:14.424637          ==> fork@plt() at 0x080482c4
[pid 30134] 13:56:14.440226 +++ process 30134 attached (ppid 30133) +++
[pid 30134] 13:56:14.444849          <== fork@plt() [eax = 0x0]
[pid 30134] 13:56:14.455462          ==> puts@plt() at 0x080482b4
hello world
[pid 30134] 13:56:14.468085          <== puts@plt() [eax = 0xc]
[pid 30134] 13:56:14.495761       <== main() [eax = 0x1]
[pid 30134] 13:56:14.519362       ==> _fini() at 0x080484a8
[pid 30134] 13:56:14.539950          ==> __do_global_dtors_aux() at 0x08048330
[pid 30134] 13:56:14.566927          <== __do_global_dtors_aux() [eax = 0x0]
[pid 30134] 13:56:14.592884       <== _fini() [eax = 0x0]
[pid 30134] 13:56:14.616904 +++ process 30134 detached (ppid 30133) +++
[pid 30133] 13:56:14.625031          <== fork@plt() [eax = 0x75b6]
[pid 30133] 13:56:14.652768 --- SIGCHLD received (#17 Child exited) ---
[pid 30133] 13:56:14.660887       <== main() [eax = 0x0]
[pid 30133] 13:56:14.685255       ==> _fini() at 0x080484a8
[pid 30133] 13:56:14.701960          ==> __do_global_dtors_aux() at 0x08048330
[pid 30133] 13:56:14.726249          <== __do_global_dtors_aux() [eax = 0x0]
[pid 30133] 13:56:14.755024       <== _fini() [eax = 0x0]
[pid 30133] 13:56:14.781833 +++ process 30133 detached (ppid 30132) +++

2. execするプログラムの解析

自分自身をexecすることで足し算という処理を進めていくプログラムを書いてみました (元ネタは何だっけな...BinaryHacks? 思い出せない)。 $ ./exec 0 5 のように起動すると、見えないところでexecve(2)を繰り返して 5+4+3+2+1 を計算し、結果を出力します。argv[1]が累積変数です。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char** argv) {
  if (argc != 3) return 0; /* usage: ./a.out 0 N */

  int accum = atoi(argv[1]);
  int n = atoi(argv[2]);

  if (n == 0) {
    printf("answer: %d\n", accum);
    return accum;
  }

  char p[32] = {0}, q[32] = {0};
  snprintf(p, 31, "%d", accum + n);
  snprintf(q, 31, "%d", n - 1);

  execlp("/proc/self/exe", "exe", p, q, NULL);
  return -1;
}

...プログラムの内容はともかく、execve(2) するようなプログラムであっても、きちんとトレースを継続できていることが下記で確認できます。

$ ../src/tracef --synthetic -flATuv ./exec 0 5   
[pid 30137] 13:59:40.506880 +++ process 30137 attached (ppid 30136) +++
[pid 30137] 13:59:40.516425 === symbols loaded: './exec' ===
[pid 30137] 13:59:40.523785 ==> _start() at 0x08048340
[pid 30137] 13:59:40.526590    ==> __libc_start_main@plt() at 0x080482e4
[pid 30137] 13:59:40.528794       ==> __libc_csu_init() at 0x08048550
[pid 30137] 13:59:40.533544          ==> _init() at 0x080482ac
[pid 30137] 13:59:40.536994             ==> call_gmon_start() at 0x08048364
[pid 30137] 13:59:40.540511             <== call_gmon_start() [eax = 0x0]
[pid 30137] 13:59:40.545888             ==> frame_dummy() at 0x080483f0
[pid 30137] 13:59:40.549673             <== frame_dummy() [eax = 0x0]
[pid 30137] 13:59:40.560435             ==> __do_global_ctors_aux() at 0x080485c0
[pid 30137] 13:59:40.585169             <== __do_global_ctors_aux() [eax = 0xffffffff]
[pid 30137] 13:59:40.618919          <== _init() [eax = 0xffffffff]
[pid 30137] 13:59:40.648524       <== __libc_csu_init() [eax = 0x8049638]
[pid 30137] 13:59:40.674937       ==> main(int argc <3>, POINTER argv <0xbff88a44>) at 0x08048414 [/home/sato/tracef/sample/exec.c:6] 
[pid 30137] 13:59:40.718245          ==> atoi@plt() at 0x08048314
[pid 30137] 13:59:40.744948          <== atoi@plt() [eax = 0x0]
[pid 30137] 13:59:40.770063          ==> atoi@plt() at 0x08048314
[pid 30137] 13:59:40.793048          <== atoi@plt() [eax = 0x5]
[pid 30137] 13:59:40.819975          ==> snprintf@plt() at 0x08048324
[pid 30137] 13:59:40.843943          <== snprintf@plt() [eax = 0x1]
[pid 30137] 13:59:40.873277          ==> snprintf@plt() at 0x08048324
[pid 30137] 13:59:40.898579          <== snprintf@plt() [eax = 0x1]
[pid 30137] 13:59:40.923149          ==> execlp@plt() at 0x080482f4
[pid 30137] 13:59:40.947893 === execve(2) called. reloading symbols... ===
[pid 30137] 13:59:40.962784 === symbols loaded: 'exe' ===
[pid 30137] 13:59:40.977023 ==> _start() at 0x08048340
...
[pid 30137] 13:59:42.522945          ==> execlp@plt() at 0x080482f4
[pid 30137] 13:59:42.546478 === execve(2) called. reloading symbols... ===
[pid 30137] 13:59:42.556684 === symbols loaded: 'exe' ===
[pid 30137] 13:59:42.568359 ==> _start() at 0x08048340
[pid 30137] 13:59:42.580952    ==> __libc_start_main@plt() at 0x080482e4
[pid 30137] 13:59:42.594888       ==> __libc_csu_init() at 0x08048550
[pid 30137] 13:59:42.607220          ==> _init() at 0x080482ac
[pid 30137] 13:59:42.616299             ==> call_gmon_start() at 0x08048364
[pid 30137] 13:59:42.625258             <== call_gmon_start() [eax = 0x0]
[pid 30137] 13:59:42.643251             ==> frame_dummy() at 0x080483f0
[pid 30137] 13:59:42.654002             <== frame_dummy() [eax = 0x0]
[pid 30137] 13:59:42.664041             ==> __do_global_ctors_aux() at 0x080485c0
[pid 30137] 13:59:42.673053             <== __do_global_ctors_aux() [eax = 0xffffffff]
[pid 30137] 13:59:42.688869          <== _init() [eax = 0xffffffff]
[pid 30137] 13:59:42.697093       <== __libc_csu_init() [eax = 0x8049638]
[pid 30137] 13:59:42.706019       ==> main(int argc <3>, POINTER argv <0xbf966364>) at 0x08048414 [/home/sato/tracef/sample/exec.c:6] 
[pid 30137] 13:59:42.719013          ==> atoi@plt() at 0x08048314
[pid 30137] 13:59:42.726840          <== atoi@plt() [eax = 0xf]
[pid 30137] 13:59:42.731108          ==> atoi@plt() at 0x08048314
[pid 30137] 13:59:42.735355          <== atoi@plt() [eax = 0x0]
[pid 30137] 13:59:42.738764          ==> printf@plt() at 0x08048304
answer: 15
[pid 30137] 13:59:42.745753          <== printf@plt() [eax = 0xb]
[pid 30137] 13:59:42.749255       <== main() [eax = 0xf]
[pid 30137] 13:59:42.752250       ==> _fini() at 0x080485e8
[pid 30137] 13:59:42.755001          ==> __do_global_dtors_aux() at 0x08048390
[pid 30137] 13:59:42.758079          <== __do_global_dtors_aux() [eax = 0x0]
[pid 30137] 13:59:42.767404       <== _fini() [eax = 0x0]
[pid 30137] 13:59:42.780894 +++ process 30137 detached (ppid 30136) +++

3. 再帰

末尾再帰で足し算を行うプログラムです。最適化の有無でトレースの出力が変化します。 このほか、tar.gzの中には 相互再帰 (mutual recursion) を行うサンプルも含まれています。

#include <stdio.h>

int sum(int n) {
  return n == 0 ? 0 : n + sum(n - 1);
}

int main() {
  int s = sum(10);
  printf("sum(10) = %d\n", s);
  return s;
}

まず最適化のかかっていない実行ファイルのトレース結果から。再帰呼び出しの様子がわかりやすく出力されています。

$ ../src/tracef -lATv ./recursion
[pid 30102] +++ process 30102 attached (ppid 30101) +++
[pid 30102] === symbols loaded: './recursion' ===
[pid 30102] ==> _start() at 0x080482b0
[pid 30102]    ==> __libc_csu_init() at 0x08048410
[pid 30102]       ==> _init() at 0x08048250
[pid 30102]          ==> call_gmon_start() at 0x080482d4
[pid 30102]          <== call_gmon_start() [eax = 0x0]
[pid 30102]          ==> frame_dummy() at 0x08048360
[pid 30102]          <== frame_dummy() [eax = 0x0]
[pid 30102]          ==> __do_global_ctors_aux() at 0x08048480
[pid 30102]          <== __do_global_ctors_aux() [eax = 0xffffffff]
[pid 30102]       <== _init() [eax = 0xffffffff]
[pid 30102]    <== __libc_csu_init() [eax = 0x80494e4]
[pid 30102]    ==> main() at 0x080483b4 [/home/sato/tracef/sample/recursion.c:9] 
[pid 30102]       ==> sum(int n <10>) at 0x08048384 [/home/sato/tracef/sample/recursion.c:4] 
[pid 30102]          ==> sum(int n <9>) at 0x08048384 [/home/sato/tracef/sample/recursion.c:4] 
[pid 30102]             ==> sum(int n <8>) at 0x08048384 [/home/sato/tracef/sample/recursion.c:4] 
[pid 30102]                ==> sum(int n <7>) at 0x08048384 [/home/sato/tracef/sample/recursion.c:4] 
[pid 30102]                   ==> sum(int n <6>) at 0x08048384 [/home/sato/tracef/sample/recursion.c:4] 
[pid 30102]                      ==> sum(int n <5>) at 0x08048384 [/home/sato/tracef/sample/recursion.c:4] 
[pid 30102]                         ==> sum(int n <4>) at 0x08048384 [/home/sato/tracef/sample/recursion.c:4] 
[pid 30102]                            ==> sum(int n <3>) at 0x08048384 [/home/sato/tracef/sample/recursion.c:4] 
[pid 30102]                               ==> sum(int n <2>) at 0x08048384 [/home/sato/tracef/sample/recursion.c:4] 
[pid 30102]                                  ==> sum(int n <1>) at 0x08048384 [/home/sato/tracef/sample/recursion.c:4] 
[pid 30102]                                     ==> sum(int n <0>) at 0x08048384 [/home/sato/tracef/sample/recursion.c:4] 
[pid 30102]                                     <== sum() [eax = 0x0]
[pid 30102]                                  <== sum() [eax = 0x1]
[pid 30102]                               <== sum() [eax = 0x3]
[pid 30102]                            <== sum() [eax = 0x6]
[pid 30102]                         <== sum() [eax = 0xa]
[pid 30102]                      <== sum() [eax = 0xf]
[pid 30102]                   <== sum() [eax = 0x15]
[pid 30102]                <== sum() [eax = 0x1c]
[pid 30102]             <== sum() [eax = 0x24]
[pid 30102]          <== sum() [eax = 0x2d]
[pid 30102]       <== sum() [eax = 0x37]
[pid 30102]    <== main() [eax = 0x37]
[pid 30102]    ==> _fini() at 0x080484a8
[pid 30102]       ==> __do_global_dtors_aux() at 0x08048300
[pid 30102]       <== __do_global_dtors_aux() [eax = 0x0]
[pid 30102]    <== _fini() [eax = 0x0]
sum(10) = 55
[pid 30102] +++ process 30102 detached (ppid 30101) +++

次に最適化あり (gcc -O1 -foptimize-sibling-calls) の場合の出力です。関数呼び出し sum(10); の中で行われているはずの足し算の様子が見えなくなりました。というわけで、最適化されたバイナリを相手にするときは少し注意が要ります。逆に、最適化がどう効いたかを調べるのにtracefを使うことも可能です。

$ ../src/tracef -lATv ./recursion_opt
[pid 30104] +++ process 30104 attached (ppid 30103) +++
[pid 30104] === symbols loaded: './recursion_opt' ===
[pid 30104] ==> _start() at 0x080482b0
[pid 30104]    ==> __libc_csu_init() at 0x080483f0
[pid 30104]       ==> _init() at 0x08048250
[pid 30104]          ==> call_gmon_start() at 0x080482d4
[pid 30104]          <== call_gmon_start() [eax = 0x0]
[pid 30104]          ==> frame_dummy() at 0x08048360
[pid 30104]          <== frame_dummy() [eax = 0x0]
[pid 30104]          ==> __do_global_ctors_aux() at 0x08048460
[pid 30104]          <== __do_global_ctors_aux() [eax = 0xffffffff]
[pid 30104]       <== _init() [eax = 0xffffffff]
[pid 30104]    <== __libc_csu_init() [eax = 0x80494c4]
[pid 30104]    ==> main() at 0x0804839c [/home/sato/tracef/sample/recursion.c:9] 
[pid 30104]       ==> sum(int n <10>) at 0x08048384 [/home/sato/tracef/sample/recursion.c:4] 
[pid 30104]       <== sum() [eax = 0x37]
[pid 30104]    <== main() [eax = 0x37]
[pid 30104]    ==> _fini() at 0x08048488
[pid 30104]       ==> __do_global_dtors_aux() at 0x08048300
[pid 30104]       <== __do_global_dtors_aux() [eax = 0x0]
[pid 30104]    <== _fini() [eax = 0x0]
sum(10) = 55
[pid 30104] +++ process 30104 detached (ppid 30103) +++

4. マルチスレッド

pthreadを使ったプログラムの解析も問題なく可能です。

#include <stdio.h>
#include <pthread.h>

void* thread_entry(void* p) {
  printf("pthread_self()=%lu\n", pthread_self());
  return NULL;
}

int main() {
  pthread_t t;
  pthread_create(&t, NULL, thread_entry, 0);
  pthread_join(t, NULL);
  return 0;
}

tracefでいうスレッドのIDは、$ ps -L で出力される "LWP" の数字や、スレッドを生成した親プロセスの /proc/pid/task/ 以下に現れる数字と同じものです。pthread_self() で得られる unsigned long int値とは別物です。混乱しがちな点なので念のため。

$ ../src/tracef  --synthetic -flT ./thread
[pid 30154] +++ process 30154 attached (ppid 30153) +++
[pid 30154] === symbols loaded: './thread' ===
[pid 30154] ==> _start() at 0x080483c0
[pid 30154]    ==> __libc_start_main@plt() at 0x08048380
[pid 30154]       ==> __libc_csu_init() at 0x08048520
[pid 30154]          ==> _init() at 0x08048338
[pid 30154]             ==> call_gmon_start() at 0x080483e4
[pid 30154]             <== call_gmon_start() [eax = 0x0]
[pid 30154]             ==> frame_dummy() at 0x08048470
[pid 30154]             <== frame_dummy() [eax = 0x0]
[pid 30154]             ==> __do_global_ctors_aux() at 0x08048590
[pid 30154]             <== __do_global_ctors_aux() [eax = 0xffffffff]
[pid 30154]          <== _init() [eax = 0xffffffff]
[pid 30154]       <== __libc_csu_init() [eax = 0x80495f8]
[pid 30154]       ==> main() at 0x080484b6 [/home/sato/tracef/sample/thread.c:11] 
[pid 30154]          ==> pthread_create@plt() at 0x080483a0
[pid 30155] +++ thread  30155 attached (ppid 30154) +++
[pid 30154]          <== pthread_create@plt() [eax = 0x0]
[pid 30154]          ==> pthread_join@plt() at 0x08048360
[pid 30155] ==> thread_entry() at 0x08048494 [/home/sato/tracef/sample/thread.c:5] 
[pid 30155]    ==> pthread_self@plt() at 0x080483b0
[pid 30155]    <== pthread_self@plt() [eax = 0xb7efcb90]
[pid 30155]    ==> printf@plt() at 0x08048390
pthread_self()=3085945744
[pid 30155]    <== printf@plt() [eax = 0x1a]
[pid 30155] <== thread_entry() [eax = 0x0]
[pid 30154]          <== pthread_join@plt() [eax = 0x0]
[pid 30154]       <== main() [eax = 0x0]
[pid 30154]       ==> _fini() at 0x080485b8
[pid 30154]          ==> __do_global_dtors_aux() at 0x08048410
[pid 30154]          <== __do_global_dtors_aux() [eax = 0x0]
[pid 30154]       <== _fini() [eax = 0x0]
[pid 30155] +++ thread  30155 detached (ppid 30154) +++
[pid 30154] +++ process 30154 detached (ppid 30153) +++
tracef: done

5. みんなだいすき例外スロー

例外がthrowされて、スタックが巻き戻る例です。

#include <stdio.h>

int c(int i) {
  if (i == 0) throw 0xff;
  return c(--i);
}

void b() { c(3); }

int a() {
  try { b(); } 
  catch(int& e) { return e; }
  return 0;
}

int main() {
  return a();
}

c()が引数 i = 0 で呼ばれたタイミングで例外がスロー(__cxa_throw@plt)され、a()までスタックが巻戻った ことがわかります。a()が0xffをリターンしていることもきちんと表示できています。C++なシンボルのdemangleもできています。

$ ../src/tracef --synthetic -flT ./thread
[pid 30110] +++ process 30110 attached (ppid 30109) +++
[pid 30110] === symbols loaded: './throw' ===
[pid 30110] ==> _start() at 0x080484d0
[pid 30110]    ==> __libc_start_main@plt() at 0x08048458
[pid 30110]       ==> __libc_csu_init() at 0x08048680
[pid 30110]          ==> _init() at 0x08048420
[pid 30110]             ==> call_gmon_start() at 0x080484f4
[pid 30110]             <== call_gmon_start() [eax = 0x2e75d4]
[pid 30110]             ==> frame_dummy() at 0x08048580
[pid 30110]             <== frame_dummy() [eax = 0x0]
[pid 30110]             ==> __do_global_ctors_aux() at 0x080486f0
[pid 30110]             <== __do_global_ctors_aux() [eax = 0xffffffff]
[pid 30110]          <== _init() [eax = 0xffffffff]
[pid 30110]       <== __libc_csu_init() [eax = 0x804982c]
[pid 30110]       ==> main() at 0x0804864c [/home/sato/tracef/sample/throw.cpp:26] 
[pid 30110]          ==> a()() at 0x08048604 [/home/sato/tracef/sample/throw.cpp:16] 
[pid 30110]             ==> b()() at 0x080485f0 [/home/sato/tracef/sample/throw.cpp:11] 
[pid 30110]                ==> c(int)(int i <3>) at 0x080485a4 [/home/sato/tracef/sample/throw.cpp:5] 
[pid 30110]                   ==> c(int)(int i <2>) at 0x080485a4 [/home/sato/tracef/sample/throw.cpp:5] 
[pid 30110]                      ==> c(int)(int i <1>) at 0x080485a4 [/home/sato/tracef/sample/throw.cpp:5] 
[pid 30110]                         ==> c(int)(int i <0>) at 0x080485a4 [/home/sato/tracef/sample/throw.cpp:5] 
[pid 30110]                            ==> __cxa_allocate_exception@plt() at 0x08048468
[pid 30110]                            <== __cxa_allocate_exception@plt() [eax = 0x9e70058]
[pid 30110]                            ==> __cxa_throw@plt() at 0x08048478
[pid 30110]                            ==> __gxx_personality_v0@plt() at 0x080484a8
[pid 30110]                            <== __gxx_personality_v0@plt() [eax = 0x8]
...
[pid 30110]                            ==> __gxx_personality_v0@plt() at 0x080484a8
[pid 30110]                            <== __gxx_personality_v0@plt() [eax = 0x7]
[pid 30110]                            ==> __cxa_begin_catch@plt() at 0x08048498
[pid 30110]                            <== __cxa_begin_catch@plt() [eax = 0x9e70058]
[pid 30110]                            ==> __cxa_end_catch@plt() at 0x08048488
[pid 30110]                            <== __cxa_end_catch@plt() [eax = 0x0]
[pid 30110]          <== a()() [eax = 0xff]
[pid 30110]       <== main() [eax = 0xff]
[pid 30110]       ==> _fini() at 0x08048718
[pid 30110]          ==> __do_global_dtors_aux() at 0x08048520
[pid 30110]          <== __do_global_dtors_aux() [eax = 0x0]
[pid 30110]       <== _fini() [eax = 0x0]
[pid 30110] +++ process 30110 detached (ppid 30109) +++

6. mainの前の処理

main関数が呼ばれるまでにどんな関数が呼ばれるか観察できます

#include <cstdio>
#include <cstddef>

// mainの前に呼ばれる関数3つ

int foo() { return 1; }
int g = foo();

struct bar {
  bar() {}
  ~bar() throw() {}
} g2;

__attribute__((constructor))
void baz() {}

// mainの前で初期化される変数value

template<const char* S, std::size_t L, std::size_t N = 0>
struct strSum_ {
  static const unsigned long value;
};

template<const char* S, std::size_t L, std::size_t N>
const unsigned long strSum_<S, L, N>::value = S[N] + strSum_<S, L, N + 1>::value; // XXX: runtime computation

template<const char* S, std::size_t L>
struct strSum_<S, L, L> {
  static const unsigned long value = 0;
};

// http://www.thescripts.com/forum/thread156880.html
template<typename T, std::size_t L> char (&lengthof_helper_(T(&)[L]))[L];
#define LENGTHOF(array) sizeof(lengthof_helper_(array))

extern const char s[] = "C++0x"; // external linkage 
int main() {
  return (int) strSum_<s, LENGTHOF(s) - 1>::value;
}

foo(), bar(), baz() 関数が、 main()の前に呼ばれる筈で、これらはきちんとトレースできています。メンバ変数valueの初期化もランタイムに起きます(コンパイル時計算になっていません。よくないコードですので真似しないでください ^^;)が、こちらは関数呼び出しによって初期化されるわけではないので残念ながらトレースできていません。

$ ../src/tracef --plt -ClAT ./before_main2
[pid 17098] +++ process 17098 attached (ppid 17097) +++
[pid 17098] === symbols loaded: './before_main2' ===
[pid 17098] ==> _start() at 0x08048370
[pid 17098]    ==> __libc_start_main@plt() at 0x08048358
[pid 17098]       ==> __libc_csu_init() at 0x080485f0
[pid 17098]          ==> _init() at 0x08048310
[pid 17098]             ==> call_gmon_start() at 0x08048394
[pid 17098]             <== call_gmon_start() [eax = 0x2e75d4]
[pid 17098]             ==> frame_dummy() at 0x08048420
[pid 17098]             <== frame_dummy() [eax = 0x0]
[pid 17098]             ==> __do_global_ctors_aux() at 0x08048660
[pid 17098]                ==> global constructors keyed to _Z3foov() at 0x080485ac [/home/sato/tracef-trunk/sample/before_main2.cpp:44] 
[pid 17098]                   ==> __static_initialization_and_destruction_0(int, int)() at 0x08048482 [/home/sato/tracef-trunk/sample/before_main2.cpp:43] 
[pid 17098]                      ==> foo()() at 0x08048444 [/home/sato/tracef-trunk/sample/before_main2.cpp:6] 
[pid 17098]                      <== foo()() [eax = 0x1]
[pid 17098]                      ==> bar::bar()() at 0x080485c8 [/home/sato/tracef-trunk/sample/before_main2.cpp:10] 
[pid 17098]                      <== bar::bar()() [eax = 0x1]
[pid 17098]                      ==> __cxa_atexit@plt() at 0x08048338
[pid 17098]                      <== __cxa_atexit@plt() [eax = 0x0]
[pid 17098]                   <== __static_initialization_and_destruction_0(int, int)() [eax = 0x141]
[pid 17098]                   ==> baz()() at 0x0804844e [/home/sato/tracef-trunk/sample/before_main2.cpp:16] 
[pid 17098]                   <== baz()() [eax = 0x141]
[pid 17098]                <== global constructors keyed to _Z3foov() [eax = 0x141]
[pid 17098]             <== __do_global_ctors_aux() [eax = 0xffffffff]
[pid 17098]          <== _init() [eax = 0xffffffff]
[pid 17098]       <== __libc_csu_init() [eax = 0x80496bc]
[pid 17098]       ==> main() at 0x08048454 [/home/sato/tracef-trunk/sample/before_main2.cpp:41] 
[pid 17098]       <== main() [eax = 0x141]
[pid 17098]       ==> __tcf_0() at 0x0804846e [/home/sato/tracef-trunk/sample/before_main2.cpp:13] 
[pid 17098]          ==> bar::~bar()() at 0x080485ce [/home/sato/tracef-trunk/sample/before_main2.cpp:11] 
[pid 17098]          <== bar::~bar()() [eax = 0x804846e]
[pid 17098]       <== __tcf_0() [eax = 0x804846e]
[pid 17098]       ==> _fini() at 0x08048688
[pid 17098]          ==> __do_global_dtors_aux() at 0x080483c0
[pid 17098]          <== __do_global_dtors_aux() [eax = 0x0]
[pid 17098]       <== _fini() [eax = 0x0]
[pid 17098] +++ process 17098 detached (ppid 17097) +++

制限事項

わかっている不具合を白状します:

$ readelf -h pie_binary | grep Type:
  Type: DYN (Shared object file)

コマンドラインオプション

おすすめは tracef --plt -CflT です。

Usage:

  % tracef [option ...] command [arg ...]
  % tracef [option ...] -p pid

Options:

  -? [ --help ]               

     このヘルプを表示

  -V [ --version ]            

     バージョンを表示して終了

  -o [ --output ] arg  

     トレース結果をstderrではなく、'arg' で指定したファイルに出力する

  --ff

     プロセスあるいはスレッド毎に別のファイルにログを記録する。ログファイル名は
     「-o で指定したファイル名」+「プロセス/スレッドID」となる。

  -f [ --trace-child ]

     fork()やclone()で生成された子プロセス、子スレッドも追跡する。

  --synthetic
  --plt

     合成シンボルもトレースの対象とする。このオプションを使うと、ライブラリ関数呼び出し
     やシステムコール呼び出し(の一部)をトレースすることができる。たとえば、printf@plt() 
     や signal@plt() など。

  -C [ --demangle ]

     C++の低レベルな関数名を可読なものに変換して表示する。

  -t [ --time ]

     出力の各行に現在時刻を付加する。

  -u [ --microseconds ]  

     出力の各行に現在時刻(マイクロ秒単位)を付加する。

  -A [ --arg ] 

     関数の引数名を表示する(EXPERIMENTAL)

  -v [ --arg-val ]

     関数の引数の値を表示する(EXPERIMENTAL, x86のみ)

  -T [ --call-tree ]  

     関数呼び出しをツリー状に表示する。解析対象がマルチプロセス/マルチスレッドの場合、
     -o および --ff オプションと同時に使用することを推奨する。

  --offset arg

     ツリー表示時の関数呼び出しあたりのオフセット量(スペースの数)を指定します。
     デフォルト値は3。

  --no-pid 

     プロセスIDを表示しない。

  -i [ --no-eip ] 

     関数のアドレスを表示しない。

  -l [ --line-numbers ] 

     デバッグ情報を利用し、関数の定義されたファイル名、行番号を表示する。

  -p [ --attach-process ] arg 

     プロセスID 'arg' のプロセスにアタッチする。

  -X [ --exclude ] arg

     関数 'arg' をトレースせず、無視する。複数指定が可能。arg にはmangleされたシン
     ボル名を指定。

仕組み (つぶやき)

気が向いたら日記に書きます。以下メモ。

  switch(...) {
  case X:
     return hoge; // leave-ret になる
  default:
  }
  return fuga();  // 自分以外の関数を呼ぶ。jmp になりがち
} // 関数の終わり

なんだかすごく楽しんだ感あり。

類似ツール

実行ファイル内の、自分で書いた関数の動的なトレースができそうなツール一覧を集めてみました。仕組みをptraceに限定しなければ、いろいろありました。 ptraceベースのツールであるtracefは、速度ではoprofileに負け、視覚化も含めた機能ではcallgrindに負けるので、strace, ltrace のように気楽に使えるツールにすることに重点をおいたつもりです。

callgrindやoprofileはやっぱりすごいです。いろいろ。

TODO

謝辞

不具合の報告

SATO Yusuke <ads01002 _at_ nifty.com> までメール and/or ブログで晒してhatenaのd:id:yupo5656にTB .. など適当な感じでお願いいたします。

ChangeLog

さいごにひとこと

ptraceシステムコールで遊んだことがあまりないなぁとふと思い、作ってみました。すでに類似ツールあるかなぁと思いつつも。 もともと、arm用のLinux上で自分が使うために作りはじめたのですが、x86対応したところで連休が終わったのでとりあえず公開します。 -- 2007.09.16 さとう ゆうすけ



$Id: index.html,v 1.7 2007/10/03 05:27:02 sato Exp $