Skip to content

Latest commit

 

History

History
86 lines (54 loc) · 8.42 KB

ch01.md

File metadata and controls

86 lines (54 loc) · 8.42 KB

系统程序员的福音

在某些场景下——例如Rust的目标场景——比竞争对手快10x或者仅仅2x是足以决定成败的事情。它决定了一个系统在市场中的命运,就像在硬件市场中一样。

——Graydon Hoare

现在所有的计算机都是并行的……并行编程才是编程。

——Michael McCool et al., Structured Parallel Programming

TrueType解析器漏洞被一个国家级攻击者用于监控;所有软件都具有安全敏感性。

——Andy Wingo

我们选择用以上三个引言来开始本书是有原因的。但首先让我们以一个谜题开始。下面的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 password or make file unreadable by others.

然后它就崩溃了。如果你在自己的机器上尝试,它的行为可能会不同。这个过程中到底发生了什么呢?

这段代码是有漏洞的。数组a的长度为1,对a[3]的访问,根据C语言标准,是 未定义行为

对于使用不可移植或错误的程序结构或错误的数据的行为,本国际标准不做任何要求

未定义行为不仅仅会导致非预期的结果,语言标准甚至允许程序在这种情况下做 任何事情 。在我们的例子中,把一个特定的值存在特定数组的第4个元素处恰巧破坏了函数的调用栈,因此导致main函数返回时,并没有正常的退出程序,而是跳转到了C标准库里从用户的家目录中的一个文件中读取密码的代码中。这显然是有问题的。

C和C++有几百条避免未定义行为的规则。它们大多都是一些常识:不要访问不应该访问的内存,不要让算术运算溢出,不要除以零等等。然而编译器并不强制这些规则,它没有义务去检测哪怕明目张胆的违反规则的行为。事实上,上述程序编译时不会有错误和警告。避免未定义行为的责任全部落到你——程序员身上。

根据经验,我们程序员并不能很好的识别出未定义行为。研究员Peng Li是Utah大学的学生,他修改了C和C++的编译器来让它们在编译时报告它们正在编译的程序中是否含有会导致未定义行为的模式。他发现几乎所有的程序都有,包括那些公认的优秀项目。想要在C和C++中避免未定义行为就和仅仅知道规则就想赢得国际象棋比赛一样不切实际。

各种偶然的奇怪信息或崩溃可能只属于质量问题,但自从1988年莫里斯蠕虫病毒使用前面显示的技术的一个变种在早期互联网上从一台计算机传播到另一台计算机以来,无意中的未定义行为就成了安全漏洞的一个主要原因。

C和C++让程序员陷入了一个尴尬的境地:这两种语言是系统编程的行业标准,但它们对程序员的要求几乎肯定会导致频繁的崩溃和安全问题。虽然我们可能已经找到了这些问题的解决方法,但这只是导致了一个更深、更复杂问题的提出:我们不能做的更好吗?

Rust替你承担责任

我们的答案对应着我们开头的三个引言。第三个引言引用自一篇报告,这篇报告中,一个叫做Stuxnet的计算机蠕虫在2010年被发现侵入了工业界的设备,并获取了受害计算机的控制权。它只是利用了解析word文档中嵌入的TrueType字体的代码中的未定义行为,没有使用任何其他技术。这段代码的作者显然没有预料到这段代码会被以这种形式利用,这说明不仅仅只有操作系统和服务器需要担心安全问题:任何需要处理来自不受信任来源的数据的软件都可能成为受害者。

Rust语言做了一个简单的保证:如果你的代码通过了编译器的检查,那么它将不会遇到未定义行为。悬垂指针,两次释放,空指针解引用都会在编译期被捕捉到。对数组的引用通过编译期和运行期的双重检查保证安全,当索引越界时,Rust不会像不幸的C语言一样出现缓冲区溢出,而是会安全地退出程序并打印出错误消息。

Rust旨在同时实现 安全易于使用 。为了对你的程序行为做出更强的保证,Rust对你的代码施加了比C和C++更多的限制,这些限制需要通过一些实践和经验才能习惯。但总体来看这门语言的灵活性和表达力都是很强的。Rust的应用范围之广已经证明了这一点。

根据我们的经验,在相信语言可以帮助我们捕获错误的情况下,我们将有勇气尝试更有挑战性的项目。修改复杂的大型程序的风险将会降低,因为我们不再需要关注内存管理和指针有效性的问题。调试起来也会简单得多,因为潜在的bug不会破坏不相关的代码部分。

当然,还有很多Rust也不能检测出的bug。但在实践中,没有未定义行为可以显著改善开发的现状。

安全的并发编程

在C和C++中并发是众所周知的难,开发者通常只有在已经证明了单线程代码无法达到所需性能的情况下才会考虑并发。但第二个引言则认为并行非常重要以至于现代计算机将它视为基本的操作。

事实证明,Rust中保证内存安全的限制也可以保证Rust程序中不会出现数据竞争。你可以在线程间安全的共享数据,只要它不是正在被修改。被修改的数据只能通过同步原语来访问。你可以使用所有传统的工具:互斥锁、条件变量、通道、原子量等等,Rust会通过检查确保你正确地使用它们。

这些使Rust能够充分利用现代多核机器的性能。Rust的生态还提供了普通并发原语之外的库来帮助你完成复杂的负载,包括处理器池、无锁同步机制例如Read-Copy-Update等。

Rust的速度很快

最后,对应我们的第一条引言。Rust遵循了Bjarne Stroustrup在他的文章“Abstraction and the C++ Machine Model”中提到的为C++设计的原则:

一般情况下,C++的实现遵循0开销原则:你没有用到的部分,将不会有开销。你用到的部分,你将不能找到更好的代码。

系统编程经常需要考虑如何将机器性能发挥到极限。对于视频游戏,整个机器都需要投入工作来为玩家创造出最好的体验。对于网页浏览,浏览器的性能制约了内容发布者可以做的事情的上限,在机器本身的限制范围内,浏览器需要将尽可能多的内存和处理器资源留给内容本身。同样的原则也适用于操作系统:内核需要把机器的资源尽可能多的留给用户程序,而不是被它们自身消耗。

但当我们说Rust很“快”的时候,到底是什么意思?一个人可以用任何通用语言写出非常慢的代码。更准确地说,如果你已经准备好认真设计你的程序来最大限度的利用底层机器的性能,那么Rust可以支撑你实现目标。这门语言的效率很高,并且能给予你控制使用多少内存和CPU资源的能力。

Rust使协作变得更简单

我们在标题中隐藏了第4条引言:“系统程序员的福音”。这是在暗示Rust对代码共享和重用的支持。

Rust的包管理器和构建工具Cargo,使用户可以很容易地使用其他用户发布在Rust的公开仓库crates.io上的库。你只需要简单地在一个文件中加上库的名字和版本号,cargo将会自动下载该库和它的依赖,并把它们链接在一起。你可以将Rust的Cargo视为NPM或者RubyGems一类的东西,只不过还同时强调完善的版本控制和可复制的构建。有很多流行的Rust库可以提供从序列化到HTTP客户端和服务器再到现代图形API等几乎任何功能。

进一步讲,这门语言本身就被设计为支持协作:Rust的trait和泛型让你能创建出拥有灵活接口的库,它们可以在很多不同的上下文中工作。Rust的标准库也提供了一组核心的基础类型,为常见的情况建立了共享的约定,使不同的库可以更容易地协同使用。

下一章旨在更具体地说明我们在这一章中提出的观点,我们通过几个小的Rust程序作为示例来展示这门语言的强大之处。