コンパイルの過程 — ソースコードから実行ファイルまで
このトピックを終えると
コンパイラとインタプリタの違いを説明できるようになり、ソースコードが機械語に変換される段階を理解し、Python、JavaScript、Cがそれぞれどのように実行されるかを知ることができます。
コンピュータはソースコードを読めない
私たちが書くコードは人間向けのテキストです。
print("Hello, World!")CPUが理解するのは機械語 — 0と1で構成された命令です。
10110000 01001000 (mov al, 'H')
11001101 00100001 (int 21h)ソースコードを機械語に変換する必要があります。この変換を行うプログラムがコンパイラとインタプリタです。
コンパイラ vs インタプリタ
| コンパイラ | インタプリタ | |
|---|---|---|
| 変換のタイミング | 実行前に全体を一度に変換 | 実行しながら1行ずつ変換 |
| 結果 | 実行ファイル(バイナリ) | なし(即時実行) |
| エラー検出 | コンパイル時に全体のエラーを表示 | 該当行の実行時にエラー |
| 実行速度 | 速い(すでに変換完了) | 比較的遅い |
| 代表的な言語 | C、C++、Rust、Go | Python、JavaScript、Ruby |
たとえれば、コンパイラは本の翻訳(全体を最初に翻訳してから出版)、インタプリタは同時通訳(話している即時に翻訳)です。
コンパイルの段階 — C言語の場合
Cコードが実行ファイルになる過程:
ソースコード (.c)
↓ 前処理 (Preprocessing)
前処理されたコード
↓ コンパイル (Compilation)
アセンブリコード (.s)
↓ アセンブル (Assembly)
オブジェクトファイル (.o)
↓ リンク (Linking)
実行ファイル (a.out / .exe)1. 前処理 (Preprocessing)
#include <stdio.h>
#define MAX 100
int main() {
printf("Max is %d\n", MAX);
}#includeを実際のヘッダーファイルの内容に置き換え、#defineを値に置き換えます。この段階はテキスト置換です。
2. コンパイル (Compilation)
前処理されたコードを解析してアセンブリコードに変換します。文法エラーはここで検出されます。
mov edi, OFFSET FLAT:.LC0
mov esi, 100
call printf3. アセンブル (Assembly)
アセンブリコードを機械語(二進命令)に変換します。結果はオブジェクトファイル(.o)です。
4. リンク (Linking)
複数のオブジェクトファイルとライブラリを1つの実行ファイルに結合します。printfの実際の実装はC標準ライブラリにあるため、リンカがこれを接続します。
# GCCがこれらのすべてのステップを一度に行うgcc hello.c -o hello./helloPythonはどのように実行されるか
Pythonはコンパイル + インタプリタのハイブリッドです。
ソースコード (.py)
↓ Pythonコンパイラ
バイトコード (.pyc)
↓ Python仮想マシン (PVM)
実行結果# バイトコードの確認import dis
def add(a, b): return a + b
dis.dis(add)# LOAD_FAST 0 (a)# LOAD_FAST 1 (b)# BINARY_ADD# RETURN_VALUEバイトコードは機械語ではありません。Python仮想マシン(PVM)と呼ばれるインタプリタがバイトコードを1行ずつ実行します。CのようにCPUが直接実行するよりも遅いですが、OSに関係なく実行できます。
__pycache__/フォルダに.pycファイルが生成されることがあります。これはバイトコードキャッシュです。ソースが変更されない場合、再コンパイルせずにキャッシュを使用します。
JavaScriptはどのように実行されるか
JavaScriptエンジン(V8、SpiderMonkey)はJIT(Just-In-Time)コンパイルを使用します。
ソースコード (.js)
↓ パース
AST(抽象構文木)
↓ インタプリタ
バイトコード実行(遅い)
↓ JITコンパイラ(頻繁に実行されるコードを検出)
機械語に変換(速い)最初はインタプリタで迅速に実行を開始し、「この関数が1,000回呼び出された」と検出すると、その部分だけを機械語にコンパイルします。実行中にコンパイルするため「Just-In-Time」と呼ばれます。
Java — コンパイルとインタプリトの正統なハイブリッド
JavaはPythonに似ていますが、少し複雑です。
ソースコード (.java)
↓ javac(コンパイル)
バイトコード (.class)
↓ JVM(Java Virtual Machine)
├── インタプリタ(最初)
└── JITコンパイラ(頻繁に実行されるコード)
↓
機械語実行// Hello.java
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}javac Hello.java # バイトコード (.class) を生成java Hello # JVMがバイトコードを実行「Write once, run anywhere」 — Javaバイトコードは、JVMがある場所ならどこでも実行されます。Windowsでコンパイルされた.classファイルがLinuxのJVMでも動作します。
静的型 vs 動的型
コンパイルと密接に関連する概念が型システムです。
// C: 静的型 — コンパイル時に型を確認
int x = 42;
x = "hello"; // コンパイルエラー!# Python: 動的型 — 実行時に型を確認x = 42x = "hello" # OK — 実行中に型が変更される| 静的型 | 動的型 | |
|---|---|---|
| 型の確認タイミング | コンパイル時 | 実行時 |
| エラー検出 | 実行前 | 実行中 |
| 型の明示 | 必須 (int x) | 不要 (x = 42) |
| 実行速度 | 速い | 比較的遅い |
| 代表的な言語 | C、Java、TypeScript | Python、JavaScript、Ruby |
実践における意味
なぜPythonはCよりも遅いのか?
→ C: CPUが機械語を直接実行
→ Python: 仮想マシンがバイトコードを1行ずつ解釈して実行
なぜTypeScriptを使うのか?
→ JavaScriptに静的型を追加 → 実行前にエラーを検出
→ TypeScript → (tscコンパイル) → JavaScript → (V8実行)
なぜDockerイメージがOSごとに異なるのか?
→ コンパイルされたバイナリはCPUアーキテクチャ(x86、ARM)に依存
→ macOS用のバイナリはLinuxで実行できない核心のまとめ
| 概念 | まとめ |
|---|---|
| コンパイラ | 全体のソースを事前に機械語に変換。高速な実行 |
| インタプリタ | 1行ずつ解釈して実行。迅速な開発 |
| バイトコード | 中間形態。仮想マシンが実行(Python、Java) |
| JIT | 実行中に頻繁に使う部分だけを機械語にコンパイル(JavaScript) |
| 静的型 | コンパイル時に型を確認(C、TypeScript) |
| 動的型 | 実行時に型を確認(Python、JavaScript) |
ビルドツールとトランスパイラ
現代の開発では、「純粋な」コンパイル/インタプリト以外にも、さまざまな変換ツールがあります。
TypeScript → (tsc) → JavaScript → (V8) → 実行
JSX/React → (Babel) → JavaScript → (V8) → 実行
Sass/SCSS → (sass) → CSS → ブラウザレンダリングトランスパイラは同じレベルの言語に変換します。コンパイラが高レベル→低レベルの変換であるのに対し、トランスパイラは高レベル→高レベルの変換です。TypeScript→JavaScript、ES6→ES5が代表的です。
「Pythonはインタプリタ言語」は半分だけ正しい — バイトコードにコンパイルした後にインタプリトします。「JavaScriptはインタプリタ言語」も半分だけ正しい — V8はJITコンパイルを行います。現代の言語は、コンパイルとインタプリトを組み合わせて、開発の容易さと実行パフォーマンスの両方を追求しています。