4

周刊(第7期):一个C系程序员的Rust初体验

 2 years ago
source link: https://www.codedump.info/post/20220227-weekly-7/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

引言:在工作里使用Rust已经有两个多月的时间了,谈谈我做为一名多年的C系(C、C++)程序员,对Rust的初体验。


一个C系程序员的Rust初体验

最近由于工作的原因,使用上了Rust语言,在此之前我有多年的C、C++编码经验(以下将C、C++简称C系语言)。

使用C系语言编码时,最经常面对的问题就是内存问题,诸如:

  • 野指针(Wild Pointer):使用了不可知的指针变量,如已经被释放、未初始化、随机,等等。
  • 内存地址由于访问越界等原因被覆盖(overflow),这不但是可能出错的问题,还有可能成为程序的内存漏洞被利用。
  • 内存分配后未回收。

连Chrome的报告都指出,Chrome中大约70%的安全漏洞都是内存问题,见:Memory safety。(不仅如此,微软的文章也显示在微软的产品中70%的安全漏洞也是内存问题,见:Microsoft: 70 percent of all security bugs are memory safety issues | ZDNet

C系语言发展到今天,已经有不少可以用于内存问题检测的利器了,其中最好用的莫过于AddressSanitizer,它的原理是在编译时给程序加上一些信息,一旦发生内存越界访问、野指针等错误都会自动检测出来。

但是即便有这些工具,内存问题也不好解决,其核心的原因在于:这些问题绝大部分都是运行时(Runtime)问题,即要在程序跑到特定场景的时候才会暴露出来,诸如上面提到的AddressSanitizer就是这样。

都知道解决问题的第一步是能复现问题,而如果一个问题是运行时问题,这就意味着:复现问题可能会是一件很麻烦的事情,有时候还可能到生产环境去复现。

以我之前经历的一个Bug来看这类工作的复杂度,见线上存储服务崩溃问题分析记录 - codedump的网络日志,这是一个很典型的发生在生产环境上由于内存错误导致的崩溃问题:

  • 不好复现,因为跟特定的请求相关,还跟线程的调度有关;
  • 本质是由于使用了被释放的内存导致的错误。

这个线上问题,记得当时花了一周时间来复现问题解决。

换言之,如果一个问题要等到运行时才能发现,那么可以预见的是:一旦出现问题,要复现问题可能要花费大量的精力,以及需要很多经验才行。如果一个问题还是在特定场景,或者用户现场才出现的,那就更麻烦了,C系程序员以往一般都是这样来保存“现场”:

  • 出现崩溃的时候保存core文件来查看调用堆栈、变量等信息。
  • 发明了各种复制流量重放的工具,比如tcpcopy等。

总而言之,运行时问题一旦出现是很麻烦的,而解决这类问题的时间是难以预期的。

Rust给这类内存问题的解决提供了另一个解决思路:

  • 一个内存地址同时只能被一个变量使用。
  • 不能使用未初始化的变量。

简而言之,凡是可能出现内存错误的地方,都在语言的语法层面给予禁止,换来的就是更多的编译时间,因为要做这么多检查嘛,而需要更多的编译时间反过来就需要更好的硬件。我想这也是Rust到了最近几年才开始慢慢流行开来的原因之一,毕竟即便是现在,一些大型的Rust项目普通的机器编译起来也还是很耗时。

“编译时间(compile time)”是一个可以预期的固定时间,能通过增加硬件性能(比如买更好的机器来写Rust)来解决;而“运行时问题”一旦出现,查找起来的时间、精力、场景(比如出现在用户现场、几百万次才能重现一次等)不确定性可就很高了。

两者权衡,我选择解决“编译时间”问题。而且,在我意识到有这样的工具能够在编译期解决大部分内存问题时,反过来再看使用C系语言的项目,几乎可以预期的是:只要代码和复杂度上了一定规模,那么这类项目都要花上相当的一段时间才能稳定下来。原因在于:类似内存问题这样的运行时问题,是需要场景去积累,才能暴露出来的,而场景的积累,就需要很多的小白鼠和运行时间了。

总结一下我的观点:

  • C系语言最多的问题就是各类内存问题,而这些问题大多是运行时问题。即便现在已经有了各种工具,解决其运行时问题也很困难。
  • Rust解决这类问题的思路,是在语法层面禁止一切可能出现内存问题的操作,换来的代价就是更多的编译时间。
  • 解决可预期的“编译时间”和难预期的“运行时问题”,我选择前者。人生苦短,浪费时间在解决运行时的各种内存问题太不值当了。

rr: lightweight recording & deterministic debugging也是出自Mozilla的另一款调试C系程序的利器,rrRecord and Replay的简称,目的还是为了解决各种运行时问题,由于运行时问题中存在着各种不确定的因素,包括:

  • 进程、线程环境,比如不同的线程调度顺序可能导致了不同的结果。
  • 输入不同的数据,能得到不同的结果。

于是,rr要解决的核心问题,就是让一个程序在运行时有一个固定的环境,它可以抓取程序运行的环境保存下来。这样在出现问题之后,就能使用它可以记录下来程序运行时的环境,不停的重放来调试解决问题。

但是,即便是这样,rr可能更适合于明确知道问题的情况下去抓取环境,不可能在线上直接打开这个工具。所以又回到前面的结论了:调试运行时问题可能面对的困难,包括场景、时间、用户现场等等不确定因素。

rrRust一样,都出自Mozilla,我想不是偶然的。Mozilla和chrome等一样,都是使用C++编码的超大型项目,而这里一定遇到了各种运行时问题,不止于内存问题,所以才要使用各种工具来辅助解决这类问题。

吃上硬件升级的红利了吗?

前面提到过,Rust目前较大的问题是编译时间过长,这可能是导致它最近几年才开始逐渐流行开来的原因。其实反过来说,在硬件升级之后,应该能尽量利用上硬件,在编译期尽量多检查出错误来,减少运行时发现问题的数量。这样,才能吃上硬件升级的红利,利用硬件来减少自己的犯错。

一方面硬件升级给了编程语言能施展更大、更快的的“舞台”,随着舞台的更新,就会有更新、更好的工具出现;另一方面做为从业者,也应该与时俱进,多学习跟进这些工具的演进。

我看到有一些人,强调自己多早就已经用C语言写代码了,但是查内存问题还在用慢的不行的Valgrind,没听过更不知道怎么用Address Sanitizer

想说如果技能点都已经不更新了,强调多早学的有什么意义?好比1950年就会打算盘,有意义吗?强调多早就用C语言类似的言论,在我看来就是“倚老卖老”,但是技术日新月异的领域,卖老的意义不大。

《Rust for Rustaceans》

推荐Rust for Rustaceans作者Jon Gjengset的油管频道:https://www.youtube.com/c/JonGjengset/playlists

有很多很有深度的Rust分享,比如:

介绍Rust缘起的文章

Infoq上的《想要改变世界的Rust语言》,是一篇介绍了Rust语言的缘起和设计目标好文章,对于了解Rust的历史、设计哲学等都有帮助。其中谈到的Rust三大设计哲学中:

  • 零成本抽象

就把“内存安全”放在了第一位,可见尽量解决运行时的内存问题都是大家很关心的问题。

查询Rust文档的浏览器插件

Rust Search Extension - The ultimate search extension for Rust,是一个方便在浏览器中快速查询Rust文档的插件,提供了各种浏览器的支持。

神鞭是一部上世纪80年代的老电影,印象里小时候在露天电影院看过,故事的梗概大概是这样的:

故事发生在清朝末年,主角是一个会使辫子神功的人,耍起辫子来能像鞭子一样抽打对手。后来八国联军入侵,加入了义和团,结果可想而知。再后来重新出现在江湖上时,不再是当年那个会耍辫子的高手,而是变成了一个神枪手了。

里面主角的有一句台词“辫子没了,神还在”,至今印象深刻,我对这句话的解读是:使用的工具,也应该与时俱进的进化,这个观点放在今天这篇对比C系和Rust的文章里,我认为是合适的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK