中級では「翻訳のタイミングで3タイプに分かれる」ことと「字句→構文→中間表現→機械語」の流れを見ました。上級では、コンパイラの内側・リンクとロード・ABI まで下り、「なぜ相手のOS/CPUだと動かないのか」を最後まで分解します。
概要 — まず全体をつかむ
詳細 — 1段階ずつ追う
コンパイラの内側 — 6つのフェーズ
アニメーション『翻訳のタイミング(コンパイル/インタプリタ/VM)』を開く
中級の「字句→構文→中間表現→機械語」を、実際の段階まで割ると6つになります。
- 字句解析 — 文字の並びを意味の最小単位(トークン)に切り分ける
- 構文解析 — トークンを文法にそった木構造(構文木)へ組み立てる
- 意味解析 ※1 — 型の整合や変数の定義済みかを検査し・意味の通らない箇所をはじく
- 中間表現(IR) ※2 — 特定CPUに依存しない共通の形に落とす
- 最適化 — IRの上で無駄を削る(使わない計算の除去・共通式のまとめ・ループの整理など)
- コード生成 — 対象CPUの機械語へ落とし・レジスタを割り当てる
肝は 4と5をCPUから切り離した こと。最適化をIRに集約すれば、あとは6の「コード生成」を差し替えるだけで、多数のCPUへ展開できます。フロントエンド(1〜3)とバックエンド(5〜6)をIRで分ける——これが現代のコンパイラの背骨です。
登場人物メモ:
- ※1 意味解析 — 文法は合っていても意味が通るかを調べる段階(型検査など)
- ※2 中間表現(IR) — 特定CPUに縛られない・最適化しやすい中間の形
- ※3 リンカ/ローダ — 部品を1つに結合する道具/実行時にメモリへ載せる仕組み
- ※4 ABI — 呼び出し規約やバイナリ形式など・機械語どうしが噛み合うための約束
やさしく言うと(中級)
コンパイルの中では、ソースが段階を追って機械語に近づきます。
- 字句解析 — 文字の並びを、意味のある最小の単位(トークン)に切り分ける
- 構文解析 — トークンの並びを、文法にそった構造(木)に組み立てる
- 中間表現 — 特定CPUに縛られない中間の形にする(最適化しやすい)
- 機械語の生成 — 対象のCPU向けの命令に落とす
この一連を含め、ソースから実行できる成果物を作る作業全体がビルドです。ライブラリの結合なども、ここで行われます。
リンクとロード — 静的と動的
コンパイルが作るのは、多くの場合まだ 部品(オブジェクトファイル) です。これを1つにまとめるのが リンク ※3、実行時にメモリへ載せるのが ロード です。
- 静的リンク — 必要なライブラリを 実行ファイルに丸ごと取り込む。単体で動き・依存の食い違いが起きにくいが・サイズが大きく・ライブラリ更新には作り直しが要る
- 動的リンク — ライブラリは外に置き、実行時に結び付ける。実行ファイルは小さく・共有ライブラリを複数プログラムで使い回せて・更新も差し替えで済む。ただし実行先に正しいライブラリが無いと動かない
動的リンクの「実行先の部品を借りる」性質が、後で出てくる 移植性 の話に直結します。借り物である以上、貸し手(OSやライブラリ)の作法が合っていることが前提になるからです。
ABI — バイナリどうしの約束事
ソースの文法(API)とは別に、でき上がった機械語どうしが噛み合うための約束 が ABI ※4 です。代表例:
- 呼び出し規約 — 関数へ引数をどのレジスタ/スタックで渡すか・戻り値をどこに置くか・後始末は誰がするか
- データの並び — 構造体の各要素をメモリ上でどう配置し・何バイト境界に揃えるか
- 実行ファイル形式 — 機械語やデータをどんな箱(LinuxのELF等)に詰めるか
ABIが違えば、たとえ同じCPUの機械語でも 関数の呼び出しがすれ違い、部品どうしが噛み合いません。コンパイル済みのライブラリをそのまま流用できるかどうかは、このABIが一致しているかで決まります。
JIT・バイトコード・マネージド
中級の「3つの道」を、実行時の視点で深めます。
- ネイティブ(C など) — 事前に機械語まで落とす。速いが・作った先のCPUとOSに縛られる
- バイトコード+VM(Java/JVM) — CPUに依らないバイトコードへ翻訳し・各環境のVMが解釈する。移植しやすい
- JIT(実行時コンパイル) — VMが、よく通る「熱い」部分だけを 実行中に機械語へ変換 して差し替える。実行時の実測(どの分岐が多いか等)を使えるぶん・事前コンパイルを超える最適化も狙える
ここから ネイティブ vs マネージド の対比が見えます。マネージドな実行(VMの上で動く)は、メモリの自動回収(ガベージコレクション)や安全検査を肩代わりしてくれて開発が楽な反面、VMという1枚の層と回収の一時停止ぶんのコストを払います。ネイティブは無駄がなく速いが・安全と後始末を自分で背負います。速さと安全・手軽さは、ここでも綱引きです。
やさしく言うと(中級)
アニメーション『翻訳のタイミング(コンパイル/インタプリタ/VM)』を開く
- コンパイル型(C など) — 実行の前にソース全体を機械語へ翻訳し、できた実行ファイルをCPUが直接動かす。翻訳済みなので速いが、翻訳結果が特定のCPU/OS向けになり環境に依存する
- インタプリタ型(Python など) — 実行時にソースを1行ずつ解釈しながら動かす。すぐ試せて手軽だが、翻訳しながら走るぶんやや遅い
- バイトコード+VM(Java/JVM) — まず環境に依らないバイトコードへ翻訳し、それを各環境のVMが解釈して動かす。両者の中間で、そこそこ速く・環境をまたぎやすい
速さと手軽さ・移植性はトレードオフで、翻訳をいつ済ませるかがそのまま性能と持ち運びやすさに効きます。
なぜ別のOS/CPUだと動かないのか
アニメーション『命令サイクル』を開く
「手元では動くのに向こうで動かない」の正体は、3つの層のどれかがズレている ことです。
- ISA(命令セット) — CPUが解釈できる機械語の語彙。x86とArmでは命令そのものが別物なので、binary-and-data のレベルで通じない
- ABI — 同じISAでも、呼び出し規約や実行ファイル形式が合わなければ部品が噛み合わない
- OSのシステムコール — 画面表示もファイルも、最後はOSへの 依頼(システムコール) で叶う。その番号や作法はLinux・Windows・macOSで別々
CPUは命令サイクル(取り出し→解読→実行)で機械語を淡々と回すだけ。だから ISAが合い・ABIが合い・OSへの依頼の仕方が合って初めて 実行ファイルは動きます。3つのどれか1つでも違えば止まる——これが「OS別・CPU別に配布物を分ける」理由であり、環境ごと箱に詰めて持ち運ぶ発想(Docker)が生まれた背景です。
⚠️ 深部の落とし穴
- 動的ライブラリ地獄 — 実行先に必要な共有ライブラリやそのバージョンが無い/食い違い、起動時に落ちる
- ABIの不一致 — 別のコンパイラや設定で作った部品を混ぜると・呼び出し規約がずれて静かに壊れる
- クロスコンパイルの取り違え — 動かす先のISA/OS向けに作ったつもりが・手元向けのままで動かない
- 最適化の副作用 — 攻めた最適化で・未定義な書き方の挙動が変わり・環境によって結果がぶれる
- JITの温まり待ち — 起動直後はまだ機械語化が進まず遅い(ウォームアップが要る)
関連する知識
理解度チェック
そのまま解けます(成績は保存されません)。無料アカウントを作ると、学習の記録と進捗の山登りが始まります。
問1. 同じCPU向けにコンパイルしたのに・OSが違うと実行ファイルが動かないおもな理由は?
問2. 静的リンクと動的リンクの違いとして正しいのはどれ?
問3. JIT(実行時コンパイル)の説明として正しいのはどれ?
問4. VMの上で動く「マネージドな実行」の特徴として正しいのはどれ?
問5. ソースの文法である「API」と、でき上がった機械語どうしの約束である「ABI」の違いとして正しいのはどれ?
問6. Linux で使われる代表的な実行ファイル形式を、英字3文字(略語)で答えてください。
問7. マネージドな実行環境(VM)が肩代わりする、不要になったメモリ領域を自動で片付ける仕組みを何と呼びますか。
問8. コンパイラが「中間表現(IR)」をわざわざ挟むおもな狙いは?