C言語がコンパイルされて実行可能になるまでの流れ
コンパイルの処理は大きく分けて下記の処理にわけられる。
この記事では、C言語の Hello World を過程毎に追い、プログラムが出来上がるまでの流れを追う。
登場するファイル
ソースコード
普通の C 言語の Hello World. 下記コードを main.c として保存する。
#include <stdio.h> int main(){ printf("Hello world!\n"); return 0; }
プリプロセス
プリプロセスとは、マクロの展開や #include や #ifdef などのディレクティブの処理が行われることを指す。
ソースファイルのプリプロセス後のソースを見るには以下のコマンドを使う。
$ gcc -E main.c
結果、下記のように大きなファイルが出来上がる。(コメント行と空白行を取り除いてある)
typedef unsigned char __u_char; typedef unsigned short int __u_short; typedef unsigned int __u_int; typedef unsigned long int __u_long; typedef signed char __int8_t; typedef unsigned char __uint8_t; typedef signed short int __int16_t; typedef unsigned short int __uint16_t; typedef signed int __int32_t; typedef unsigned int __uint32_t; typedef signed long int __int64_t; typedef unsigned long int __uint64_t; typedef long int __quad_t; typedef unsigned long int __u_quad_t; ... extern FILE *popen (__const char *__command, __const char *__modes) ; extern int pclose (FILE *__stream); extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__)); extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)); extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ; extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)); int main(){ printf("Hello world!\n"); return 0; }
このファイルは空行とコメント行を取り除いても 344 行もあった。
コンパイル
プリプロセスが終わったら、晴れて次の作業でアセンブラに変換される。
アセンブラソースに変換された時点で処理を止めるには、-E オプションをつける。すると、拡張子が s のファイルが出来上がる。
この処理はコンパイラが行ういくつかの内、「コンパイル」と呼ばれているので「狭義のコンパイル」と呼ばれる。
$ gcc -S main.c
ちなみに、上のプリプロセスの段で表示されたプリプロセス後のソースを適当なファイルに保存し、それを gcc -S しても結果は変わらない。
$ gcc -E main.c > preprocess.c $ gcc -S preprocess.c # 上記コマンドと同一結果(ファイル名は異なる)
さて、アセンブラに変換されたソースを見てみよう。
"Hello world!" という文字列にラベルが付けられ、puts 関数の呼び出し時に使用されていることがわかる。
$ cat main.s .file "main.c" .section .rodata .LC0: .string "Hello world!" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $.LC0, %edi call puts movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3" .section .note.GNU-stack,"",@progbits
ここで、gcc のオプションをあれこれつけてアセンブラソースを見てみると結果が変わるのが見て取れる。
例えばデバッグオプション -g. 上記のアセンブラは 26 行だったのが、デバッグオプションをつけると 226 行に膨れ上がる。
$ gcc -g -S main.c $ cat main.s .file "main.c" .text .Ltext0: .section .rodata .LC0: .string "Hello world!" .text .globl main .type main, @function main: .LFB0: .file 1 "main.c" ... .LASF10: .string "/home/user/work" .LASF7: .string "char" .ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3" .section .note.GNU-stack,"",@progbits
アセンブル
狭義のコンパイルで得た main.s をアセンブルし、オブジェクトファイル(バイナリ)の生成を行う。
アセンブラソースファイルからオブジェクトファイルの生成は、as コマンドで行う。
下記コマンドでは -o オプションで出力ファイル名を main.o と指定している。指定しなかった場合、a.out という名前になる。この a.out は Assembler Output の略。
$ as -o main.o main.s
as コマンドを通したら、実行はできないもののもうバイナリです。
$ file main.o main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
$ hexdump -C main.o 00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 00000010 01 00 3e 00 01 00 00 00 00 00 00 00 00 00 00 00 |..>.............| 00000020 00 00 00 00 00 00 00 00 30 01 00 00 00 00 00 00 |........0.......| 00000030 00 00 00 00 40 00 00 00 00 00 40 00 0d 00 0a 00 |....@.....@.....| 00000040 55 48 89 e5 bf 00 00 00 00 e8 00 00 00 00 b8 00 |UH..............| 00000050 00 00 00 5d c3 00 00 00 48 65 6c 6c 6f 20 77 6f |...]....Hello wo| 00000060 72 6c 64 21 00 00 47 43 43 3a 20 28 55 62 75 6e |rld!..GCC: (Ubun| 00000070 74 75 2f 4c 69 6e 61 72 6f 20 34 2e 36 2e 33 2d |tu/Linaro 4.6.3-| 00000080 31 75 62 75 6e 74 75 35 29 20 34 2e 36 2e 33 00 |1ubuntu5) 4.6.3.| 00000090 14 00 00 00 00 00 00 00 01 7a 52 00 01 78 10 01 |.........zR..x..| 000000a0 1b 0c 07 08 90 01 00 00 1c 00 00 00 1c 00 00 00 |................| 000000b0 00 00 00 00 15 00 00 00 00 41 0e 10 86 02 43 0d |.........A....C.| 000000c0 06 50 0c 07 08 00 00 00 00 2e 73 79 6d 74 61 62 |.P........symtab| 000000d0 00 2e 73 74 72 74 61 62 00 2e 73 68 73 74 72 74 |..strtab..shstrt| 000000e0 61 62 00 2e 72 65 6c 61 2e 74 65 78 74 00 2e 64 |ab..rela.text..d| 000000f0 61 74 61 00 2e 62 73 73 00 2e 72 6f 64 61 74 61 |ata..bss..rodata| 00000100 00 2e 63 6f 6d 6d 65 6e 74 00 2e 6e 6f 74 65 2e |..comment..note.| 00000110 47 4e 55 2d 73 74 61 63 6b 00 2e 72 65 6c 61 2e |GNU-stack..rela.| 00000120 65 68 5f 66 72 61 6d 65 00 00 00 00 00 00 00 00 |eh_frame........| 00000130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| ...
バイナリから情報を得るには objdump, readelf が大活躍。
$ objdump --header main.o main.o: file format elf64-x86-64 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000015 0000000000000000 0000000000000000 00000040 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 1 .data 00000000 0000000000000000 0000000000000000 00000058 2**2 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000000 0000000000000000 0000000000000000 00000058 2**2 ALLOC 3 .rodata 0000000d 0000000000000000 0000000000000000 00000058 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .comment 0000002b 0000000000000000 0000000000000000 00000065 2**0 CONTENTS, READONLY 5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000090 2**0 CONTENTS, READONLY 6 .eh_frame 00000038 0000000000000000 0000000000000000 00000090 2**3 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
$ readelf --syms main.o Symbol table '.symtab' contains 11 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5 6: 0000000000000000 0 SECTION LOCAL DEFAULT 7 7: 0000000000000000 0 SECTION LOCAL DEFAULT 8 8: 0000000000000000 0 SECTION LOCAL DEFAULT 6 9: 0000000000000000 21 FUNC GLOBAL DEFAULT 1 main 10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
objdump コマンドで逆アセンブルなんぞも可能。
$ objdump -d main.o main.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <main>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: bf 00 00 00 00 mov $0x0,%edi 9: e8 00 00 00 00 callq e <main+0xe> e: b8 00 00 00 00 mov $0x0,%eax 13: 5d pop %rbp 14: c3 retq
リンク
ではいよいよこのオブジェクトファイルを実行可能にしてやりましょう。
リンクとは、必要なライブラリとの結合を行い、実行可能な形式のファイルを生成する処理のこと。
$ gcc -o main main.o
リンクを行うと、晴れて実行可能なバイナリのできあがり。改めて過程を眺めたあと動くのを見ると喜びもひとしおというもの。
$ ./main Hello world!
ldd で依存ライブラリを見る。
$ ldd main linux-vdso.so.1 => (0x00007fff67ea4000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd7722a7000) /lib64/ld-linux-x86-64.so.2 (0x00007fd772688000)
ld-linux-x86-64.so.2 はリンクローダーと呼ばれるもので、実行時に依存ライブラリをメモリ上に持ってきて結合してくれるもの。
libc.so は定番中の定番の C ライブラリ。
linux-vdso.so は Linux 上で動くバイナリに付与される仮想的なライブラリ。実体は存在しない。高速化のために存在しているらしい。
まとめ
ソースコードが実行可能になるまでの過程を一度見ておくと色々捗る
- 作者: 江添亮
- 出版社/メーカー: KADOKAWA
- 発売日: 2015/09/25
- メディア: 単行本
- この商品を含むブログ (2件) を見る