コンパイルの流れ

こんにちは、めのんです!

今回はコンパイルの流れについて解説することにします。
PHPではソースコードを書けばいきなり実行することができました。
けれどもCでは実行するまでにコンパイルが必要だということを以前お話ししましたね。

大まかなコンパイルの流れ

そのコンパイルですが、コンパイラの内部では実はいくつかのステップにわかれています。
簡単にいうと次のような流れになります。

  1. 前処理(プリプロセス)
  2. (狭義の)コンパイル
  3. アセンブル
  4. リンク

GCCでも上のような流れでソースファイルが生成されています。

では、実際に以前の「hello.c」を使って確かめてみましょう。
念のため「hello.c」を再掲載しますね。

/* hello.c */
#include <stdio.h>

int main(void)
{
  puts("hello, world!");
  return 0;
}

前処理(プリプロセス)

このソースコードを最初に前処理してみましょう。
次のコマンドで前処理を行うことができます。

gcc -E hello.c > hello.i

gccに「-E」オプションを付ければ前処理だけを実行することができます。
結果は標準出力に書き込まれますので、リダイレクトして「hello.i」に書き込んでいます。
末尾が「.i」のファイルは前処理済みのソースコードとして扱われます。

もしエラーメッセージがでたときは何か打ち間違いがあるか、開発環境のインストールに失敗している可能性があります。
もう一度確認してから試してみてください。

できれば「hello.i」の内容をのぞいてみましょう。
普通のテキストファイルですからテキストディタで内容を見ることができるはずです。
前処理の結果は環境によって異なりますが結構な長さだと思います。
これは「stdio.h」の内容が展開されているためです。
今は何のことかわからなくてもかまわないので、#includeでこんなにたくさんの取り込まれているということだけ理解していただければOKです。

(狭義の)コンパイル

前処理が終わりましたので次は(狭義の)コンパイルです。
「狭義の」と但し書きしているのは、前処理からリンまですべて含めてコンパイルと呼ぶこともあれば、リンク以外の部分だけをコンパイルと呼ぶこともあるからです。

先ほどの「hello.i」を(狭義の)コンパイルするには次のコマンドを実行します。

gcc -S hello.i

「gcc」コマンドのオプションを変えれば、コンパイルに関する処理は何でも行わせることができます。
ここでは「-S」オプションを指定していますね。
処理の結果、今度は「hello.s」というファイルができていると思います。

末尾が「.s」で終わるファイルはアセンブリ言語のソースファイルです。
これも単なるテキストファイルですから内容をのぞいてみましょう。
意味は全然分からないかもしれませんが何となく雰囲気だけつかめればOKです。

もしかしたらピン!と来た方もいらっしゃったかもしれませんが、(狭義の)コンパイルもCのソースコードからアセンブリ言語のソースコードに変換するための一種の前処理なんです。

アセンブル

「hello.s」はアセンブリ言語のソースファイルですから、アセンブルを行って機械語に変換する必要があります。
先ほどの「hello.s」に対して次のコマンドを実行してみてください。

gcc -c hello.s

今度は「hello.o」というファイルができたと思います。
「hello.o」はバイナリファールなので内容をのぞいてみることは簡単ではありません。
もしobjdumpというツールがインストールされていれば、「objdump -D hello.o」といったコマンドで逆アセンブルすることも可能です(今回は詳しく解説しません)。

末尾に「.o」が付くファイルは「オブジェクトファイル」といいます。
この形式は重要なので覚えておいてください。

リンク

「.o」で終わるオブジェクトファイルには機械語が格納されています。
でも、この形式のままではまだ実行することができません。
最後にリンクという処理を行ってやっと実行できるようになります。

ソースファイルを複数に分割するとき、PHPではincludeやrequireを使って取り込むことしかできませんでした。
Cにも#includeがありますが、それとは別にソースファイルを分割する方法があります。
さきほどのオブジェクトファイルをいくつか作っておいて、それをあとからにくっつける(リンクする)ことで実行ファイルを作ることができるのです。
オブジェクトファイルを作るのに必要なソースファイルを、#includeで取り込まれるものも含めて、「翻訳単位」といいます。

