你一定不能錯過的 Rust 記憶體安全指南

語言: CN / TW / HK

關注「 Rust程式設計指北 」,一起學習 Rust,給未來投資

原文:https://hashrust.com/blog/memory-safety-in-rust-part-2/

譯者:韓玄亮(一個熱愛開源,喜歡 rust 的 go 開發者)

介紹

在「 rust 中的記憶體安全 — 1 [1] 」中,討論了記憶體安全性的概念以及不同語言實現記憶體安全的各種技術。幾乎所有的語言都只聚焦一個方面上,要麼是記憶體安全,要麼是程式設計師控制。而 Rust 的獨特之處就在於它不會做出這種取捨 —— 程式設計師可以同時獲得記憶體安全和控制。

:ledger:  不是所有可以用 C++ 編寫的程式都可以用  Safe Rust 編寫。正如馬上要看到的,在 Rust 中不可能出現不可控的別名,這你可以放心。Rust 在預設模式下是記憶體安全,但如果開發者真的想擁有 C++ 那樣不受約束、控制的風格,他們可以使用  Unsafe code

別名/可變性/安全

要安全地釋放一個物件,那銷燬時必須沒有對它的引用,否則最終將得到一個懸空指標。

類似地,如果一個執行緒想要將一個物件傳送給另一個執行緒,那麼傳送執行緒上不能有對它的引用。這裡有兩個因素:別名和可變性。如果物件沒有被銷燬或通過執行緒傳送,那麼引用它並沒有什麼問題。只有當兩者結合時,你才會遇到麻煩。

根據這一觀察結果,Rust 解決記憶體安全的方法是:簡單地同時禁止別名和可變,而 Rust 是通過所有權和借用來實現這一點。

所有權

當您在 Rust 中建立一個新物件時,被賦值變數成為該物件的所有者。 例如在下面的 Rust 程式碼中,變數 v 擁有 Vec 例項:

let v: Vec<i32> = Vec::new();

當 v 超出可表達範圍時,Vec 被丟棄。 一個物件在同一時間只能有一個所有者,這確保只有所有者才能刪除該物件。這避免了重複釋放 ( double-free ) bug。如果 v 被賦值給另一個變數,則所有權轉移  ( v → v1 ):

let v1 = v;   // v1 is the new owner

因為 v1 現在是所有者,所以不再允許通過 v 訪問:

v.len();    // error: Use of moved value

:ledger: 雖然 c++ 也有 move 語義,但它不能防止你引入一個 move 後使用的 bug。

所有者可以改變物件:

let mut v =Vec::new();    // mut is needed to mutate the object
v.push(1);

但是因為沒有別名,所以問題不大。

雖然開發者在 Rust 中所能做的就是擁有值並傳遞它們,雖然這是一個相當受限的程式設計體驗;但是幸運的是,Rust 允許從所有者那裡 借用

借用

借用引入了別名。我們可以使用 引用, 從所有者那裡借來:

let v:Vec<i32> = Vec::new();
let v1 = &v; // v1 has borrowed from v
v.len(); // fine
v1.len(); // also fine

與所有者不同,可以同時存在多個借用的引用:

let v:Vec<i32> = Vec::new();
let v1 = &v; // v1 has borrowed from v
let v2 = &v; // v2 has also borrowed from v
v.len(); // allowed
v1.len(); // also allowed
v2.len(); // also allowed

但是在所有者銷燬後,借用者不能再訪問所有者指向的記憶體區域資料,否則會導致一個 bug ( use-after-free )。

bug (use-after-free)。
let v1: &Vec<i32>;
{
let v =Vec::new();
v1 = &v;
} // v is dropped here
v1.len(); // error:borrowed value does not live long enough

因此,即使可能存在別名,Rust 也會確保引用的生命週期不會超過被引用的物件,從而再次避免了別名和可變帶來的 bug。

到目前為止,所有的借用都是不可變的。不過可變引用一定會在程式中出現,但正如接下來要看到的,Rust 足夠聰明, 在引入可變性的同時是不允許出現別名

可變借用

雖然可以有多個共享引用,但一次只能有一個可變引用:

let mut v:Vec<i32> = Vec::new();
let v1 = &mut v; // 第一個可變借用
let v2 = &mut v; // 第二個可變借用
v1.push(1); // error:cannot borrow `v` as mutable more than once at a time

在允許可變引用進行變數可變時,Rust 就通過禁止其他引用(共享的或可變的)來消除別名。 這些借用規則防止懸空指標的出現。 如果 Rust 同時允許可變引用和不可變引用,那麼記憶體可能通過可變引用變得無效,而不可變引用仍然指向那個無效的記憶體。 例如,在下面的程式碼中,如果允許這樣的程式碼通過,v1 就可以訪問無效的記憶體:

let mut v = vec![0, 1, 2, 3];    // 可變所有者
let v1 = &v[0]; // 不可變借用
v.push(4); // Vec內部指向的記憶體區域發生改變,之前的緩衝區無效
let v2 = *v1; // error: 訪問無效記憶體區域

但是,相比之下類似的程式碼在 c++ 中是允許編譯成功的。

生命週期

上面我們已經討論過 Rust 不允許同時使用別名和可變以防止記憶體安全問題,但在這幾節中我一直在討論 Rust 是如何在編譯時實現這一記憶體安全目標。而 Rust 是怎麼實現的呢?

Rust 通過跟蹤變數的生命週期來實現這一點。直觀地說,變數的生命週期與其作用域有關。

let v1: &Vec<i32>;//-------------------------+
{// |
let v =Vec::new(); //-----+ |v1's lifetime
v1 = &v;// | v's lifetime |
}//<-------------------------+ |
v1.len();//<---------------------------------+

所以編譯器會比較各種變數的生存期,以確定是否發生了什麼可疑的事情。

例如,在上面的程式碼中,v1 的壽命超過了所有者 v,這是不允許的。上面示例中的生存期稱為詞法生存期,因為它們是由變數作用域推斷出來的。實際上,Rust 有一個更復雜的生命期實現,叫做 非詞法生命期 [2]

生命週期是一個很大的話題,我不可能在這篇文章中涵蓋所有的內容。你可以在  Rustonomicon [3] 中瞭解更多關於生命週期的資訊。

總結

在這篇文章中,我們討論了所有權和借用的概念,以及它們如何幫助實現 Rust 的記憶體安全。許多記憶體安全問題歸結為一個事實,即語言本身同時允許可變和別名,比如 C++。

Rust 在編譯期能檢測這些記憶體安全問題的能力使其成為系統程式語言的有力競爭者。

References

[1] rust 中的記憶體安全 — 1:  https://hashrust.com/blog/memory-safey-in-rust-part-1/

[2] 非詞法生命期:  https://smallcultfollowing.com/babysteps/blog/2016/04/27/non-lexical-lifetimes-introduction/

[3] Rustonomicon:  https://doc.rust-lang.org/nomicon/lifetimes.html

推薦閱讀

覺得不錯,點個贊吧

掃碼關注「 Rust程式設計指北