用Rust解决C语言的隐患

题记:相对于其它语言,使用Rust开发更能避免低级错误。

简介

对笔者而言,Rust越用越顺手,接触越多也就越不能抵抗它的魅力,也因此才有了本文的诞生——希望大家能了解到这种语言的妙处。

对大众来说,Rust最大的卖点在于它能确保代码的安全性,这是Rust相对于C语言的一个极大优势,也是令Rust与众不同的关键所在,这也是本文的重点。

为了让大家对Rust的优势有所了解,我们选择了这个地方入手——Rust是如何令开发者的日常工作更加轻松、更加惬意的。本文详细列举了样例,阐明Rust是如何完全地消弭那些继承自C语言的诸多隐患。这一优势再加上Rust的新潮功能,就促成了Rust符合人体工程学的体验——bug更少,代码更好 (维护者半夜也能睡个好觉)。

100%的安全性

在列举例子之前,我们先来讨论一下Rust所使用的方式究竟安全在哪里。

Rust中的大多代码被称为“安全”代码,确保代码100%的安全性。这个百分之百并非统计学意义上的,它没有达到编译器希望的那样完美,但只要代码能够编译,内存安全性和data-race freedom就能够保证。

当然,这些措施无法避免开发者引入的逻辑错误,也就是说在极少数情况下,这些规则是可以打破的。这种情况下,开发者所编写的代码被称为“不安全的”代码。这类代码限制很少,开发者可以任意编写,但这样做的代价是:编译器不再确保安全性,结果可能会一塌糊涂。

隐患

空指针引用(NULL Dereference)

声名狼藉的程序分段错误(Segmentation Fault)是C语言的常见问题,而通常NULL dereferences是第一大诱因。如果开发者忘记了检查所返回的指针是否正确性,就可能会导致空指针引用。

uint8_t* pointer = (uint8_t*) malloc(SIZE); // Might return NULL
for(int i = 0 ; i < SIZE ; ++i) {
    pointer[i] = i; // Might cause a Segmentation Fault
}
  • 在Rust中

Rust处理这类指针错误的方式非常极端,在“安全”代码中粗暴简单地禁用所有裸指针。此外在“安全”代码中,Rust还取消了空值。

不过不用担心,Rust中存在一个优雅的替代方案——引用和借贷的方式。本质上来说,这些引用(references)还是那些老指针,但有了生命周期(Lifetimes)和借贷(Borrowing)规则,系统就能确保代码的安全性。

 
let my_var: u32 = 42;
let my_ref: &u32 = &my_var; // <-- This is a reference. References ALWAYS point to valid data!
let my_var2 = *my_ref; // <-- An example for a Dereference.

释放内存后再使用(Use After Free)

这一弊端会产生严重的漏洞,导致黑客随意操控你的代码。

下面有一个样例:

 
uint8_t* pointer = (uint8_t*) malloc(SIZE);


...


if (err) {
  abort = 1;
  free(pointer);
}


...


if (abort) {
  logError("operation aborted before commit", pointer);
}
  • 在Rust中

像C++一样,Rust也使用资源获取即初始化(Resource Acquisition Is Initialization)的方式,这意味着每个变量在超出范围后都一定会被释放,因此在“安全的”Rust代码中,永远不必担心释放内存的事情。

 
fn foobar() {
    let foo = Hashmap::new();
^  
|  
|   {
|   let bar = Vec::new();
|   ^
|   |
|   | 
|   |
|   |
|   V
|   } // `bar` will be freed once we get here
|  
V  
} // `foo` will be freed once we get here

但Rust不满足于此,它更进一步,直接禁止用户访问被释放的内存。这一点通过Ownership规则实现。

  • 在Rust中

变量有一个所有权(Ownership)属性,owner有权随意调用所属的数据,也可以在有限的lifetime内借出数据(即Borrowing)。

此外,数据只能有一个owner,这样一来,通过RAII规则,owner的范围指定了何时释放数据。最后,ownership还可以被“转移”,当开发者将ownership分配给另一个不同的变量时,ownership就会转移。

比如:

 
let foo = Hashmap::new();
{
    {
       let bar = foo; // foo's ownership has been moved!
    } // the Hashmap will be freed here
}

当向函数传递变量时,也会出现ownership转移,比如:

 
let foo = Hashmap::new();
{
    {
       take_ownership(foo); // foo's ownership has been moved!
      // the Hashmap will be freed at the end of `take_ownership`
    }
}

而且被转移的数据是无法使用的。

 
let foo = Vec::new();
{
    {
        take_ownership(foo);
    }
}
foo.push(42);


// main.rs:7:5: 10:8 error: use of moved value: `foo` [E0382]
// main.rs:7     foo.push(42);
//               ^~~

另外:

执行Copy特性的类型也会被复制,比如执行Copy特性的原始整数类型:

 
let foo = 42;
{
    {
        i_copy(foo);
    }
}
println!("{}", foo); // foo still owns the data

返回悬空指针(Dangling Pointers)

