470 likes | 618 Views
コンパイラの解析 (1). プログラムのリンクと実行. Table of Contents. プログラムはどうやって動くか リンカのコマンド gdb libgcj. プログラムはどうやって動くか. メモリ上にプログラムを展開して、プログラムカウンタをプログラム開始位置に指定する その他、レジスタの初期化、ワークの確保など だれが、どうやって、どんなプログラムをメモリ上に配置する?. プログラムをメモリ上に配置. Hello.exe や a.out などは実行バイナリと呼ばれる 実行ファイルをプログラムローダに渡して、メモリ上に展開してもらう
E N D
コンパイラの解析 (1) プログラムのリンクと実行
Table of Contents • プログラムはどうやって動くか • リンカのコマンド • gdb • libgcj
プログラムはどうやって動くか • メモリ上にプログラムを展開して、プログラムカウンタをプログラム開始位置に指定する • その他、レジスタの初期化、ワークの確保など • だれが、どうやって、どんなプログラムをメモリ上に配置する?
プログラムをメモリ上に配置 • Hello.exeやa.outなどは実行バイナリと呼ばれる • 実行ファイルをプログラムローダに渡して、メモリ上に展開してもらう • ローダにあった実行バイナリを作れば、プログラムは実行できる
コンパイラ・ドライバ • gccやclは「コンパイラ・ドライバ」 • 実行バイナリを生成するところまで一気に行う • コンパイラ・ドライバは次の作業を行う • コンパイル -> コンパイラの役目 • アセンブル -> アセンブラの役目 • リンク -> リンカの役目
コンパイラ・ドライバ (2) • コンパイラの役割 • ソースプログラムを、アセンブルファイルに変換 • アセンブラの役割 • アセンブルファイルをオブジェクトファイルに変換 • リンカの役割 • 複数のオブジェクトファイルをかき集めて実行バイナリに変換
コンパイラの役割 • C言語などのプログラムを、ターゲットマシンのアセンブルプログラムに変換 • 関数名などは解決しない • 高級言語->アセンブル言語へのトランスレータ • gcc –S hello.c • -> hello.s が作成される
コンパイラの作成するコード .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 • どちらも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シンボル • プログラムエントリ • 後述のcrt1.oに含まれる • putsシンボル • 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からプログラムは開始 • ここからmainが呼び出される • ただし、__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 /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 • 共有ライブラリを実行時にロードする • ELF形式のバイナリ • ld -dynamic-linker /lib/ld-linux.so.2 • Linux版のダイナミックローダ • 共有ライブラリを一つでも使用してたら必須 • 今回はputsを使ったので必須
-lc • libc.soというC言語の標準ライブラリをリンク • putsを使うだけでもリンクが必要 • ただし、実体はlibc.soにない • 実際に使われる際に動的にリンクされる • 前掲の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 … puts@plt: jmp *(_GLOBAL_OFFSET_TABLE_+12) 最初は動的リンクを行う リンカを呼び出すプログラム 2回目以降はputsの実体を 呼び出す
/usr/lib/libc_nonshared.a • C言語のプログラムを起動するために必要な処理を静的にプログラムへリンク • 含まれる関数 • __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 Disassembly of section .init: 00000000 <_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: 00000000 <.init>: 0: c9 leave 1: c3 ret • _init()の続き • crti.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 (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でプログラムが停止するようにブレークポイントを設定 (gdb) break _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 • プログラムカウンタ以降の命令を表示 (gdb) x/4i $pc 0x804828c <_start>: xor %ebp,%ebp 0x804828e <_start+2>: pop %esi 0x804828f <_start+3>: mov %esp,%ecx 0x8048291 <_start+5>: and $0xfffffff0,%esp • Examine memory/4Instructions • $pc はプログラムカウンタの位置を保持している
gdb (6) – si • 一命令だけ進める (gdb) si 0x0804828e in _start () • Step Instruction
gdb (7) – display/i $pc • 常に現在の命令を表示 (gdb) display/i $pc 1: x/i $pc 0x804828e <_start+2>: pop %esi • DisplayInstruction
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 • プログラムを終了させる (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の機能を全て実現するには、下記のことも考慮しなければならない • Javaの名前空間とオブジェクトファイルの名前空間 • クラスの登録 • クラスの初期化 • インスタンスの生成 • ポリモーフィズムの実現 • 配列の扱い • インスタンスの破棄 • ガーベジコレクタとの調和 • 例外の処理 • synchronizeの処理
続く • ちょっとしたルールの解析方法 • libgcjを外部から完全に利用するまでの作業 • おそらく全3~5回くらい