コンパイラは何をしているのか

高級言語で書かれたプログラムの実行形式への変換

一般に、CやC++、Pascalなどの高級言語で書かれたプログラムは、 いくつかの手順を経て実行形式に変換される。 具体的な手順は使用するシステムや言語、コンパイラなどによって異なるが、 一般的には以下のようなものとなる。

前処理(preprocess)

プログラミング言語によっては、 ソースプログラムに対して前処理を行うようになっているものがある (CやC++には前処理があるが、Pascalには一般に前処理がない)。 そのような言語では、プリプロセッサ(preprocessor)によって 前処理を行うことにより、ソースプログラムがコンパイラによって コンパイルできる形式に変換される。

コンパイル(compile)

ソースプログラム(あるいは、前処理されたソースプログラム)は、 コンパイラ(compiler)によってコンパイルされる。 コンパイルされた結果は、アセンブリ言語で書かれたプログラムである。

アセンブル(assemble)

アセンブリ言語で書かれたプログラムは、アセンブラ(assembler)によって 機械語(マシンコード)で表現されたプログラムに変換される。 このプログラムのことを、オブジェクトプログラム(object program)という。

連結編集(linkage edit)

(複数の)オブジェクトプログラムは、リンケージエディタ(linkage editor)に よって連結編集され、1つの実行可能なプログラムにまとめられる。 最終的に出来上がったプログラムのことを、実行形式(executable)という。 連結編集のことはリンク(link)ともいい、リンケージエディタのことを リンカ(linker)ということもある。

実行形式のプログラムを実行すると、そのファイルの内容がオペレーティング システム(あるいは、ローダ(loader)というプログラム)によって主記憶に コピーされ(その処理をロード(load)という)、 実際にプログラムが動く。

オブジェクトプログラムが(一般に)アセンブリ言語で書かれたプログラムを アセンブルしたものであるということからもわかるとおり、 オブジェクトプログラムでは、さまざまな主記憶上の番地にアクセスする ことになる。具体的には、関数を呼び出したり、変数を参照したり、 条件分岐をするようなときには、主記憶上の番地がわかっている必要がある。 一般に、これらの参照先には名前がつけられている。 この名前のことを、記号(symbol)という。 オブジェクトプログラムには、どの記号がオブジェクトプログラム中の どの番地にあるかを示す表が含まれている。 の表のことを、記号表(symbol table)という。

連結編集では、単に複数のオブジェクトプログラムを1つにまとめるだけではなく、 記号表を元にオブジェクトプログラムを書き換えるという処理も行う。 例えば、あるオブジェクトプログラムAが別の オブジェクトプログラムBの中にある関数fを呼び出しているとする。 この場合、リンケージエディタは、オブジェクトプログラムBの記号表を 参照して関数fの番地がいくつになるのかを調べ、 その番地を呼び出すようにオブジェクトプログラムAの 該当部分を書き換える。 このような処理の結果、 番地の書き換えが行われずに残ってしまった記号は、 未定義記号(undefined symbol)となる。 連結編集の段階で未定義記号が残ると、実行形式を完成させることができず、 エラーとなる。


gccコマンドの動き

みなさんがコンパイラだと思っている gccは、 実は、コンパイラドライバ(compiler driver)と呼ばれるプログラムであり、 Cなどのプログラミング言語で書かれたソースプログラムから実行形式を 作り出すための処理を行う。

gccは、「必要に応じてコンパイラや アセンブラ、リンケージエディタなどのプログラムを呼び出す」という 処理を行っている。gccコマンド自体が直接コンパイルを行っているわけではない。

以下に、gccを用いてCで書かれたソースプログラムから実行形式を作り出す までの処理の流れを説明する。

前処理(preprocess)

gccは、C言語のソースプログラムをコンパイルするときには、 まず前処理を行う。

前処理は、 gccのバージョン 2 までは プリプロセッサ(cppコマンド)によって行なわれ、 前処理された結果は 「.i」という拡張子のついたファイルに保存される。

gccのバージョン 3 では、前処理はcc1コマンドによって行なわれる。 そのため、明示的に指定しない限り、 次のコンパイル作業まで一括して処理が進む。

なお、gccは、-Eオプションを指定して実行すると、前処理のみを行い、 それ以降の処理を行わないようになっている。前処理した結果は、 標準出力に出力される。

コンパイル(compile)

