最新电影在线观看,jrs低调看直播,avav天堂,囯产精品宾馆在线精品酒店,亚洲精品成人区在线观看

Rust是如何避開C開發中的隱形坑?

C常見的代碼安全風險

軟件開發的Bug定位在軟件開發中一直是一個熱門的話題,在日常開發中絕大部分的Bug也由內存問題負責,其他則因為操作溢出、邏輯錯誤、時序等問題引發。據微軟的安全中心部門的統計報告顯示70%的內存Bug,谷歌安全也在博客中顯示在安卓bug中由90%的因內存問題觸發。在日常的嵌入式開發中,內存問題尤為關注,一旦出現內存bug,可能導致嵌入式系統輕易崩潰,也因此嵌入式C工程師在寫完代碼后需要花幾倍于寫代碼的時間去調試定位Bug。

那么在處理這些常見的潛在風險時,Rust與C分別是如何處理的呢?

C/Rust 代碼處理對比

操作溢出

對于基礎的數據類型來說,每個類型都有有效范圍,如果代碼中的操作對數據的操作無意超過了該數據的范圍,則可能會引起一些隱藏的風險。對于C/C++來說,沒有大部分的操作中,沒有強制檢查數據操作結果是否溢出,從而將溢出的風險可能轉移到后續的其他關聯的邏輯。

for (int8_t i = 0; i < 256; i++) {     
    // 進入了死循環 
}

// 移位溢出 
uint16_t Number = 0;                         
for (size_t i = 0; i < 32; i++) { 
    Number <<= 1; 
}

int8_t ch = 128;         
if (ch > 0) {    
    // 可能不能進入 
}

uint8_t x = 255;     
// 溢出 
x += 1; 

Rust則在數據溢出方面有著嚴格的限制,通常大部分的溢出能能在編譯階段直接報錯,運行時候的溢出邏輯通則panic終止(Debug模式下),有效保證立即發現風險點,能精確準確到問題的根源。

let val: i8 = 128;     //編譯報錯

let mut idx: i8 = 0  
loop {  
    idx += 1;
    if idx >=  128 {   // 編譯報錯 
        break;    
    }
}

let mut val: u32 = 0xffff_fffe;           
let mut inc: u32 = fun();   
val += inc;           // 運行時候如果溢出則 panic終止    

數組寫溢出

數組寫溢出的問題在C項目可能經常有發生,通常索引的值并不那么明顯,在數組修改時可能操作會正常返回,但一旦寫到未知的區域,則可能引起連鎖的安全事故,且不易定位。通常需要專業的工具如ASan等去實時監控寫數組溢出異常,非常耗費內存資源。在嵌入式資源有限的硬件中,通常無法部署。

char buf[] = "hello world";     
char buf2[20];   
int idx = fun();            
buf[idx] = 'H'; // 索引超限,但運行可能修改位置位置的內容程序可繼續運行      

Rust則利用天生支持索引檢測,且運行高效,不會耗費過多的資源。在常量索引中通常在編譯時候就能檢測出并報錯,運行時則會檢測索引有效值,非法則panic停止運行,嚴謹的檢查將每個細微的問題準確暴露出來。當然也容許在生產環境中關閉panic避免影響業務。

let mut buf: [u8; 10] = [0; 10];   
buf[12] = 1;                    // 編譯報錯   
buf[fun()];                     // 運行可能panic到此行,程序終止
let mut buf:[u8; 10] = [0; 10]; // 使用迭代器遍歷數組,高效簡潔且安全
for v  in &mut buf {       
    *v = 1;         
}

數組訪問溢出

數據訪問超限通常在C開發中也存在,雖然可能沒有寫溢出嚴重性,但也可能給生產帶來潛在的Bug風險,需要大量的時間去分析定位。

char buf[] = {'1', '2', '3', '0'};   //沒有結束字符'\0'   
size_t len = strlen(buf);            // 返回錯誤的長度       
char ch = buf[5];                    // 返回非法值     

Rust在數組的訪問也檢查嚴格,通常使用迭代器進行讀或寫操作,代碼既簡潔易懂,且運行效率與手寫循環無差別,這也是Rust常用和推薦的遍歷方法。

let mut buf: [u8; 10] = [0; 10];    
let v = buf[12]                 // 編譯報錯   
let v = buf[fun()];             // 運行可能panic到此行,程序終止




let mut buf:[u8; 10] = [0; 10]; // 使用迭代器遍歷數組,高效簡潔且安全
for v  in &mut buf {  
    println("{}", v);         

指針對齊錯誤

在ARM處理器中,通常對于數據的取值需要嚴格的地址對齊,否則會引起總線中斷死機。在C開發中經常會遇到一些日志打印問題,為了查看一些地址的值,可能誤觸發死機。

char buf[] = "1234";
printf ("%d", (int)*(buf + 0));  // 地址可能未對齊,可能進入總線錯誤中斷

struct test_t {  
    uint16_t a;    
    uint8_t b;  
    uint32_t c;
}

struct test_t t;                   
uint32_t b = (uint32_t)(uint32_t *)(&t.b); 

Rust有這很多高級語言的特性,如打印函數通常在編譯時候會自動獲知數據的類型,無需手動指定打印的類型,既節省了編碼量又安全。在數據地址分配方面會自動優化地址,滿足安全運行邏輯。

let c = "1234";   
println("{}", c);

struct Test {       //編譯時通常順序會重新優化,節省空間或注重安全
    v16: u16;     
    v8: u8;
    v32: u32;
}

內存申請和釋放問題

嵌入式C中通常避免少使用內存分配的接口,主要原因是因為容易引起內存問題,但在一些特殊業務中又不得不大量使用,因此在一些大型工程中,定位內存泄露、內存釋放兩次、操作已釋放的內存、釋放非法內存等問題非常困難,通常需要經驗豐富的嵌入式工程師才能解決,為避免這種問題,通常不得不采取靜態代碼分析如cppcheck``splint,sanitize等工具去掃描代碼,但是一些內存問題即使通過靜態代碼分析工具也很難檢測出,只能在運行時候出問題才能排查。

uint8_t *ptr = malloc(1024);  
free(ptr);  
free(ptr);                  // 釋放兩次           
*(ptr + 1)     = 1;         // 操作已經釋放的問題            

uint8_t *buf = malloc(1024); //不釋放    
buf[1024] = 1;              // 操作非法內存   

uint8_t *vptr = 232323;  
free(vptr)                  // 非法釋放內存          

Rust則在在內存安全方面有著無與倫比的優勢,利用變量的生命周期原理自動釋放空間,高效利用內存。這一點在有限的嵌入式資源中尤為重要。讓嵌入式開發如Go、Java等高級語言一樣簡單,無需過多關注內存的申請和釋放問題,只專注于業務功能邏輯,Rust編譯器會保證你的代碼不會在內存上翻車。

let buf:[u8;10];              
printfln!("{}", buf);   //編譯報錯,使用未初始化的內存                 

{
    let mut vec = Vec::new(); 
}
vec.push(1);             // 編譯報錯,vec已經釋放,不給你再操作

聲明:本內容為作者獨立觀點,不代表電子星球立場。未經允許不得轉載。授權事宜與稿件投訴,請聯系:editor@netbroad.com
覺得內容不錯的朋友,別忘了一鍵三連哦!
贊 2
收藏 3
關注 19
成為作者 賺取收益
全部留言
0/200
成為第一個和作者交流的人吧