用Rust開發的四大優勢!C與C++有上百條避免未定義行為的規則,這些規則大多是常識

用Rust開發的四大優勢

讓我們從一個謎題談起。下面的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具備高效的預設機制,可讓你控制記憶體的使用情況,以及處理器應專心處理哪裡。

🍎たったひとつの真実見抜く、見た目は大人、頭脳は子供、その名は名馬鹿ヒカル!🍏

return-to-libc這種攻擊現在基本上會被防禦住了smiley-think.gif

rust還是先弄個c++相容層吧smiley-shock.gif

我要成為幸運的一般會社員

現代的電腦語言抱怨「C與C++有上百條避免未定義行為的規則」根本是毫無意義的事!根據Wiki百科的說明,C與C++語言的出生年代是:

C語言:1972年 (50年前)

C++語言:1983年 (39年前)

而最早的Windows版本是在1985年:

Windows 1.0 / 1985年11月20日 (36年前)

也就是說,在C與C++語言出生的年代,連毫無實用性可言的Windows 1.0都還不知道在哪裡!IBM PC第一代使用8088、8086的年代,當年也只有MS-DOS:

初始版本    1981年8月12日(​41年前)

C語言誕生的時候,甚至連MS-DOS都還沒出生!當年的電腦環境、速度,都不是現代電腦環境可以相比。在CPU運算速度有限的環境,想要做出編譯時嚴密審查,絕對不會出錯的「完美電腦語言」根本是不切實際!

C語言相對當年的BASIC、PASCAL、FORTRAN、COBOL等高階電腦語言,已經是相對的簡單、快速,主要的目的是希望能取代太過直接,複雜又難維護Assembly組合語言,而不是打造出一個「完美電腦語言」。也因此C語言是唯一可以內含Assembly組合語言的中高階電腦語言。

現代新的電腦語言,可以強調這個語言有什麼優點、特質,希望新人可以學習新的電腦語言。電腦進步速度飛快,就像新的iPhone 14的執行速度已經遠超過第一代iPhone不只14倍。如何學會新的電腦語言比較重要,寫一本書去批評一個40、50年前的「活化石」等級的電腦語言,根本毫無意義。

不好意思,這是在解決「相簿沒法留言了」的問題,順便說明一下。

🍎insoler社群網站CEO、兼站長兼顧問兼工程師

來編寫kernel和driver其實C非常夠用了smiley-think.gif

rust只不過是增加了幾個scope resources 方法smiley-tongue-out.gif

真要到現代C++的地步smiley-think.gif那個東西是可以描述世間一切模型的smiley-yell.gif所以變得非常麻煩或者會變得非常囉嗦smiley-anger.gif

我覺得真有用的是精簡的C++規範smiley-cant.gif

我要成為幸運的一般會社員