域名新聞網(wǎng)站種子資源
簡介
近年來,越來越多的組織表示,如果新項(xiàng)目在技術(shù)選型時(shí)需要使用系統(tǒng)級開發(fā)語言,那么不要選擇使用C/C++
這種內(nèi)存不安全的系統(tǒng)語言,推薦使用內(nèi)存安全的Rust
作為替代。
谷歌也聲稱,Android 的安全漏洞,從 2019 年的 223 個(gè)降低到 2022 年的 85 個(gè),經(jīng)過分析,谷歌認(rèn)為內(nèi)存漏洞減少的情況,主要與 Rust 代碼的比例增加有關(guān)。在 Android 13 中,就已經(jīng)有約 21%的新原生代碼以 Rust 開發(fā)。
微軟也宣布,Rust 將正式入駐 Windows 系統(tǒng)內(nèi)核;AWS在其基礎(chǔ)設(shè)施中越來越多地使用 Rust ;2022 年 12 月,Linux 內(nèi)核 6.1 發(fā)布,包括最初的 Rust 支持. . .
作為后來者,Rust是怎么做到內(nèi)存安全,且受到越來越多人的青睞呢?要知道,換做使用C/C++開發(fā),可能只有高級C/C++開發(fā)人員寫出的代碼才能如此穩(wěn)定,Rust是怎么保證任何一個(gè)使用它的人都能寫出內(nèi)存安全的代碼的呢?
下面,針對在C/C++中幾種常見的內(nèi)存安全問題為例,簡單分析下。
懸空指針
懸空指針主要是指,在C/C++中,某個(gè)對象已經(jīng)被釋放了,但是在某個(gè)角落還有一個(gè)指針指向這個(gè)對象,這個(gè)指針就是一個(gè)懸空指針。當(dāng)代碼運(yùn)行到這個(gè)地方,解引用這個(gè)懸空指針時(shí),就會(huì)出現(xiàn)未定義的行為。
Rust解決這個(gè)問題的辦法就是Rust的精髓所在——生命周期。
int main()
{std::string *ptr = nullptr;{std::string = str;ptr = &str;}printf("%s", ptr->c_str());return 0;
}
上面是典型的C++中出現(xiàn)野指針的場景,這段代碼編譯器不會(huì)發(fā)出任何抱怨。
程序入口定義了一個(gè)std::string
類型的指針ptr
,并初始化為nullptr
。進(jìn)入代碼塊后,在代碼塊中創(chuàng)建一個(gè)局部變量str
,并且讓ptr
指向這個(gè)局部變量。當(dāng)執(zhí)行流結(jié)束這個(gè)代碼塊后,棧上的變量str將會(huì)被釋放,但是此時(shí)指針ptr 還是指向這個(gè)局部變量str,代碼塊后續(xù)任何解引用指針ptr的地方都將是一個(gè)不可預(yù)期的行為。
我們用Rust實(shí)現(xiàn)一下這段代碼。
fn main()
{let str_ref;{let str_obj: String = String::new();str_ref = &str_obj;}println!("{str_ref }");
}
相同的邏輯,只是Rust中將指針改為了引用(引用就是一個(gè)指針)。當(dāng)執(zhí)行流結(jié)束代碼塊之后str_obj將會(huì)被釋放,但是此時(shí)str_ref
還指向這個(gè)局部變量。嘗試編譯一下。
不出意外的,Rust的生命周期檢查器發(fā)現(xiàn)了這個(gè)問題,報(bào)錯(cuò)信息是borrowed value does not live long enough
,他說str_obj的生命周期不夠長,引用str_ref在str_obj的生命結(jié)束后還在使用。
Rust在編譯時(shí)會(huì)嘗試為每個(gè)引用和被引用的對象分配一個(gè)生命周期,生命周期完全是Rust在編譯期虛構(gòu)的產(chǎn)物,在運(yùn)行期,引用就是一個(gè)地址,所以生命周期不會(huì)有任何運(yùn)行期開銷。有了生命周期,在編譯期,生命周期檢查器就會(huì)對比被引用對象和引用之間的生命周期關(guān)系,如下:
黃色框表示str_obj的生命周期;引用str_ref 的生命周期是,str_ref 從初始化開始到str_ref 最后被使用的地方之間的代碼塊就是str_ref 的生命周期,所以這里白色方塊表示引用str_ref 的生命周期。生命周期檢查器準(zhǔn)則之一是,引用的最大生命周期不能超過被引用對象的生命周期,很明顯,這里違反了這條規(guī)則,所以無法通過編譯。
Rust解決野指針最重要的方法就是生命周期,這里只是介紹了最簡單的一個(gè)場景,在學(xué)習(xí)Rust時(shí),一定要理解生命周期的含義。
緩沖區(qū)溢出
在C++中,以vector
為例,想要以索引的方式訪問某個(gè)對象時(shí),我們通常會(huì)使用vector的at
方法進(jìn)行訪問,at方法會(huì)進(jìn)行數(shù)組越界檢測,這很安全。
但是vector可以通過data
方法返回一個(gè)C/C++的原生數(shù)組,當(dāng)我們對原生數(shù)組進(jìn)行索引操作時(shí),完全是一種走鋼絲的行為。
因?yàn)闆]有任何越界檢測,此時(shí)如果發(fā)生緩沖區(qū)溢出,將會(huì)是一個(gè)未定義的行為。如果影響了其他變量,那么這將會(huì)是一個(gè)非常難排查的問題;如果改動(dòng)了不可寫的地址,那么會(huì)導(dǎo)致程序崩潰;如果運(yùn)氣好溢出的部分沒有影響到任何對象,那看起來將會(huì)是一切安好,但是我們并不總是有那么好的運(yùn)氣。
這種未定義的行為絕對不是我們想要的。來看看Rust是怎么做的。
fn main() {let vec: Vec<i32> = vec![1,2,3];let vec_ref: &[i32] = &vec[0 ..];for i in 0 .. 4 {println!("{}", vec_ref[i]);}
上面是在rust中創(chuàng)建了一個(gè)vector——vec,其長度為3(內(nèi)容為1、2、3),然后一個(gè)引用vec_ref(指針)指向這個(gè)vec。
緊接著使用引用vec_ref故意進(jìn)行了一次緩沖區(qū)溢出的輪詢操作, 此時(shí)我們能夠正常通過編譯。這當(dāng)然能夠編譯通過,千萬不要妄想Rust能夠在編譯期解決緩沖區(qū)溢出這種主要在運(yùn)行期出現(xiàn)的問題。
但是cargo run
運(yùn)行時(shí)
可以清楚的看到導(dǎo)致了panic
,提示長度是3,但是index也是3,出現(xiàn)了緩沖區(qū)溢出的訪問。也就是說Rust對于緩沖區(qū)溢出的訪問會(huì)有一個(gè)已定義的行為——導(dǎo)致線程panic。但是新問題又來了,為什么一個(gè)引用(指針)vec_ref
也有長度信息呢?
如果只是一個(gè)普通的引用當(dāng)然不會(huì)有長度信息,但是這里的引用vec_ref是對一個(gè)連續(xù)數(shù)據(jù)vec的引用。在Rust中,vec_ref準(zhǔn)確的說是一個(gè)切片。對一個(gè)連續(xù)數(shù)據(jù)的引用(切片),引用本身是一個(gè)胖指針,即該引用占兩個(gè)機(jī)器字(普通引用只是一個(gè)普通指針,內(nèi)存上只占用一個(gè)機(jī)器字)的內(nèi)存,第一個(gè)機(jī)器字是被引用的連續(xù)數(shù)據(jù)的首地址;第二個(gè)機(jī)器字是連續(xù)數(shù)據(jù)的長度。
下面是打印兩種引用占用內(nèi)存大小的代碼。
fn main() {let vec: Vec<i32> = vec![1,2,3];let vec_ref: &[i32] = &vec[0 ..];let num: i32 = 3;let num_ref: &i32 = #println!("vec_ref size_of:{} num_ref size_of:{}",std::mem::size_of_val(&vec_ref), std::mem::size_of_val(&num_ref))
}
輸出為vec_ref size_of:16 num_ref size_of:8
。說明,引用(切片)vec_ref
占用16Bytes,引用num_ref
占用8Bytes,我的電腦是64位的電腦,剛好是兩個(gè)機(jī)器字和一個(gè)機(jī)器字。
除了切片之外,Rust中的原生數(shù)組也是帶有長度信息的,所以在使用原生數(shù)組出現(xiàn)緩沖區(qū)溢出時(shí),也會(huì)導(dǎo)致已定義的行為。
綜上,因?yàn)榫彌_區(qū)溢出主要是一個(gè)運(yùn)行期的行為,所以Rust也沒辦法做到在編譯期解決這個(gè)問題,但是通過胖指針的方式,Rust做到了在運(yùn)行期如果出現(xiàn)緩沖區(qū)溢出,那一定會(huì)有一個(gè)已定義的行為——線程panic。這肯定好過C/C++中緩沖區(qū)溢出后,各種未定義的奇葩問題。
對空指針進(jìn)行解引用
C/C++中對空指針解引用導(dǎo)致的崩潰問題更多的是開發(fā)人員個(gè)人編程習(xí)慣導(dǎo)致的。
在C/C++中,一個(gè)更好的編程習(xí)慣是在解引用指針之前,先對指針進(jìn)行判空操作,但是這樣簡單的一個(gè)判斷邏輯常常因?yàn)殚_發(fā)同學(xué)的“自信”,導(dǎo)致在很多地方偷懶忽略,然后直接對指針解引用后開始操作。往往越是自信不會(huì)為空的地方越是會(huì)給我們帶來最承重的打擊。
針對空指針解引用,首先Safe Rust中只有引用沒有指針,這里的引用和C++中的引用類似,本質(zhì)也是一個(gè)指針。Safe Rust中,在使用一個(gè)引用之前,必須對引用賦值,否則無法通過rustc的檢測。
fn main() {let s: String = String::new();let s_ref: &String = &s;
}
s
是一個(gè)String類型的變量,s_ref
是對s的一個(gè)引用。只有對s_ref賦值后才能對s_ref進(jìn)行使用。rustc通過強(qiáng)制檢測你的編碼實(shí)現(xiàn),杜絕了空指針的使用。
當(dāng)然,一定存在一個(gè)場景。某個(gè)引用,其需要引用的對象可能在程序運(yùn)行之初并沒有被創(chuàng)建,隨著程序的運(yùn)行才創(chuàng)建,創(chuàng)建后還需要讓這個(gè)引用指向這個(gè)剛創(chuàng)建的對象,也就是說需要Rust支持引用一開始為空,隨著程序的運(yùn)行才被賦值的情況。
上面這種場景下需要采用Option
。Rust中,一切可能為空的東西都需要使用Option進(jìn)行包裹,不僅僅是引用。
fn main() {let mut s_ref_option: Option<&String> = None;let s: String = String::new();s_ref_option = Some(&s);
}
這一次s_ref_option因?yàn)榭赡転榭?#xff0c;所以被聲明為Option<&String>
類型的None
,語義為,有一個(gè)T
為&String
類型的Option,這個(gè)Option目前包裹的值是None
,但是后面可能會(huì)賦值,所以后續(xù)要想獲取s_ref_option中包裹的&String
時(shí),你需要進(jìn)行檢查,因?yàn)椴淮_定后面會(huì)不會(huì)賦值。
緊接著,s才被創(chuàng)建,然后使用Some
包裹后賦值給s_ref_option。
通過Option
獲取其包裹的值通常有兩種做法,一種是安全的,一種是不安全的。安全的操作是在使用之前對Option進(jìn)行判空,顯而易見這很安全。
// 使用 s_ref_option 時(shí)判空
if let Some(v) = s_ref_option {//... v 是&String
}
但這在Rust中也不是強(qiáng)制的,開發(fā)人員也可以以一種不安全的方式使用Option——直接獲取Option中包裹的值。
// 不判空直接獲取Option中包裹的值
let s_ref: &String = s_ref_option.unwrap();
這和C/C++中直接進(jìn)行空指針解引用并沒有什么區(qū)別。
但是好在可以通過rustc中內(nèi)置的靜態(tài)代碼檢測工具clippy
,對代碼進(jìn)行掃描,如果檢測到代碼中有使用unwrap,那么直接報(bào)error
,clippy幫助檢查代碼中是否有這種危險(xiǎn)的使用。這可以理解為是Rust編程的一種規(guī)范,讓不寫unwrap作為Rust編程規(guī)范的一部分。
clippy中可以通過設(shè)置clippy::restriction
集中的unwrap_used
這條規(guī)范達(dá)到我們的目的,具體可以看我的另一篇博客 Rust代碼靜態(tài)分析工具Clippy淺析
綜上,Rust通過編譯器,強(qiáng)制檢測引用(指針)在使用之前必須賦值解決了這個(gè)問題。對于可能為空的對象,配合clippy使用,對于是否可以直接解引用可能為空的對象的選擇權(quán)留給開發(fā)者,也不為是一種比較好的方案。
非法釋放內(nèi)存
C/C++中存在非法釋放內(nèi)存的情況,比如double free、非法釋放棧上的內(nèi)存等等,這些操作都會(huì)導(dǎo)致程序的崩潰。
作為非GC系的語言,Rust也面臨釋放內(nèi)存資源的問題。但是當(dāng)你真正開始使用Safe Rust時(shí)會(huì)發(fā)現(xiàn),你基本不需要關(guān)心內(nèi)存的釋放,因?yàn)镽ust將C++中的精華RAII發(fā)揮到了極致。
對于需要進(jìn)行內(nèi)存管理的對象類型,其都會(huì)實(shí)現(xiàn)Drop
特型,定義如下:
pub trait Drop {// Required methodfn drop(&mut self);
}
實(shí)現(xiàn)該特型的類型,其實(shí)例在被釋放前都會(huì)調(diào)用這個(gè)方法,類型的實(shí)現(xiàn)者可以在drop
中釋放自己管理的資源,這和C++中的析構(gòu)函數(shù)一樣。RAII在Rust中被大量采用,所以作為一個(gè)Rust的開發(fā)者,在Safe Rust中,你基本不需要再去進(jìn)行內(nèi)存管理。
總結(jié)
Rust作為一顆冉冉升起的新星,已經(jīng)得到了越來越多人的認(rèn)可,將其壓入你的技術(shù)棧,一定會(huì)是一個(gè)不錯(cuò)的選擇。