跳轉到

1.1 2015:Rust 1.0 與記憶體安全的系統語言之爭#

五十年的幽靈:記憶體錯誤#

1972 年,Dennis Ritchie 在貝爾實驗室發表 C 語言,配上 Unix,塑造了此後半世紀的系統軟體世界。C 的設計哲學是「信任程式設計師」:指標可以直接做算術、陣列不做邊界檢查、記憶體要自己 malloc/free。這帶來極致效能,也埋下了三類至今仍在流血的漏洞:

  1. 緩衝區溢位(buffer overflow):1988 年的 Morris 蠕蟲利用 gets() 的溢位癱瘓了當時網際網路約一成的機器;2014 年的 Heartbleed 則是 OpenSSL 中一個未檢查長度的 memcpy。
  2. Use-after-free 與懸垂指標:記憶體釋放後指標仍被使用,攻擊者可以重新配置該塊記憶體來劫持控制流。瀏覽器安全漏洞中這類 bug 長期佔大宗。
  3. 資料競爭(data race):多執行緒同時讀寫同一塊記憶體且沒有同步,結果依排程而定,難以重現、難以除錯。

微軟在 2019 年公開統計:歷年修補的安全漏洞中,約 70% 起因於記憶體安全問題;Google 的 Chromium 也給出相近數字。也就是說,半個世紀來系統工程師花了無數時間在修「同一種錯」。

防線的演進:為什麼以前防不住#

業界不是沒有嘗試。1990 年代有各種「安全 C」方言(Cyclone,2001 年康乃爾大學,把 region 分析加進 C);執行期有 AddressSanitizer(2012 年,Google,Dmitry Vyukov 與 Konstantin Serebryany 開發)、Valgrind;語言上有 Java(1995)用 GC 消除手動釋放,但 GC 的暂停(stop-the-world pause)與不可預期的延遲使它不適合核心、驅動、即時系統;Go(2009)同樣有 GC,且執行期較肥。系統層級始終缺一個「沒有 GC、又要編譯期保證記憶體安全」的語言。

Mozilla 的答案#

2006 年,Mozilla 工程師 Graydon Hoare 開始個人專案 Rust,目標明確:寫瀏覽器這種數十萬行、高度併發、絕不能被記憶體錯誤攻破的軟體。2009 年 Mozilla 贊助,2010 年對外公布,2015 年 5 月 15 日發布 Rust 1.0,承諾 API 穩定。2017 年 Servo 團隊的元件被 Firefox 以 Oxidation 計畫納入;2021 年 Linux 核心決定接受 Rust 作為第二語言;2023 年起 Android、Windows、Chrome 都陸續以 Rust 重寫關鍵元件。這是三十多年來第一個打進系統層的新語言。

Rust 的關鍵發明是所有權(ownership)系統:把「這塊記憶體由誰負責釋放」變成編譯期可驗證的規則。它源自 2010 年前後學術界對線性型別(linear types)與 region inference(Tofte & Talpin, 1994)的研究,由 Hoare 團隊工程化。代價是學習曲線——但換來的是:不需要 GC、執行期與 C 相當、卻在編譯期就擋掉上述三類錯誤。

一個對照實驗#

以下 C 程式能編譯、能執行、然後出事:

// C:use-after-free,編譯器頂多給個警告
#include <stdlib.h>
#include <stdio.h>

int main() {
    int *p = malloc(sizeof(int) * 100);
    free(p);
    p[0] = 42;          // 寫入已釋放記憶體:未定義行為
    printf("%d\n", p[0]);
    return 0;
}

同樣的邏輯在 Rust 中直接編譯失敗:

fn main() {
    let mut v = vec![0i32; 100];
    let p = &mut v;
    drop(v);                 // v 的所有權已交給 drop
    // p[0] = 42;           // ← 編譯錯誤:v 已被移動(moved)
    println!("{}", p[0]);
}
error[E0382]: borrow of moved value: `v`
  --> src/main.rs:6:22
   |
4  |     let mut v = vec![0i32; 100];
   |         ------- move occurs because `v` has type `Vec<i32>`
5  |     drop(v);
   |          - value moved here
6  |     println!("{}", p[0]);

Rustc 不只告訴你錯,還告訴你「值在哪一行被移動」——錯誤訊息本身就是教材。這種「把執行期災難變成編譯期錯誤」的能力,是本書全部章節的地基。

資料競爭也一起解決#

更驚人的是:Rust 的所有權規則配上 Send/Sync trait,能在編譯期直接拒絕資料競爭。C 的 pthread、C++11 的 std::thread 都只提供機制不提供保證;Rust 則是「沒有鎖就不能跨執行緒共享可變資料,除非你明確說出同步策略」。我們會在第 3.1 節詳細展開。

本書的立場#

本書不打算把 Rust 當成「更好的 C」,而是把它當成探索 Linux 系統的顯微鏡:因為 Rust 讓我們能安全地直接呼叫 syscall、操作檔案描述器、包裝 epoll 與 clone,錯了編譯器會叫,所以我們敢在教科書裡做從 fork/exec 一路到微型 Docker 的完整實作——這在 C 語言教材中通常只能給片段程式碼。

習題#

  1. 用 rustup 安裝 Rust,寫一個故意觸發 E0382 的程式,閱讀完整錯誤訊息。
  2. 查 Morris 蠕蟲(1988)的歷史,寫 300 字說明它如何改變了資安產業。
  3. 比較 GC 語言(Go)與 Rust 在「低延遲伺服器」場景的取捨,列出 GC 暫停對 tail latency 的影響。