前処理のすんだソースプログラムは、コンパイラ(cc1コマンド)によってコンパイル される。コンパイルの結果のアセンブリ言語で書かれたプログラムは、 「.s」という拡張子のついたファイルに保存される。

なお、gccは、-Sオプションを指定して実行すると、コンパイルまでの処理を行い、 それ以降の処理を行わないようになっている。 結果は、「.s」という拡張子がついたファイルに保存される。

アセンブル(assemble)

アセンブリ言語で書かれたプログラムは、アセンブラ(asコマンド)によって 機械語(マシンコード)に変換される。結果のオブジェクトプログラムは、 「.o」という拡張子のついたファイルに保存される。

gccでは、-cオプションを指定して実行すると、 この段階で処理を止める(次に説明する連結編集を行わない) ので、オブジェクトファイルを生成することができる。

連結編集(linkage edit)

最後に、リンケージエディタ以下のオブジェクト プログラムを1つにまとめ、実行形式を作る。

  • ソースプログラムを前処理/コンパイル/アセンブルした結果 生成されたオブジェクトプログラム
  • プログラムの実行にあたって初期化や後処理などを行う 特別なオブジェクトプログラム (これを一般にランタイムルーチン(runtime routine)という) である、crt0.o。(C RunTime)
  • -lオプションで指定されたライブラリに アーカイブされたオブジェクトプログラム(後述)

なお、リンケージエディタのコマンド名は、 gcc バージョン2ではld、バージョン3以降ではcollect2である。

生成された実行形式は、 -oオプションを用いて指定したファイル名で保存される。 指定されなかった場合には、「a.out」という名前のファイルに保存される。

なお、gccは、-vオプションを指定することによって、上記の処理の様子を 観察することができるようになっている。


ライブラリ

複数のファイルをまとめて1つのファイルにすることを、 アーカイブ(archive)という。 複数のオブジェクトプログラムを一つのファイルにアーカイブしたものを、 ライブラリ(library)という。

例えば、C言語でプログラムを記述するとき、一般に printf()scanf()などの関数を使うことが多いが、 これらの関数のオブジェクトプログラムはライブラリとして用意されている。 このオブジェクトプログラムは、連結編集の段階で実行形式に組み込まれる。 このことを言い換えると、以下のようになる。

一般に我々がC言語でプログラムを書くときには、 main()をはじめさまざまな関数を使うことになる。

プログラムの中で使う関数は、原則としてすべて自分で書かなければならないが、 printf()などの標準的な関数は書かなくてもよい。 なぜなら、それらの標準的な関数はすでに先人によって書かれており、 それをコンパイルして作った オブジェクトプログラムがライブラリとして提供されているからである。

したがって、我々は、それらの関数を改めて自分で書く必要はなく、 単にそのライブラリから利用したい関数のオブジェクトプログラムを取り出して 連結編集し、実行形式を作ればよい。

UNIXでは、オブジェクトプログラムのアーカイブにはarという コマンドが使われる。arコマンドによって作られたファイルには一般に 「.a」という拡張子がつけられる。ライブラリの場合には、通常、 「lib〜.a」というファイル名が付けられる。

プログラムのコンパイル時ではなく、実行時に連結編集を行う ライブラリ(これを動的リンクライブラリ(dynamic link library)という)には、 「lib〜.so」のようなファイル名が付けられるのが一般的である。 余談であるがWindows系OSでは「〜.dll」のようなファイル名が付けられる。 システムにもよるが、動的リンクライブラリは、 複数の実行形式で同じものを使う場合には主記憶上で共有されることが多い。 そのようなライブラリのことを、共有ライブラリ(shared library)という。 動的リンクライブラリおよび共有ライブラリについての詳細は、ここでは省略する。

ライブラリのファイルは、(システムによって異なるが)/usr/lib、 /usr/local/libなどのディレクトリに置かれる。一般ユーザが自分で 新たにライブラリを造ることもできる。

C言語で使われるprintf()などの標準的な関数は、 libc.aというライブラリに まとめられている。gccは連結編集するときに自動的にlibc.aを用いるので、 libc.aに含まれている関数を使うときにはgccに明示的にライブラリ名を 指定する必要がない。しかしそれ以外のライブラリ (例えば数学ライブラリである libm.a)を使う場合には、 (例えば-lmのように)明示的にライブラリ名を指定する必要がある。


全体の流れ

ここまでに説明したことをまとめた図を以下に示す。

コンパイラがやっていること(gcc ver2 ベース)

コンパイラの仕事

ライブラリの作られ方

ライブラリ