Phase #07

コンパイルの過程


1.ソースファイルから実行ファイルまで

この章ではソースコードから実際に実行ファイルが作られる過程を詳しく見ていきます。
さて、今までソースファイルから実行可能ファイルを作る過程のことを単に「コンパイル」と読んできたのですが、これは実は複数のプロセス(処理)をまとめてそう読んでいたのです。細かく見ていくと下のような流れで実行ファイルが作られます。
コンパイルの過程
大きく2つの部分に分かれていますね。まずコンパイラ部分。これはソースファイルをオブジェクトファイルと呼ばれるものに変換します。オブジェクトファイルはコンピュータが直接内容を認識できる、機械語(マシン語)と呼ばれる形式で構成されたファイルです。機械語はコンピュータの内部回路(具体的にはCPU)が直接処理できる形式ですべての処理はこの機械語の組み合わせで実現されています。機械語そのものについてはハードウェアについての知識を要するのでここでは詳しく述べません。C言語と機械語の関係はおおまかに述べるとすればこんな感じでしょう。
多くの方は普段日本語を使っていると思うので日本語の文はすらすら読めますね。しかし、他の(英語などの)言語を使っている文はパッと見ただけでは分かりづらく、翻訳しなくてはなりませんね。これがここにも当てはまるのです。コンピュータにとってC言語は分かりづらいのだけど、機械語は直接理解できるのです。少し話はずれますが、この「翻訳」作業をあらかじめ全部しておいて実行するプログラムの実行方式「コンパイラ方式」といい、プログラムの実行時に「少しずつ翻訳して実行」をくり返す実行方法「インタプリタ方式」と呼びます。
では、何故最初から機械語でプログラミングしないのかといえば、機械語は「数字の羅列」のようなものなので人間にはとても分かりづらく、効率が悪いからです。C言語のように人間にとって分かりやすい(コンピュータ)言語「高級言語」と読びます。高級の逆の「低級言語」というのも存在します。低級言語人間には高級言語より理解しにくいのですが、コンピュータにとっては高級言語よりも簡単に翻訳できる、つまり機械語に近いコンピュータよりの言語です。
次にリンカ部分。オブジェクトファイル自体は実行可能な機械語でできているのですが、プログラムとしては不十分です。プログラムを正しく実行できる形式にするにはエラーが起きたときの対策やキー入力や画面への表示などの機能を備えている必要があります。しかし、これらをいちいちコーディングするのは難しく手間のかかる作業です。
そこで、便利なようにあらかじめいろいろな機能を組み込んだオブジェクトファイルを用意しておくのです。これが「ライブラリ」と呼ばれるもので前の章で少し説明した標準ライブラリC言語の規格の上で組み込むべき機能が決められていて、もっとも基本的な機能が組み込まれたライブラリなのです。
このライブラリファイルとコンパイラが生成したオブジェクトファイルを1つの実行可能なファイルにする作業がリンクなのです。
それではコンパイルにおいての中心部分、コンパイラがどのようにコンパイルしていくかをより詳しく見ていきましょう。

2.プリプロセッサ


さて、上の図を見て貰えば分かると思いますが、コンパイラも複数のプロセスを行います。
このなかのコンパイラというのは実は高級言語のC言語を直接オブジェクト、つまり機械語へと変換せずにアセンブリ言語とよばれる低級言語に変換しているのです。このアセンブリ言語をオブジェクトに変換するのは「アセンブラ」とよばれる部分なのです。しかし、実際には特別な必要がない限りアセンブリ言語でコーディングすることはなく、コンパイラの出力がそのままアセンブラへと流れるため、それほど意識しなくても構いません。
コンパイラ/アセンブラの処理の様子
より活用できるのはコンパイラの前段階の「プリプロセッサ」です。これはソースファイルをコンパイラが処理する前にそのための前準備をする働きがあります。
プリプロセッサはプログラマが単独で使うことができますが、それよりも今までと同じようにコンパイラを通して使うほうがよりスマートです。コンパイラはプリプロセッサの処理が必要と判断すると自動的にプリプロセッサを呼び出してくれます。
では、プリプロセッサに処理させたい場合どうするのでしょうか?プリプロセッサへの指示はソースファイルに「 # 」で始まる制御命令を記述することによって行えます。この制御命令は次のような形式になっています。

 ・「#」で始まる制御命令
 ・「#」は通常行の一番先頭に記述する
 ・最後にセミコロン「 ; 」はつけない

