翻訳環境と実行環境
C言語の実装には、必ず二つの独立した環境が存在する。
- 翻訳環境:ソースコードを機械語の命令に変換する段階
- 実行環境:生成された実行ファイルが実際に動作する環境
翻訳環境の処理フロー
Cプログラムは、複数の .c ファイルから構成されることが一般的である。これらのファイルはそれぞれ独立して処理され、最終的に一つの実行可能ファイルに結合される。
- 各 .c ファイルは、コンパイルプロセスを通じて対応するオブジェクトファイル(Windows: .obj、Linux: .o)に変換される
- これらのオブジェクトファイルと、必要なライブラリ(標準ライブラリやサードパーティ製ライブラリ)がリンクされ、最終的な実行ファイルが生成される
コンパイルプロセスはさらに三段階に分解できる:
- プリプロセッシング(前処理)
- コンパイル(言語翻訳)
- アセンブル(アセンブリ言語から機械語への変換)
プリプロセッシング
この段階では、ソースファイルとそのインクルードするヘッダファイルが処理され、拡張子 .i の中間ファイルが生成される。
gcc -E source.c -o source.i
主な処理内容:
#defineマクロをすべて展開し、削除#include指令を再帰的に解決し、対応するヘッダの内容を挿入#if、#ifdef、#else、#endifなどの条件コンパイルを評価して、不要なコードを除去- コメントをすべて削除
- 行番号とファイル名情報を追加(デバッグ情報の生成に使用)
#pragma指令は保持し、後段のコンパイラが利用
この段階が終了すると、ソースファイルにはマクロやインクルード指令は一切存在せず、単一の拡張されたコードの塊となる。エラーの原因がマクロ展開やヘッダの包含ミスである場合、この .i ファイルを確認することで問題を特定できる。
コンパイル
プリプロセス済みの .i ファイルを、アセンブリ言語(.s ファイル)に変換する。
gcc -S source.i -o source.s
この段階では、以下の処理が連続して行われる:
- 字句解析:文字列をキーワード、識別子、リテラル、演算子などの「トークン」に分割
- 構文解析:トークン列を構文木(AST)に変換。式や文の構造を階層的に表現
- 意味解析:型の整合性、変数の宣言・使用の妥当性、型変換の可能性などを静的に検査。型ミスマッチや未定義変数の使用など、構文的には正しいが意味的に誤ったコードを検出
この段階で発生するエラーは「コンパイルエラー」と呼ばれ、実行前に必ず修正が必要である。
アセンブル
アセンブリ言語の命令を、対応する機械語命令に直接変換する。1行のアセンブリ命令はほぼ1対1で機械語命令にマッピングされる。
gcc -c source.s -o source.o
この段階では最適化は行われず、純粋なバイナリコードへの変換のみが目的である。生成される .o ファイルは、CPUが直接実行できないが、リンク可能な形式の機械語である。
リンク
複数のオブジェクトファイルとライブラリを統合し、一つの実行可能ファイルを生成する。
主な処理:
- アドレス割り当て:各オブジェクト内の関数や変数に、実行時のメモリ上の物理アドレスを割り当てる
- シンボル解決:複数のファイル間で参照されている関数・変数(例:main() が呼び出す printf())の実体を特定
- リロケーション:参照先のアドレスを実際のメモリ位置に置き換え、ジャンプ先やデータアクセスを修正
たとえば、a.c で定義された関数 calculate() を b.c から呼び出している場合、リンク時にその関数の実際のアドレスが b.c の呼び出し箇所に埋め込まれる。
実行環境
生成された実行ファイルが動作する環境では、以下のステップが順に実行される:
- OSによってプログラムがメモリに読み込まれる(スタンドアロン環境では手動でロードされる場合もある)
- プログラムのエントリポイントである
main()関数が呼び出される - 実行中、関数のローカル変数や戻りアドレスはスタックに格納され、グローバル変数や静的変数は静的メモリ領域に保持される
- main関数が正常終了するか、異常終了(例:セグメンテーションフォールト)によりプログラムが停止