コンパイラの解析 (1) プログラムのリンクと実行
Table of Contents プログラムはどうやって動くか リンカのコマンド gdb libgcj
プログラムはどうやって動くか メモリ上にプログラムを展開して、プログラムカウンタをプログラム開始位置に指定する その他、レジスタの初期化、ワークの確保など だれが、どうやって、どんなプログラムをメモリ上に配置する?
プログラムをメモリ上に配置 Hello.exeやa.outなどは実行バイナリと呼ばれる 実行ファイルをプログラムローダに渡して、メモリ上に展開してもらう ローダにあった実行バイナリを作れば、プログラムは実行できる
コンパイラ・ドライバ gccやclは「コンパイラ・ドライバ」 コンパイラ・ドライバは次の作業を行う 実行バイナリを生成するところまで一気に行う コンパイラ・ドライバは次の作業を行う コンパイル -> コンパイラの役目 アセンブル -> アセンブラの役目 リンク -> リンカの役目
コンパイラ・ドライバ (2) コンパイラの役割 アセンブラの役割 リンカの役割 ソースプログラムを、アセンブルファイルに変換 アセンブルファイルをオブジェクトファイルに変換 リンカの役割 複数のオブジェクトファイルをかき集めて実行バイナリに変換
コンパイラの役割 C言語などのプログラムを、ターゲットマシンのアセンブルプログラムに変換 gcc –S hello.c 関数名などは解決しない 高級言語->アセンブル言語へのトランスレータ gcc –S hello.c -> hello.s が作成される
コンパイラの作成するコード 一部省略 int main(int argc, char** argv) { .LC0: .string "Hello, world!" main: pushl %ebp movl %esp, %ebp subl $8, %esp andl $-16, %esp subl $28, %esp pushl $.LC0 call puts leave ret int main(int argc, char** argv) { puts("Hello, world!"); } 一部省略
アセンブラの役割 アセンブルプログラムをオブジェクトコードに変換 gcc –c hello.s as –o hello.o hello.s 同一ファイル内のシンボルはここで解決できる ファイルをまたぐシンボルはここでは解決しない gcc –c hello.s as –o hello.o hello.s どちらもhello.oを作成
オブジェクトファイルの解析 objdumpコマンドが便利 例 objdump –t hello.o : シンボルを表示 objdump –d hello.o : プログラムを逆アセンブル 例 objdump –d hello.o
objdump –d hello.o 未解決なので シンボルテーブルを参照している $ objdump -d hello.o hello.o: file format elf32-i386 Disassembly of section .text: 00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: 83 ec 1c sub $0x1c,%esp c: 68 00 00 00 00 push $0x0 11: e8 fc ff ff ff call 12 <main+0x12> 16: c9 leave 17: c3 ret 未解決なので シンボルテーブルを参照している
リンカの役割 オブジェクトコードをまとめて実行バイナリにする シンボルはこの時点で全て解決する gcc hello.o ld hello.o
リンクエラー ld hello.o だとリンクできない! リンクにはシンボルの全ての情報が必要 $ ld hello.o ld: warning: cannot find entry symbol _start; defaulting to 08048094 hello.o(.text+0x12): In function `main': : undefined reference to `puts' リンクにはシンボルの全ての情報が必要
リンクに必要なもの _startシンボル putsシンボル プログラムエントリ 後述のcrt1.oに含まれる C言語標準関数 後述のlibc.so.6に含まれる
リンカのコマンド ld /usr/lib/crt1.o \ hello.o \ -dynamic-linker /lib/ld-linux.so.2 \ -lc \ /usr/lib/crti.o /usr/lib/crtn.o hello.oをリンクして実行可能にするだけで、これだけのものが必要
リンカのコマンド (1) ld /usr/lib/crt1.o \ hello.o \ -dynamic-linker /lib/ld-linux.so.2 \ -lc \ /usr/lib/crti.o /usr/lib/crtn.o
crt1.o (1) プログラムエントリのための_startを含む mainではなく_startからプログラムは開始 ただし、__libc_start_mainを経由 $ objdump -t /usr/lib/crt1.o | grep main 00000000 *UND* 00000000 main 00000000 *UND* 00000000 __libc_start_main
mainが無いときのエラー $ gcc nomain.c : undefined reference to `main‘ /usr/lib/crt1.o(.text+0x18): In function `_start': : undefined reference to `main‘ crt1.oをリンクする際のエラーなので、初心者には不親切?
crt1.o (2) 次の2つも呼び出す (どちらもlibcが持つ) プログラムの初期化、終了処理に使える __libc_csu_init: 実行前に呼び出す __libc_csu_fini: 実行後に呼び出す プログラムの初期化、終了処理に使える これらもリンクしないと実行できない
リンカのコマンド (2) ld /usr/lib/crt1.o \ hello.o \ -dynamic-linker /lib/ld-linux.so.2 \ -lc \ /usr/lib/crti.o /usr/lib/crtn.o
ld-linux.so.2 共有ライブラリを実行時にロードする ld -dynamic-linker /lib/ld-linux.so.2 ELF形式のバイナリ ld -dynamic-linker /lib/ld-linux.so.2 Linux版のダイナミックローダ 共有ライブラリを一つでも使用してたら必須 今回はputsを使ったので必須
-lc libc.soというC言語の標準ライブラリをリンク 実際に使われる際に動的にリンクされる putsを使うだけでもリンクが必要 前掲のld-linux.so.2の仕事
libc.soの実体 実はただのリンカスクリプト /lib/libc.so.6 の動的リンク /usr/libc_nonshared.aの静的リンク /* GNU ld script Use the shared library, but some functions are only in the static library, so try that secondarily. */ OUTPUT_FORMAT(elf32-i386) GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a )
/lib/libc.so.6 標準関数の実体を持つライブラリ $ objdump -T /lib/tls/libc.so.6 | grep puts … 00508980 w DF .text 000001be GLIBC_2.0 puts
動的シンボル解決 Linux/i386では、シンボルを動的に解決するためのコードが自動で挿入される call puts@plt … jmp *(_GLOBAL_OFFSET_TABLE_+12) 最初は動的リンクを行う リンカを呼び出すプログラム 2回目以降はputsの実体を 呼び出す
/usr/lib/libc_nonshared.a 含まれる関数 __libc_csu_init _initを呼び出す __libc_csu_fini _finiを呼び出す そのほかにも色々と
リンカのコマンド (3) ld /usr/lib/crt1.o \ hello.o \ -dynamic-linker /lib/ld-linux.so.2 \ -lc \ /usr/lib/crti.o /usr/lib/crtn.o
crti.o, crtn.o _init, _finiを解決する __libc_csu_(init|fini)から呼び出される
_init()@crti.o callに続きが無い Disassembly of section .init: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: e8 fc ff ff ff call 7 <_init+0x7> callに続きが無い これだとハングアップする?
crtn.oの意味 crtn.oの.initセクションをダンプしてみる Disassembly of section .init: 0: c9 leave 1: c3 ret _init()の続き crti.oと組み合わさって一つの関数init()
セクションのマジック セクションの結合 ld … crti.o crtn.o の順に並べると_init()は一つの関数として完成する 複数のオブジェクトにまたがる同一セクションは、リンカによって1箇所にまとめられる コマンドラインに指定した順序を保持する ld … crti.o crtn.o の順に並べると_init()は一つの関数として完成する crti.oとcrtn.oの間に.initセクションを持つオブジェクトをはさめば、_init()に任意のコードを追加できる
セクション オブジェクトコードはセクションごとにプログラムやデータを配置する セクションごとにまとめてメモリ上に配置される .text Read only, Executable, Initialized プログラムを配置する .data Read/Write, Initialized 初期化するデータ(グローバル変数など) .bss Read/Write 実行時に割り当てられるデータ(スタック) セクションごとにまとめてメモリ上に配置される
gdb (1) 実行バイナリの解析はGNU Debuggerが便利 プログラムの挙動を1命令ずつ追える gdb a.out ソースコードが手元になくても気合でトレースできる gdb a.out
gdb (2) – start 起動するとプロンプトが表示されて停止 $ gdb a.out GNU gdb Red Hat Linux (6.3.0.0-1.132.EL4rh) … (gdb) 起動するとプロンプトが表示されて停止 (gdb) 以降にgdbのコマンドを書く
gdb (3) – break _start _startでプログラムが停止するようにブレークポイントを設定 Breakpoint 1 at 0x804828c
gdb (4) – run プログラムを開始する (gdb) run Starting program: /home/arakawa/tmp/a.out Breakpoint 1, 0x0804828c in _start () 先ほど設定したブレークポイントにヒット
gdb (5) – x/i $pc プログラムカウンタ以降の命令を表示 Examine memory/4 Instructions 0x804828c <_start>: xor %ebp,%ebp 0x804828e <_start+2>: pop %esi 0x804828f <_start+3>: mov %esp,%ecx 0x8048291 <_start+5>: and $0xfffffff0,%esp Examine memory/4 Instructions $pc はプログラムカウンタの位置を保持している
gdb (6) – si 一命令だけ進める (gdb) si 0x0804828e in _start () Step Instruction
gdb (7) – display/i $pc 常に現在の命令を表示 Display Instruction 1: x/i $pc 0x804828e <_start+2>: pop %esi Display Instruction
gdb (8) – example こんな感じで次々と追える 0x080482a8 in _start () 1: x/i $pc 0x80482a8 <_start+28>: call 0x804827c (gdb) x/i 0x804827c 0x804827c: jmp *0x8049490 (gdb) x/2i *0x8049490 0x8048282: push $0x8 0x8048287: jmp 0x804825c (gdb) x/2i 0x804825c 0x804825c: pushl 0x8049484 0x8048262: jmp *0x8049488 (gdb) x/i *0x8049488 0x4a6b90 <_dl_runtime_resolve>: push %eax
Gdb (9) – q プログラムを終了させる Quit (gdb) q The program is running. Exit anyway? (y or n) y Quit
シンボル解決 シンボルはリンカが解決する 下記のようなプログラムでも“コンパイル”は可能 リンカが動くまでにシンボルが揃っていればよい int main(int argc, char** argv) { puts("Hello, world!"); }
libgcj GNU Java Compiler (gcj)が使用するJavaの実行時ライブラリ Java VM + Java APIをコンパイルしたもの これを外側から使用すれば、Javaコンパイラの作成が可能
java.lang.Math.sinの外部利用 (1) ちょっとしたルールさえ知っていれば、JavaのAPIをC言語からも使える 例:sin.c double _ZN4java4lang4Math3sinEd(double); int main() { printf("sin(3.14) = %lf\n", _ZN4java4lang4Math3sinEd(3.14)); }
java.lang.Math.sinの外部利用 (2) 実行例 で、ちょっとしたルールって? $ gcc sin.c -lgcj $ ./a.out sin(3.14) = 0.001593
libgcjの利用にあたって Javaの機能を全て実現するには、下記のことも考慮しなければならない クラスの登録 クラスの初期化 インスタンスの生成 ポリモーフィズムの実現 配列の扱い インスタンスの破棄 ガーベジコレクタとの調和 例外の処理 synchronizeの処理
続く ちょっとしたルールの解析方法 libgcjを外部から完全に利用するまでの作業 おそらく全3~5回くらい