前章で、CPUは「数字の列(機械語)を命令として実行するだけ」だと分かった。では、私たちが書く int x = 3 + 4; のような人間の言葉は、どうやってあの数字の列になるのか。
この章は、Part 2の入り口だ。高級言語と物理の間に架かる「抽象化のはしご」を、一段ずつ降りていく。
抽象化のはしご
プログラムは、いくつもの層を経てCPUに届く。
高級言語 (C, Python, ...) ← 人間が読み書きする
│ コンパイル
▼
アセンブリ言語 ← 機械語に1対1で対応する人間可読の形
│ アセンブル
▼
機械語 (0と1) ← CPUが直接実行する数字の列
│
▼
CPU (フェッチ・デコード・実行) ← 第3章の世界
各層は「下の層の複雑さを隠す」ために存在する。抽象化とは、下の面倒を見なくて済むようにする仕組みだ。だが、隠されているだけで消えてはいない。「おまじない」を解くには、はしごを降りて下の層を覗く必要がある。
機械語 — CPUの母語
機械語は、命令を表す数字の列だ。前章の自作CPUで 0x01, 0, 3(LOAD r0, 3)と書いたが、あれがまさに機械語だった。
実物のx86-64 CPUでは、例えば「レジスタに5を入れる」命令はこうなる:
機械語(16進): b8 05 00 00 00
意味: mov eax, 5 (eaxレジスタに5を代入)
b8 が「eaxに即値を入れろ」というオペコード、続く 05 00 00 00 が値の5(リトルエンディアンの32ビット)。CPUはこの5バイトを読んで、レジスタに5を入れる。人間には解読困難だが、CPUにとっては明快な母語だ。
アセンブリ — 機械語に名前をつける
機械語の数字は人間に厳しすぎる。そこで、機械語と1対1で対応する、人間が読める記号を割り当てた。これがアセンブリ言語だ。
b8 05 00 00 00 → mov eax, 5
mov(move)、add、sub、jmp(jump)といったニーモニック(記憶しやすい記号)で命令を書く。アセンブリを機械語に変換するツールをアセンブラと呼ぶ。
「3 + 4」をアセンブリで書くとこうなる(x86-64・簡略化):
mov eax, 3 ; eax に 3 を入れる
add eax, 4 ; eax に 4 を足す → eax = 7
; 結果は eax レジスタに入っている
第3章の「フェッチ・デコード・実行」を思い出してほしい。この各行が、まさに1つの命令としてCPUに読まれ、実行される。アセンブリは機械語に名前を付けただけなので、CPUの動きがそのまま見える。
C — 「移植可能なアセンブリ」
アセンブリは強力だが、CPUの種類(x86, ARM…)ごとに命令が違う。同じプログラムを別のCPUで動かすには、全部書き直しになる。
この問題を解いたのがC言語だ。1972年、ベル研究所のデニス・リッチーが開発した。Cは「人間に読みやすく、かつ機械語に近い」絶妙な位置にいる。しばしば「移植可能なアセンブリ」と呼ばれる。
int main(void) {
int x = 3 + 4; // 人間に読みやすい
return x;
}
このCコードは、コンパイラによって、各CPU向けの機械語に翻訳される。同じソースコードから、x86用にもARM用にも機械語を生成できる。これがCの革命だった。
コンパイルの4段階
gcc hello.c -o hello という1コマンドの裏で、実は4つの工程が走っている。
hello.c
│ ① プリプロセス (cpp) #include や #define を展開
▼
hello.i
│ ② コンパイル (cc1) C → アセンブリに翻訳
▼
hello.s (アセンブリ)
│ ③ アセンブル (as) アセンブリ → 機械語(オブジェクトファイル)
▼
hello.o (機械語)
│ ④ リンク (ld) ライブラリと結合し実行ファイルへ
▼
hello (実行ファイル)
実際に、Cがどんなアセンブリになるか覗いてみよう。
# Cコードをアセンブリに変換して確認する
$ gcc -S -O0 add.c -o add.s
$ cat add.s
add.c が:
int add(int a, int b) {
return a + b;
}
だとすると、生成されるアセンブリ(x86-64・抜粋)はおおむね:
add:
mov eax, edi ; 第1引数 a を eax へ
add eax, esi ; 第2引数 b を足す
ret ; eax(結果)を返す
a + b という1行が、mov と add と ret の3命令になった。Cの1文が、複数の機械語命令に展開される。この対応関係が見えると、「なぜこの書き方は速い/遅い」が理解できるようになる。
なぜ抽象化を降りると強くなるのか
高級言語だけを使っていると、次のような「おまじない」に出会う。
- 「なぜ
intは 4バイト(32ビット)なの?」→ CPUのレジスタ幅と機械語の設計に由来する - 「なぜ整数の桁あふれが起きるの?」→ 第1章の8ビット/32ビットの表現範囲そのもの
- 「なぜCは速くてPythonは遅い?」→ Cは機械語に直接コンパイルされ、Pythonは1行ずつ解釈実行されるから
抽象化のはしごを降りて「下で何が起きているか」を知ると、上の層の挙動が必然として理解できる。これがDIVEの一貫したテーマだ。
まとめ — 「おまじない」の消し方
- 抽象化のはしご:高級言語 → アセンブリ → 機械語 → CPU。各層は下の複雑さを隠すが、消してはいない
- 機械語はCPUの母語:命令を表す数字の列。アセンブリはそれに人間可読な名前を付けたもの
- Cは移植可能なアセンブリ:機械語に近く、かつCPUの違いをコンパイラが吸収する
- コンパイル=翻訳:Cの1文が複数の機械語命令に展開される。この対応が性能理解の鍵
「プログラムが実行される」とは、翻訳された機械語をCPUが読むこと。その距離感が掴めた。
次章では、この実行の最中に、関数呼び出しやローカル変数がメモリのどこに、どう置かれるのか——スタックとヒープの世界へ降りていく。
「コンパイラとは、人間の怠惰と機械の厳密さの間を取り持つ、最も偉大な翻訳者である。」