跳轉到

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{addr}(a[i]) = \text{base}(a) + i \cdot \text{sizeof}(T) \]

\(\text{sizeof}(T)\) 由型別系統在編譯期決定——這就是 C 型別系統存在的核心理由。同時 Ritchie 保留 Thompson 的關鍵決定:陣列名即位址,得到 C 最著名的恆等式:

\[ a[i] \equiv *(a + i) \equiv *(i + a) \equiv i[a] \]

最後一式在語法上完全合法——加法交換律使 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.