コンパイルの4段階
gcc は内部で4つの工程を順に実行しています。まず前処理(プリプロセス)で #include や #define を展開し、次にコンパイルでC言語をアセンブリへ翻訳、続いてアセンブルで機械語のオブジェクトファイル(.o)へ変換し、最後にリンクで必要なライブラリと結合して実行ファイルを完成させます。-E で前処理だけ、-S でアセンブリまで、-c でアセンブルまで(.o生成)と、途中段階で止めることもできます。各段階を意識すると、エラーがどの工程で起きたか切り分けやすくなります。
gcc hello.c -o hello と打つと一瞬で実行ファイルができますが、その内部では実は4つの工程が順番に走っています。gcc は1枚岩のプログラムではなく、複数の専用ツールをまとめて呼び出す「指揮者」のような存在で、ソースを実行ファイルにするまでを、前処理・コンパイル・アセンブル・リンクという4段階に分けて進めます。普段はこの4つが裏で連続して実行されるため意識しませんが、各段階が何をしているかを知っておくと、エラーがどの工程で起きたのかを切り分けられるようになり、トラブル対応が格段に楽になります。ここでは、ソースが実行ファイルへ姿を変えていく過程を、一段ずつたどっていきます。
第1段階 前処理(プリプロセス)
最初の工程は前処理で、プリプロセッサが担当します。ここではコンパイルの本番に入る前の「下ごしらえ」が行われます。具体的には、#include で指定したヘッダファイルの中身をその場に展開し、#define で定義したマクロを実際の値や式へ置き換え、#ifdef などの条件で不要な部分を取り除きます。重要なのは、この段階ではまだCの文法を解釈しておらず、あくまでテキストとしての置き換え・展開を行っているだけだという点です。たとえば #include <stdio.h> という1行は、stdio.h の中身そっくりそのままに差し替えられます。この段階の結果だけを見たいときは gcc -E hello.c と打ちます。-E は前処理だけで止めるオプションで、出力は通常そのまま画面に流れます。ヘッダを展開した結果なので、たった数行のソースが数百行に膨らんでいるのが分かります。マクロが期待通りに展開されているか確かめたいときにも、この -E は役立ちます。
第2段階 コンパイル(狭義)
下ごしらえの済んだコードを、次にCの文法に従ってアセンブリ言語へ翻訳します。これが狭い意味でのコンパイルです。コンパイルという言葉は、広義にはソースから実行ファイルを作る一連の流れ全体を指しますが、狭義にはまさにこのC言語をアセンブリへ翻訳する工程を指します。アセンブリ言語は、機械語に非常に近い、CPUの命令を人間がかろうじて読める形で書いたものです。この工程では、変数や式や制御構造(if や for など)が、レジスタ操作やジャンプ命令といった低レベルの手続きへ落とし込まれます。最適化(-O2 などの指定)が効くのも主にこの段階で、コンパイラが処理を組み替えて速く・小さくしようとします。文法エラー(セミコロンの欠落や型の不一致など)の多くも、この段階で検出されます。アセンブリまでで止めたいときは gcc -S hello.c とします。-S を付けると hello.s というアセンブリのテキストファイルが生成され、中を見るとCPU向けの命令が並んでいるのが確認できます。
第3段階 アセンブル
アセンブリ言語のテキストを、CPUが直接解釈できる機械語へ変換するのがアセンブルです。担当するのはアセンブラというツールです。この工程で生成されるのがオブジェクトファイルで、拡張子は .o です。オブジェクトファイルの中身はすでに機械語になっていますが、まだ単体では実行できません。なぜなら、printf のように他の場所(ライブラリ)にある関数の実体がまだ結び付いておらず、参照だけが空欄のまま残っているからです。この段階で止めるには gcc -c hello.c と打ちます。-c はアセンブルまで(=オブジェクトファイル生成まで)で止めるオプションで、実行すると hello.o ができます。大きなプログラムをソースごとに .o へ分けてビルドする手法は、この工程を個別に行うことで成り立っています。
第4段階 リンク
最後の工程がリンクです。コンパイルで生成された複数のオブジェクトファイルどうしや、printf などライブラリ側にある関数の実体を結びつけて、ようやく1つの実行ファイルを完成させます。この作業を行うプログラムをリンカと呼びます。第3段階で空欄のまま残っていた関数の参照が、ここで実際のアドレスへと埋められ、すべてのピースがつながって動かせる状態になります。たとえば標準ライブラリにある printf の実体は、このリンクの段階で実行ファイルへ結び付けられます。printf のような標準Cライブラリの関数は gcc が自動でリンクしてくれますが、数学関数のように別のライブラリに分かれているものは、リンクするライブラリを自分で指定する必要があります(この指定方法は後の段階で扱います)。普段の gcc hello.c -o hello は、前処理からこのリンクまでの4段階を一気通貫で実行し、途中の中間ファイルは内部で使い捨てて、最終成果物である実行ファイルだけを手元に残してくれているわけです。
各段階を意識すると何が良いか・よくある失敗
4段階を理解しておく最大の利点は、エラーメッセージの切り分けです。たとえば「ヘッダが見つからない」という類のエラーは前処理の段階、「型が合わない」「文法が変だ」というエラーはコンパイルの段階、そして undefined reference という未定義参照のエラーはリンクの段階で起きています。とくにこの undefined reference は初学者を悩ませる定番で、「関数を呼んでいるのに、その実体をリンクできていない」というリンク段階の失敗を示します。原因はライブラリの指定漏れや、必要な .o を渡し忘れていることが大半です。エラーがどの工程のものかが分かれば、見るべき場所が絞れます。各段階の中間ファイルを観察したいときは、オブジェクトファイルの中身を nm でシンボル(関数や変数の名前)一覧として、あるいは objdump で逆アセンブルして確認できます。コンパイルがブラックボックスではなく4つの明確な工程の連なりだと分かると、ビルドのトラブルにずっと冷静に向き合えるようになります。