碁峰資訊

讓我們從一個謎題談起。下面的C程式會產生什麼?

int main(int argc, char **argv) {
unsigned long a[1];
a[3] = 0x7ffff7b36cebUL;
return 0;
}

今天早上,這段程式在Jim的筆電中印出:

undef: Error: .netrc file is readable by others.
undef: Remove pazssword or make file unreadable by others.

然後崩潰了。當你在自己的電腦裡執行它時,你可能會看到不一樣的行為。為何如此?

這段程式有缺陷。陣列a只有一個元素長,所以根據C語言標準,a是一種未定義行為(undefined behavior):它是在使用「不可移植的,或錯誤的程式結構」或「錯誤的資料」時發生的行為,國際標準未規定此時該怎麼辦?

未定義行為不只會造成不可預測的結果,該標準明確地允許程式做任何事情。這個例子將值存入陣列的第四個元素剛好破壞了函式呼叫堆疊(call stack),因此當main函式return時,程式不會優雅地退出,而是跳到標準C程式庫的程式裡,從用戶的主目錄裡的檔案提取密碼。程式沒有正常地執行。

C與C++有上百條避免未定義行為的規則,這些規則大多是常識:不要存取不該存取的記憶體、別讓算術運算子溢位、不要除以零……等,但是編譯器並不強制執行這些規則,它甚至沒有義務檢查公然違規的行為。事實上,上述的程式在編譯時不會出現錯誤或警告,避免未定義行為的責任完全落在你這位程式設計師身上。

據經驗,程式設計師不會妥善地記錄它們。研究員Peng Li在猶他大學就學時,曾經修改C與C++編譯器,讓它們編譯出來的程式可以回報自己是否執行了某種未定義行為。他發現,幾乎所有程式都執行了未定義行為,包括高標準且備受尊敬的專案。如果有人認為他可以在C與C++裡避免未定義行為,那就相當於他認為只要了解下棋規則,即可贏得棋局。

偶發的奇怪訊息或崩潰也許是一種品質問題,但無意間寫出來的未定義行為向來是安全缺陷的主因之一,這種安全缺陷最早可追溯到1988年出現的莫里斯蠕蟲(Morris Worm),它使用上述技術的變體,透過早期的網際網路從一台電腦感染另一台電腦。

所以C與C++讓程式設計師面臨一種尷尬的情境:這些語言是系統程式設計的業界標準,但是它們對程式員的要求,幾乎保證會持續帶來層出不窮的崩潰與資訊安全問題。我們的謎題帶來一個更大的問題:我們真的沒辦法做得更好嗎?

Rust為你承擔重任

我們的答案可以從以下引言裡找到。這個引言是指2010年有一隻電腦蠕蟲入侵工業控制設備,利用未定義行為和許多其他技術來控制受害者的電腦,那個未定義行為出現在一段解析TrueType字體的程式裡面。

可以確定的是,那段程式的作者並未想到它會被那樣子使用,這個故事告訴我們,不是只有作業系統和伺服器需要擔心安全問題。

民族國家的攻擊者利用TrueType解析器的缺陷來監視別人,所有軟體都是安全敏感的。——Andy Wingo。

Rust語言給你一個簡單的承諾:只要編譯器認為你的程式沒有問題,它就沒有未定義行為。懸空指標(dangling pointer)、重複釋出(double-free)、對空指標解參考……等問題都會在編譯期抓到。陣列參考是透過編譯期與執行期檢查來保護的,所以不會出現緩衝區溢位(buffer overrun):Rust會產生一條錯誤訊息,並安全地退出,而不會像可悲的C程式那樣。

此外,Rust的目標是用起來既安全且愉快。為了更有力地保證程式的行為,Rust對你的程式施加了比C和C++更多的限制,你必須透過練習與經驗來習慣這些限制。但是整體來說,這個語言比較靈活,而且更有表現力,Rust程式及其應用領域的廣度可以證明這一點。

