30代エンジニアの備忘録のカバー画像

30代エンジニアの備忘録

組み込み開発・信号処理・プログラミングの技術メモと短編小説

コンパイラとリンカの仕組み|ソースコードが実行ファイルになるまでの4ステップ

シリーズ この記事は「コンパイル・リンク・ビルド(組み込み基礎)」の一部です。 シリーズ目次を見る

(2026年5月時点)C/C++ のソースが実行可能なバイナリへ至るまで、ここでは プリプロセス → コンパイル → アセンブル → リンク の四段階に分けて見ます。実際の gcc / clang は多くの場合、これらをまとめて呼び出すドライバとして振る舞います(-E、-S、-c などで途中成果物だけ出すこともできます)。

段階に分ける読み方のメリット:変更のあったソースだけ再処理でき ビルド時間を抑えやすい、中間成果物(.i .s .o)を切り出すと 再現・比較がしやすい、問題が出たとき どのツールの責務か切り分けやすい、の3点が実務で効きます。

ビルドプロセスの全体像とは?

ソースコード(.c)が実行ファイルになるまでには、4段階の変換が行われます。各段階で異なるツールが担当し、入力と出力のフォーマットが変わります。 参考:GCC Internals(公式)

ビルドプロセスの全体フロー
ビルドプロセスの全体フロー
ソースコード(.c) → [プリプロセッサ] → 展開済みソース(.i)
                 → [コンパイラ]       → アセンブリ(.s)
                 → [アセンブラ]       → オブジェクト(.o)
                 → [リンカ]           → 実行ファイル

Step 1:プリプロセス(テキスト置換)とは?

プリプロセッサは、コンパイル前にソースコード上のディレクティブ(#で始まる行)を処理します。実際のコンパイルが始まる前の「下準備」の段階です。

プリプロセスの処理内容
プリプロセスの処理内容
  • #include:指定されたヘッダファイルの内容をその場に展開(コピー)する。stdio.hをincludeすると数千行のコードが挿入される
  • #define:マクロを定義し、使用箇所を値やコードに置換する。定数定義やインライン関数的な用途に利用
  • #ifdef / #endif:条件コンパイル。プラットフォームごとのコード切り替えやデバッグ用コードの有効化/無効化に使用
# プリプロセス結果を確認するコマンド
gcc -E hello.c -o hello.i
# hello.i には stdio.h の内容がすべて展開され、数千行になる

# マクロの展開を確認
echo '#define MAX(a,b) ((a)>(b)?(a):(b))
int x = MAX(3,5);' | gcc -E -

undefined reference(未定義参照)は通常この段には出ません(まだリンカに届いていません)。

Step 2:コンパイルで何が起きる?

コンパイラは展開済みソースコードを解析し、ターゲットCPUのアセンブリ言語(.s)に変換します。この段階で以下の内部処理が行われます。

コンパイラの変換プロセス
コンパイラの変換プロセス
  1. 字句解析・構文解析:ソースコードの文法チェックと抽象構文木(AST)の構築
  2. 意味解析:型チェック、スコープ解決、暗黙の型変換の検出
  3. 最適化:不要コードの削除、ループ最適化、インライン展開など(-O0〜-O3で制御)
  4. コード生成:ターゲットCPUのアセンブリ命令を出力
# アセンブリ出力を確認
gcc -S hello.c -o hello.s

# 最適化レベルの違いを比較
gcc -S -O0 hello.c -o hello_O0.s  # 最適化なし
gcc -S -O2 hello.c -o hello_O2.s  # 標準的な最適化

この段での典型エラーは文法・型まわりが中心で、undefined reference の文言が出るのは通常より後段(リンク)です。

💡
最適化レベル -O2 は本番ビルドの標準選択です。-O3 はさらに攻撃的な最適化を行いますが、コードサイズが増加することがあります。組み込みでは -Os(サイズ最適化)も頻繁に使われます。

Step 3:アセンブルで .o ができる理由は?

アセンブラがアセンブリ言語のテキストを、CPUが直接実行できるバイナリ(機械語)に変換し、オブジェクトファイル(.o)を生成します。

この段階で生成されるオブジェクトファイルにはまだ外部関数のアドレスが未解決で、「穴」が空いた状態です。printf()のような標準ライブラリ関数の呼び出しアドレスは、次のリンクステージで埋められます。

# オブジェクトファイルの生成
gcc -c hello.c -o hello.o

# 中身の確認
objdump -d hello.o  # 逆アセンブル
nm hello.o          # シンボルテーブル表示

オブジェクト単体では外部シンボルが nm 上 U(未定義)のまま残ることがありますが、undefined reference というリンカメッセージは次のリンクで顕在化しやすいです。

Step 4:リンクで undefined reference が出るのはなぜ?

リンカが複数のオブジェクトファイルとライブラリを結合し、最終的な実行ファイルを生成します。主に2つの仕事を行います。

リンカの結合プロセス
リンカの結合プロセス
  • シンボル解決:各.oファイルの未定義シンボル(外部関数呼び出し等)を、定義を持つ.oファイルやライブラリと紐付ける
  • アドレス決定:各関数・データの最終的なメモリアドレスを確定し、再配置情報に基づいてアドレスを書き込む
# リンクの実行
gcc main.o utils.o -o myapp

# ライブラリのリンク
gcc main.o -lm -lpthread -o myapp  # 数学/スレッドライブラリ
💡
undefined reference to ... は主にこのリンク段で出ます。関数の実体がどの .o・.a・.so 側にも解決できないときに発生し、-l の不足・リンク順・パスを疑うのが典型的です。

分業アーキテクチャの利点は?

一発で通すビルドでも動きはしますが、中間成果物を残さないと差分ビルドや原因切り分けがしづらい場面が増えます。各段階を分離する設計には、次のような利点があります。

  • 差分ビルド:変更したソースファイルだけ再コンパイルすればよく、大規模プロジェクトのビルド時間を大幅に短縮可能
  • ライブラリの再利用:他のプロジェクトで作られた.oファイルや.a/.soを組み込める
  • 多言語混在:C、C++、アセンブリなど異なる言語のオブジェクトファイルをリンク可能
  • ツールチェーンの独立性:コンパイラやリンカを個別にバージョンアップ・最適化でき、メンテナンスが容易