コンパイルの一連の流れ

gccのオプションをフル活用して、C言語のソースコードをコンパイルして実行ファイルを生成するまでの一連の流れについて書いてみます。


普段、何気なくhello,worldのコードをコンパイルするときは


$ gcc hello.c

$ ls
a.out hello.c

とやります。(-oをつけれれば実行ファイル名を指定できますが。。)

a.outは

「assembler output」

の略だそうです。

ではassemblerとは何なのでしょうか。このあたりをきちんと理解するためには、しっかりとコンパイルの流れを理解しなくてはいけないようです。

コンパイルの厳密な流れを追ってみます。

1:ソースファイルの作成

vimでもemacでも何でもいいので、テキストエディタでソースコードを編集します。



2:プリプロセサを用いて、/usr/include以下においてあるソースファイルにヘッダファイル(インクルードファイル)をインクルードする。

$ gcc -E hello.c > hello.i

これにより生成したhello.iはさまざまな関数の定義がインクルードされているため、900行近いコードとなっている。



3:コンパイラによって、プリプロセッサによってヘッダファイルをインクルードされた状態のソースコードをコンパイルして、アセンブラプログラムを生成する。アセンブラプログラムとは、機械語と1対1で対応した最も低級な言語のことを言う。狭義にコンパイルという場合、このアセンブルコードを生成する過程のことをコンパイルと呼ぶ。

$ gcc -S hello.i

これによりたった20行からなるアセンブラコードが生成するわけである。




4:アセンブラ言語で書かれたアセンブラコードをアセンブルして、機械語(オブジェクトファイル。まだ、実行ファイルとは呼べない。)に翻訳する。

$ gcc -c hello.s

これにより、オブジェクトファイルhello.oが生成される。



5:最後はオブジェクトファイル同士をリンクさせる必要がある。c言語は関数の塊である。インクルードファイルでインクルードされるのはあくまで関数の宣言のみであり、関数本体のオブジェクトファイルはlibc.aというオブジェクファイルの中に書いてある。4で生成したオブジェクトファルhello.oの中には、main関数は記述してあるが、printf()関数本体の記述がないため、libc.a内に書いてあるprintf()本体の機械語による記述をhello.oと”リンク”させてあげる必要がある。(#このように頻繁に使用する関数を標準関数とよび、結局最初からアセンブルされて機械語の形式でリンク待ちになっているわけですね。いちいちアセンブルするのは二度手間、三度手間、百度手間ですからね)

$ gcc hello.o

ちなみに、libc.aの置いてある場所は、
$ locate libc.a | grep libc.a
/usr/lib/i386-linux-gnu/libc.a
/usr/lib/i386-linux-gnu/xen/libc.a

である。


6:このような一連のプリプロセス、コンパイル、アセンブル、リンクの過程を経て生成したのが、a.outとう実行ファイルである。これを実行するには、フルパスで指定してあげればよく(カレントディレクトリのパスが通っていることはまずないだろうから)

$ /a.out
Hello, world!!


以上がソースコードから実行ファイルができるまでの一連の流れである。
この流れが理解できれば、分割コンパイルの仕組みも比較的容易に理解できると思う。