いくつか基本的な制御命令を見てみましょう。

A.定義マクロ


定義マクロはソースファイル中の語句(=マクロ)を指定した語句に置き換えます。

 #define マクロ名 語句 

・#defineで定義された「マクロ名」(識別子の形式)がソース上に現れるごとに「語句」へと置き換える
・プログラムを分かりやすくするために、通常マクロ名は大文字で記述して区別する

何度でも出てくる定数やある特別の意味を持った数(円周率のような数)などを数ではなく、言葉のマクロで記述することでプログラムが読みやすくなり、ミスも少なくなります。

○ソースプログラム

#define DATA_SIZE 5
#define MAX_COUNT 1000


void main (void)
{
 int data [DATA_SIZE] ;
  〜
 if (count > MAX_COUNT) {
  〜
  while (index < DATA_SIZE) {
  〜
}

 → プリプロセッサ → ○処理されたプログラム

void main (void)
{
 int data [5] ;
  〜
 if (count > 1000) {
  〜
  while (index < 5) {
  〜
}

 → コンパイラ へ

B.引数付き定義マクロ


単純な置き換えだけでなく、引数を使った置き換えもできます。

 #define マクロ名 (引数の並び) 語句 

・基本的には引数のない定義マクロと同じ
・演算の優先順位に注意。語句中の引数は ( ) で囲む
・マクロ名と引数の「 ( 」間に空白は入れてはいけない
・副作用を起こさないような記述をする

[ 例 ] 演算の優先順位が変わらないように「 ( ) 」を使う
#define SQUARE(N) N * N
ret = SQUARE(x) ;とソースにあった場合は
  ↓
ret = x * x ;と置き換わり、正しく演算できる
ret = SQUARE(x + y) ;とソースにあった場合は
  ↓
ret = x + y * x + y ;と置き換わり乗算が優先されるので、
目的の演算結果と異なってしまう
#define SQUARE(N) (N) * (N)
ret = SQUARE(x + y) ;とソースにあった場合は
  ↓
ret = (x + y) * (x + y) ;と置き換わり、正しく演算できる
ret = num / SQUARE(x + y) ;とソースにあった場合は
  ↓
ret = num / (x + y) * (x + y) ;と置き換わり乗算になってしまうので、
目的の演算結果と異なってしまう
#define SQUARE(N) ((N) * (N))
ret = num / SQUARE(x + y) ;とソースにあった場合でも
  ↓
ret = num / ((x + y) * (x + y)) ;と置き換わり、正しく演算できる

[ 例 ] マクロ名と引数の「 ( 」間に空白は入れてはいけない
#define SQUARE (N) (N) * (N)
ret = SQUARE(x + y) ;とソースにあった場合は
  ↓
ret = (N) ((N) * (N))(x + y) ;と置き換わり、誤った式になる

これはマクロ名と引数の間に空白があるので、定義マクロと判断され、
マクロ名「SQUARE」が語句「(N) ((N) * (N))」に置き換えられてしまったためにおこります。
なお、マクロの呼び出し側は空白が入っていても問題ありません。

 z = SQUARE (x + y) ;

[ 例 ] マクロの副作用に注意
#define DEBUGPRINT(N) if (N > 0) printf("%d"), N)
DEBUGPRINT (dat++) ;とソースにあった場合は
  ↓
if (dat++ > 0) printf("%d", dat++) ;と置き換わり、副作用の可能性がある

プログラマとしては dat の値を +1 した値を表示つもりでも dat が正の場合 dat++ が2回行われてしまいます。このように意図しない作用を起こすこと副作用と言います。
コーディングをする際にこのような副作用の起こす可能性のあるものを書かないようにしないといけません。


Jump to Phase #06-2Phase #06-2: 関数の基礎(2) へ
Phase #07-2: コンパイルの過程(2) へJump to Phase #07-2

△戻る
▲トップに戻る