C语言老手都知道,向stack-bound变量返回指针很糟糕, 返回的指针会指向未定义内存。虽然这类错误多见于新手,一旦习惯堆栈规则和调用惯例,就很难出现这类错误了。

下面是一个C语言的例子:

 
uint8_t* get_dangling_pointer(void) {
    uint8_t array[4] = {0};
    return &array[0];
}


// Returns a dangling pointer to a previously stack allocated memory
  • 在Rust中

事实证明,Rust的lifetime check不仅适用于本地定义变量,也适用于返回值。

与C语言不同,在返回reference时,Rust的编译器会确保相关内容可有效调用,也就是说,编译器会核实返回的reference有效。即Rust的reference总是指向有效内存。

 
fn get_dangling_pointer() -> &u8 {
    let array = [0; 4];
    &array[0]
}


// main.rs:1:30: 1:33 error: missing lifetime specifier [E0106]
// main.rs:1 fn get_dangling_pointer() -> &u8 {
// 

限于篇幅本文所述有限,不过还是有个问题值得一提,那就是生命周期的管理通常是在后台操作中进行,某些时候编译器不会自动推算返回reference的生命周期,这种情况下只需明确指定就可以了。

 
fn get_static_string() -> &'static str {
    "I'm a static string!"
}


// This works because we are returning a string with a `static` lifetime.
// A static lifetime simply means that it'll live for the entire duration of the program

超出访问权限(Out Of Bounds Access)

另一个常见问题就是在访问时,访问了没有权限的内存,多半情况就是所访问的数组,其索引超出范围。这种情况也出现在读写操作中,访问超限内存会导致可执行文件出现严重的漏洞,这些漏洞可能会给黑客操作你的代码大开方便之门。

近来这方面最著名的就是 Heartbleed bug,可以参见相关消息

下面有个简单样例:

 
void print_out_of_bounds(void) {
    uint8_t array[4] = {0};
    printf("%u\r\n", array[4]);
}


// prints memory that's outside `array` (on the stack)
  • 在Rust中

在这种情况下,Rust利用运行时检查以减少这种不必要的行为,非常方便。

下面是一个样例:

 
 fn print_panics() {
    let array = [0; 4];
    println!("{}", array[4]);
}


// thread '<main>' panicked at 'index out of bounds: the len is 4 but the index is 4', main.rs:3

结论

目前来说,Rust似乎前途无量,本文只对Rust用于保护代码安全性的规则做了简单一瞥,经过精心提炼的规则可以让开发者避开明显的陷阱,轻松惬意地编程。

原文发布于微信公众号 - CSDN技术头条(CSDN_Tech)

原文发表时间:2017-01-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏网站那些事

强制更改社保卡密码,跟社保局的碰撞试验

昨天早晨,我妈叫我把回来重庆后参加工作的社保缴纳的截图发给她,她给村里的一个类似于会计工作的人,用于统计整个村的社保缴纳情况,我一想,这个简单呐,以前在惠州的时...

38930
来自专栏大数据杂谈

如何用 Python 执行常见的 Excel 和 SQL 任务

作者:ROGER HUANG 本文翻译自:http://code-love.com/2017/04/30/excel-sql-python/ 来源:https:...

59560
来自专栏小詹同学

爬点重口味的 。

小弟最近在学校无聊的很哪,浏览网页突然看到一张图片,对面的女孩看过来(邪恶的一笑),让人想入非非啊,一看卧槽,左边这妹子彻底赢了,这(**)这么大,还这么漂亮,...

17220
来自专栏小樱的经验随笔

BugkuCTF 计算器

前言 写了这么久的web题,算是把它基础部分都刷完了一遍,以下的几天将持续更新BugkuCTF WEB部分的题解,为了不影响阅读,所以每道题的题解都以单独一篇文...

285100
来自专栏性能与架构

条件语句小技巧

“高性能javascript” 这本书中提到了一个“查找表”的概念,建议在有大量离散值要测试时,if-else 和 switch 都比使用查找表慢很多,依据是运...

28570
来自专栏用户2442861的专栏

2014 360校园招聘技术类笔试题

原文:http://blog.csdn.net/lanxuezaipiao/article/details/41892553

17310
来自专栏jouypub

Spring Task中cron表达式详解

_{秒}:取值范围(0-59),不允许为空值,若值不合法,调度器将抛出SchedulerException异常

21210
来自专栏牛客网

蚂蚁金服暑期实习生一面总结

72020
来自专栏点滴积累

geotrellis使用(十)缓冲区分析以及多种类型要素栅格化

目录 前言 缓冲区分析 多种类型要素栅格化 总结 参考链接 一、前言        上两篇文章介绍了如何使用Geotrellis进行矢量数据栅格化以及栅格渲染,...

38780
来自专栏崔庆才的专栏

使用requests+正则表达式爬取猫眼电影排行

本节中,我们利用requests库和正则表达式来抓取猫眼电影TOP100的相关内容。requests比urllib使用更加方便,而且目前我们还没有系统学习HTM...

78170

扫码关注云+社区

领取腾讯云代金券