根據我們的經驗,「相信這個語言能夠抓到更多錯誤」可以鼓勵我們嘗試更有企圖心的專案。如果記憶體管理和指標有效性等問題可以解決,那麼修改大量、複雜程式的風險就會降低。而且,如果bug絕對不會破壞不相關的部分,偵錯將容易許多。

當然,Rust無法偵測的bug仍然很多,但在實務上,將未定義行為排除可以大幅改善開發品質。

平行程式設計被馴服了

在C與C++裡面使用並行(concurrency)是出了名的困難。開發者通常只會在無法用單執行緒程式來實現性能目標時,才會轉而使用並行。但是以下引言認為,平行化對現代電腦而言太重要了,不能視為最終手段。

現在的電腦都是平行的……設計平行程式等於設計程式。——Michael McCool等,《Structured Parallel Programming》。

事實上,在Rust裡面確保記憶體安全的那些限制,也保證了Rust程式不會出現資料爭用。你可以在執行緒之間自由地分享資料,只要它不會改變即可。會改變的資料只能使用同步基元(synchronization primitive)來操作。你可以使用所有的傳統並行工具,包括互斥鎖(mutex)、條件變數等。Rust會檢查你有沒有正確地使用它們。

所以Rust是一種充分利用現代多核心電腦能力的優秀語言。除了一般的並行基元之外,Rust生態系統也提供許多其他的程式庫,可協助你將複雜的工作負擔平均分給許多處理器、使用Read-Copy-Update之類的無鎖同步機制等。

然而,Rust仍然很快

最後要討論這條引言。

在某些情況下(例如,Rust所針對的情況),比競爭對手快10倍甚至2倍是決定性因素,它決定了一個系統在市場上的命運,和硬體市場一樣。——Graydon Hoare。

Rust的目標與Bjarne Stroustrup在其論文「Abstraction and the C++ Machine Model」中敘述的C++目標一致:一般來說,C++的實作應遵守零額外開銷(zero-overhead)原則:用不到的東西不需要為它付出代價,甚至更進一步,用最好的程式來編寫你使用的東西。

系統程式設計的目標通常是將機器的性能推到極限,對遊戲而言,是讓整台機器全力為玩家創造最棒的體驗。對網頁瀏覽器而言,瀏覽器的效率就是網頁內容的作者可發揮的上限。在機器固有的限制之內,你必須盡量把記憶體與處理器的工作重點放在內容本身。同樣的原則也適用於作業系統:kernel必須把機器的資源留給用戶的程式使用,而不是自己耗用它們。

但Rust「很快」是什麼意思?任何通用的語言都可能寫出緩慢的程式。比較準確的說法是,如果你準備投入資源,設計出充分利用底層機器的程式,Rust會支持你的付出。Rust具備高效的預設機制,可讓你控制記憶體的使用情況,以及處理器應專心處理哪裡。(摘錄整理自本書第一章,碁峰資訊提供)

 書名  Rust程式設計第二版(Programming Rust, 2nd Edition)

Jim Blandy, Jason Orendorff, Leonora F. S. Tindall/著;賴屹民/譯

碁峰資訊出版

定價:1,200元

 作者簡介 

Jim Blandy

Jim Blandy從1981年開始寫程式,並自1990年開始編寫自由軟體。他曾經製作GNU Emacs、GNU Guile與GNU Debugger,目前負責研發Mozilla的Firefox。圖片來源/Jim Blandy

Jason Orendorff

Jason Orendorff目前參與GitHub的未公開的專案。他曾經參與Mozilla的SpiderMonkey JavaScript引擎專案。他的興趣包括語法、烘焙、時間旅行,以及協助人們了解複雜的主題。圖片來源/Jason Orendorff

Leonora Tindall

Leonora Tindall是型態系統愛好者和軟體工程師,她使用Rust、Elixir與其他高階語言,來為醫療保健和資料所有權等高影響力領域建構可靠、強韌的系統軟體。圖片來源/Leonora Tindall

熱門新聞

Advertisement