1972-C編譯器與可攜性#
案件摘要#
1969 年,Bell Labs 從 Multics 這個龐大專案的失敗中撤出,Ken Thompson 在廢棄的 PDP-7 上重新找到寫小型系統的樂趣——Unix 誕生了,但它是用組合語言寫的,綁死在機器上。Dennis Ritchie 從 Martin Richards 的 BCPL 中找到線索:可攜的編譯器技術。經過 B 的實驗與 PDP-11 位元組定址的逼迫,1971–1972 年 C 語言成型:B 加上型別、加上結構。1978 年 K&R 的《The C Programming Language》成為聖經;Unix 用 C 重寫,成為史上第一個可攜作業系統。本案的真相是:C 不只是語言,而是「讓系統軟體獲得自由」的移植工程。
前因 -- 為什麼會有這個案子#
- Multics 的教訓:1964–1969 年 Bell Labs、MIT、GE 合作的 Multics 是巨型分時系統,目標宏大卻曠日廢時。Bell Labs 退出後,Thompson 與 Ritchie 反而懷念「小而美」:一個程式做一件事。
- PDP-7 上的 Unix:1969 年 Thompson 用組合語言在 PDP-7 寫出最早的 Unix 與 B 的前身。組合語言的代價:換機器就要全部重寫。
- BCPL 的線索:Martin Richards(1969)的 BCPL 是無型別、word 導向的語言,一切資料都是一個機器字。它的編譯器設計成可攜——前端與機器無關,只有碼產生器需要換。這是本案最重要的物證。
- PDP-11 的逼迫:1970 年 Bell Labs 買進 PDP-11——它是位元組定址(byte-addressable)而非字定址機器。B 的「一切皆 word」模型在它上面效能低落,無法表達 byte 與浮點數——型別系統被硬生生逼了出來。
線索與推理 -- 數學式、程式、理論#
BCPL → B → C:演進對照表#
| 特性 | BCPL (Richards, 1969) | B (Thompson, 1970) | C (Ritchie, 1971–72) |
|---|---|---|---|
| 型別系統 | 無型別(一切是 word) | 無型別(word/cell) | char、int、浮點、指標、struct |
| 定址模型 | 字定址 | 字定址 | 位元組定址 |
| 陣列/字串 | 向量 | 向量 | 陣列 + NUL 結尾字串 |
| 結構化 | 函數、區塊 | 函數 | 函數、struct、模組化標頭檔 |
| 編譯器 | 可攜(前端/後端分離) | 直譯式(TMG→threaded code) | 編譯到 PDP-11 碼,後可攜 |
| 系統影響 | 影響 B | PDP-7 Unix 早期工具 | Unix 重寫、可攜化 |
型別被逼出來的數學#
字定址機器上,第 \(i\) 個 word 的位址是 \(\text{base} + i \cdot w\) ( \(w\) 為字長)。B 的指標算術假設 \(w=1\) (以 word 為單位),因此在 PDP-11( \(w=1\) byte)上每次間接存取都多付了錯誤倍率。Ritchie 的解法:型別攜帶大小資訊,指標算術按所指型別縮放:
\(\text{sizeof}(T)\) 由型別系統在編譯期決定——這就是 C 型別系統存在的核心理由。同時 Ritchie 保留 Thompson 的關鍵決定:陣列名即位址,得到 C 最著名的恆等式:
最後一式在語法上完全合法——加法交換律使 i[a] 與 a[i] 同義,這是「陣列名退化解為指標」這條語意規則的極端推論。
指標與陣列的數學關係:驗證程式#
#include <stdio.h>
int main(void) {
int a[4] = {10, 20, 30, 40};
int *p = a; /* 陣列名退化解為指向首元素的指標 */
/* a[i] ≡ *(a+i) ≡ *(i+a) ≡ i[a] —— 同一個位址算術 */
printf("%d %d %d %d\n", a[2], *(a+2), *(2+a), 2[a]); /* 30 30 30 30 */
/* sizeof(T) 縮放:p+1 前進 4 bytes(int),不是 1 byte */
printf("%p -> %p\n", (void*)p, (void*)(p+1));
return 0;
}
輸出顯示四種寫法同值、且 p+1 的位址差正好是 sizeof(int)——型別系統與位元組定址的妥協由此可驗。
可攜編譯器的關鍵技巧#
BCPL/C 的編譯器分層:前端(詞法、語法、語意、中間碼)與機器無關;後端(碼產生器)針對機器重寫。移植成本從整個編譯器降為後端:
原始碼 C ──→ [前端:與機器無關] ──→ 中間碼/抽象碼 ──→ [後端:每機器一份] ──→ 機器碼
Ritchie 的 PDP-11 C 編譯器早期採「七種表示式碼」的中間形式;後續 Portable C Compiler(John Reiser, 1970s 末)把後端機器描述進一步壓縮。配合 Pascal 的自舉手法(見 1970-Pascal與自舉),移植一台新機器的成本降到「寫後端 + 自舉」兩步。
小型 C 編譯器骨架(偽代碼)#
一個單趟小 C 編譯器(如後來 Robert Pike 等示範的風格)的骨架:
compile(原始檔):
tokens = lex(src) # 詞法分析:字元流 → 符號流
ast = parse(tokens) # 語法分析:遞迴下降/運算子優先
symtab = semantic(ast) # 語意:型別檢查、sizeof 標注
ir = gen_ir(ast, symtab) # 中間碼:與機器無關
asm = emit(ir, target) # 後端:目標機器碼
return asm
# 運算式解析的優先級爬升(precedence climbing),C 的核心語法引擎:
parse_expr(min_prec):
lhs = parse_unary()
while (op = peek()) 且 prec(op) >= min_prec:
consume(op)
rhs = parse_expr(prec(op) + 1) # 左結合:+1;右結合:不升
lhs = Node(op, lhs, rhs)
return lhs
parse_expr 的優先級爬升正是 C 運算式(a + b * c < d = e 那種惡名昭彰的優先序)被正確解析的引擎——* 的優先級高於 +、賦值最低且右結合,全部藏在 prec() 這張表裡。
結案 -- 後果與影響#
- 史上第一個可攜作業系統:1973 年 Unix 用 C 重寫完成;從此 Unix 移植到 Interdata 8/32(1977)、VAX(1978)——「作業系統跟著機器走」的時代結束。
- K&R 聖經(1978):Kernighan 與 Ritchie 的《The C Programming Language》只有 228 頁,卻定義了此後十年的 C;1989 年 ANSI C 標準化(C89)。
- 語言帝國:C++(1983)、Objective-C、Java、C#、Go 的語法血統皆源自 C;今日從核心到韌體到嵌入式,C 仍是系統軟體的通用語。
- 編譯器工程:前端/後端分離成為標準架構,GCC、LLVM 的整體設計皆循此案;「用 C 寫 C 編譯器並自舉」成為所有新語言的入場券。
關鍵人物與文獻#
- Dennis Ritchie(Bell Labs):C 之父,1983 圖靈獎(與 Thompson 共得)。
- Ken Thompson(Bell Labs):Unix 與 B;Martin Richards:BCPL。
- 文獻:
- D. M. Ritchie, "The Development of the C Language," HOPL-II, 1993.
- B. W. Kernighan, D. M. Ritchie, The C Programming Language, Prentice-Hall, 1978.
- M. Richards, "BCPL: A Tool for Compiler Writing and System Programming," SJCC, 1969.