補助ツール

  • バイナリからの情報の取り出し
  • タグジャンプ

バイナリからの情報の取り出し

概要

目的によっては、バイナリファイルを調べることで、ビルドプロセスを読み解く手間を除くことができます。

$ cat z.c
#if FOO
int foo() { return 0; }
#else
int foo() { return 1; }
#endif
int main () { return foo(); };

mainの返り値を知るにはFOOがビルドプロセスのどこかで定義されているかどうかを調べる必要がある。

nmの概要

  • バイナリから関数名、変数名(シンボル)を取り出すことができます。
  • 型の名前は取り出せません。
$ nm [-D] バイナリファイル
アドレス クラス シンボル
アドレス クラス シンボル
...

$ nm -D /lib64/libc.so.6 | grep ' fprintf$'
000000366fe4f2e0 T fprintf

主要なシンボルクラス

CLASS DESCRIPTION
T,t Text(Program code)
D,d Data with initial value
B,b BSS(Data with no initial value)
U Undefind
  • 大文字はグローバルスコープ(リンクすることで外部から参照可能)
  • ローカルスコープ(外部から参照できない)

オブジェクトファイルのスコープとC言語のスコープ

int X = 1;
static int S = 2;

int
main(int argc, char** argv)
{
      int l = 3;
      static int s = 4;

      return X + S + argc + l + s;
}
$ gcc -O0 foo.c
$ nm a.out
...
0000000000600870 d S
000000000060086c D X
000000000040049c T main
0000000000600874 d s.1712
...

nmの-Dオプション

  • -Dオプションで動的ライブラリのリンクにかかわるシンボル(dynamic symbols)を表示する。
  • 動的ライブラリのリンクにかかわらないシンボル(normal symbol)はrpmbuildがパッケージ作成時にstripしてしまう。
  • debuginfoパッケージがインストールされている場合、-Dなしでnmを実行するとnormal symbolについても表示できる。
$ cat foo.c
int main(void) {return 0;}
$ gcc foo.c
$ nm a.out | grep main
nm a.out | grep main
                 U __libc_start_main@@GLIBC_2.2.5
000000000040049c T main
$ strip a.out
$ nm a.out | grep main
nm: a.out: no symbols

objdumpによる逆アッセンブル

$ objdump [-S] -d バイナリファイル
アドレス <関数名>:
        マシンコード                   オプコード オペランド
        ...
 ....
  • debuginfoパッケージがインストールされている場合 -S オプションをつけるとソースコードをインラインで表示できる。

逆アッセンブル出力の例

int foo (void) { return 0; }
int main(void) { return foo(); }
$ gcc -O0 foo.c
$ objdump -d  a.out
...
000000000040049c <foo>:
  40049c:       55                      push   %rbp
  40049d:       48 89 e5                mov    %rsp,%rbp
...
00000000004004a7 <main>:
  4004a7:       55                      push   %rbp
  4004a8:       48 89 e5                mov    %rsp,%rbp
  4004ab:       e8 ec ff ff ff          callq  40049c <foo>
...
$ objdump [-S] -d バイナリファイル
アドレス <関数名>:
        マシンコード                   オプコード オペランド
        ...
 ....
  • debuginfoパッケージがインストールされている場合 -S オプションをつけるとソースコードをインラインで表示できる。

大雑把な逆アッセンブル出力の読み方

  • %で始まる名前はレジスタの名前
  • callqは関数呼び出し (x86_64)
  • $0xで始まる文字列は即値
  • jで始まるオプコードはたぶんジャンプ (x86_64)
  • retqはC言語で言うところのreturn
  • *はC言語と同じく参照
  • # は objdumpによるコメント
  • <関数名+アドレス> は 関数の定義された箇所からのオフセット

最適化の影響

  • rpmbuildは -O2 オプションを指定してgccを呼び出す。

    $ rpm --showrc | grep -e -O2
    -14: __global_cflags    -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 \\
    -fexceptions -fstack-protector --param=ssp-buffer-size=4 \\
    %{_hardened_cflags}
    
  • コンパイラの最適化によって呼び出し関係がソースコードの通りでなくなることに注意する。

  • あくまでソースコード読解の補助に使う。

最適化の影響の例

int foo (void) { return 0; }
int main(void) { return foo(); }
$ gcc -O2 foo.c
$ objdump -d a.out
...
0000000000400390 <main>:
400390: 31 c0                   xor    %eax,%eax
400392: c3                      retq
400393: 90                      nop
...

関数fooの呼び出しがインライン展開されてしまっている。

タグジャンプ

概要

  • ソースコードから関数名や変数名などをその定義箇所とともに収集してタグファイルに記録しておく。
  • エディタでタグファイルを読み込んでおくと、名前を指定することでその定義をいつでもすぐに見ることができる。
  • エディタと組合せて使うのが前提となる。

emacsの場合(1)

etagsを含むemacsパッケージをインストールする

# yum -y install emacs

etagsコマンドにソースコードファイルの名前を与えてTAGSファイルを作る

$ find . -type f -name '*.[ch]' | etags -

emacsにTAGSファイルを読み込ませる:

M-x visit-tags-table
Visit tags table (default TAGS): TAGSファイルへのパス

emacsの場合(2)

「名前」の定義へジャンプする:

M-.
Find tag (default M-.): 名前

同じ名前の異なる定義へジャンプしたい場合:

C-u M-.

ジャンプ前の場所に戻りたい場合(繰り返し実行可能):

M-*

viの場合(1)

ctagsコマンドにソースコードファイルの名前を与えてtagsファイルを作る

$ find . -type f -name '*.[ch]' | ctags -L -

viにTAGSファイルを読み込ませる:

:set tags=tagsファイルへのパス

viの場合(2)

「名前」の定義へジャンプする:

:tag 名前

カーソルの直下に「名前」がある場合:

C-]

複数の定義がある場合のジャンプ先の選択

g C-]

ジャンプ前の場所に戻りたい場合(繰り返し実行可能):

C-t

ジャンプの履歴(スタック)の表示:

:tags

ドキュメント

emacs

M-x info-emacs-manulのTagsの項目に詳細な説明があります。

vi

:helpのtagsrch.txtの項目に詳細な説明があります。