では、いよいよリンクを終わらせて実行ファイルを作ってみましょう。

gcc -ohello hello.o

これで実行ファイルができたと思います。
Windowsなら「hello.exe」が、LinuxやmacOSなら「hello」ができているはずです。
記念に実行してみてくださいね。

翻訳フェーズ

先ほどまではGCCというツールが内部で実行している処理になぞらえて、大まかな流れを解説しました。
標準規格で定義されているコンパイルの流れは「翻訳フェーズ」といって、もっと厳密に定義されています。
JIS X2030:2003から該当箇所を引用してみます。

5.1.1.2 翻訳フェーズ
次に示すフェーズによって,翻訳上の構文規則間の優先順位を規定する。

(1) 必要ならば,物理的なソースファイルの多バイト文字を,対応するソース文字集合に,処理系定義の方法で,写像する(この際,行の終わりを示すものに対して改行文字を導入する。)。3文字表記を,対応する単一の文字の内部表現に置き換える。

(2) 逆斜線文字(\)の直後に改行文字が現れた場合,それらの2文字を削除する。これによって物理ソース行を接合して論理ソース行を作成する。一つの物理ソース行においてこの接合の対象となるのは,その行の最後の逆斜線文字だけとする。空でないソースファイルは,改行文字で終了しなければならない。さらに,この改行文字の直前に(接合を行う前の時点で)逆斜線文字があってはならない。

(3) ソースファイルを,前処理字句(6)及び空白類文字(注釈を含む。)の並びに分割する。ソースファイルは,前処理字句の途中又は注釈の途中で終了してはならない。各注釈を,一つの空白文字に置き換える。改行文字を保持する。改行文字を除く空白類文字の並びを保持するか一つの空白文字に置き換えるかは,処理系定義とする。

(4) 前処理指令を実行し,マクロ呼出しを展開する。さらに,̲Pragma単項演算子式を実行する。字句連結(6.10.3.3参照)の結果として生成される文字の並びが国際文字名の構文規則に一致する場合,その動作は未定義とする。#include前処理指令に指定された名前をもつヘッダ又はソースファイルに対して,フェーズ(1)からフェーズ(4)までの処理を再帰的に行い,すべての前処理指令を削除する。

(5) 文字定数及び文字列リテラル中のソース文字集合の各要素及び各逆斜線表記を,それぞれに対応する実行文字集合の要素に変換する。対応する要素が存在しない場合,ナル(ワイド)文字以外の処理系定義の要素に変換する。

(6) 隣接する文字列リテラル字句同士を連結する。

(7) 字句を分離している空白類文字は,もはや意味をもたない。各前処理字句を字句に変換する。その結果,生成された字句の列を構文的及び意味的に解析し,翻訳単位として翻訳する。

(8) すべての外部オブジェクト参照及び外部関数参照を解決する。その翻訳単位中に定義されていない関数及びオブジェクトへの外部参照を解決するため,ライブラリの構成要素を連係する。これらすべての翻訳出力をまとめて,実行環境上での実行に必要な情報を含む一つのプログラムイメージを作る。

難しい書き方をされていますのですぐに理解できないと思いますが大丈夫です。
翻訳フェーズには8段階があるということは見てすぐに分かると思います。
最初に解説した前処理からリンクの流れが、この8段階のどれに該当するのかを見ていくことにしますね。

翻訳フェーズの(1)から(4)が前処理に相当します。
(5)から(7)が(狭義の)コンパイルに相当しますが、アセンブルも(7)に含まれます。

そして最後の(8)がリンクに相当します。

ここのところ難しい話が続いていますけど、ついて来れていますか?
まったくのプログラミング初学者が相手ならここまで詳しくは解説できません、
この講座はあくまでもPHPプログラマーを対象としていますので、一歩も二歩も踏み込んだ解説をしています。
その点、どうかご理解ください。

さて、ここまでが前振りです。
次回からはどんどんコードを書いていくことになると思います。

このブログでは「PHPプログラマーのためのC講座」以外の内容も書きていきたいと考えていますので、ときどき全然違う記事をはさむことがあると思います。
これについてもあらかじめご了承ください。

今回は長くなってしまいましたがこれで終わります。
それでは次回をお楽しみに!