C++不是遗留负担,Rust也非万能解药
几周前,我读到又一篇对内存安全问题过于轻描淡写的文章,基本上对变革的必要性持否定态度。接下来的周末,我开始看到一些安全领域权威人士的轻率回应,他们基本上认为,如果你不比我大一第一学期放弃早上8点的课程更快地放弃C和C++,那你就既不负责任又危险。
我将从简要概述开始,然后详细探讨这个问题。我将尝试以大多数软件行业从业者都能理解的水平,呈现各方观点。
这意味着,我一定会(故意)简化一些更技术性的部分。这篇文章已经太长了,而论点本身并不是关于技术本身,而是经济问题在各方都极其复杂,我们需要找到一种方式,接受他人做出我们不喜欢的决定,同时仍能帮助世界变得更好。
需要考虑的因素很多,所以我希望你能继续阅读,并仔细思考其中的细节。

简单版
- 安全问题确实比许多人想象的要严重得多,对于新项目来说,放弃C/C++绝对会让许多人受益,而不仅仅是从安全角度考虑。但是!
- 淘汰应用程序中已使用的所有C代码的成本和风险远高于许多人想象;替换部分关键软件的替代方案可能需要十年或更长时间才能真正发挥作用,且整体效益并不明确。
- 当我们考虑安全性时,会有许多隐藏的复杂性,这些复杂性使问题变得非常复杂,以至于说“Rust 比 C 更安全”可能是正确的,但实际上并非完全如此。
- 选择编程语言这样看似简单的事情,其经济性实际上非常复杂。安全并非唯一需要考虑的非功能性因素,无论你做什么,总会有内存不安全的代码存在(只要底层架构本身就不安全),而且试图快速淘汰C语言会带来许多负面后果。
- 系统语言被过度使用;C 与 Rust 之间的选择是错误的,因为像 Go 这样的编译语言在经济上往往是一个更好的全面解决方案。尤其是 Go,它的性能足以满足绝大多数用例的需求,而且安全,并且可以很好地访问低级系统 API。
部分安全从业者已开始反感
我深知,仅仅提出“安全可能并非绝对高于其他一切的优先级”这一观点,就会让某些领域内的人感到被个人攻击,仿佛我在质疑安全行业存在的必要性。
恰恰相反,我认为如果我们能更加全面,并更好地理解领域外的权衡取舍,安全行业将比现在更加繁荣。有些人可能需要被强迫关注安全问题,但非安全人员才是大多数,其中许多人持有平衡观点——他们关心安全,但希望避免在安全上投入过多时间和资金。
很久以前,我曾目睹一位安全人员与业务部门争论,当安全人员坚持己见时,我突然明白过来,便问道:
如果你认为安全是首要关注点,那为什么还要使用电脑呢?
人们每天都在接受风险。我们知道,每次外出时都有可能感染病毒。我们知道,每次开车时都有可能发生事故并丧生。
但众所周知,人类在现代风险评估方面表现糟糕。我们倾向于对风险水平进行极端的夸大或低估。
通常,安全行业可能假设普通人会严重低估风险。在某种程度上,这是正确的。当我于1998年首次进行安全代码审计时,工程师确实普遍低估了内存问题的风险。当时,如果你给我一份不是由丹·J·伯恩斯坦(通常简称为djb)编写的C或C++代码,可以肯定其中存在可被利用的内存错误,毫无例外。我甚至在那些我敬佩的安全领域专家的代码中也见过这类错误。
但如今世界已大不相同。当时,整个行业普遍低估了风险,对一个网络连接易被篡改、代码易被利用的世界感到满意,因为他们要么根本没有考虑过这些问题,要么认为这些问题不会成为重大因素。
当时没有“补丁星期二”,也没有广泛的隔离措施或其他有效的补偿控制措施。
科技界其他领域最终不得不承认自己错了,这得益于整个安全行业的不懈努力。正如格雷格·霍格伦(Greg Hoglund)当年常说的那样,连接性、可扩展性和复杂性的结合形成了一场完美风暴。没有人能否认这一点,因此安全行业对硬件架构、网络协议乃至编程语言设计都产生了巨大影响。
但事情并没有想象中那么简单,因为我们(作为一个行业)常常无法真正理解行业外人士的视角。
如果我们能牢记这一点,行业就能提升其可信度,并实现更快的发展。而我们需要这一点,因为,如我们将看到的,我们不仅还有很多事情需要完成,而且有许多重要变革是无法迅速实现的。
内存安全问题有多严重?
我将从承认问题开始。早期认真对待问题对我的职业生涯大有裨益——近25年前,我合著了第一本面向开发者的安全书籍,那时连跨站脚本攻击(XSS)等基本网络安全问题都尚未出现。不久之后,我还合著了《C和C++安全编程指南》。
这些书籍取得了成功,并帮助我崭露头角,我之所以从事这些工作,是因为我确实非常重视内存安全。当时,比现在更多软件是用C/C++编写的,而且它们几乎普遍更容易出现内存安全问题,而且更容易被利用。
我开发了研究工具和库来帮助缓解这个问题,并看到多年来许多其他人也做了同样的事情。但是!通常,开发人员对安全性的关注程度远不及安全专家。尽管有许多工具,C程序员往往不使用它们。
内存安全问题常被视为最严重的漏洞类别,因为当此类问题可被利用时,通常会导致完全执行权限。通常,此类问题可被远程利用,有时甚至无需任何身份验证。
然而,目前关于内存安全问题“数量众多且对高级攻击者而言易于发现和利用”的声誉是错误的。
千禧年之交时这一说法确实成立,但如今已不再如此。从安全角度来看,内存不安全代码的影响仍然极其严重,但并非高到无法忽视。当你考虑强有力的经济理由,说明为何不应放弃内存安全语言时,你可能会合理地认为,即使考虑风险调整,你的决定也是明智的。
风险已发生变化
我将重点探讨为何C和C++代码中的内存安全问题可能不像许多人认为的那样风险极高。本节内容不应被解读为选择这些语言的理由——我们将在本文后文探讨在何种情况下权衡利弊可能合理。在此,我将明确承认其他语言天生更安全;我只是在质疑“安全程度究竟高出多少,尤其是在我们拥有适当控制措施并保持警惕的情况下?”
世界已经发生了许多变化,这些变化直接影响了风险水平(在两个方向上),包括:
- 我们使用的硬件架构和操作系统在不牺牲太多性能的情况下,很好地帮助阻止了可利用性,从1998年的StackGuard一直到英特尔最近在控制流强制执行和ARM内存标记方面的工作。
- C++在标准库的用户界面设计上做了大量工作,力求确保普通C++用户不会使用存在内存安全隐患的API。另一方面,尽管C语言作为编程语言不断演进,但在这一领域却显得更加保守。
- 全面披露运动兴起,漏洞研究成为一个职业领域,导致最常见的C组件受到相当程度的审查,并帮助大幅提升了C程序员对这类问题的认识。
- 学术界停止了 C++ 的教学,转而教授 Java,然后是 Python。
- 一些新的语言作为合适的系统语言出现了大幅增长,但它们都关注内存安全,最著名的是 Rust,还有 Zig、Nim 以及其他一些语言。
- 向云计算的迁移以及现代技术堆栈的其他优点都是伟大的抽象,但它们往往会增加攻击面。另一方面,它们促进了隔离,可以限制影响范围。有趣的是,由于我们不再将大量可逆二进制代码交到用户手中,专有代码往往受益于本质上的“安全通过模糊性”,尽管我不得不承认这一点(尽管容错设计通常会让攻击者能够自动化尽可能多的“攻击尝试”)。
上述内容的一些后果值得考虑:
- C 和 C++ 代码中报告的许多内存问题都被视为可利用的,尽管实际上确实存在一些问题在实践中可能无法利用。在我从事漏洞研究的早期,如果我遇到一个明显的内存错误,不仅很有可能它是可利用的,而且对我来说构建一个可工作的利用程序会非常容易。我同意,作为一个行业,我们最好假设发现的任何内存问题都是可利用的,因为通常可以找到其他漏洞来链式利用,使其成为可利用的。然而,构建可工作的漏洞利用程序已经变得极其困难,从一项容易掌握的技能变成了相当罕见的技能。因此,真正的零日漏洞利用通常会被战略性地使用,最常见的是由政府使用。
- 漏洞研究领域的经济规律往往扭曲风险认知。这是因为此类错误在经济中最具价值,如我将在下文讨论的。然而,这意味着跨语言比较CVE是值得怀疑的。但这也意味着,有价值的漏洞要么存在于不太可能被利用的地方,要么经历一个“负责任披露”周期,这意味着人们得出结论认为,一个良好的补丁程序可以缓解风险是有道理的。
- 有时,选择用C语言编程的人确实有充分的理由。这些理由值得深入理解。虽然越来越少的新手认为必须学习C语言才能成为高效程序员,但嵌入式系统等领域仍普遍认为C语言是更实用的选择。
以比较不同语言的CVE数量为何通常具有误导性为例——Linux内核最近正式获得了为其自身代码库发布CVE的能力。但从他们的角度来看,任何已知的漏洞都可能存在他们无法理解的安全影响,因此现在在Linux内核中发现的每个漏洞都会获得自己的CVE,尽管其中大部分并非可被利用的内存问题。
理解漏洞利用难度降低
在此,理解内存错误的演变有助于理解漏洞利用难度。我将尽量避免深入的技术解释,因为我认为最需要理解这一点的人,恰恰是那些(完全合理地)永远不会在低级别工作、因此无需理解这些细节的人。这将再次简化问题。
从某种意义上说,这可能属于多余的细节。一方面,找到优质漏洞利用的难度大幅增加并不重要,因为若放弃C语言,这一类问题将不再构成威胁。
另一方面,漏洞研究人员如今比20年前付出多得多的努力,却发现的漏洞却少得多,这表明实际风险比过去低(尤其是在有良好补偿控制措施的情况下)。20年前,我可能会认为任何用C语言编写的程序都存在易于利用的漏洞,而且很可能我是对的。
如今,我倾向于认为许多C程序中确实存在问题,但如果你做好设计并聘请合适的人员审查代码,发现下一个漏洞的经济成本已高到足以让我不再认为任何C代码都会导致系统被攻破。
在我刚进入这个领域时,情况往往非常简单。如果你能找到一个作为数组的局部变量,就有很大可能诱使程序写入数组之外的区域。而且,内存布局非常可预测,因此相对容易找出如何利用这种情况。
具体来说,局部变量通常保存在程序栈中。当你进入一个函数时,数据会被压入栈中,当你退出时,数据会被弹出栈中(大致如此)。这与能够在函数调用中存活的长期内存分配(堆存储)不同。
例如,常见的代码如下:
void
open_tmp_file(char *filename)
{
char full_path[PATH_MAX] = {0,};
strcpy(full_path, base_path);
strcat(full_path, filename);
// Do something with the file.
}
对于初学者来说,上述代码可能看起来无害。它创建了一个数组,该数组被初始化为零,其大小为操作系统支持的路径最大长度(无论该长度是多少)。然后,它将某个基础目录名称复制到该数组中,最后将文件名附加到数组末尾。
但即使在当今系统中,如果攻击者能够控制此函数的输入或 base_path
的输入,也很容易让该程序崩溃。
其中一个原因是C语言不会跟踪字符串的长度。在C语言中,strcpy
函数按字节逐个复制,直到遇到值为0的字节(即所谓的NULL
字节)。同样,strcat
函数通过向前扫描full_path
中的第一个空字节,然后从filename
中复制内容,直到遇到NULL
字节。在任何情况下,这两个函数都不会检查它们的操作是否超出 fullpath
的长度。因此,如果你传入的字符数超过 PATH_MAX — len(base_path)
,就会写出缓冲区末尾。
在“过去的糟糕日子里”,这简直是轻而易举的事,利用起来非常简单。
传统上,程序栈会将自身运行时数据与用户数据混杂存储,这正是传统“栈溢出”漏洞如此易于利用的原因。
每次调用新函数时,栈都会获取程序应返回的代码位置的内存地址。因此,一旦发现这种情况,你只需确保精心构造一个利用程序(即在任何点发送的恶意数据),使其覆盖该返回地址,并用指向你自己利用程序的指针替换它……该利用程序通常还包含可执行指令,以执行任何操作。载荷的可执行部分通常被称为shellcode,尽管获得交互式登录(即shell)并不一定是目标。无论如何,当漏洞利用成功后,攻击者通常能够从那时起运行任何他们想要的代码。
从技术角度来看,当时最复杂的部分是shellcode本身,因为它通常需要至少汇编级别的知识。
然而,你不必自己编写shell代码,因为一直都有现成的有效载荷可供使用。
为什么不总是进行边界检查?
这是一个好问题。有人可能会认为,我们可以让编程语言始终生成代码来检查任何访问的边界,然后就大功告成。
我个人对此表示理解。我常以 Python 的巨大成功为例,说明在多数情况下,性能甚至不如安全性重要。因此,所有语言(试图)通过动态代码来保证不会发生越界写入,也就不足为奇了。
但,如果在所有地方都进行这种检查,它绝对会对性能产生显著影响,而确实存在一些领域,性能至关重要。
例如,如果你是一家内容分发网络(CDN),试图以成本效益的方式处理海量连接,很容易认为额外的硬件成本可能使该业务不再值得经营,除非运行的软件能够跳过低风险的边界检查。
而用Python编写的应用程序通常“足够快”(尽管许多人会持不同意见,正如官方Python中多次重写和新增实验性JIT编译器所示),但如果Python运行的每一行代码都经过全面边界检查,它是否仍能保持足够的性能?
我们使用的绝大多数软件都大量依赖用C或C++编写的底层系统代码,即使这种依赖是间接的。不仅操作系统是用这种语言编写的,而且典型的编程语言在运行时利用的许多库也是低级库。
当然,你可以“用 Rust 重写它”。但即使我们应该这样做(见下文),显然要达到这个目标还需要经历一个漫长而艰辛的过程。
请注意,Rust 能够接近 C 的速度,部分原因是编译器基本上可以在编译时“证明”何时可以跳过大多数边界检查。
但实际上,在 C 之上设计类似的 API 并不难,如果严格使用,可以避免内存错误,同时最大限度地减少运行时代码的生成。例如,在我们上面的示例中,为字符串提供一个始终跟踪长度并进行完整检查的 C API 并不难。我们可以为数组和其他数据类型提供类似的 API。我们甚至可以为指针提供这样的严格性。
这基本上就是 C++ 成功做到的,这也是为什么 Bjarne Stroustrup 似乎对政府告诉人们不要使用他的语言(由于内存安全问题)感到非常不满。我并不编写现代 C++,但从 API 文档来看,似乎很容易避免任何内存问题。
然而,对于上述问题,随着规模的扩大,细节问题会产生巨大影响。如果整个操作系统和 Python 使用的所有库都经过全面检查,那么 Python 可能会变得过于缓慢。
但在现实中,这可能只会让大量软件变得更加不具成本效益。实际上,经济因素通常是软件开发中最重要的考量,而这些经济因素必须高度围绕软件用户的体验展开(例如,通常只有在性能低于预期时才会关注性能问题)。
因此,如果你在编写底层系统代码,并且希望它被广泛采用,那么它必须具有广泛的适用性,并且在内容分发网络(CDN)或其他大型科技公司的规模下,必须具备成本效益。
几乎没有人会希望自己的编译器在不需要进行边界检查时仍然生成边界检查。理想情况下,我们希望能够证明何时可以安全地跳过检查,并确保在安全的情况下生成机器代码(无论是手动还是通过编译器)。
遗憾的是,在大多数情况下,几乎不可能获得绝对的保证。我们需要决定我们认为可接受的利用风险水平。
而且,如我将讨论的,任何声称风险应该绝对为0的人都过于不切实际……即使不考虑经济因素。
溢出内存错误:缓解措施的历史
关于我们愿意接受多少风险的问题,会让我们思考“我们目前正在接受多少风险?”
因为如果答案是“不多”,那么我们需要考虑添加边界检查是否值得。
实际风险水平难以精确量化。最可靠的方法是与那些毕生从事漏洞研究并实际证明可利用性的人士交流。
任何具备资质且亲历过那段时期的人都会认同,千禧年左右的C/C++程序堪称可利用性的宝库。我亲身经历过,只要具备基本的底层编程技能,利用起来就轻而易举。
然而,过去二十五年间已实施了大量缓解措施,我们如今身处截然不同的环境。要证明可利用性已变得极为困难,因此我们常常在未获证明的情况下,仅凭内存错误可能提供足够的攻击立足点,便默认存在利用可能性。而我大多数仍在从事此类工作的朋友都会承认,这已不再是“易如反掌”的事,而是难上加难。
虽然难以量化门槛提升的具体程度,但它确实已高得惊人。
要理解当前的风险,我们需回到起点。在此,让我们回到上述提到的简单栈溢出问题。
事实上,虽然我展示的代码确实是一个内存错误,而且我确实可以在25年前利用它,但确定是否能实际构建一个可行的利用程序的想法是如此令人望而生畏,以至于我不会去尝试。
代码仍然是错误的,因为它没有进行边界检查。它仍然可以用来让程序崩溃(从某种意义上说,这是一个安全问题)。如果你想看看,这是我现场编写示例代码并演示它仍然会崩溃的示例(我的打字速度慢和愚蠢的错误不收取额外费用)。
然而,仅仅因为它是内存错误,并不意味着它容易被利用,甚至可能无法被利用。
尽管上述代码很糟糕,但StackGuard从1998年开始就很好地解决了这个问题。基本思路是:当程序启动时,选择一个至少64位的随机数。每次调用函数时,将其压入栈中。
然后,每次从函数返回时,检查随机canary是否完整。如果不完整,则崩溃而不是返回。
而过去,人们可以确定性地确定写入什么内容以使程序正常运行,直到它开始执行你的shell代码。现在,一个简单的漏洞利用不再有效,除非程序以某种方式泄露了它的canary。
当然,确实存在可以绕过上述问题的场景(尤其当能与另一个漏洞链式利用时),但StackGuard彻底消除了部分问题,并显著提升了漏洞研究者的工作难度。
软件利用社区(包括“黑客”和漏洞研究社区)不得不不断加大绕过技术的研发力度,但通常至少能找到在特定条件下绕过部分缓解措施的案例。例如,上述缓解措施在内存动态分配时无效,因为该内存会被单独保存在堆中。
当然,程序不会将函数返回地址保存在堆中。然而,许多实际程序(尤其是使用C++动态分派的程序)会将函数指针保存在堆中,并通过它们动态选择要调用的函数。
其中一种更有效且广为人知的防御措施是地址空间布局随机化(ASLR),该技术在操作系统层面实现。基本上,每次程序启动时,操作系统都会尽可能随机化数据的存储位置。借助ASLR,如果随机化程度足够高,攻击成功的概率将低到需要尝试的次数可能与宇宙中的原子数量相当。
实际上,情况并不完全如此——有时攻击者认为有价值的特定位之间的距离可能无法随机化,因为可能存在需要知道这些距离(或在较小范围内)的理由。而且,如果操作系统在随机化方面过于激进,可能会导致程序无法正常运行。这一点在操作系统内核中尤为明显——在内核中实现有意义的随机化非常困难。
尽管如此,这仍是一种极为有效的技术,突然间让攻击变得极其困难。
如果你从未深入研究过这里的技术细节,但具备一定的技术背景,你可能会提出两个重要问题:
- 系统难道不应该将用户的程序数据与内部状态保持距离吗?
- 如果有效载荷必须驻留在堆或栈中(其他内存通常不可写),难道我们不能阻止这些区域执行代码吗?
对于第一个问题,不仅每个线程使用一个栈更容易实现,而且通常更快,因为硬件通常会直接支持程序栈。最终,尽管进程具有虚拟地址空间以防止其他进程访问,但在同一个进程内,任何代码都可以访问该进程中的任何内存单元。
尽管如此,重新排列内存仍然是有价值的。例如,在堆溢出中,函数指针是诱人的攻击目标。将所有函数指针存储在静态分配的表中或单独的内存堆中,显然比将函数指针随意散布在内存中的典型做法更好。
至于第二个问题,在系统级别完全可以防止从栈或堆执行代码,这是一种值得拥有的缓解措施。然而,某些环境(包括部分编程语言)会利用堆栈或堆来实现自身的动态功能(如闭包形式的lambda
函数)。不过,对于大多数程序而言,这种缓解措施几乎无需成本,且能进一步提升安全门槛。
如今,当出现内存问题时,攻击者通常无法直接执行代码。但假设你正在攻击一个用 Python 编写的程序,并且能够利用 Python 底层 C 实现中的内存错误来写入内存中的任意位置。
在底层,Python 实现了一个虚拟机。有一些“指令”可以存在于堆或栈中,这些指令会被 Python 的内置代码检查,而该代码会根据指令的不同执行不同的操作。
事实上,当我们谈论内存是“不可执行”时,实际上只指直接在底层系统处理器上执行的内容,而非应用程序级虚拟机中的行为。
因此,即使你攻击的程序其可执行代码段不可写入,且你能写入的所有数据均为不可执行,你仍可修改控制可执行代码行为的数据。
作为攻击者,如果没有虚拟机,你可以自己创建一个,使用一种称为“返回导向编程”(ROP)的技术。基本上,利用内存错误,你试图整理程序的数据,使其在程序内存中跳转,执行你希望它执行的操作(通常目标是让它生成一个登录shell,然后你就可以合法地运行任何你想要的程序)。
ROP 本质上非常困难,因为它通常要求攻击者同时在栈和堆上对数据进行整理,而这本身就很困难。再加上地址布局随机化,你会发现大多数涉及越界写入的内存错误实际上非常难以利用,而且通常需要将多个漏洞串联起来,大多数情况下还需应用 ROP。
最近,英特尔引入了控制流完整性(CFI)作为一项专门用于阻止ROP的选项。
还记得我们说过将返回地址移出栈通常没有意义吗?英特尔认为让世界停止这样做在实践中太难了,但相反,它将返回地址复制到一个影子栈上。当函数返回时,它确保返回位置的一致性。
这显然对栈溢出有效,但如果攻击者完全跳过对栈的直接写入呢?例如,在ROP中,攻击者通常会通过操纵数据导致跳转指令失效,从而执行他们希望的代码。当该代码遇到‘返回’语句时,可能会返回至CFI预期的位置。
不过,CFI 还能验证调用点。ROP 通常涉及跳转到函数中间位置,而 CFI 可以阻止这种行为。此外,它还能确保函数仅从应被调用的位置被调用。
CFI 并不会阻止我们对 Python 虚拟机的攻击。但对于没有嵌入虚拟机的程序而言,CFI 极有可能使 ROP 风格的攻击变得更加困难(从而变得不切实际),尤其是对于那些使用 CFI 的程序。
总之,尽管我们可能无法准确量化现代缓解措施阻止了多少百分比的内存漏洞,但很可能在应用所有易于获取的系统缓解措施后,许多此类漏洞根本无法被利用(大多数应用程序默认启用了这些缓解措施,但 CFI 相对较新,尚未广泛采用,且存在一些未应用这些缓解措施的场景;详见下文)。
但这一领域极为复杂,我一直对参与讨论的各位的创造力深感钦佩。尽管大多数漏洞早已不再是千篇一律的模板,也无法通过简单串联多个漏洞来利用,但漏洞开发社区也以同样令人惊叹的方式进行了创新,因此我们必须始终假设内存问题很可能被利用。是的,与20年前相比,具备相关技能的人数极其稀少,但我们不应假设这一数字会降至零。
因此有人可能会合理地推断:“为何要冒这个风险?显然最好彻底消除这一类问题。”从一方面来说,我同意这一观点,因为门槛很高,但顶尖人才往往能发现并串联多个漏洞。他们需要什么。是的,这些问题在 Rust 和其他语言中可能会出现,但只要 C 语言存在,它们就永远是 C 语言的大问题。
但话说回来,大多数有资格做这件事的人要么是向政府提供漏洞利用,要么是进行负责任的披露,这通常意味着,如果你及时打补丁,风险就可以得到很好的缓解。
如果你被政府针对,以至于他们愿意冒着暴露0-day漏洞的风险来控制你,他们很可能还有其他更可靠的方法。坦率地说,xz
并不是我们见过的唯一长期策略;政府会在能获取所需信息的地方安插间谍,这包括安全公司内部。
这就是为什么许多最警惕的安全人员讨厌那些庞大的安全产品——无论初衷如何,你最终都会面临更大的攻击面,而且很可能面临更大的风险,尤其是如果你担心国家行为体。
我预计,随着时间的推移,硬件平台将继续提高标准。如果我们幸运的话,再过十年,我们可能甚至接近这样一个点,即此类缓解措施几乎与完整的边界检查同样有效,但成本却低得多(至少在它们可用的环境中是这样)。
但在此之前,人们认为这些问题虽然是真实的担忧,但他们正在投入足够的资源来实施补偿性控制措施,以避免承担与其他选项相关的所有成本,这种观点并不完全不合理。
其他内存错误
在C和C++中,越界访问并非唯一被归类为“内存错误”的情况。
这些语言以用户手动承担内存分配与释放责任而闻名。虽然它们提供了辅助内存管理的库,但与许多其他语言不同,你仍需自行决定何时以及如何释放堆内存。
除了纯粹的数组越界错误外,还存在其他诸多问题,包括:
- 若释放仍被其他对象使用的内存,可能导致敏感信息泄露(如指向ROP目标的指针),部分原因在于C语言不会清零内存或确保数据在使用前已被写入。此类“释放后使用”(Use-After-Free)漏洞也可能轻易导致任意数据写入。
- 如果能让程序释放已释放的内存,即可破坏内部内存管理数据结构,这同样可能导致严重后果。仅需在内存分配前对同一内存块调用两次
free()
,就可能导致程序崩溃。 - 如果程序通过数学计算来确定需要分配多少内存,而攻击者可以迫使计算结果足够大,那么数字可能会“溢出”,导致分配的内存比实际需要的少,从而引发缓冲区溢出(整数溢出问题)。
此类问题在代码中相当常见,因为手动释放内存的时机往往难以判断,而C语言程序员在手动操作时常会出错。自动内存管理(如垃圾回收)通常能解决大部分此类问题……除非你能利用内存管理中的漏洞。
垃圾回收语言中的内存管理器通常极其复杂,且已有多个垃圾回收器(包括大多数浏览器 JavaScript 引擎)遭到重大漏洞利用。
现代缓解措施
如上所述,C++ 已经做了大量工作来帮助程序员规避上述问题:
- 标准库以一种特殊方式编写,使得函数返回时,大部分资源(特别是栈分配的内存)会自动释放,无需用户手动操作。
- 同样,C++ 的库完全避免使用原始指针,而是使用提供“引用计数”自动内存管理的封装类。
- C++ 标准数据结构的 API 可防止数组越界错误。
- C++ 拥有强大的类型系统,相对于这些类型而言,程序的安全性通常可以得到较高的保障。这意味着,只要遵循其安全边界,即可确保类型错误(即数据类型与预期不符)会被始终捕获。
- C++ 早已拥有垃圾回收器。Boehm 回收器于 1988 年发布,至今仍在维护。
- 针对 C++ 的静态分析工具丰富多样,可识别代码是否违反 C++ 核心指南。
上述工具的优势在于,许多C++程序员已从中受益。
而C程序员则通常无法享受这些相同的好处。尽管C标准与C++一样经常更新,但与C++不同的是,C在变更方面要保守得多,不会像C++那样添加诸如垃圾回收器等便利功能。
C 程序员可以获得一些相同的优势:
- 存在针对 C 的垃圾回收器。事实上,用于 C++ 的 Boehm 垃圾回收器同样适用于 C。
- 存在相当优秀的静态分析工具,主要围绕 CLang 编译器生态系统展开。
但总体而言,C更倾向于将自身定位为一种可移植的汇编语言。我们稍后会详细讨论这一点。
为什么观点如此偏颇?
对局外人而言,此前的讨论可能与您听到的“证据”相悖。例如,我曾听许多人声称:“大多数 CVE 漏洞都源于内存安全问题”。我最近又读到了一篇引用此观点的文章(https://herbsutter.com/2024/03/11/safety-in-context/),其中也给出了一个有趣的统计数据:当时(2024 年 3 月),C/C++ 代码中有 61 个 CVE,而 Rust 代码中只有 6 个 CVE。
所有这些都是事实。但这些数据都不是风险的良好指标:
- 我们已经明确,统计内存错误与统计可利用的内存错误是不同的。后者远更为重要,且有充分理由对此提出质疑。
- C/C++就像蟑螂泛滥一样,你可能日常看不到它,但如果你知道在哪里寻找,你会发现它无处不在。尽管C/C++在很长一段时间内都不是最流行的语言,但旧的构建块仍然被广泛使用和维护。而且,它们仍然是任何新系统级代码的常见选择。因此,对于我们使用的绝大多数软件而言,无论它们是用什么语言编写的,在幕后可能都存在用 C/C++ 编写的关键隐藏依赖项。因此,说 Rust 占 C/C++ CVE 总数的 10%,实际上感觉对 Rust 来说是惊人的高,对 C/C++ 来说是惊人的低,因为与 C 和 C++ 相比,Rust 的生产代码实际上很少。
- 即使所有有 CVE 的 C/C++ 错误都容易被利用,数据也绝对偏向于突出内存漏洞,这是由于利用的经济性。
如我之前所提,政府(以及一些企业)确实会为他们可以在操作中“战术性”使用的漏洞支付费用。在许多情况下,双方都曾被报道过为每个漏洞支付六位数的金额,有时甚至支付到七位数。
参与这一领域的漏洞研究人员通常仅凭一个漏洞就能获得相当于普通技术人员一年薪资的收入。而且确实有人每年能出售多个此类漏洞。
如果我是购买漏洞的政府,在决定购买哪些漏洞时,我最看重的几个因素是:
- 可靠性。由于各种因素(包括随机性),许多漏洞只能偶尔成功。然而,有些漏洞却是绝对可靠的,每次都能成功。
- 普遍性。特别是,我会关注目标群体中软件的普及程度。也就是说,如果你想针对某个特定的外国实体,你最有可能在他们那里找到哪些软件?
- 隐蔽性。通常,我希望任务不被发现,并尽量减少失去工具的风险。我见过一些漏洞利用,可以远程触发常见的Web服务器软件,然后利用另一个漏洞提升权限(逃离容器),接着禁用SE Linux,整个过程无需写入磁盘,且仅生成一条日志消息——该消息仅表示SE Linux已停止运行。这在“隐蔽性”方面表现相当出色。
- 执行能力。通常,我希望能够使用工具来辅助我的操作。因此,完整的执行能力是必不可少的。不过,即使在低级别的利用中,基于数据的攻击也已经成为一个相当重要的趋势。但我认为,许多“大型攻击者”也喜欢在高级云利用中使用数据驱动型攻击,因为通过凭证或用户数据可以实现很多目标。不过,如果我拥有完全执行权限,我不仅能实现这些目标,还能做更多事情。
- 持久性。理想情况下,该漏洞不太可能被常规发现并修复。
某些类型的漏洞,如命令注入攻击,可以影响大多数编程语言,并在上述多个方面表现出色。但一个真正优秀的内存错误通常会表现得更好。
由于内存错误的内在价值,以及发现和利用此类漏洞的技术挑战,内存错误在漏洞研究社区中是最受尊敬的漏洞类型。
因此,它们比更常见的问题获得更多关注和媒体报道。这意味着其他语言编写的代码中可能存在大量相对容易发现的漏洞,但这也意味着你更可能在使用的代码中遇到此类漏洞,从而面临更高风险。
另一方面,随着时间的推移,利用漏洞作为入侵手段的难度和可靠性整体下降,因此看到国家行为者转向其他方法并不令人意外,我们将在后文中讨论这些方法。
为什么人们不切换?
到目前为止,我们看到C语言确实比任何高级语言更容易受到内存错误的影响。
尽管缓解措施相当有效,但它们不足以成为人们不切换的唯一原因(至少在C语言的情况下是这样。在C++的情况下,情况有所不同)。
那么,是什么关键因素阻止了人们切换?
为了讨论的方便,我们将假设人们是理性的(如果人们行为不理性,反而会更倾向于使用C语言)。
首先,即使所有人都同意C应该消失,考虑到我无法列举出任何认为COBOL不再是常见语言是一种耻辱的人,这一过程仍将耗时极长。尽管如此,仍有大量COBOL应用程序不会在短期内消失。
这些COBOL应用程序仍然存在,因为它们无法被替换。我听说过的每一个此类应用程序(其中大多数是金融领域长期运行的服务),其所有公司都曾在多个阶段尝试过从COBOL迁移,但最终都失败了。
这类应用程序通常规模庞大,对企业核心业务至关重要且极为复杂。但它们已稳定运行了很长时间。
多年来,大多数人曾尝试从头开始构建全新系统,但最终发现要实现功能对等并达到相同稳定性所需的成本将高出数倍。五十年的经验积累与 bug 修复已融入其中,但通常缺乏充分的文档记录。
那么,如何才能有信心进行替换?例如,假设你有一个系统每天处理超过$1万亿美元的交易,且已稳定运行数十年。在明知一旦出错可能导致整个业务崩溃的情况下,你需要什么条件才会愿意将其中超过极小部分的交易迁移到新系统?
传统的整体迁移方式过于不切实际。即便在那些老旧的COBOL系统上,人们也曾尝试过逐步替换小部分代码。这种方法最终可能奏效,但也可能导致灾难性失败,且多家公司已认定其成本与风险不值得承担。因此,尽管企业愿意支付高薪留住愿意维护COBOL代码库的优秀人才,但经济因素才是支撑该语言持续存在的主要原因。
C(连同C++)的普及程度远超COBOL,且其作为核心技术的基础已近50年。
你可能认为这是夸张,但事实绝非如此。目前,Python已连续多年成为全球最受欢迎的编程语言。其主导实现版本始终以C语言编写,核心库的大部分也由C代码支撑。
JavaScript是另一种流行的高级语言。是的,它通常在浏览器中运行,而浏览器本身通常是庞大的C++项目。
即使是那些故意不使用C类语言编写的编程语言,通常也会依赖于C代码。通常,像DNS这样的低级服务会被卸载到C代码中,通常在“标准”库中。
但归根结底,所有主流操作系统主要都是用C语言编写的,辅以部分汇编语言。我们日常使用的绝大多数网络服务也主要以C或C++实现。在嵌入式系统领域,至今仍几乎闻所未闻会使用C或C++以外的语言。即便在Rust生态圈中,你仍能找到一些C代码。
这背后有其原因。以下是一份不完整的列表:
- 经过多年发展,大量C代码已广泛应用并获得高度信任,若要迁移至其他语言,世界将不得不承受巨大的代价。
- 许多低级任务在更安全的语言中需要进行更多复杂操作,尤其因为这些语言更注重防御性编程。
- 许多嵌入式环境受到的限制非常多,因此引入更多依赖性强的编译系统语言是不现实的。与 Rust 相比,C 语言非常紧凑且简单。
- 在市场上,具备低级编程能力的开发人员中,C 和 C++ 程序员的人数远多于其他语言的程序员。
- Rust 并不特别容易阅读或编写,而且目前还没有其他值得一提的替代语言(Zig 正在发展中)。要求那些在 C 或 C++ 领域磨练了 20 至 30 年的程序员基本抛弃他们的机构知识,这尤其是一个巨大的挑战。没有人愿意体验像在糖浆中游泳的感觉。下文将对此进行详细讨论。
几周前,我在谈论这个话题时,提到了运营核心服务的公司从设计良好的 C 代码迁移到 Rust 时会面临巨大的努力。我对话的人表示,这很容易,因为所有部分都有 IETF RFC,比如 SMTP 和 IMAP。
我希望这足够好。RFC 留下了大量“应”和“可”,并且往往需要时间来跟上技术演进,而不是在事物被构建之前就定义一切。大约30年前,我编写了Mailman的第一版,它根本不需要实现一个完整的SMTP服务器,而我至今仍对那些不符合规范的新用例导致的互操作性问题心有余悸,这些问题会以稳定的节奏不断浮出水面。
人们围绕这些核心协议编写邮件客户端和邮件服务器已有超过50年的历史,而世界上充斥着许多本应被支持却未被定义或不符合规范的行为。其中一些问题会以极低频率出现,且难以复现。
如果你是谷歌或微软(目前运营着绝大多数邮件服务),你愿意放弃那些经过数十年验证、被信任能覆盖过去几十年所有边角案例的代码吗?尤其是明知围绕这些代码缺乏良好的知识库的情况下?
现在考虑一下,我们今天运行的软件已经积累了一定的信任。当然,在过去的“黑暗时代”,Sendmail曾广泛使用且漏洞百出,因为它并非以防御性方式编写。
在Unix平台上,Postfix已活跃开发25年,采用C语言编写。尽管其安全记录并非完美,但总体表现良好,且从设计之初就大量采用最小权限原则。
尽管Postfix从一开始就更注重安全性,且坦白说更易于配置和使用,但它至少花了十年时间(可能更久)才基本取代Sendmail,主要原因在于兼容性问题。不仅需要时间重新发现可能扰乱业务的重要异常,还需向担心业务中断的人证明其已足够成熟,不会在该维度上构成风险。
同一时期,丹·伯恩斯坦(Dan Bernstein)开发了另一款新兴的 SMTP 服务器 qmail,其设计更为防御性(但仍采用 C 语言编写)。其唯一已知问题是 2005 年出现的整数溢出漏洞,当时这属于新型漏洞类别(而丹曾辩称该漏洞在实际场景中根本无法被利用)。
然而,qmail不仅比Postfix和sendmail难用得多,而且无法达到必要的互操作性水平,无法让人们大规模采用它。因此,qmail的开发在25年前就停止了(尽管仍有活跃的衍生版本;它们仍未广泛使用,我无法想象它们会被用于任何可能因漏洞造成经济影响的场景)。
因此,为了“用 Rust 重写它”,并成功取代 Postfix(更不用说 Exchange 了),必须做到以下几点:
- 从头开始构建一个符合所有标准的软件。
- 获得足够的实际应用场景,以发现并解决成熟软件已能处理的大部分“陷阱”。
- 融入足够的易用性,让人们愿意大规模采用它。
- 密切关注邮政领域的新进展(近年来,尤其是安全领域,新增内容层出不穷,且这种趋势不会停止)。
- 逐步说服世界进行转型。
在最佳情况下,这一过程不会迅速完成;这需要十年时间才能看到成果。虽然我渴望看到这一变化,但另一方面,对于一个旨在取代本就不被视为重大风险的事物的十年期项目,实在难以感到兴奋。
当然,上述论点适用于许多用 C 语言编写的旧基础设施。例如,看到 Linux 社区开始在代码库中尝试使用 Rust,这真是太好了。但就目前而言,这只是一个尝试,如果它发展成更重要的东西,那么迁移肯定不会很快。
C 语言的长期利基
在我看来,Rust 进入内核是一个了不起的成就。多年来,许多人一直游说允许C++进入内核,但从未实现。反对的理由很简单——C++的抽象层级过高。内核基本上位于软件栈的底层,不应承担任何不必要的开销,因此内核团队需要能够分析性能问题,这意味着他们需要看到C代码如何映射到生成的汇编代码。
根据我的经验,Rust 在这方面确实更接近 C。然而,Rust 在这方面肯定并不比 C 更好,在某些任务上,Rust 反而会成为障碍。
许多任务都是低级任务,属于真正的系统级任务,例如内存管理、设备驱动程序、暴露新硬件功能等。
是的,您可以在 Rust 中完成这些任务,但相比之下,这非常费力,通常会导致利用 Rust 的“不安全”功能,在这种情况下,您会面临同样的风险,那么为什么不用 C 语言编写呢?
此外,就像 Linux 内核不包含标准 C API(因为它们在该上下文中没有意义;它们在需要时提供自己的内部 API)一样,Rust 无法使用自己的 API;它们必须使用内核的 API。
我们使用的硬件架构在指令层面上几乎没有内置的安全性。当然,即使 C 语言糟糕的基本类型系统也比直接编写架构要有用得多。
在任何现实的软件系统中,如果你深入到最低层,总会有需要编写代码来针对这些不安全平台的情况。
此外,如上所述,绝大多数嵌入式系统仅使用 C 语言。这不仅是因为在某些 CPU 较弱的环境中,每个周期都可能至关重要。还因为其他资源也有限:
- 内存可能极为珍贵,包括栈空间、磁盘空间、缓存、寄存器等。编译后可执行文件的大小可能成为问题,而运行时垃圾或肥大抽象结构占用的任何不必要空间也可能成为问题。
- 针对此类环境的开发工具通常不支持其他语言。仅为该平台获取一个能生成代码的编译器可能已是一大挑战,更不用说提供针对该平台的专用工具了。
- 由于许多因素,包括空间限制和测试周期可能涉及的困难,此类环境通常几乎没有外部依赖性。即使是这些环境的标准库也可能完全缺乏线程等基本功能。如今,较新的系统语言一直在努力保持其标准库的大小相当小,但它们仍然太大(而且构建时间往往过长,尤其是 Rust),无法在嵌入式领域达到“足够好”的程度。
C语言在这一领域蓬勃发展,这也是C标准为何会极力缩减必须提供的API范围,远超其他系统语言所做的原因之一。
遗憾的是,嵌入式世界通常不支持许多(或任何)常见的安全缓解措施。从安全角度来看,它们还存在其他缺点,例如难以获取基本密码学所需的熵。此外,它们也没有足够的资源来轻松升级存在缺陷的软件。
然而,这些限制更多是出于商业权衡,因为供应链中的成本是一个重大关切。许多在这种限制下运行的软件之所以部署在低端硬件上是有原因的,而相关产品如果携带更高层次平台的所有负担,往往将无法生存。
这不仅仅是“成本”问题。更强大的硬件需要更多的电力,而如果你正在考虑可穿戴设备或其他可能需要长时间依靠电池供电的设备,这些因素对人们来说可能比安全风险重要得多。
这些是商业权衡,而C语言是唯一一种愿意妥善支持此类环境的非汇编语言(当然,C++在一定程度上也能做到,但它是嵌入式领域中唯一真正有影响力的其他语言)。
人们常称C语言为“可移植的汇编语言”。这其实并不准确;在我看来,C语言的抽象层次远高于汇编语言。当我从事编译器开发或其他系统级任务时,若被要求放弃C语言转而使用汇编语言,那简直是荒谬至极。我只会在无法用C正确实现(或无法轻松正确实现)时才降级到汇编语言,而这种情况通常只需几条指令。此外,每次降级到汇编语言时,我都会浪费大量时间,因为这种情况非常罕见(且每种现代架构都极为复杂),我不得不花更多时间查阅文档。
C 语言处于一个独特的空间,它比汇编语言更高,但比其他任何系统编程语言(当然包括 C++ 和 Rust)更低。它基本上处于两者之间,抽象出了许多平台可移植性问题,但仍然足够基本,如果你了解架构和编译器,只需查看 C 源代码,就可以可靠地预测将生成什么代码。
许多任务在这个层面上完成效果最好。对于此类任务,转向 Rust 并不像转向汇编语言那样需要做大量额外的工作,但仍然需要做很多额外的工作,只是为了编写“不安全”块。虽然这有助于限制可能存在安全漏洞的区域,但此类“不安全”块的需求越多,我越会预计净收益为负。
有些人问我:“这是否意味着你不认为 C 是一种真正的编程语言?” 远非如此。我认为汇编是一种真正的编程语言。但是,我认为我们最好使用有意义的轴来对语言进行分类,尽可能地定义它们。通常,使用 Rust 或 C 的人非常关注性能(他们是否应该关注这是另一个问题)。较少见(但仍常见)的是,资源消耗是一个重要问题。在我看来,这个维度的一端是性能,另一端是用户体验,因为这是沿着这个维度移动时的主要权衡。
我根据这个维度将事物分类如下:
- 汇编语言,它们本质上是低级别的,缺乏有用的抽象,因此我认为人们应该只在少数特殊情况下直接使用它们(包括构建自动针对它们的编译器、解锁功能以及非常明确的优化努力,这些努力有大量数据表明工作应该完成,而编译器根本无法达到最佳状态)。
- 预汇编语言,这类语言试图将一切尽可能贴近硬件,同时赋予程序员对性能的最大控制权。在此类语言中,我们可能需要在性能上做出一些平均意义上的妥协,以换取更好的可移植性和可维护性。但我们仍应在提供基础性功能的层面上编写代码,这些功能应能被作者未曾考虑的软件大规模利用。操作系统内核无疑属于这一类,也许一些基本的低级库设施也属于这一类,但大多数任务可能不需要在这个层面上处理。现实情况是,C 是唯一属于这一类的语言。Rust 也许有一天会达到这个水平,但我认为今天还远未达到。好吧,这个名字很糟糕,但我没有更好的了。
- 系统语言. 在我看来,这类语言专注于在合理范围内提供尽可能高的性能,同时在绝大多数情况下仍能保证安全性(允许对不安全机制进行受控访问)。通常,这些语言会竭尽全力避免传统的垃圾回收(或选择性地放弃垃圾回收)。这意味着您可能需要手动进行一些内存管理工作。这已成为语言开发中的一个“热门”类别,包括 C++、Rust、Zig、D 和 Nim 以及许多其他鲜为人知的语言。现在,手动内存管理通常比 C 语言更省力,更不易出错。不过,我认为 Rust 使内存管理比 C 语言的最佳实践更难,但安全性却更高。Go 的性能现在已经足够好,甚至可以满足一些人对系统语言的要求,但我不会将它归入这一类别,因为它缺乏对手动内存管理的一流支持。
- 编译型语言。在这些语言中,性能仍然是一个重要考量,但开发者体验同样至关重要。到了这个阶段,开发者无需过多担心内存管理问题。此外,这类语言通常拥有较为完善的生态系统。类型安全也是重点,且拥有良好的类型系统。Go、Java和C#是该领域的长期代表,我认为TypeScript也应归类于此,因为它通常是完全编译的,且非常重视安全性。我可能更倾向于将JavaScript归类到后者(尽管它通常也是编译的;这里的命名确实不够完美;只是这种关联性大多成立)。
- 脚本语言. 在这些语言中,性能并非优先考虑的重点。快速开发被高度重视,导致语言功能非常丰富。通常人们不希望等待编译结果,甚至希望在不重新运行程序的情况下看到更改。在此軸上,動態特性傳統上比類型安全更受重視,儘管這種情況已開始改變(如某些令人咋舌的附加系統儘管如此仍變得相當流行)。
在這個軸上,安全性實際上差異並不大。是的,真正的编译型语言通常会给予安全极大关注,系统语言比C语言更优。显然脚本语言会牺牲部分安全性,但如今它们在考虑安全性的意愿上与编译型语言不相上下。
在此维度上,C语言和汇编语言在内存安全性方面确实处于明显劣势,但正如我们讨论的,这种差距并不像想象中那么大,因为:
- 在桌面/服务器环境中,安全往往是高优先级,通常存在非常有效的缓解措施来降低风险。如果开发者充分利用这些缓解措施,并在设计与实现中投入足够思考,我们或许能获得相当合理的信心(例如Postfix)。
- 其他系统级语言仍会暴露不安全特性,并因此引发问题。
- 大多数语言都有依赖项(通常包括编译器),这些依赖项可能使用内存不安全的语言编写。
- 此外,如我们在最后将讨论的,其他语言至少在一点上比C语言更不安全。
再次强调,这并非试图论证C语言不具有独特且显著的风险。我们经常看到大型C语言目标被攻击:浏览器、内核、网络服务。尽管这种情况比以前更少见且更难实现,但漏洞利用仍层出不穷。快速向这类应用添加新代码会增加攻击面。而且,不得不依赖大量补丁措施同时生效并不理想,尤其当这些措施并未部署在最令人担忧的领域,如几乎从未更新的嵌入式系统。
然而,从事操作系统、浏览器等开发的人员通常都是希望做正确事情的聪明人。他们显然对提升安全性感兴趣,我们需要理解其中存在许多我们通常未考虑的因素。
也就是说,很难想象在未来几年内,您选择的操作系统和浏览器会完全用 Rust 编写,而不包含不安全的块,但可以合理地预计,人们会付出大量努力,慢慢朝着这个方向发展。我们已经可以看到,不仅 Linux 接受了 Rust,微软也非常重视它,并将其用于重要部分。但我与之交谈的人也明白其缺点,并采取了务实的态度。
过早优化作为语言特性
我一直认为,无论喜欢与否(明确地说,我不喜欢),C语言在生态系统中有着强大的存在理由,且短期内不会消失,因为目前尚无合适的替代方案。
我认为更令人担忧的是,许多程序员系统性地高估了性能的重要性。我的观察:
- Python长期以来一直非常流行,尽管在很大程度上它比C慢50–80倍(而系统和编译型语言通常在2–5倍之间,很少超过10倍)。
- 大多数语言选择在相关性能数据可用之前就已做出。
- 事实上,收集性能数据本身就非常罕见,尽管以性能为名做出的决策却屡见不鲜。
- 尽管人们常说关于代码与编译器及/或架构交互的传统智慧通常是过时且错误的,但这种情况依然存在。你经常听到“通常最好相信编译器”的说法,而我们中许多给出这种建议的人随后却忽视了它。
我认为任何诚实的系统程序员都会承认,过早优化是一个巨大的陷阱,他们自己也曾多次陷入其中。
我本人也经常陷入这种情况。当然,我确实会尝试进行测量,尤其是在性能是明确目标的情况下。例如,当我从事无锁、无等待哈希表(及其他数据结构)的研究时,我进行了大量比较测试,例如实现了至少15种不同实现,这些实现往往在非常细微的方面有所不同,以便我能在多种不同工作负载下进行受控测试。
另一方面,我多年来养成的习惯难以完全改变。我仍然有时会忍不住将通过引用传递的函数参数复制到局部变量中,以确保该值在函数中频繁使用时位于寄存器中,尽管:
- 如果我进行测量,这在所有情况下几乎肯定与用户体验相关的性能问题无关。
- 通常,我使用的语言支持链接时优化(LTO),即在知道可执行文件中所有模块后进行额外优化。30年前我使用的环境中不存在LTO(模块都是单独编译的,假设未来可能与任何模块链接),而且编译器也不会冒着在模块间复制指针后面的内容的风险,以防存在线程。我尚未深入研究我使用的编译器是否会这样做,但它们确实可以这样做,如果这有影响,它们很可能确实会这样做。当然,这在实践中可能并不重要,而且在我过去做过的任何事情中可能也不重要。不,这只是我吸收的“常识”。
人们在估算性能(和风险)方面向来不擅长,而那些倾向于高估风险的人,往往会选择系统语言(或C语言,尤其是对风险方面漫不经心的人),而实际上使用普通的编译型语言可能就足够了。事实上,在许多情况下,甚至Python也足够用。例如,Dropbox在关键任务中使用Python运行得非常出色。
个人认为,作为行业,我们应更加关注人们在性能方面的错误选择,而非安全问题,因为:
- 如果引导人们不要过度估计性能需求,安全问题往往会自然得到显著改善。
- 过度估计性能带来的间接经济损失,往往比低估安全风险的成本对企业造成的影响更大。例如,与编译型语言甚至我们的最高级语言相比,C 和 Rust 程序员往往会花费大量时间进行内存管理,并发现自己花更多时间来理解低级问题。你真的需要放弃垃圾回收器吗?因为仅此一项就往往会严重影响开发时间,更不用说标准库中更丰富的抽象功能可以降低成本的好处了。
也就是说,我倾向于让人们放弃 Rust,转向 Go,就像我倾向于让人们放弃 C,转向 Go 一样。如果我摘下“安全专家”的帽子,尝试形成一个尽可能客观的观点,我认为这比从 C 转向 Rust 的好处更有意义,即使我忽略了我们一直在讨论的 Rust 的所有隐性成本。
这并不是说系统语言,甚至预编译器在某些情况下不是正确的选择。我只是认为我们应该经常对这个问题提出质疑,并努力让人们客观地审查他们做出决定的所有方面:
- 是否真的因为我们可以证明存在性能需求?如果是,那就证明它。
- 否则,考虑切换成本可能是合理的,尤其是在我们已经拥有使用特定语言的资源时。
- 如果不是,那么什么最有可能帮助实现其他目标……更快地构建功能?或者质量很重要(这会促使我选择 Go 或其他编译语言,因为脚本语言由于其动态性质,往往难以提高质量,即使在像 Rust 这样具有非常强大的类型系统的系统语言中,低级代码似乎也会隐含更多隐藏的“陷阱”。
我们仍然需要有人学习C
在大多数选择系统语言的场景中,我们应该鼓励他们选择更高层次的语言,而不是在没有数据支持的情况下过早优化。
大多数情况下,这将是个人产品和公司正确的经济决策。然而,整个行业仍然需要以某种方式继续培养C程序员。
尽管COBOL的历史悠久,但C语言的寿命将长得多。如今,具备实际COBOL经验的人寥寥无几,但如果50年后具备实际C语言经验的人同样稀少,情况将不会乐观,因为:
- 无论你信不信,尽管 COBOL 已经很老了,但它比 C 语言更高层次、更易于使用,因此,考虑到 C 语言与其他编程语言相比有多么复杂,50 年后要找到能维护关键系统的优秀人才将困难得多。
- 毫无疑问,50 年后仍将有大量关键的 C 语言系统广泛使用(很可能包括操作系统内核)。
- 它有助于维持系统级创新的持续发展。
我所指的最后一点是,50年后,我们仍然需要能够成为底层架构专家的人员,并帮助软件开发人员利用硬件改进。
在过去30多年里,我们的行业受益于大量人才涌入编程领域,其中一些人走上了这条道路。但:
- 我们也在语言层面为人们提供了比30年前高得多的抽象层次。JavaScript、Python、Go和Java在抽象硬件细节方面做得如此出色,以至于只有对系统技术极感兴趣的人才会去学习。
- 如果人工智能辅助开发变得极其高效,大多数开发者从当前水平到掌握系统编程的差距可能会显著扩大(这并非全是坏事)。
- 如果我们以某种方式停止了大部分 C 开发,并且大多数人无法学习它,而又没有合适的替代品(我向您保证,Rust 并不是),那么我们将大大拉大系统语言与实际硬件之间的差距。
C 虽然令人讨厌,但与目前任何其他语言相比,它无疑是理解编程方面低级架构的更好踏脚石。
如果这个跳板不存在,那么愿意深入底层架构以通过软件改进硬件的人数将大幅减少,因为学习基础知识并完成简单任务所需的努力程度将高到让更多感兴趣的人认为自己无法做到,或不愿承受这种痛苦而放弃。人类是极具目标导向的生物,我们倾向于不追求那些被视为难以实现的目标,以维护自身心理健康。
如果有一种预编译语言能够纠正C语言中最严重的错误(在我看来,C语言对数组的处理是最严重的问题,但我的问题清单非常长),并且在开发过程中尽可能多地进行分析(不仅包括通过clang项目提供的 sanitizers,还包括运行时安全工具如Valgrind),我会感到更加安心。
Rust 目前最接近这种替代语言,但在我看来,过分强调函数式范式会削弱对底层命令式冯·诺伊曼架构的理解。
为什么有人不会选择 Rust
我们已经讨论过其中的一些原因,但总体而言,我听到过以下几种说法:
- “我们的应用程序不需要用系统级语言编写;垃圾回收对我们来说没问题。”
- “我们对已知的语言和生态系统感到满意,不想花太多时间学习新的生态系统。”
- “我们觉得 Rust 的学习曲线很陡,很难编写。”
- “我们没有足够熟练的人(大家一起学习似乎是在浪费资源)。”
- “其他人用 Rust 编写的代码往往难以理解。”
- “构建时间往往很长,通常有太多的外部依赖。”
我意识到,Rust 在很短的时间内就变得非常受欢迎,而且理由非常充分。就个人而言,我非常欣赏 Rust 的一些成就;我也像受虐狂一样享受着与借用检查器斗争的早期过程,因为我能够欣赏到原始的技术成就,而且能够有效地使用 Rust 让我感到自己很聪明。
尽管如此,我还是亲身经历了上面列出的许多情况。大约三年前,我读到一篇写得非常糟糕的学术论文,于是决定看看是否有人已经将其实现。我找到了两篇对同一论文的不同实现,但只有两篇,而且它们都恰好是用 Rust 实现的。我曾用 Rust 做过一些事情,所以认为这没问题。但这两个实现都非常简洁,难以理解。如果我不知道它们实现了相同的算法,我永远不会猜到,因为它们使用了截然不同的表达方式,看起来完全不同。
这让我想起了 90 年代中期阅读别人的 Perl 代码:Rust 有时感觉像是一种“只写”语言。
而且,我已经写过足够多的Rust代码,也与许多Rust开发者交流过,因此可以自信地断言,相当一部分人会发现学习Rust颇具挑战性,且需要较长时间才能在Rust中达到与当前首选语言相当的生产力水平。
我之所以这么说,即使我已经阅读了谷歌的那篇试图驳斥“Rust难以学习”的博客文章。我读过这篇文章,但除了他们没有分享任何真实数据外,这篇文章还有以下问题:
- 那些认为自己“在 Rust 中效率很高”的人的情绪被谷歌员工所扭曲,因为他们习惯于构建自己的内部 C++ 工具堆栈,而该堆栈经过多年发展已经非常复杂且控制力很强,因此,仅仅摆脱这些限制就足以让他们感到效率很高。
- 调查用户感受而不是代码本身的指标会带来难以纠正的隐性偏见(看看政治民意调查中为纠正此类问题所付出的努力,但仍然存在巨大的失误)。谷歌员工不想在调查他们对 Rust 看法的人面前表现得软弱。
- 即使数据还有点用,也没有与其他语言的直接比较。该博客文章声称他们的结果与任何其他语言相同,但他们明确使用了“据传”一词。
- 谷歌的员工往往也是最熟练的。说“我们认为在谷歌很容易”并不具有普遍性。
事实上,据我所见,Rust 主要是在技术精英中流行起来。我认为,任何在 Python 和 Rust 方面都有过重要工作经历的人都会直觉地认为 Python 对大众来说更容易。
就我个人而言,Rust 明显是为技术精英设计的,他们对数学函数和递归有直觉上的理解,而正是这一点让我无法接受它。
我希望编程能够更加平等。我认为世界上还有许多聪明能干的人,他们不在我们的行业,也不在科学领域,如果他们能更轻松地将想法转化为计算机执行,他们可以为世界做出惊人的贡献。就像我不喜欢看到学习系统编程的门槛太高一样,我非常关心降低编程的入门门槛(在我看来,Python在这方面为世界做出了最大的贡献)。
但从根本上讲,Rust 是一种很棒的语言,熟悉它的人应该在合理的情况下使用它。不过,我认为人们应该对经济问题更坦诚一些:
- 我们应该推动人们更深入地思考他们选择背后的经济问题,特别是尽可能地推动人们优先选择编译型语言,而不是系统语言,因为大多数情况下,编译型语言的整体经济效益可能会更好。
- 当存在预编译需求时,我们不应强迫人们避免使用 C/C++。质疑选择是可以的,但世界上对编程语言的需求永远不会被单一语言满足,即使在系统领域也是如此。
- 我们应积极推动Zig等系统语言的开发与成功,甚至探索真正能在特定领域替代C语言的方案,以便未来能以超越COBOL的方式终结C语言的时代(尽管千禧年前曾有强力推动尝试)。
目前,Zig 及其生态系统在满足许多系统编程需求方面远远落后于 Rust,但它对这个问题采取了一种更平等的方法。在 Zig 和 Rust 之间,Zig 对那些使用过编译语言甚至脚本语言编程的大多数人来说,是一种更易于使用的语言。
部分原因是 Rust 的根源牢固地扎根于函数式编程领域,该领域的基本原理是数学上的纯函数。世界上确实有一些人能直观理解这些概念,但这类人通常具备深厚的数学背景。
另一方面,Zig仍是一种常规的命令式语言。其核心原则本质上是“向他人提供详细指令”。即使是孩子也能理解这类概念(尽管他们通常不愿遵循)。事实上,我所见过的每一个成功的预编程项目(如Scratch)都是命令式的。
功能编程被广泛认为难以理解且难以采用的事实,已持续了65年,这让我认为每类编程语言都应包含一种强大的过程式语言。
我记得麻省理工学院曾以 Scheme 作为第一门编程语言而自豪,当时其他学校都在教授 C++,并宣扬功能编程范式。但他们最终也放弃了将功能语言作为入门语言。
但我对函数式编程范式以及函数式语言总体上持积极态度,因为它们确实具有独特的优势,尤其是更有利于编写更可靠且易于分析的代码(我30年前获得的由Simon Peyton Jones亲笔签名的《函数式语言的实现》一书至今仍是我的珍藏品)。
我认为函数式编程范式蕴含着如此巨大的价值,因此我相信在每个抽象层次上都应拥有一个优秀的、通用的函数式语言,甚至可能延伸至汇编语言之前(我不确定短期内函数式汇编语言会有多少实用性。但如果被证明是错误的,我也会感到兴奋!)
另一方面,我认为面向对象编程范式实用性有限,最好彻底消失,或至多成为一个被弱化的特性。
Rust 可能比 C 更具安全风险(目前而言)
当知名安全领域专家做出“使用非内存安全语言构建关键应用程序是不负责任的”这类言论时,我感到失望。这不仅因为他们忽视了经济复杂性并简化了复杂决策,还因为即便仅从安全角度考虑,尽管C语言的内存安全问题确实严重,但其风险是否比其他语言更糟糕并不明确。
具体来说,C程序通常具有较少的外部依赖项,而这些依赖项往往是使用最广泛的软件组件(如C标准库)。
其他大多数语言在支持程序员利用其他程序员的工作方面要好得多。从商业角度来看,这在某种程度上是一件好事。但从安全角度来看,更多的依赖不仅会增加我们的攻击面,还会让我们更容易受到供应链攻击。
去年我读到的一件事给我留下了深刻的印象,那就是 Hacker News 上关于 Rust 的评论(https://arc.net/l/quote/roohdlws),但该评论是在关于 Swift 的帖子中发表的:
我目前对 Rust 的问题是依赖地狱。每个顶级依赖项都有数百个子依赖项。是的,其中一些非常常见,比如 serde 或 rand,或者奇怪的是,有些 crate 似乎只是为了在文件系统上创建目录?我原本指望 crates 的一个子集能拯救局面,但当像 tonic 这样的东西带来 100 多个细粒度的一次性子依赖项时,我认为这是行不通的。目前我只能捂住耳朵大喊“我的代码是内存安全的,我敢于并发!”但内心却在想“我的依赖树深处藏着什么可怕的东西,又是哪个状态演员把它留到以后处理的?”如果这听起来像 paranoid,那就看看最近 pypi 恶意包的问题吧。我知道我可以自己做,但这需要花钱,如果没有tokio或tonic,而且crates也不那么容易使用,也许谷歌会制作一个单一的grpc crate?
xz事件是最近最引人注目的供应链攻击案例,但这只是行业幸运地发现的一个案例。
Rust 使引入外部依赖变得非常容易,与 JavaScript 生态系统非常相似,它似乎鼓励了许多微小的依赖。这使得监控和管理问题变得更加困难。
但 Rust 的情况比大多数语言更糟糕,因为核心 Rust 库(由 Rust 项目正式维护的主要库)大量使用了第三方依赖。该项目需要承担责任,并对他们的库进行监督。
在我看来,这一直是软件领域最大的风险之一。我可以编写相对安全的 C 代码,但很难信任我使用的任何单一依赖项,更不用说将其扩展了。
妥善保障依赖项供应链的安全性是一个比编写安全C代码“难得多”的问题。个人而言,我只会引入标准库之外的依赖项,如果要可信地替换其功能所需的工作量如此之大,以至于如果不引入依赖项,我会选择不做这项工作。
在这方面,C 比 Rust 更好,但并不是特别好。部分原因是 C 标准库(我总是愿意使用它;核心语言实现和运行时是必然的)并不全面。编写大量 C 代码的人最终会自己构建东西,并保留它们,并在数十年的时间里不断调整它们,包括哈希表等基本数据结构。
我个人一直更关注减少依赖性而非缓冲区溢出。减少内存安全问题有直接的方法(稍后会稍作讨论),而且在大多数应用中并不难实现。
但要逐一审查每一个依赖项?迄今为止,供应链安全方面所做的最佳努力也难以应对近期类似xz
事件的攻击(注:我的拼写检查器,这并非历史书中的xyz事件,但我确实试图借此引发联想)。国家行为体愿意打持久战,一旦开发者对你的某个下游依赖项建立起信任,就在其中植入后门就变得相对容易,而且这种后门很可能被视为意外漏洞,即使其他人发现它。
在xz
事件中,后门并未直接进入源代码库,因此更明显地表现为后门。但我们早已知道,隐蔽的后门与漏洞难以区分。
我记得在软件安全行业早期(大概25年前),曾与史蒂夫·贝洛文讨论过后门问题。他讲述了自己在贝尔实验室工作时的一段经历:他发现了一个相当隐蔽的内存错误,这个错误一直存在于前员工的代码中,他的直觉告诉他这是故意设置的后门。它看起来像是故意放置的,如果我记得没错的话,当时确实存在一些矛盾。但既然它与漏洞无异,他又如何证明呢?
当然,那是在过去,许多内存错误通常可以导致可靠的漏洞利用。但即使在今天,情况也并不容易,尤其是考虑到,尽管我们现在有更多的同行评审文化,但仍有大量代码在“评审”时并未被充分审查,因为审查者并未关注正确的问题。
此外,代码审查比编写代码难得多,这也是我预计在不久的将来不会使用基于大语言模型(LLM)的代码生成技术的原因之一——它将程序员变成了既编写要求又审查代码的“产品经理”。目前,对我来说,“只是”做一名工程师似乎更容易一些。
无论如何,依赖项越多,隐性信任圈就越大,攻击面就越大,供应链风险也就越大。
这使得 Rust 在供应链安全方面风险特别大,而 C 则表现相当不错。就现代风险而言,对于任何最终可能被纳入国家可能想要攻击的组织所使用的代码而言,这似乎是一个非常实际且重大的风险。
C 在缺乏依赖性方面(通常会带来较低的攻击面)具有很大的优势,但仍然不能使其成为最合适的经济选择。在考虑所有经济因素后,选择 Rust 可能仍然更明智,但安全方面的论点对我来说并不够有说服力。
总体而言,我认为 Rust(以及几乎所有编程语言)都应拥有自己的标准库。引入所有依赖项,并愿意承担责任。
此外,我通常主张语言应在其标准库中融入更多功能,尽管近期趋势是减少功能。从安全角度看,这在技术上确实会增加攻击面。但实际上并非如此:
- 如果开发者觉得需要从外部引入特定的通用功能,他们很可能会这么做,无论标准库是否提供该功能。
- 特别是对于链接时优化而言,移除语言标准库中未被实际使用的部分并不困难,这将攻击面降低到大致相同水平(尽管许多非系统语言并不关注链接时优化)。
- 语言维护者通过承担责任,不仅能更好地专注于评估人们可能使用的解决方案中的安全风险,还更可能寻找有助于降低攻击面的架构效率。
最终接触代码的人可能会减少,也可能不会减少,但语言应该愿意对人们可能需要的功能负责,尤其是当像 Rust 一样,安全性被认为是使用它的主要动机之一时。
像Go和Python这样拥有庞大标准库且由语言维护者承担责任的语言,在我看来是最佳案例。虽然更多人会接触代码,但DIY模式往往是错误选择。而那些愿意承担责任并提供环境让开发者能够专注于减少依赖(如果他们认为这很重要)的组织,是值得肯定的。
是的,Python 已经变得非常流行,以至于很多人使用外部依赖项,而且有几个流行的包管理器。然而,从供应链的角度来看,Python 仍然比 JavaScript 处于一个大大更好的位置,后者因对一些微不足道的包存在隐藏依赖而闻名于开发者。
建议
坦率地说,虽然我认为从严格的安全角度来看,供应链问题可能会使 C 语言比 Rust 更具优势,但这是 Rust 可以轻松消除的优势。一旦消除,C 语言就会面临内存管理的问题。我在这里并不是要主张使用 C 语言而不是 Rust,而是要表明,语言选择的决定远比人们随口说出的那些话要复杂得多。
我已经涵盖了大量内容(并且已经删减了相当一部分,包括大量示例代码),但让我尝试聚焦于可操作的部分。
对于团队
- 探索选择的整体经济性。 在选择技术时,相互挑战,思考更广泛的经济影响,并尝试用数字来支持你的假设。这涉及许多方面,但在本讨论的背景下,它通常会推动你选择 Go 这样的语言,而远离 Rust 或 C,因为人们往往凭直觉做出选择,而直觉通常会高估性能的重要性。
- 若不愿提前分析,切勿从系统语言入手。 相反,应从Go、Swift、Java、C#或其他任何优质编译型语言开始。需记住,若直觉强调性能,Python在各类场景下均具备可接受的性能(记得Dropbox吗?),而编译型语言的效率仍将远超其水平。
- 请考虑安全性。 仅仅因为您选择了 Rust,并不意味着您就能免费获得安全性。坏事仍然可能会发生在您身上。请不要被“思想领袖”的言论所蒙蔽。
- 避免不必要的依赖。我在此对“不必要”的定义保持模糊;你需要具备专业知识并评估所有经济因素。但请注意,减少依赖往往带来其他好处,从更短的构建时间到更少的测试范围,再到因 API 变更或下游依赖项的 bug 带来的风险降低。
- 了解你所依赖的组件。 除了试图理解依赖扫描工具提供的传递图外,还值得深入挖掘以了解这些工具未发现的内容。你的运行时可能某个地方链接了某些C库,当下一次xz事件出现时,能够更轻松、更诚实地评估风险会有所帮助。
- 尽量确保外部安全审查。 对于企业软件而言,这几乎已成为当今的必然要求,因为大型买家会要求提供相关证明。但每个人都应考虑如何定期促进此类审查,并将其视为机遇而非单纯的合规性检查。
- 若选择C语言,需提供决策相关文档。我之所以强调这一点,是因为安全问题确实存在,人们有权了解你已妥善处理。在我看来,如果遵循官方建议,C++并不属于同一类别,但许多人实际上并未真正遵守,因此我建议谨慎行事,对C++也采取相同做法。
- 若选择C语言,需主动有效地解决内存安全问题。例如,若可行,使用Boehm垃圾回收器。否则,在不可行时采用最佳实践技术。
需要注意的是,许多C开发者依赖不安全的原始类型并非因为缺乏对安全风险的了解,而是因为他们通常依赖于一些大型第三方库(如OpenSSL或其他加密库),而如何以一种简单、可移植的方式将Boehm垃圾收集器应用于这些第三方库并不清楚(尽管在许多情况下,如果你自己编译源代码, 实际上只需重新定义 malloc()
等函数以调用 Boehm API 即可,而在其他情况下,虽存在一些低级技术可解决问题,但其实现过于底层而不推荐使用)。
即使对于那些正在构建自己的轻量级内存管理(内存池分配正变得流行,这可以被视为一种非常轻量级的垃圾收集形式)的人来说,许多常见的依赖项(如OpenSSL)都提供了直接向库传递内存分配器的方法,只要你愿意寻找。
如果对此有足够的兴趣,我可以详细讨论这一点(尽管我更希望你采用其他语言)。
针对安全行业
这里我当然指的是行业领军人物,但任何足够重视安全、基于安全考量做出决策并愿意与他人讨论相关问题的人士均在讨论范围内。
- 最重要的是,请记住,仅仅因为你认为这是一个糟糕的安全决策,并不意味着它在整体上就是一个糟糕的决策。 尽量倾听非安全领域的人士,并了解他们的优先事项。你可能对“FUD”(恐惧、不确定性和怀疑)感到不满,但将复杂问题过度简化以推动“不惜一切代价实现安全”的做法,本质上就是在传播FUD。C语言的使用只是一个例子。但请记住,即使是安全影响,也远比我们当下所想的要复杂和微妙得多。
- 确保行业考虑更广泛的经济因素。 当然,在行业之外,安全的重要性远不如在行业内部。但我认为这远不止于此——如果安全行业试图将过多工作推给其他行业,我们至少会破坏更多可信度,甚至可能造成重大经济损害,例如通过大幅提高小型企业和个人的成本及法律风险,导致他们干脆放弃。
- 助力解决遗留软件问题。我也希望淘汰C语言,但现实是,C语言的危害性将远超COBOL,且有其合理性。单纯主张“用安全语言重写所有代码并尽快迁移”并非解决方案。这充其量是理想化的,从风险管理角度看完全不切实际。就像用Postfix替换Sendmail一样,我们可以取得进展,但必须循序渐进,且必须更加务实。
简而言之,是的,让我们100%淘汰C和C++,但要务实,并与行业其他领域做好协作。我们的目标应是100%让自己失业(或,既然我们没有失业风险,就努力朝着一个行业规模惊人缩小的世界迈进……即使这可能在遥远的未来)。
对于整个行业
总体而言,整个软件行业应与安全行业合作,共同探讨如何应对风险。具体而言:
- 帮助确定如何保持一个新鲜的人才管道,这些人能够理解软件和硬件的边界。这意味着,我们最终需要让有技能的人以一种比今天用C语言做预组装任务风险小得多的方式来做这些任务(而不是试图阻止这些任务发生)。如果答案不是“另一种编程语言”,那很好,但如果确实需要另一种语言,双方都必须认真对待以确保成功,因为世界上充斥着那些没有市场前景的语言。
- 请敦促他人展示他们在更广泛经济决策中的工作成果,不要假设世界像“传统智慧”所描述的那样简单。这是一个快速变化的目标。
- 语言设计师,除了你们已经考虑的安全性方面,请也花更多时间和精力思考第三方依赖的影响,意识到在贡献与安全性之间取得平衡是困难的。我们需要更好的务实解决方案。
- 在所有情况下,尽量假设你们在系统性误判的事情上是错误的,比如性能和安全性。也要假设你们可能在任何方向上都是错误的!现在,尝试收集一些硬数据,以更好地了解现实情况。
反馈
我很乐意讨论这个话题或接受任何反馈。如我所说,我很乐意继续学习并重新评估我的观点。然而,这篇文章将在我长假期间发布,所以请随时与我联系,但我可能无法迅速回复。
本文文字及图片出自 C isn’t a Hangover; Rust isn’t a Hangover Cure
我认为它们是编程语言
我不知道,你确定它们与遗留负担没有关系吗?
我开始学习 Rust 后,就经常出现遗留负担症状。
如果你想体验真正的遗留负担,那就去研究BCL吧。我发誓,当我喝得酩酊大醉并编写BCL代码时,犯的错误比清醒时少。
你找到你的Ballmer峰值了吗?
我遗留负担时做的第一件事就是打开笔记本电脑,将屏幕亮度调到最大,然后开始编程。
C 和 Rust 是什么?它们能构建什么?它们是擎天柱之类的东西吗?
这比遗留负担还糟糕。
那么,你会用什么医学术语来描述 C++ 和 Rust 呢?更像是疟疾和氯喹,还是更像是麻风病和氯法齐明?
天啊,这是个医疗网站,是的,我要当医生了
我认为我们遇到了一个类别错误
等等,我不能把腐蚀的金属刮进嘴里当遗留负担解药吧?
医生来救场了 💊
但它们像国际象棋游戏吗?
在遗留负担时设计的
作者的经验和专业知识远超于我,但我觉得这篇文章中的一些观点仅在理论上成立。例如,强调部分为我所加:
成功到什么程度才算成功?根据谷歌的标准,这些努力尚未达到其自身设定的指标:
摘自《通过设计实现安全:谷歌对内存安全的看法》(https://research.google/pubs/secure-by-design-googles-perspective-on-memory-safety/)。当然,谷歌只是一个数据点。
我之所以说这种方法和类似的方法只是“理论上”有效,是因为它们假设这些方法会得到严格使用。任何需要严格、谨慎使用的 API,除非在最严格、最谨慎的情况下,否则不会得到严格、谨慎的使用。这就是为什么默认设置如此重要的原因。
与什么相比?再次,根据谷歌的说法,它肯定不会比C++难。你可以合理地认为这是一个较低的标准,但在同一次演讲中,演讲者指出,绝大多数开发人员对等效的Rust代码的正确性更有信心。
你也可以说 Rust 与 C 相比更难理解。我对此有不同的看法,但这种说法更易被接受。
这个常被提及的观点已被他人解释得比我更清晰,但我会快速总结我的想法。显式、需手动启用的
unsafe
代码段会明确告知审核人员和工具。它们可被搜索、可审计,且理想情况下应尽量少用。满足 Rust 的不变量并非易事,但一旦“不安全”代码被认为是安全的,就可以将其包装在安全的抽象中,并认为它是可靠的。这正是安全系统语言应该做的。认为这会使此类语言像 C 语言一样不安全,这种说法并没有特别充分的依据。这是与 Rust 形成对比吗?对于我手工制作的 RISC-V 处理器,我没有发现这是一个问题。我最初用 C 语言编写了固件,然后转换到 Rust。Rust 通常不使用“肥胖抽象”,而是以“零成本抽象”为荣。如果我发现自己依赖于某些运行时抽象,比如
RefCell
,我可以选择编写自己的抽象,其使用方式与在C中一样需要谨慎。我最近实际上已经这样做了。该部分中的其他观点则更为合理。参见上文。
我还有更多想法,但时间不允许我继续展开。我并非有意挑起争端,如果我的言辞让您感到不快,我深表歉意。我的偏见确实显而易见。然而,我认为这篇文章的推理存在问题。或许更准确地说,它充满了个人观点,却缺乏充分的论据支撑。
没错,这就是我的看法。我认为,无论多么专业的团队,都无法编写出一个没有内存安全错误的C或C++应用程序,而这些错误可能会演变成严重的安全漏洞。在我看来,这一点现在应该是不容争议的。一种极具效果的自谦方式,就是用模糊测试工具攻击你的代码,然后看着它崩溃。而这仅仅是 bug 的冰山一角!
“Git gud”(提升技能)绝非可行路径,即便搭配最先进的静态分析工具和模糊测试工具亦是如此。
而这将——软件经理们请注意——在未来成为一个更大的问题,因为今天学习现代C++的人在大型代码库中只使用了现代C++功能中的一小部分,而他们已经不再参与这些项目的维护。维护旧代码始终困难,但由非专家“现代化”的旧C++代码将实际上变得无法维护,因为没有人掌握全部C++知识,更不用说这些特性如何相互作用,而初学者编写的代码虽是现代C++,但语言的特性却大相径庭。
那么,Linux内核中的安全漏洞在哪里?
到处都是。人们不断在内核中发现漏洞,尽管它可能是地球上最关键的安全软件。嘿,内存安全漏洞甚至会“退化”,因为内核没有特别良好的测试文化。
我对他的论点有一个重大问题,即他似乎认为提案是立即重写并替换所有C和C++代码。正如我已经在这里评论和解释的,没有理智的人会提出这样的建议,即使C++以某种形式存在很长时间, 这并不意味着它是启动新项目或学习编程的良好选择。
感谢您深入的评论,u/omega-boykisser
普通 Rust 开发人员
真正的贡献者
因为 Rust,我打了破伤风疫苗 💉
他还有彩虹袜子和其他东西。
我对你的其他观点没有异议,但:
大多数有 Rust 经验的开发人员。因此,这可能存在偏见。当然,开发人员的信心很难客观量化。
我建议观看完整演讲(仅30分钟),Bergstrom会详细说明参与调查的人员及调查背景。
https://www.youtube.com/watch?v=QrrH2lcl9ew
是的,我猜这种偏见确实很难避免。引用的数据是85%,演讲者对此似乎非常兴奋。我当然希望这一数据具有普遍性,而不仅仅适用于谷歌的这次演讲,但只有时间才能证明。
说实话,这归结于熟悉度。
在C和C++中,基础语言与许多其他语言相似,因此初看时更容易;但深入研究后,每个代码库都有自己的实现方式、构建系统,在C中依赖于基本结构,源文件/头文件的位置及管理方式,以及不同的编码标准(尤其是现代C++)。
另一方面,Rust 和 Zig 则截然不同,乍一看可能会让人感到不适应。我个人认为,从一开始就制定编码标准、构建系统和依赖管理器,即使从长远来看,也能避免这种情况。
这就像哥伦比亚语一样,我讲英语 😂
差不多,我是意大利人 xD。
请告诉我您不理解的部分,我会尝试重新表述。
仍然因为 Rust 而感到不安 💉
我认为恰恰相反,作者根据自己作为安全行业资深人士的经验,详细解释了他(在我看来是细微的)观点,而不是(幸运的是)支持或反对某种特定语言的人。
你从文章中摘录的段落让人觉得这是对Rust的批评,但实际上并非如此——如果说有什么不同,作者对C(以及在一定程度上对C++)的批评要多得多。
你忽略了文章在这里真正要表达的观点,即使用大量`unsafe`来编写Rust应用程序的经济可行性。这无疑比用 C 语言编写相同的应用程序需要花费更多的精力,而且完全关注内存安全而不是生产力并不适合所有项目。
遗憾的是,Rust 社区如此迅速地排除了那些没有明确赞扬 Rust 的略微相反的观点(我可能也会因为说这话而遭到反对)。一位在安全行业拥有 25 年经验的人撰写的一篇深思熟虑的文章,至少是一个值得认真对待的有趣观点。
……我有一个愚蠢的问题:
为什么你要用 Rust 编写一个主要使用“不安全”的 API?这就像使用类型脚本,但将所有东西都标记为“任何”类型一样。
我同意,根据我(有些有限的)编写不安全 Rust 的经验,如果不不断思考如何封装不安全因素并使一切变得更美好,就几乎不可能设计出 API。
这正是文章的重点——你可能不应该为此使用 Rust。对于某些(小)类别的应用程序,你不可避免地会遇到许多不安全的情况,这使得 Rust 的价值主张变得不那么吸引人。
“但至少你还有安全部分”的论点对(内存)安全性采取了绝对主义的态度——而(文章认为)这并不总是合适的。
我很想知道作者对我的评论有什么看法!
我认为这是对该部分核心观点的简化。事实上,经济可行性在文中从未被提及,“成本”一词仅在技术语境下出现。似乎更像是对这些不同语言使用体验的讨论。
无论如何,这并不重要。我并非完全反对整体观点,只是指出其中一个我认为缺乏依据的论点。
为了进一步阐述这一点,你在另一条评论中提到:
我认为这在实践中并不存在。在我最初的评论中,我提到我为定制 RISC-V 处理器编写了一些 Rust 代码。这包括一个完全手写的 HAL,其中包含大量内存映射 I/O。在约2.1万行严格代码(不算特别庞大)的代码库中,我仅有251处unsafe调用。这并不意味着仅有251行unsafe代码;为了保险起见,我们将其乘以5倍。6%的unsafe比例是否过高?难道我这个完全手写嵌入式设备不是滥用unsafe的典型案例吗?
我不是内核开发人员,但我认为此类代码的情况也与此类似。我个人从未遇到过“大部分不安全”的 Rust 代码,尽管我认为这是可能的。
我认为这真的不公平。我努力做一个好的 Rust 社区成员,并尝试考虑其他观点。我还多次指出,Rust 尚未达到这个水平 对于大多数嵌入式工作而言,尤其是在现有生态系统中。
实际上,“不安全”仅限于非常小的代码区域。例如,当你写入硬件寄存器等操作,或是实现复杂数据结构时。
你能详细介绍一下你自行设计的RISC-V处理器吗?我一直想为它编写一个模拟器,很想听听你是如何实现的。
这是一个简单、非流水线化的设计,专为小型FPGA而打造!实际上,按照官方规范实现基本指令集架构(ISA)的过程出人意料地简单。用软件编写模拟器应该比我的经历要容易一些,但如果你是第一次接触指令集架构,可能会相当棘手。
精彩的回答。这篇文章实际上是一堆垃圾和个人观点,与现实相去甚远。当我看到类似“解决安全问题成本太高”的表述,接着提到“NTLM”、“C”、“C++”等技术时,我的警钟立刻响起。各位,确实有少数人从头开始编写了整个操作系统:SerenityOS。他们从头开始构建了网页浏览器。如果少数人能做到,为什么像微软这样的跨国公司却无法有效解决NTLM等问题?保持现状当然更轻松,但对我来说,我不想使用垃圾软件并冒着金钱或隐私风险,只因你们认为“做正确的事成本太高” 😀
公平地说,约翰主要是重复行业经验和说法——这不仅仅是一个观点。我不断听到这样的说法:“迁移技术堆栈很容易”、“用 Rust 重写可以很快完成”……
事实上,这取决于许多因素,其中一些约翰提到了。大多数原因与语言本身无关(例如,可能涉及经济、风险、工具、平台支持、现有生态系统和人才池等)。对于某些项目和公司来说,这可能相对容易。对其他项目而言,这则是一项充满风险的巨大工程(例如,可参考文章中关于COBOL的类比)。
SerenityOS是一个非常令人印象深刻的项目,充分展示了当一小群经验丰富的开发者全情投入到他们热爱的项目中时,项目可以达到的效率。然而,该项目是从零开始开发的,甚至没有尝试与任何旧版本兼容。
例如,如果微软要从头重写 Windows,那将是完全不同的情况。Windows 的核心价值在于其能够运行石器时代软件二进制文件,并实现完全向后兼容。每个怪癖、每个伪装成特性的 bug、每个未记录的定时行为等都必须在重写中复现——而主要规格就是旧代码。不存在一个操作系统可以遵循的“Windows 规格”。
(顺便说一句,微软正在解决NTLM问题](https://techcommunity.microsoft.com/t5/windows-it-pro-blog/the-evolution-of-windows-authentication/ba-p/3926848))
来自现实世界的一个更现实的案例是 Mozilla Firefox。经过大约十年的努力,从 C/C++ 迁移到 Rust,Rust 仍然只占 Firefox 代码库中系统语言的不到 25%。迁移完成还需要再花几十年时间吗?而这家公司正是 Rust 的发明者。
经济因素——使用 Rust 编写新代码不经济,证据在哪里?
是否有任何事实或测量结果表明这一点,比如谷歌的人进行了研究得出了相反的结论,还是这只是感觉?
嗯,在大多数情况下,我认为这不是问题。相反,在条件合适的情况下,情况恰恰相反。我也记不起在文章中读到过这样的说法(除了可能有些公司里精通 C 语言的人比精通 Rust 的人多)。
经济性问题更多在于重写现有代码的成本与重写带来的价值之间的权衡。综合考虑,这并不总是划算的。此外,成本可能远超单纯的编码工作,例如招聘新员工或培训现有员工、投资新工具和持续集成(CI)、制定新的编码规范等。
与所有其他浏览器具有相同的功能和兼容性吗?
Rust 比 Java 更易读的原因有很多。
通过限制、实践和代码检查,你可以走得很远。
例如,如果你禁止直接使用所有标准 C 库函数,并强制开发者通过安全封装和类型进行操作,就可以避免许多陷阱。这通常可以通过静态检查来强制执行。
你还可以采用某些模式和类型来缓解常见的内存不安全实践(如遍历数组、正确初始化结构体等)。
我认为他指的是语言本身,而非C++代码的普遍情况。我认为C++语言及标准库(STL等)提供了几乎能解决所有内存安全相关问题的功能,这应被视为成功。
问题在于当你脱离这些标准工具,进入“危险领域”(指针运算、自由形式的循环边界、new/delete等)。这基本上相当于 Rust 中的“不安全”,只是没有注释而已。
非常有趣。我一直在考虑研究一下,对于我手动编写的、具有手动编写的 ISA(非 RISC-V)的处理器来说,让 Rust 运行起来有多难,以及 Rust 对于 ROM 启动代码(目前为 16 KB 的编译 C/C++ 代码 IIRC)来说有多适合。
显然,我还需要学习 Rust,但我不确定启动一个新的目标架构会有多难。我目前只有一个 GCC 后端——它可以重复使用吗?
你有你的代码的链接吗?
遗憾的是,还没有。我有些…野心,想在将一切开源之前发布一个可爱的小产品。
我认为手工制作的 ISA 相当棘手。由于 Rust 使用 LLVM 作为后端,我认为您需要对 LLVM 进行调整。我本人从未尝试过,但我确信使用 gcc 更容易管理。
是的,我尝试过LLVM但失败了。GCC让我更快地开始工作——尽管事后看来,LLVM可能在长期来看更好。GCC并不是特别容易使用——其中有很多“魔法”在起作用。
这并不完全准确。虽然内核(或任何其他嵌入式项目)无法使用标准库,但 Rust 始终提供不需要操作系统支持的“核心”库。这些库包括对(静态分配的)UTF-8 字符串、迭代器、常见数学运算等的支持。
此外,如果你注册了自己的内存分配器,你还可以使用内置的“alloc”库,其中包含动态大小的数组、字符串和其他堆分配的数据结构。
这两者都使用与标准库等价的实现,只是在不同的模块/命名空间下。由于这种组合方式,你可以重用标准库的大部分代码。例外的是需要与操作系统交互的 API,例如
std::fs
(用于处理文件系统)。这在嵌入式环境中是没有意义的。需要补充的是,如果您已经拥有 malloc 和 free,那么实现 rust 分配器特性只需大约 10 分钟。
没错,基本上只需让编译器知道要调用哪些函数,就这样。挺不错的!
这是正确的,而且在很多情况下都适用。
不过也值得注意的是,Linux 内核的分配函数比 malloc/free 更微妙一些(例如,由于 GFP 标志),而且分配在内核上下文中更容易失败,需要避免 panic!(),因此 Rust 库的一些假设并不适用。
目前正在修复这个问题,支持可失败的分配,但尚未完全实现。
(Rust 在处理多个不同的分配器方面也不太擅长,这可能是 Zig 更适合的一个领域。尽管这是一个更小众的情况。
Rust 已经添加了大多数标准库函数的 try_* 版本,这些函数可能会因分配错误而失败。
Zig 更擅长“在程序开始时占用 4 GB 内存,然后再也不与内核通信”的程序。
是的:这些就是我想到的函数。在内核中使用这些函数的补丁已获接受,但尚未合并到 Linus 的代码库中(预计会在 6.10 版本中加入)。目前仍需处理 GFP 标志的内核特定细节,但已是一大改进。
以下是我数十年来软件开发经验中的一些有趣发现:
大多数公司根本不进行单元测试,或者测试量远不足够。我并不是说要实现100%的代码覆盖率,包括所有分支和条件语句。但即使是那些进行单元测试的公司,也只是有少数几个测试用例,而且这些用例自编写以来从未运行过,现在都已失效。我甚至不确定是否存在反比关系。也就是说,并不是少数公司达到100%,而大多数公司为零,且存在线性关系,而是大多数公司为零,只有极少数公司接近100%。
很少有公司进行集成测试:参见上述内容。
很少有公司拥有严格的手动测试系统,但他们通常会进行一些模糊的常规操作。
静态代码检查仅因越来越多的IDE将其作为默认功能而逐渐普及。很少有公司“实施”了这一功能。
大多数代码审查完全偏离了重点。他们不会坚持要求单元测试、集成测试、性能测试等,而是专注于Jira评论的格式规范、严格遵守公司代码风格指南、评论指南以及工时记录。我不是开玩笑,许多公司在代码审查时对代码的实际运行情况仅做粗略检查。真正的代码审查应从一系列问题开始,如编译器警告、静态检查器警告、高代码覆盖率、性能测试、功能测试,然后或许,仅仅或许会查看代码风格。但如果因评论之类的问题发生两秒钟的争论,工程资源就在被浪费。
大多数公司没有真正的架构或设计。他们只是让新东西“适应”旧东西。偶尔有人会进行一次 UML 狂热,制作一些很快就会过时的读我文件。
很少有公司以任何真正的方式衡量他们的软件。性能是:“那很流畅。”但很少有人从统计学上探索这一点。因此,如果存在某种垃圾收集堆积,或偶尔但定期的缓存抖动,这并不是问题;因为无法以可靠的方式测量它,它只是被归类为“无法重现”的 bug 集合。
文档往往比单元测试更重要。许多公司已经将Doxygen文档优化得非常到位。然而,当有人手动重写生成的文档,夸耀自己拥有多么强大的功能时,多年以来无人察觉。我敢打赌,内部产品生成的Doxygen文档中,不到万分之一会被阅读。然而,Doxygen注释风格常常是代码审查的重要组成部分。最近,集成开发环境(IDE)开始利用这些注释,因此从技术上讲它们正在变得有用,但很长一段时间以来,这只是在浪费资源。我指的不是公共库,而是由少数人维护的内部代码。(或无人维护)
很少有开发者在复杂性达到一定程度后知道自己在做什么。他们对系统模块化后出现的内部通信、网络或线程等新兴特性一无所知。他们不知道存在计算机工程方法论来建模、设计并确保这些系统在无需经历无休止重大重构的情况下正常运行,直到迫不得已发布“黄金版本”。
大多数公司距离持续集成/持续交付(CI/CD)还很遥远,发布一个版本几乎就是一个独立的项目。
认证狂热者常常瘫痪技术栈。某个“资深”程序员只精通一门技术,且拥有该领域的满墙认证证书。你得从他冰冷的手中夺走这个技术栈。可惜那是个C++03标准、单一操作系统、糟糕的数据库,以及自2008年以来从未更新过的库。
还有无数其他问题。所以:
这是个笑话。
我有时必须使用 C++ 而不是 Rust。Rust 提高了我的 C++ 水平,但现实情况是,Rust 让你在必须使用 C 或 C++ 处理的事情上有些懒惰。这就是钟形曲线的作用。一些程序员会对边界检查等非常执着。他们非常注重单元测试。他们使用现代工程方法等来规划他们的软件。他们可以使用 Rust 或 C 或任何其他语言。
其他程序员则是懒惰的人。完全的懒惰者。他们不喜欢 Rust,因为它不会让他们完全懒惰;他们不使用 Rust。
有些程序员只是懒惰,但并不是懒惰者。他们喜欢 Rust,因为他们可以基本相信,如果编译成功,那么他们已经完成了最低要求,这是非常可靠的。他们不会进行良好的单元测试等。然而,Rust 本质上保持了软件的安全性。这并不是绝对的。
从统计学上讲,Rust 会削减大部分的钟形曲线,剩下的较低部分也不会犯通常的错误。
我读到一个关于工厂教练的荒谬故事。他被请到一家缺陷率极高的工厂。他问:“这里谁能正确地制作一个零件?”所有人都举起了手。他接着说:“两个”,依此类推。然后他说:“你们所要做的就是每次都正确地制作那个零件。”
当然,他们又回到了同样的高缺陷率。关键是建立一个缺陷难以出现的系统。使用 C 语言时,缺陷很容易出现。
工厂顾问应该问的是:“我们为什么会犯这些错误,如何才能使大多数错误难以出现或不可能出现?” 然后,他们就会建立工厂版的 Rust。
完全正确。Rust 旨在将软件质量控制从易犯错误的程序员和技能与工艺水平参差不齐的团队转移到编译器上,编译器将一致、全面地应用于每个程序员和所有团队。整个行业质量控制不一致是过去 20 年来所有黑客攻击和数据泄露等问题的根本原因之一,而编译器强制执行的防护措施是解决方案的重要组成部分。
认为这篇帖子所讨论的内容意味着大多数程序员不会犯错。
基本上,如果我没有犯错,那就意味着我没有编译我的代码。
也许这是一个不受欢迎的观点:换句话说,Rust 使劳动力成本更低廉 😉
必须反复强调的一点是,C++ 和 Rust 都很难。它们都需要花费大量精力来创建复杂的软件。创建复杂的软件确实很难。
但是,你在 Rust 中做的工作是富有成效的工作,为编译器在未来几年内为你保驾护航奠定了基础。你在 C++ 中做的大部分工作都是无益的工作,你必须自己保驾护航,永远无法确定自己是否真的做对了,然后每次重构时都要重新做一遍,因为你没有工具来帮助编译器为你提供同样彻底的保护。
还有一点是,如果你有一个经验参差不齐的团队,你可以拥有一组经验丰富的核心开发人员,他们创建框架,让团队的其他成员在上面工作,限制他们能做的事情,并尽可能地防止他们做错事。Rust 和 C++ 团队都可以做到这一点。
但C++团队虽然可以提供一个简化且不易误用的接口来处理领域逻辑,却无法提供一个让经验不足的开发者能够在不犯内存错误的情况下使用的接口,至少在没有持续的监督的情况下是这样,而这种监督会占用核心开发者的时间。
Rust 团队可以提供一个更易于经验不足的开发人员使用的接口,并处理(和规范化)许多所有权问题。而且,他们知道经验不足的开发人员只能造成逻辑错误。他们无法造成内存问题,导致系统其他部分定期出现无故故障。
逻辑错误可以在两种情况下通过自动和手动测试来解决。但内存问题却无法解决。可以投入大量时间和精力使用许多第三方工具来帮助降低这种可能性,但它们永远无法像 Rust 编译器在每次编译时那样做到。
非常好的观点。我基本上同意,尽管我必须承认,我的专业领域更倾向于 C++ 而不是 Rust。
但是:
……我对此并不完全认同。当然,在 Rust 世界里,情况在这方面更好,但“无法”是一个很强的词。我认为这很大程度上取决于架构和代码库。
例如,如果你试图“保护”一个现有的代码库,那么完全防止内存错误几乎是不可能的。但如果你从一开始就设计安全接口并限制可操作范围(例如通过代码检查工具禁止指针运算和不安全类型转换,强制开发者使用迭代器和
<algorithm>
而非自由形式循环等),我并不认为这是不可能完成的任务。至于内存所有权,这可能更具挑战性,但通过合理的架构设计和严格的规则,也大多可以解决。
作为一个极端的例子,我在一个大型的C++代码库中工作,其中malloc/new/free/delete被完全禁止(这是汽车行业中常见的要求)。所有内存均采用静态分配(位于 BSS 段)。起初这会让人感到难以接受,但其优势在于许多类型的内存错误会自动消失,性能更加可预测(无内存分配开销且缓存通常处于活跃状态),且可轻松确定内存使用上限(这对嵌入式系统开发至关重要)。
对于大多数人来说,完全禁止动态分配内存可能是难以接受甚至不可能实现的,但当然,这确实避免了一类问题。
但更大的问题是未定义行为,这类问题难以捕获。使用迭代器时,即使在看似最无害的代码中也可能发生问题。例如,在向向量添加元素时持有迭代器,99.9%的情况下它都能正常工作,但其中一次可能发生重新分配,导致迭代器失效,而你却继续使用它。在 99.9% 的情况下,在你使用它之前,其他任何东西都不会覆盖该内存。因此,它测试完全正确,并且很可能通过许多模糊测试。但在实际应用中,它经常在各种情况下运行,以至于这种情况开始发生。
经验不足的开发人员无法在复杂的代码库中可靠地避免此类问题。Rust 使这种错误不可能发生。
是的,我同意。除非真的有必要,否则我不推荐使用动态分配(但这是一项有用的练习——它教会你以不同的方式思考内存资源)。
这又回到了我(可能有争议)的观点之一:Rust 注重的是经济性,而不是安全性,因为使用 Rust,你可以雇用更多的新手开发人员,而且你不必费脑子思考和进行大量测试等。
另一方面,当你对代码设计更加宽松时,就有可能出现其他类型的错误(逻辑错误而非未定义行为)。
我并不是说哪一种更好,但两种方法都有其陷阱,实现良好软件质量也没有万能的解决方案。了解你的领域至关重要,而有些领域天生就更具挑战性(例如需要更多细节关注)。
不完全是,但这将使程序员能够制作更好的产品,因为他们无需浪费精力在无关紧要的事情上。这是因为我们同时处理的复杂性存在狭窄的限制。
哈哈,也许吧。寻找熟练的 Rust 程序员可能需要更多的前期成本,但产品的维护成本会更低。
Rust 还有另一个方面的帮助。在许多公司中,优秀的程序员虽然工作努力,但面临很大的时间压力,而产品管理层并不理解测试、良好实践等的重要性。(这种情况尤其发生在软件不是主要产品的技术公司,而普通经理的能力一般。) 基本上,一旦某些代码编译并勉强运行,它们就会被从他们手中拿走。
对于这些情况,Rust 使编写达到最低质量要求的代码变得更容易,因为当代码编译时,它已经变得更好。
我认为不是一些,而是几乎所有人都认为测试是“对我们有限资源的糟糕分配”。
是的。当然,选择或不选择特定的编程语言可能是基于一些理性考虑。一切都有利有弊。
问题是,许多决策实际上是被合理化了,实际上是信仰体系的体现。比如不进行适当的测试、积累技术债务、不编写规格说明,以及许多类似的事情。没有证据表明省略测试或至少一些规格说明会使软件开发更快。
还有一件事:
原帖提到替换大量C++代码的成本。
C++无疑已深入人心,且有诸多因素支撑这一现状,例如熟悉并偏好C++的开发者数量、使用C++的公司数量、工具链、嵌入式领域的支持(如BSP)、书籍、与C的熟悉度, 在Unix和Linux上的相对易于接口,稳定的C ABI,闭源面向对象和图形用户界面库的商业化,调试器,高度优化的编译器,等等。因此,存在许多反馈循环。
但,仅以软件领域的一个完全任意的例子来说,15年前类似的垄断地位对微软Windows而言是成立的,而如今许多这类因素已消失或显著弱化。这同时也意味着部分反馈循环的方向已发生逆转。
不同意。选择项目中使用的语言是一个有意识的决策,就像选择是否强制执行规则(如单元测试、代码检查、设计模式等)一样。
例如,如果你选择 Rust 而不是 C++,但你选择忽略你提到的所有因素,那么无论如何,你都会得到一个质量极差、充满错误且不安全的软件。选择一种“好的”语言并不能解决这些问题。例如,内存安全只是你将面临的安全问题中的冰山一角。
虽然我完全同意像 Rust 这样的语言可以降低一些障碍,让开发人员更高效(因为他们不必思考太多),但我也完全不同意内存安全是保持软件安全性的因素这一观点。
正如约翰在文章中(且以该领域的权威身份)所描述的,内存安全问题已远不如几十年前那样严重,因为从硬件层面到软件层面的诸多缓解措施已得到实施(例如栈执行攻击如今几乎不可能实现)。
我认为,真正的稳定性和安全性问题往往出在其他地方。例如,软件质量的普遍缺乏(如你所提到的)是一个更大的问题,而实施协议和算法时对细节的忽视(例如,如果协议实施不当,加密通信链很容易被打破)也是一个问题。而这些几乎与使用哪种语言无关。
以一个轶事为例,根据我的经验,Jenkins(用Java编写)因垃圾回收活动频繁而崩溃的频率(例如每四个月一次)远高于Linux内核(用C编写,且JVM在其上运行)因缓冲区溢出而崩溃的频率(至今从未发生过)。这里起主要作用的并非语言本身,而是软件架构和质量。
根据 CVE 统计数据,实际上,这并不是冰山一角。
无论如何,内存安全并不是 Rust 解决的唯一问题。当您在实际项目中使用它时,这一点就非常明显了。
Rust 提供了许多工具来鼓励正确性。整个生态系统非常乐于利用这些工具来制作难以(甚至不可能)被滥用的 API。在 Rust 中,提高软件质量的最佳途径通常也是最简单的途径,即使不是,Rust 也做出了一些决定,使以后很容易根除不成熟的代码。
因此,我承认我没有数据来支持这一说法。我认为,问题的本质在于,很难以一种允许不同领域和问题类别进行定量比较的方式来测量漏洞的频率和风险。这更多是对全球大多数软件质量低下的观察,而在安全性和稳定性方面,许多(甚至大多数?)开发者都缺乏相关知识。
例如,Converso 事件 (别错过“但等等——情况会糟糕得多”部分)显然与内存安全无关(客户端采用React Native实现),只是协议实现不完善和领域知识匮乏所致。据我所知,此案例并未产生任何CVE漏洞。然而,任何能够访问互联网的人都可以访问所有用户的凭据/密钥、消息和元数据,对于一个端到端加密的聊天应用来说,这是一个非常明显的漏洞。
编辑:我的观点是,虽然 Rust 可以帮助解决一些问题(正如你指出的那样),但当问题超出了语言的范围(质量、架构、协议、数据等)时,它无法拯救 Converso 这样的项目免于出现漏洞。
我不太明白你的意思,难道你踩到钉子需要破伤风疫苗💉?
我需要指出,作者(John Viega)在安全领域颇有名气,并撰写了多本安全相关书籍:https://www.amazon.com/stores/author/B001H9PYUE
重要的是,他共同发明了Galois计数模式,该模式是TLS 3.0中3个默认密码规范中的2个的一部分。
这是一篇非常出色且富有洞见的读物。核心观点——系统语言被过度使用——确实成立。
文章对这种过度使用背后的经济原因探讨不够——我对Rust非常熟悉,但对Go一无所知,因此对于性能敏感的应用程序,我宁愿选择Rust而非Go,即使Go在某些场景下同样适用。我实际上更倾向于在许多应用场景中使用OCaml,但掌握该语言的人太少,因此经济论证的一部分在于长期维护成本。
关于内存问题的可利用性讨论较为复杂——在Linux等通用操作系统上通常较为严苛。而在嵌入式目标上往往相对容易——而如今此类目标正日益增多
同意,我倾向于认为“系统语言”重新受到关注/过度使用的部分经济原因,是云计算/按需计费模式的产物。在过去,我们通常会过度配置服务器,因此CPU或内存效率的微小提升大多无关紧要。随着世界逐渐转向为云服务提供商的巨额利润买单,并且在许多环境中能够直接看到自身成本与资源使用之间的关联,对那些没有虚拟机启动时间或即时编译器的快速编译语言的兴趣有所增加。
我从未将云/按需计费视为决策方程的一部分。这是一个合理的担忧,但我认为情况并未如此极端。决策通常基于人们的知识、流行趋势以及该语言未来市场的综合考量。
是的,云非常昂贵,并且存在强烈的供应商锁定,例如最大输出速率和输出费用。从 python 后端转换到同等的 go/rust 版本,可以将运行时间和内存使用量减少一个或两个数量级。
我在我曾就职的公司中见过两次这样的情况,也听说过许多其他例子。
你好,我是作者。感谢你的赞美。
最初,我打算尽量全面,但即使在删减大量内容后,它仍远未达到那个程度。
最终,我不得不放弃,只希望能够让人们看到,这个问题远比大多数人想象的要复杂得多。
不过,我确实喜欢深入探讨安全领域之外的经济论点,因为整个行业或许能在安全方面取得最大进展,如果能找到解决一些更大(非安全)经济障碍的好方法的话。
我的工作是为一家大型SoC供应商提供网络安全服务。我思考的很大一部分是,在可用的安全工程预算中,什么才是最佳的投资回报率。
重写有时是正确的选择,但相对较少,而且必须有非常充分的理由。
我认为你捕捉到了这一点。我的领域更接近嵌入式和系统工作,其中“go”(作为一个例子)并不是一个可行的选项。基本上是 C、C++、Rust 或 Ada(Zig 和 Pony 等语言很有意思,但无法大规模投入生产)。不过,经济上的论点仍然成立。
在考察现实世界中的漏洞时,我希望人们能阅读一些文章,比如 http://bits-please.blogspot.com/ 2016/06/trustzone-kernel-privilege-escalation.html?m=1 这样的文章,再谈论C语言漏洞是否容易构造。
我很好奇——作为一名网络安全专业人士,您是否有关于“C语言漏洞”(如内存安全问题等)与其他类型软件质量相关漏洞(如协议实现错误、未测试的代码路径、参数验证缺失等)相比的常见程度的信息或建议?
我的直觉是内存安全问题只是冰山一角,但缺乏数据支持。
这确实是个棘手的问题,且随时间推移而变化。我能分享的最佳信息来自Mitre CWE列表https://cwe.mitre.org/top25/archive/2023/2023_top25\ _list.html。
在2023年最危险的25个软件弱点中,第1、4、7、12、17位与内存安全错误相关,因此可以说这些问题仍然普遍且具有挑战性。
我怀疑有人能提供未测试代码路径的准确数据,但协议问题和参数验证无疑也是问题所在——据我所知,这些问题在各种编程语言中都存在,而内存漏洞通常只会在允许大规模使用手动内存管理的语言(C、C++、Rust 及相关语言)中大量出现。
我之前提到的部分——实际上利用漏洞比人们想象的更难——可能需要进一步解释。
您需要从威胁模型开始——换句话说,当我设计系统时:它需要保护什么(资产)、保护对象是谁(攻击者特征)以及需要达到何种保护级别(安全保障级别),因为安全措施必然会产生成本。这些要求将与系统其他所有要求一同纳入考量。
很快就会发现,构建安全环境是一项艰巨的任务,因此通常需要将最重要的资产隐藏在专用环境中——例如可信执行环境(TEE)、可信平台模块(TPM)或安全元件(SE)。在大多数情况下,这些资产是加密密钥。以安卓手机为例,你的密码和其他敏感信息由一个名为Keymaster(或在新版本中称为Keymint——两者是同一功能)的组件保护,而Keymaster的关键组件位于一个专用环境中。
这带来两个后果:这些资产存储在专为该任务设计的环境中,但该环境也成为高价值的攻击目标。我之前提到的“Bits Please!”攻击,是某位后来成为谷歌Project Zero负责人的人对高通TEE进行的原型漏洞利用。该攻击通过复杂地利用多个漏洞(如缓冲区溢出、函数输入验证错误等),结合一些非常微妙且复杂的编程技术,最终实现远程利用(例如通过恶意 Android APK)。这种攻击值得投入大量时间开发,因为如果最终能够实现远程利用,其价值将非常高。
这种方法在智能卡和TEE等系统的攻击评估方法中得到了正式化。如果你感兴趣,可以查看可信执行环境(TEE)的保护配置文件,网址为https://globalplatform.org/specs-library/tee-protection-profile-v1-3/。该文件(以非常正式的语言)解释了 TEE 的保护目标,附件 A 描述了我们如何评估不同类型的可能攻击。
如果您从以上内容中得到任何启示,那就是攻击系统的方法有很多种。内存漏洞是常见的攻击方式,虽然 Rust 并不是万能的,但它确实大大降低了此类错误的发生频率。
它对其他类型的错误帮助不大(尽管它默认会检查整数溢出,而 C 语言不会——这会降低性能,但 Rust 为性能优先于正确性的(罕见)情况提供了不检查整数算术的版本)。
非常感谢!
我应该如何解读这个排名?它是根据风险还是频率排序的?而且既然它们是 CWE,而不是 CVE 或漏洞利用,我们能否推断它们与实际漏洞和漏洞利用的映射频率?(抱歉问了这么多问题,我会尝试自行查找更多信息)
我通常喜欢这种方法。这与 OpenBSD 的“默认安全”理念相似:您不会受到限制或禁止做某些事情,但必须明确启用潜在的不安全行为和功能。
我们在 C++ 代码库中也有类似的解决方案,需要显式标注进行原始指针运算的代码,否则此类操作会被禁止并由代码检查工具拦截。这是一种警示标志,需要文档说明、动机解释和更严格的代码审查,且仅在极少数情况下允许使用(因此开发者倾向于避免使用,因为这需要额外努力,且仅在严格必要时使用)。
CWE的排名基于弱点可能导致漏洞利用的风险。如果你点击一个示例(例如https://cwe.mitre.org/data/definitions/787.html),它会提供示例以及一些(绝非完整列表)CVE、潜在缓解措施等。它并非完美无缺,但在我所知的公开资源中,它是最好的。
就CWE而言,内存管理错误始终被视为漏洞(其他许多类别的逻辑错误也是如此)。CVE关注的是在实际环境中已被证明可被利用的漏洞(通常以实验室或研究人员提供的“概念验证”形式呈现)。CWE会对CVE进行一定程度的根本原因分析,以确定导致漏洞被利用的错误。
这一部分(即在实际中可被利用的漏洞)非常困难。通常需要数月时间才能开发出利用程序,而且往往需要多个漏洞的配合。相关工作主要在封闭的“专家”小组中进行,旨在寻找方法对漏洞的潜在可利用性进行分类,而无需实际构建利用程序。这确实非常困难,且目前尚无公认的可靠方法来实现这一目标。
在实际操作中,进行正式安全认证的组织(如我之前提到的GlobalPlatform的TEE保护配置文件认证)会与实验室合作对攻击进行分类,但这种分类通常是保密的。
若此类知识对您(或雇主)至关重要,可考虑加入本行业相关的专业机构(若存在)。否则,可与信誉良好的实验室合作,定期对产品进行渗透测试。这类实验室通常至少是相关机构的成员。
这是正确的做法——成熟开发流程的良好标志,它鼓励在可能的情况下采用安全实践,并仔细审查少数不适用安全实践的场景,理解在某些情况下工程需求需要放宽限制。
我认为需要考虑的一个因素是生态。我们程序员一直被不断改进的硬件宠坏了。但我们很快就会达到极限,因此在资源有限的情况下,提高效率是继续扩展的途径。这正是气候变化和资源过度消耗背景下我们所需的。
我同意我们应该更好地利用现有资源(不过我毕竟是个性能极客),但遗憾的是,我猜情况只会越来越糟。目前看不到任何实际的限制。更糟糕的是,新出现的“大锤”——人工智能,正被用于解决过去通过经典编程解决的问题,而人工智能推理消耗的资源要多出几个数量级。
Go 确实很棒。一个人可以以惊人的速度完成一个相当大的项目。它不会妨碍你,让你可以专注于需要做的事情。
如果我使用的是带有垃圾回收器的语言,那么使用Go进行快速开发相比Java或C#有什么意义?后者拥有更成熟的开发环境和更丰富/深入的标准库。
这就是问题的关键。我已经有了 C# 和 TypeScript 进行 GC,可以快速敲出托管代码,还有 Rust 用于需要工作且第一次编写时就能正常运行的东西。Go 感觉像是一个中间的选择,据我所知,这是一个不错的选择,但尚未做到足够的差异化,无法让我放弃已经熟悉的东西。
没错。我几乎不再使用 Go 了,因为 Go 的应用领域非常狭窄。Rust 更难编写,但生成的代码更可靠(经过编译器检查,没有空值,没有数据竞争)。C# 也更可靠(更好的泛型,空值检查)。此外,Rust 和 C# 都更符合人体工学(迭代器/集合方法),而且更少出现自毁性错误。Go 拥有卓越的运行时性能,但这并不足以令人信服。
对于构建单一目标而言,直接生成静态二进制文件是更简单有效的方案,且这是我所知主流语言中跨平台编译最简便的方式。虚拟机和 Gradle 的烦人问题仍让我难以深入探索 Android 开发。
然后是语言本身的简洁性。它真的非常适合直接解决问题,编写结构体和接口,循环和条件语句,完成。我从未深入研究过企业级面向对象编程方法,包括那些复杂的类结构和所有“设计模式”。我能理解它在大型系统中的应用,但除此之外(毕竟我们讨论的是快速开发)。
Go在并发方面也堪称最佳实践之一,尽管Elixir支持者可能有不同意见,而我也不确定新推出的Java虚拟线程表现如何。
说得有道理。选择Go确实有诸多优势。
我既了解 Go 语言,也熟悉 Java/Kotlin。对于需要命令行界面(CLI)或需要在尽可能短的时间内完成任务的场景,我会选择 Go 语言……但对于其他场景,我可能会选择 Kotlin(或如果我希望项目在长期维护中更简单易管理,则选择 Java——因为我多年来在 Kotlin 的生态系统中遇到了许多变化带来的问题)。
你可以使用Java的GraalVM创建一个二进制文件,以避免JVM的缓慢启动,但这远不如直接使用javac编译那么简单,而且速度极慢。我对C#不了解,但我想它的编译模型更类似于Java/GraalVM而非Go?
是的。C# 默认与 Java 采用相同的模型,且目前正在开发中但已相当可用的 AOT 单二进制支持方案。
正是单二进制自包含部署这类特性让 Go 语言备受青睐。C# 虽已具备此功能一段时间,但并非始终如此。
它走上了 PHP 的道路,即将一种不太理想的语言与一个方便的工具集场景结合起来。
Go 可以生成一个启动速度很快的单一静态二进制文件,而其他语言的标准库有多么丰富,如果你确定自己不需要它们,那就无关紧要了。
编写 Go 语言无需 IDE,且其编译结果为单一二进制文件,无需安装运行时框架。这一点本身就是巨大的优势。
这些特点也适用于C#。这并非对Go的贬低,但这些并非优势。我对现代Java不够熟悉,无法判断它是否也符合这些标准,因此我将避免对此发表意见。
我曾使用Java进行开发而无需IDE。虽然有些麻烦,但完全可行,只是需要运行的控制台命令会变得冗长。使用IDE会容易得多(对大多数语言都适用)。如今几乎每种语言都有LSP(语言服务提供商),因此任何利用LSP的编辑器都能处理IDE中常见的代码检查/重构功能。
这对于C#来说~不~(编辑)成立。单一可执行文件的方案通过消除依赖地狱极大地简化了部署流程。在我看来这是个巨大优势。
C#早已支持自包含的单文件构建。我们已在生产环境中使用多年,部署在未安装.NET的服务器上,涵盖Windows和Linux系统。而AoT技术还能生成更小更快的可执行文件。
我收回之前的说法。看来他们发布这个功能时,正好是我停止使用 C# 的时间。现在有这个选项确实很酷。
只需一行代码:
id ‘org.graalvm.buildtools.native’
无论人们怎么说,我就是不喜欢它缺乏单子式错误处理、缺乏 Rust 的 Result、Option 和 Iterator 提供的人体工学设计,以及缺乏特性系统(如 Haskell 的类型类)。对我来说,这是 Go(对于个人项目)或类似 Go 的“简单”语言的绝对淘汰标准。
我个人希望的是,Rust 能够使用 GC 代替生命周期来处理简单的高级业务/应用程序代码。
那么,OCaml 呢?
Scala?
你把 r/gleamlang 描述为你的梦想语言 LOL…
是的,我理解这种愿望。但似乎当你排除手动内存管理时,你可以通过类型系统做到如此地步,以至于许多人开始认为这种语言过于深奥和困难。就像Haskell、Scala等。更高阶类型、依赖类型和其他非常酷的东西成为可能,如果语言允许,你基本上无法忽视语言的这一部分。这种理解和使用它的义务让人们对这些语言望而却步,我认为。
这只是 Haskell(准确地说,是非可变片段)而已。
谷歌说,Rust 和 Go 团队一旦进入状态,生产力是相同的。
我从未写过一行 Go 代码,而且在可预见的未来可能也不会写。不过,我读过关于 Go 的文章,尤其是 fasterthanli.me 上关于 Go 的文章,几乎让我打消了尝试它的念头。
我认为这很疯狂。我太好奇了,忍不住想全心全意尝试一下,然后形成自己的看法。
我稍后会仔细阅读那篇博客。我快速浏览了一下,但依我之见,它并不令人信服。
我对阿莫斯的文章有完全不同的看法。很多最终否定这些文章的人都聚焦于Windows使用场景,但这其实与文章核心无关。如果你最终阅读了这些文章(尤其是《我想要摆脱Golang的疯狂之旅》和《我们用来继续使用Golang的谎言》),请尽量不要过分关注这一点。
更重要的是,文章探讨了依赖不稳定的API设计、错误假设以及未能充分利用类型系统等更广泛的问题。
值得注意的是,Go的API并非特别针对Linux或Unix系统设计,而是更倾向于Plan 9。如果我记得没错,其中一篇文章曾提到这在普通Linux系统上会引发问题。
我不是全职写软件的。我尝试过几种编程语言,但如果未来出现问题时只有我一个人能解决,那我没必要去尝试太多语言。至于业余时间的项目,我主要用Python或Rust,因为我最喜欢用这两种语言。我曾研究过Go语言,但它对我来说没什么吸引力,我宁愿把时间花在其他事情上。
当然,每个人都有自己的选择。
我讨厌 fasterthanli.me 上那篇文章,因为它充满敌意和攻击性,而且非常不平衡。但作者非常能干,他的观点在事实上是正确的。这篇片面的文章中一个巨大的疏忽是,Rust 也有很多问题。Typescript 也有问题。Java、C#、…Go 比其他语言有更多的缺陷,但每种语言都有一些你必须应对的问题。我个人经常使用 Go,但后来停止使用它了,因为对我来说,Rust“更好但更难”和 Typescript“较弱但 DX 更好,最重要的是也是浏览器语言”之间的差距非常小。此外,Node 是单线程的,因此产生并发错误的可能性更小(但并非不可能)。尽管如此,这篇文章中的问题都没有让我停止使用 Go。如果你喜欢 Go 且在 Go 中高效工作,就不要停止使用它!
你指的是哪篇文章?是那篇“我想要摆脱 Mr. Golang 的疯狂之旅”吗?
是的……我记得读过那篇文章。当时我正在做一个Go项目 :-)……那真是令人感动。我知道他的观点是正确的,但仍然觉得这篇文章不公平。
[删除]
这完全不是我的立场,请再读一遍我上面的评论。这篇文章有问题,因为它没有对Go的优缺点进行平衡评估。据我所记得,它甚至没有提到Go的任何优点。
我每天都使用Go,强烈反对说它“打脸”或“让路”。我认为对于大多数业务逻辑类软件,C#是更好的选择。
是的,这篇文章比标题好多了。
这只是一篇隐晦地支持 Go 的文章。但作者对 Rust 的批评完全适用于他们选择 Go 作为编程语言的决定,这是个知识上的缺陷。
Go的类型系统更弱,导致逻辑错误和漏洞的概率更高。
尽管Go自诩为并发语言,但其并发支持中存在明显的内存安全问题。
解决方案?使用更好的内存管理工具,并希望数据竞争检测器能捕获这些问题。
“获得更好的清理工具”也是 C 语言中内存安全问题的解决方案,难怪 C 语言程序员们如此喜欢 Go 语言。
Go 语言使并发代码的编写变得更容易。Rust 语言使安全并发代码的编写变得更容易。
后者比前者难得多。
Rust 程序员总是抱怨我 🤔 他们需要破伤风疫苗 💉 也许这会有帮助
来源?
Go 内存模型:
示例:https://blog.stalkr.net/2015/04/golang-data-races-to-break-memory-safety.html
而 https://www.uber.com/blog/data-race-patterns-in-go/ 展示了多种常见的数据竞争模式。
一个例子是,Go语言的默认Mutex会与被保护的数据一起加锁——无论是否获取Mutex,你都可以访问该数据。
相比之下,Rust Mutex 会锁定数据周围,您必须获得 Mutex 才能访问数据。
这表明数据竞争是可能的,但这并不一定意味着内存安全受到影响。例如,在 Java 内存模型下,数据竞争不会破坏内存安全。
然而,Go 内存模型特意为多字节结构(如许多内置函数)提供了这种可能性:
如果你可以在不使用FFI或你的“我即将做一些危险的事情”版本的情况下,随意发生内存损坏(在Go和Rust中是不安全的),那么你不是一个内存安全的语言。安全的Rust没有这个问题。Go本可以采取Erlang的强制消息传递路线,这在没有借用检查器的情况下解决了问题。我认为这是Go团队的失误。
作为作者,我至少 5 年没有用 Go 编写过任何代码了,但在这段时间里,我用 Rust 和 C 编写过代码(还有大量的 Python 代码)。
由于我并不太喜欢这两种语言,而且我认为自己是一个系统程序员,所以我还用 Nim 完成过一些大型项目,我可能会写一些关于这些项目的文章(但并不一定推荐)。
当时,Zig还未达到我所需的水平,但它是接下来我将尝试的语言。
我很想听听用Nim编写的项目!
我为专业嵌入式项目编写了大量Nim代码。虽然我没有用它做过裸机项目,但它是我的首选语言,用于编写在嵌入式Linux上运行的应用程序或硬件开发接口。与其他语言相比,我认为Nim在这一领域表现得相当出色:它运行速度快、易于确定性编程、易于编写、编译通过后通常能正常工作,并且可以轻松导入到Python代码中。它允许你快速搭建原型,同时又足够成熟以确保长期可用性。我认为最大的缺点是缺乏维护良好的库、默认构建系统不太理想,以及有时会遇到语言本身的奇特问题。
感谢反馈,我将在未来一到三周内整理相关资料。
我看到了以下内容
我当时想……什么?C 和 Rust 不是编译语言吗?
如果你认为这篇文章是关于Go的,那你一定错过了文章。
有趣的是,我没有这样理解。总体来说,我认为这篇文章非常平衡且富有信息量,它并没有试图抬高或贬低任何特定的语言。至少,它不像是为了这个目的而写的。
这篇文章不断在“开明的中间派立场”和找些奇怪的借口来否定改进之间摇摆。
C 存在内存安全问题。但这没关系,因为情况并不那么糟糕™️。而且它不会对你提供任何帮助,这也不错。
这篇文章的 2/3 内容都花在内存安全问题上,但我想这些问题并不重要。但 Rust 却因为可能存在的供应链问题而遭到否定。C 更安全吗?那么尽管 Go 存在完全相同的问题(除了随机的 git 存储库之外),但还是推荐使用 Go。
嗯?
我觉得这篇文章中有些地方关于选择合适工具的建议很好,但没必要写得这么长。而且它显然不需要淡化实际问题来支持作者的奇怪观点。
我在文章中丝毫没有推广C语言。我认为人们对自身安全态势的看法以及实现安全措施的困难程度存在不切实际的期望。
C语言极其不安全,但人们过于迅速地忽视了切换所带来的经济成本(或至少严重低估了这些成本),不试图理解其好处(例如,在嵌入式领域,它几乎是唯一拥有足够生态系统的选择),并且完全忘记了我们作为缓解措施的其他控制手段。
与此同时,Rust(至少目前而言)也有其自身的大风险,人们选择忽视这些风险,而且同样难以让人们有效地使用它,尽管方式不同。
我不是安全专家,但我可以评论固件,我认为这不是一个好点。嵌入式 C 的生态系统很大,但生态系统的质量如此糟糕,以至于它可能实际上是负面的。
我使用 Rust 已经将近两年了,在此之前,我从事 C 固件工作大约十年。我估计我在 Rust 上的工作效率是 C 的 1.5 到 2 倍。Rust 的工具要好得多,相比之下,C 就像是在用棍子和尖锐的石头一样。
在阅读了你的内容(仅限亚马逊简介)后,我对你持有这些观点的原因更加困惑。如我之前提到的,整篇文章读起来像是你不想冒犯任何人。它不断在寻找这个“经济且务实”的解决方案之间来回摇摆。
我不知道你怎么看,但对我来说,这意味着“什么都不做”。应对气候变化的经济和务实解决方案是什么?什么都不做。那么汽车安全呢?什么都不做。
对我来说,这同样适用于软件开发。如果你有一支C语言程序员团队,那么务实且经济实惠的解决方案就是直接用C语言编写。不过,这样做永远无法让事情变得更好。
我不知道你怎么看,但我希望事情能变得更好。我并不特别在意解决方案是否务实或经济实惠。况且,你提到的那些缩小差距的措施本身就不是务实或经济实惠的。
为C++编写新的静态分析器并不务实。编写像Valgrind这样的软件也不经济。最终,有人必须选择“不务实且不经济”的道路并采取行动。否则,事情不会变得更好。
这就是我对你的文章感到不满的地方。这篇文章用了很多文字来表达“什么都不做”。
我完全不同意,结尾部分已经足够明确,无需更多文字解释。
如果人们无法更深入地理解权衡关系,并因过于简化的观点相互指责,那么事情就不会以应有的速度改善。我认为(并且确实认为)安全行业已经产生了巨大影响,而我的核心观点是,忽视其他考量会让我们成为糟糕的商业伙伴,并导致长期影响大幅减弱。
我确实建议人们摆脱对性能的盲目追求,但在系统编程领域,对考量范围有更深入的理解,将有助于更好地优先分配资源。请注意,当 LF 决定将开源安全作为优先事项时,“用 Rust 重写每个主要网络服务”是 OSS 的一项重大投资领域,但似乎优先级非常低,而致力于开发更适合高度受限环境的工具链,似乎优先级非常高,但据我所知,这一点并未得到考虑。同样,我认为应该加大对易用性的投资(平均而言
我能够产生一些影响的部分原因在于我是个实用主义者。我认为,如果对需求和权衡有足够深入的理解,就能让所有人达成一致;当自然激励机制阻碍正确行为时,强制人们做正确的事应是一个可选方案(正如在披露运动初期所必需的那样)。但问题不在于此。世界已经朝着这个行业迈进,但这个行业可能过于追求检测和响应,而对预防的易用性和成本效益的投资却太少。
需要明确的是,已经有一些投资,Rust 就是一个例子。但当我们系统性地投资不足时,如果我们不合理地使用有限的投资,然后责怪用户,就会造成更大的问题。
没错,这是因为 Rust 已经存在了。这是务实且经济的解决方案。至少与编写可能无法正常运行的新工具相比是这样。
我想指出的是,我认为 Rust 并不是万能的。我在 Rust 中从事完全普通的后端工作。如果我在做出决定时在场,我会推荐 C#(尽管我非常喜欢 Rust)。我确信有些情况下它并不适合。也许 Rust 不适合的地方比适合的地方更多。
但是,C(和 C++)不适合的地方也太多。我甚至同意你文章中的这一部分。我的问题是,每当你为了改进而进行重写(甚至重构)时,如果你将它纳入“这经济吗?”的方程式中,答案总是“不”。
对于启动新项目也是如此。为什么一家公司要选择培训所有开发人员学习新语言,而他们可以继续做他们一直以来都在做的事情?
抱歉,但我本应是这里理想主义者。然而,你比我更有信心。
不,Go、Java 和 C# 已经存在,对于 Rust 将要解决的大多数问题来说,它们更合适。Rust 在被选中用于适当的用例时,可以在剩余的问题上取得进展。
但它并不会让系统编程对新手更容易,也不会为一些人提供足够的过渡,使他们从当前的工具转向Rust,从而获得净收益,尤其是如果你要求他们重写所有旧代码,这本身就是一个值得怀疑的要求。
我个人认为,Zig 将会成为一条更加实用的过渡路径。
以下是我作为一名从事网络工作、希望更接近系统并全面发展的人对此的看法。我发现 C++ 和 Rust 一样难,但经过一些指导和简单的编写,我能够编写相当多的 Rust 代码并理解它。我的代码似乎也足够安全,可用于网络服务等用例(这匹马跑得真快)。
如果我只是阅读一些官方文档、视频和课程,就能用 C++ 达到这种安全、快速的效果吗?
我们能否真正意识到,是的,Rust 确实使新手能够轻松地编写出非常不同的东西?
我没有从我目前的工具进行“过渡”,我只是习惯于为不同的语言使用不同的工具…
为了更清楚地说明我的观点。用 Rust 重写网络驱动程序(已经实现了)比花几年时间制作工具,然后重构其他 内核 代码以使用该工具更务实。
至于第二部分,我不同意。Rust 试图为新用户提供良好的体验。这就是为什么有完整的免费学习书籍、优秀的文档、对错误信息的关注以及多个包容和友好的社区。它不会隐藏你在编写大型系统时将面临的复杂性。它甚至不会试图让你与现有代码(如 Go(Cgo 不是 Go…)或 JVM 和 .Net 运行时)的接口变得过于困难。
我认为 Zig 很酷,尽管它并非安全性的典范。它在某些方面有所改进,但也引入了边界检查(以及整数溢出检查),而你似乎也不太喜欢这些。在我看来,Zig 代码中出现段错误相当常见(例如来自编译器、ZLS、zigmod 等)。
我认为你略微夸大了 Rust 在(至少是 Linux)内核空间中的准备程度,以及其他缓解措施和工具的成本。至少在内核中,我们已经非常擅长推出此类改进(例如,看看内核自我保护项目以及 coccinelle 等重构工具)。
Linux 内核中的 Rust 驱动程序非常出色且令人鼓舞,但绝对还未完全“准备就绪”——Rust 仍被视为一种实验,尽管其发展速度超出了所有人的预期,但还有很长的路要走。(对于内核的某些部分而言,这条路会比其他部分更长。
幸运的是,这并不是一个二分法:我们可以同时做到这两点,事实上,情况正是如此。在尝试 Rust 移植和重写有意义的地方,它们正在进行;而在其他地方(以及在等待 Rust 重写的同时),C 代码正在得到显著改进,并采取了缓解措施。
是的,我绝对不想阻止人们改进现有的 C 代码。Rust 可能还不适合 Linux 内核,这完全有可能。也许它永远都不适合,我对此也完全接受。
然而,我上面提到的观点主要是关于“经济”方面的论点。将工具引入内核固然很好,但如果这些工具不存在,你就无法做到这一点。我无法想象研究并创建像Coccinelle这样的工具是经济或务实的。一旦它工作并引起关注,当然可以,但在那之前呢?
我从文章中得到的印象是“反正也没那么糟糕”,因为有这样的工具存在。但如果每个人都抱有这种态度,这些工具就不会存在。
是的,我同意这里使用“经济性”这个词,虽然我认为它本身并不错误,但确实给人一种不好的感觉。我也不确定如何更恰当地表述,以区分“在某些情况下,减少问题而非消除问题可能是下一步的正确选择”和“这个问题不重要”。不过,我认为作者说得对,在某些情况下,C 仍然比 Rust 更适合,而且即使在关键系统中,我们一生中仍然会看到 C 代码。
幸运的是,同时进行大规模的“重写一切”和较小的增量改进是合理的。
此外(虽然这有点离题),值得一提的是,对于Coccinelle而言,它正被适配为Rust的工具[is being adapted to Rust](https://rust-for-linux.com/coccinelle-for-rust).I。我认为它在那里不会失去作用:无论使用何种语言,都有许多理由对系统性问题进行大规模修复。即使对于内存安全净化器来说,虽然它们对 Rust 的作用可能较小,但也不是完全无用,因为它们可以用来帮助验证不安全的代码等。
我同意,能够用它编写整个图形驱动程序使其具备生产就绪性。当然不是内核的所有部分,但驱动程序肯定可以。
没错:对于许多驱动程序,它确实已准备好投入使用——只是还需要一些最后的调整才能将其合并到上游。例如,它们依赖于许多[不稳定功能](https://github.com/Rust-for-Linux/linux/issues/2),许多子系统抽象仍在由子系统维护者审查。
但我们肯定正在朝着这个方向前进——在“在常见情况下可以正常工作”和“足够稳定,可以成为默认构建的一部分”之间还有许多事情要做。这仍然需要 Rust 上游和内核上游做一些工作。
我认为,成功尝试使用 Rust 的 js 和 python 程序员的数量是反对这一说法的一个有力数据点。当然,其中成功的人并不多,但比例仍远高于其他系统语言(C、C++、Ada 等)。
我个人认为 Zig 不会成为非系统程序员的成功选择,因为它在防范内存错误方面保护不足,而这正是非系统程序员面临的最大挑战。但时间会给出答案。
目前,Rust 正在开发一个符合 C89 标准的目标,它使用借用检查器的信息来输出“安全 C”。这是逐步整合的最佳途径之一,直到您为芯片获得 GCC 或 LLVM 等现代编译器为止。
重点不在于“C语言并不那么糟糕”,而在于“到2024年,利用C语言漏洞往往相当困难”。对于Linux等系统而言,这一说法大致成立(取决于威胁模型),但对于缺乏进程、ASLR及其他缓解措施的嵌入式系统而言,情况则大不相同。
这可能是真的,但现存的海量C代码正在从另一个方向施加压力。基本上每天都会发现新的可利用漏洞。
当昨天(?)有人宣布一个影响所有PHP版本的glibc缓冲区溢出漏洞时,很难认真对待这篇文章。
更不用说作者根本不理解 rust(和 unsafe),却假装能够提出细致入微的意见。
这就是为什么正确的做法是重写那些特别敏感和/或有问题的区域。这基本上就是谷歌在 Android 中所做的(我个人最喜欢的是重写 NFC 堆栈——旧的堆栈完全是一堆垃圾)。
我同意这一点。但作者似乎并不这么认为,至少目前还不这么认为。
嗯,你从我文章中引用的内容与重写敏感/有问题区域的重点无关。
我支持任何对更广泛世界有利的经济决策,但我觉得我在文章中已经非常明确地表示,我乐见所有关键的C语言代码以更安全的方式重写,但要以务实的方式进行,避免制造不必要的麻烦和风险。
是的,你已经明确表示了对文章中所有内容的赞成和反对。无需在 reddit 评论中重复这一点。
这段话基本上是在说,尽管 Rust 在 C 语言的基础上进行了各种改进,但你仍然不应该使用它,因为可能会出现供应链问题。在你提出的两个问题中,这是你没有驳斥的一个。我还能如何理解这一点呢?
如果你正在编写一个低级系统,比如上面的安卓系统,无论经济性如何,你都没有太多选择。要么是 C、C++ 或 Rust(如果你是苹果公司的话,还有 Swift)。我认为 Rust 是这里最务实的选择。如果你同意,那么你的评论的意义何在?
听着,你不需要和随机的互联网混蛋争论。去享受你的假期吧。
它并没有说你不应该使用Rust。只是说这是人们应该考虑但尚未考虑的事情之一。
然而,上一次我在一个全新的系统级项目中为自己做出选择时,我决定尝试使用Nim,结果喜忧参半(我可能会很快写一篇关于这次经历的文章)。Zig 对我来说还不够成熟,但现在可能已经成熟了,我对它最乐观。
这种看法太糟糕了。首先,C 和 Rust 从什么时候起不是编译型语言了?其次,这些语言都是通用编程语言。C 只是一个功能有限的非常古老的语言。
而 Rust 是一种具有大量现代功能的现代语言。它比 Java 或 C# 更高级,可与 Swift 或 Kotlin 等其他现代语言相媲美。它适合低级编程,但这并不意味着它不适合通用编程。
[已删除]
在非 GC 语言中,GC 仍然是一个问题的情况也需要特别注意。真正的问题实际上是任何类型的动态内存分配。在任何语言中的解决方案都是声明一切为静态。现在你可以在 Java 中进行高频交易。
这到底是什么意思?永远不要使用堆?
是的
不,“停止世界,消耗所有内存带宽”这种情况偶尔发生对应用程序本身以及服务器上的其他应用程序都非常有害。只需切换到符合malloc规范的分配器并避免愚蠢操作,就能解决大多数程序的性能问题。
现代 GC 无需停止世界并消耗所有内存。
如果 Golangs GC 对您的工作负载来说太慢,您也无法编写标准的 Rust。
[deleted]
如果你有严格的实时要求,而 Go 的 GC 太慢,当然你不会用 Go 编写它,因为它太慢了……
在 Rust 中,您 100% 需要担心内存和性能问题。它不会做任何神奇的事情。许多简单或原始的进程,尤其是那些快速分配和释放内存的进程,例如 JSON 解析,其惯用的 Rust 实现会比 GC 更糟糕。Rust 中没有免费的午餐,生命周期只是另一种形式的 GC,只能进行本地推理,无法进行批量操作。
我的观点是,如果你有这样的要求,而 Golang GC 又太慢,你就无法像正常一样编写 Rust,因为正常的容器分配行为都是不可接受的。你可能需要使用一个巨大的竞技场,然后将整个东西扔掉,或者根本不进行任何动态分配,只是重复使用一个固定的缓冲区。你完全可以使用 Rust 来应对这种要求苛刻的环境,但它看起来不会像典型的 Rust。
我认为他并不是说它们没有被编译,我认为他只是漏了一个词,他的意思是“C 与 Rust 并不是一个错误的选择,因为(其他)编译型语言(如 Go)往往更好……”
“往往更好”是有争议的。
我喜欢编写 Rust,仅仅因为它有学习曲线,并不意味着当出现一种“更简单的”语言时,它就必须立即被扔进垃圾箱。“更简单”也不意味着立即“无错误”或能够制作出更好的软件,通常情况下,更复杂的语法和更大的词汇量能够制作出更周全的解决方案,甚至比用更少的术语实现的复杂逻辑更清晰。
Rust 和 WASM 之间的重叠也非常有趣,这是超越 TypeScript 的一步。
完整的引用是:“系统语言被过度使用;C 与 Rust 是一个错误的选择,因为像 Go 这样的编译语言在经济上往往是一个更好的全方位答案。尤其是 Go,它具有足够好的性能,足以满足绝大多数用例的需求,而且安全,并且可以很好地访问低级系统 API。”所以他并不是想说“Go 比 Rust 更好”,他只是说对于许多问题,你并不需要 Rust 提供的功能,使用其他东西可以节省时间和金钱。
好吧,假设一个相对随意的场景,我们必须排除使用其他非常流行且完全可行的语言,如Java或C#,这些语言在企业环境中我经常发现基本上是标准。那么也许我们可以选择一种快速且编译型的语言。
我们可能会从后端部门调配一些开发人员,因为他们总体上比我的前端同事更经验丰富且能力更全面。但我们确实需要两者兼备,我们很少只做后端开发。目前我们没有专门从事Go或Rust开发的工程师,只有一些业余时间的实验和学习。
因此,我们可能需要对所有开发人员进行Rust或Go的培训。
因此,根据项目的长短和要求的明确程度,我们要么有很长的时间来学习技能,要么需要匆忙完成。很容易认为使用 Go 是最好的选择,因为我们可以先进行开发,但还有许多因素需要考虑。
无论如何,这都会是一个非常奇怪的情况,因为我们通常不会同时学习和使用一种技术。
编辑:我在这项计划中排除了 C++,但它与 Rust 几乎有相同的论点,但如果公司还没有相关技能或资深人员,那么它肯定更难做对、更难配置、更难构建一个好的工具集。Cargo 使 Rust 比任何 TypeScript 或 Java 都更易于使用。
EDIT2:你难道不喜欢写长篇大论,只为了被“Rust 不好”派的人打低分吗?
这完全偏离了重点。许多 C、C++ 甚至 Rust 代码都可以用 Go、Java 或 C# 等语言编写。人们喜欢以性能或内存分配控制作为理由,但他们不会提供基准测试,或者会在热路径上大量使用 malloc/free,所以我并不相信这种说法。
Rust 的优势远不止性能(或内存安全性)方面。
还有更多,但遗憾的是,讨论总是围绕着这个特定的投票进行。当你将内存和线程安全性与所有其他东西结合在一起时,这就是关键所在。
与用 Go、Java 或 C# 编写的大型多线程应用程序相比,用 Rust 编写的大型多线程应用程序出现逻辑错误的可能性要小得多。
人们真的喜欢忘记 Rust 的起源,它是一种无畏的并发和零成本抽象语言。这自然会让你想到借用检查器,这自然会让你想到没有 GC,这自然会让你想到它被用于低级代码。
但无畏、零成本的并发性仍然存在。这就是为什么你会选择 Rust 而不是那些语言,而不是因为单线程性能。
但即使性能并不重要,为什么还要选择 Go 而不是 Rust 呢?Rust 凭借其强大的类型系统,似乎更适合一般用途,它可以帮助避免与内存安全无关的错误。Rust 只是碰巧速度很快,但我认为人们真正喜欢的是它的类型系统和整体设计。
学习曲线更容易吗?我从未学过 Go,但 Rust 以难学而著称。
Go 显然更容易学习。因此,与他人合作会容易得多。就我个人而言,我不会相信我的一半同事有能力维护 Rust 代码,但 99% 的人应该都能做到。
当然,Go代码的性能可能更差,bug更多,或可读性更低,但对于那些“意外”导致几小时停机不算大问题,或只需重启应用程序继续运行的场景,这些往往并不重要。在许多应用程序中,初级开发人员能够向数据库添加新字段并将其贯穿整个基本CRUD应用程序的能力,比安全性更重要。
我认为情况恰恰相反。如果代码在他们修改后能够编译通过,那应该没问题。你可以检查是否存在更简洁的实现方式,以及整体方向是否正确,但无需费力去寻找数据竞争或在边界情况下可能导致运行时错误的问题。没有解包操作,没有 panic,显然没有 unsafe 关键字,而且代码似乎能正常工作?那就没问题。即使在Java中,你也需要考虑一些问题,比如,这个对象是否重写了Eq/hashCode方法以用作哈希表的键?这个对象是否会被序列化,如果是,是否需要调整某些内容?例如,在方法中通过接口接受一个列表,但出于某种原因会修改它。然后它只在运行时的一种特定情况下失败,因为列表的实现恰好是一个不可修改的列表。或者,也许现在没有地方会传递一个不可修改的列表,所以这只是以后可能会出现的问题。Rust 根本就没有这些问题。我对 Java 的经验比较多,对 Go 的经验不多,所以才举了这些例子,但我觉得情况应该差不多。
谁说 C 和 Rust 不是编译型语言?仅仅因为文章试图承认语言的高级程度存在一个光谱?
我很乐意就实质内容展开讨论,但你对文章的实质内容连边都没沾上。
你并没有直接这么说,但句式结构暗示了这一点——“但像……这样的编译型语言”通常用于对比。可能是编辑疏忽吧。
请注意,文章中之前有一组定义,以便将它们分组进行讨论。虽然不够清晰,但有几个人提前阅读了文章,却没有人能帮我改进这一点 🙂
“像 Go 这样的编译型语言比 C 和 Rust 更好”还能是什么意思呢?
说“Go 类型的编译语言”并不意味着 C 和 Rust 不是编译语言,这只是一个愚蠢的观点,因为如果你花 2 秒钟阅读这篇文章,你会发现很多证据表明,早在这句话之前,我就已经清楚地意识到这个事实了。
别怪读者,要怪就怪你自己写得不好
我没怪读者;你得先读完才能算读者 🙂
没错,这可不是重点,老大
没错。C++如今被宣传为高效的系统语言,但它最初设计时是通用语言,其部分领域后来被Java取代。
同意系统语言被过度使用,但推广Go作为替代方案是疯狂的。C可能不是过时语言,但Go绝对是在给你头上砸砖头。如果你用一种真正优秀的垃圾回收语言作为对比,那会是一个更好的论点。
公平地说,Go的运行时(GC、并发性)相当不错,只是语言本身很糟糕。
没有重要玩家主张立即放弃C,而这么多文章将此视为当前讨论的焦点令人沮丧。
这只是一个稻草人论点。
谈论“为什么 C 仍然存在”是毫无意义的:有数十亿行代码需要更改,这会花费大量资金。
不要再用 C/C++ 编写新代码了(或者,如果你坚持使用该语言,至少要使用内存安全的抽象)。考虑使用其他语言的替代品作为重构的一部分。
你认为为什么 Rust 如此大力推广 C++ 接口?用更高安全性的代码替换部分代码是完全可行的。
这是因为“讨论”中充斥着许多不重要但声音嘈杂的人,他们占据了整体话语权的过大份额。
和其他事情一样。
我的意思是,如果你想投入精力,我建议过滤掉噪音。
除非你的目标是与那群人互动。
没有单一的群体,只是Reddit/Twitter/Hacker News上的人。
如果你是一家拥有大量、长期运行、重要的 C++ 代码基础的公司,这实际上是一个加快重写速度的好理由。
为什么?今天,你有数万名经验丰富、谨慎的 C++ 开发人员,他们并不太贵,而且可以学习 Rust。
另一方面,十五年后,可能会有越来越少的经验丰富的C++开发者,而且他们的薪资可能会更高。考虑到C++的复杂性,要快速培训一些初级开发者来掌握它可能并不现实。C++曾被斯特劳斯特鲁普比作“瓦萨号战舰”(https://en.m.wikipedia.org/wiki/Vasa_(ship))——它可能就是那种你希望在它还能航行时就离开的船只。
许多因素相互影响,使 C++ 仍然被广泛使用,但一旦一些基本因素发生变化,这些相互影响可能会发生逆转。
你需要一个 C API 或网络连接来使用 Rust 和 C++,这是一个巨大的限制。
无论如何,你都需要某种系统 API,比如 Linux/Unix 上的 syscall API。C API 本质上是用于与现有二进制代码进行交互的,有些语言没有它,或者在某些语言中它只是一个次要功能,比如 Go 或 Java,或者许多 Lisp 语言,它们通常会为 syscall 之上的所有内容提供自己的运行时。
C++ 没有真正的二进制 API,它也使用 C API。
与 Java 不同的是,Rust 的优势在于它不需要平台/JVM 进行内存管理,而内存管理也会将其限制在这样的平台上。
在用户空间/系统边界之间进行垃圾回收是可能的,Lisp 机器已经做到了这一点(emacs 也是如此,它也是一种虚拟机,JVM 至今仍在模拟它)。
当然,还有 JVM 和 NET,它们也有相同的功能。
使用 C 接口对 Rust 来说并不是必须的,独立的 Rust 程序并不需要它们。
我不能直接交换整个程序。对于迁移来说,这并不是一个好的工作水平…
还有用于连接 Rust 和 C++ 的包装库。fish shell 的人已经使用这些库将他们的项目逐块迁移,这无疑是一个更合理的方法。https://github.com/fish-shell/fish-shell/blob/master/doc_internal/fish-riir-plan.md
https://github.com/fish-shell/fish-shell/discussions/10123
https://old.reddit.com/r/rust/comments/183odpt/media_fishshell_has_finally_been_converted_into_a/
我同意,在嵌入式领域,这一切仍然更加复杂…
这是一篇非常棒的文章,我是一个热爱 Rust 的人。
话虽如此,但我认为一旦你掌握了该语言的精髓,Rust 就并不难。迫使你以不同的方式设计程序是让 Rust 程序更安全的一部分原因。这也是为什么即使使用 unsafe,它在超低级别上也难以使用的原因。
我同意它是一个很好的 C++ 替代品,而不是 C。虽然我不会说它比 C++ 更难(我认为恰恰相反)。你只需要以不同的方式做事情。嘿,如果要求程序员为了安全而以不同的方式编写 C/C++ 代码是合理的,那么要求程序员为了正确性而以不同的方式编写 Rust 代码也是合理的。
我在业余时间自学 Rust(我是一名高级开发人员),你说的没错。一旦掌握了它,它实际上是一种非常简单的语言。我绝对觉得 C++ 在超过“hello world”阶段后更复杂。至少对我来说是这样。
对于任何想深入学习的开发者,我认为学习这门语言最好的方法之一是制作一个TCP发布/订阅服务器和客户端。让服务器支持多线程。你几乎会接触到这门语言的所有主要部分,无论是好的还是坏的。
完全同意。我接触过的人中,做过这两件事的人都说了类似的话。
以智能指针为例。我曾就职于一家公司,该公司要求每个人都使用智能指针,尤其是 std::unique_ptr。如果你已经在使用 unique_ptr,那么你基本上就理解了借用。这占 Rust 复杂性的半壁江山!
Rust 采用了最佳实践,并使其成为非可选项。如果你将 C++ 与所有需要记住的最佳实践以及正常的惯用 Rust 进行比较,Rust 实际上更简单(而且更安全)。
谢谢;我个人长期以来对 C++ 特别反感,这就是为什么我在文章中没有花太多时间讨论它。
我其实对 Rust 非常熟悉(只是不太喜欢关于这个话题的讨论)。但我看到许多聪明的人认为它毫无必要地难以理解。
我认为,在许多方面,人们可能会感到困难,或者觉得它与他们的思维模式非常契合。因此,我认为很多人觉得 Rust 容易理解是一件好事。
但确实有很多人并不这样做,而这对我个人来说是一个问题,因为我希望看到尽可能多的编程语言能够被任何想投入时间的人轻松掌握。
公平地说,C语言在这方面并不算更好。就系统语言而言,我非常希望 Zig 能成为既擅长满足使用案例需求,又对大多数背景的人来说相对容易采用的新进入者。
作为一个花了很多时间研究 Rust 的人,我有些合理的抱怨。去读读 matklad(Rust Analyzer 的主要作者)的《Hard Mode Rust》吧。那些试图从系统中榨取最后 1% 性能的人必须做很多 Rust 不太喜欢的事情。
我认为你对 Rust 的评价很公正。你不必告诉我你擅长 Rust,从你写的东西就可以看出来。
我认为,在系统层面上,Rust 最让我感到沮丧的是,不安全并不够不安全。理想情况下,我可以使用 Rust 语法进入不安全模式并获得 C 控制权。目前,你仍然受许多规则的约束。也许在 Zig 对 Rust 施加压力的情况下,这种情况会发生。我本人还没有使用过它,但它的许多功能非常吸引人。我有点希望自己能找到一个尝试它的借口。
无论如何,我“怀疑”那些在 Rust 上苦苦挣扎的聪明人并不是在语言上苦苦挣扎,而是在努力改变自己的方式。例如,他们试图将旧的思维模式应用到一种可能对他们不友好的语言上。讽刺的是,越成功的人越难改变自己的方式。如果这是真的,我们可能会发现,尽管 Rust 非常复杂,但初级程序员却出人意料地擅长使用它。
我并不是按照你的理解来理解这句话的……我的意思是,我认为 Rust 是一种很好的语言(就我认为任何语言都是好的而言),我很高兴看到它受到欢迎。多年来,我个人用 Rust 编写过一些小程序,但一直没有用它来编写大型项目,主要是因为:1)这些项目无需使用系统语言即可完成;2)我更愿意将大量精力投入到转换我的 goto 系统语言上,因为我认为这会更容易让人们上手。也许有一天,数据会非常清晰,让我改变对第 2 点看法,但在此之前,我还是坚持我的看法。关于初级程序员,这是个很好的观点。然而,我很多的第一手经验都是与计算机科学研究生打交道的。有一段时间,我在纽约大学教编程语言课,看到很多人都在努力学习 Rust。
也许它作为系统语言已经足够好了,但我怀疑 Zig 最终会既全面又更容易学习。
而在系统编程之外,我通常会建议人们远离系统语言。
又一篇试图说服我转用Go语言的帖子。
AI艺术,然而,是癌症。
针对作者在帖子中提出的一些观点:
“C++不会过时”
语言确实可能过时,或者与特定问题领域的匹配度可能过时。
COBOL已被提及作为例子。需要注意的是,COBOL的过时并不意味着COBOL代码会消失——只是没有新的项目使用COBOL,因为这没有意义。
认为掌握 COBOL 的人备受追捧且薪资丰厚,这只是一个流传的观点。事实上并不完全准确。Stack Overflow 开发者调查已显示,例如 Clojure 岗位的薪资高于 C++——且尤其是一些资深开发者正在转向其他语言。
“不可能重写所有现有的 C 和 C++ 代码”
没有人建议一次性重写所有旧的 C++ 代码;这是不可能完成的任务。但用 Rust 逐一重写对安全至关重要的库和接口是合理的。而且这已经发生了,例如 rusttls。
“C++ 将会长期存在”
存在并不等同于广泛使用。汇编语言仍然存在,但除了少数专业领域外,它并不广泛使用。COBOL 仍然存在,但它是一门好语言吗?
实际上,语言偏好可以在相对较短的时间内发生变化,如果一种新语言在生产力或功能方面具有足够大的优势。这种情况确实发生过,例如汇编语言。在20世纪80年代初,许多微型计算机应用软件仍然是用汇编语言编写的。15年后,几乎不再使用汇编语言。汇编语言是一个有趣的例子,因为它与 Rust 中的 C 语言一样,给程序员提供了更大的自由度:无需遵守调用约定,无需将变量保持为局部变量,无需将控制流限制为 if、while、for。你可以在任何地方使用 longjump()。你可以将每个地址作为函数指针。但当一个好的优化编译器能够生成同样快甚至更快的 C 代码时,这种自由就毫无意义了。事实上,纯 C++ 代码(没有汇编、显式向量化和非常机器特定的内置函数)并不比同等的 Rust 代码快,而 Rust 则始终如一地快速。
Rust 与 C++ 相比也是如此。在 unsafe 块之外,Rust 限制你不要做那些很少是好主意的事情,这些事情不会让你的代码更快。而使用当今的编译器,所有的代码都是符号计算,尤其是包括调用 STL 等东西的 C++ 代码。
“函数式编程风格不适合低级代码”
确实存在难以用纯函数式风格编写的算法。但有两点需要注意。首先,像 C++ 这样的老语言对函数式编程支持不佳,因为它们需要显式类型声明和手动内存管理。因此,这通常是语言本身的限制,而非问题领域的要求。其次,正如作者所写,性能并非总是最重要的。像Clojure这样的语言默认使用纯函数(无副作用),已证明即使存在性能开销,函数式编程风格在并发编程问题(如Web应用服务器)中仍能带来显著优势。
“难以学习”
作者认为,与 C++ 相比,Rust 难以学习。但学习编程是困难的,尤其是低级语言的编程。我做的第一个较大的 C 程序是一个信号处理研究项目,我花了整整六个星期来寻找一个最终由 atan2() 返回错误结果引起的错误。当我更改嵌入式程序的内存布局后,这个错误就消失了。这容易吗?如果使用 Rust,我就可以节省这六周的时间了。
还有一件事……C++ 编程标准长达数千页,几乎没有人能完全掌握整个语言,而《Programming Rust》的篇幅则比较易于掌握。有人可能会说C++也有入门书籍,但这些书籍在某些方面显得过于简单,比如对多线程、良好的内存管理、大型代码的组织,以及像“唯一定义规则”或“有符号整数溢出是未定义行为”这类陷阱的讲解都较为简略。
生产力
我们将看到,Rust 编码者的生产力显著提高的报告是否会成为一致的观察结果。我对此持乐观态度,因为编译器警告、工具、依赖管理、构建工具等各方面都提高了十倍。有趣的是,这些优势给各级程序员带来了好处,从新手到必须编写多线程代码的专家。此外,用户喜欢像 ripgrep 这样的工具,因为它们由于高效的多线程而速度更快。(当然,也可以用 C 或 C++ 实现快速高效的多线程,就像可以用汇编语言编写一个类似 Photoshop 的程序一样,但没有人这样做——在 60 年内编写一个非常快的 C 语言 grep。)
没错。完全正确。
小测验:谁说过“过早优化是万恶之源”?
太多人陷入这个陷阱,担心基于某个可能与他们的用例无关的基准测试得出的个位数甚至更小的性能差异,然后用一种专门用于实现系统代码的语言来编写他们第N个命令行 JSON 解析器( seriously,别再写这些了…)。
无论是 C、C++ 还是 Rust,都无所谓!对于大多数编程来说,它们都糟糕透顶,而我作为一个使用 C 并对其充满敬意的人,是这样说的!
Python 之所以成为最受欢迎的编程语言,是有原因的。人们蜂拥而至 Node.js,尽管在内心深处,大家都知道 JavaScript 并不完美!Java 相比 C++ 拥有更多代码量,Go 相比 Rust 同样如此,这背后都有其原因。
除非有特殊原因,否则没有人应该被迫使用需要手动管理内存或与借用检查器的复杂性搏斗的语言。
当然,有些情况下这是无法避免的,但这些情况只是例外,而不是常态。
哈哈。TS 和 Python 工具都糟糕透顶,真的非常糟糕。
简单的 Rust 与 TypeScript 难度相当,但它是一个更全面的语言,具有 Result/Option、Match 语句、无空值、if let 等功能。Newtypes、enums 和 traits 非常棒。
你可以用 Cargo 在几分钟内启动一个 Rust 项目,它自带电池;TS 简直是一团糟;Python 更是一团糟。Cargo 取代了十几种工具,并制定了人人都遵守的标准,没有“风格指南”,没有关于分号的争论;我不需要在运行时和环境之间来回切换。
当然,JS 和 Java 有大量时间和代码积累,它们在学校教授且有数十年历史。Python 曾经是简单的脚本语言,但现在已沦为灾难。
但没错,我可以在记事本中编写 JS/Python 代码并在浏览器/终端运行。这足以交付项目吗?当然不行。这甚至不足以满足持续集成需求。
对不起,到底是谁说用 Rust 或其他语言取代世界上所有的 C 程序是件轻而易举的事呢?
C 语言是伏特加
Rust 是啤酒。
所有编程都是毒品。
不要沉迷于它们的高级特性,它们会毁了你(大概吧)。
(这种混乱由“深夜太晚”带来)
我使用其他语言越多,对C的敬意就越深。
因此,C 和 Rust 之间的竞争是一场骗局,它们可能都和高级语言一样优秀,但在我看来,它们就像两个孩子在争论谁更优秀一样,你们都比不上更高级的语言,你们都需要打屁股和停课…
看到这个吸引眼球的标题,我本以为会看到另一篇充满个人观点的文章,但这实际上是我读过关于这个话题的最佳文章之一。写得真好!
“现代C++”的问题在于:
f
是否存在使用后释放的漏洞?这是一个相当标准的“接收一个常量容器并遍历它”的模式。典型的C++程序中包含数十甚至数百个此类模式的实例。
那么,它是安全的,对吧?答案是:可能安全,但无法证明,而且这取决于程序的其他部分。以下是一个导致
f
在内存被释放后继续使用的程序。这些函数单独看都完全合理。正是
f
函数内部对g_vec
的偶然别名导致了程序故障。随着程序规模增大,这种模式会变得极其难以避免,也极其难以检测。全局变量在几乎所有语言中都是有问题的,对吧?
这与全局变量无关。如果将f和g定义为某个类的成员函数,且该类将gvec作为成员变量,你仍会遇到相同的问题。
我理解正确吗?你只是在循环中使用迭代器时破坏了它?
没错。因为在
f
中同时存在对g_vec
的可变引用和不可变引用,导致f
中的迭代器可能变成悬空指针。终于有人在这件事上说出点常识了!
我同意作者关于大型依赖树的观点。作为一名 C++ 开发者,我竭尽全力将依赖项降到最低(我知道,从其他人那里得知,在 Go 中仅使用标准库也能做到极致)。
无论如何,这场讨论并非像白宫那些骗子想让我们相信的那样非黑即白,其中存在微妙的因素,将其摆上台面是有好处的。
在这里帮助我的是构建一个软件服务而非库。我有一个实现为三层系统的C++代码生成器。后端层是最大的层,且无需具备可移植性。这大大简化了依赖问题。
Go 是一门很棒的语言,但因为我不喜欢粉丝,所以我会在它身上撒尿。Rust 和 C/C++ 比 Go 更快,从我读过的基准测试来看,即使是带 AOT 或 JIT 的 .NET 8 也比 Go 更快或相当,那么为什么选择 Go 呢?你可以用 C# 编写快速易用的代码,或者用 Rust 编写更快的代码
这很有趣,因为在我的开发者圈子里,Rust 有最多的粉丝。
典型的 Rust 装腔作势的粉丝:“Rust 是 C/C++ 的杀手”。
还有一个典型的 Rust 粉丝:“我实际上对 C/C++ 及其典型用例一无所知,除了几乎完成了 Rustlings 的枚举部分之外,我还没有用 Rust 构建过任何值得一提的东西。”
C 不再应该被视为一种语言了,每次面试时,当被问到这个问题时,我都会说我可以,但只有在必须的情况下才会用。
为什么?
它是不必要的,而且通常被错误地使用,我厌倦了修复其他开发者的糟糕代码,更不用说追踪混乱的 C 与混乱的 C++ 或 C# 或 Java 或 Python 的区别,这会变得非常复杂。
rust 完全是一派胡言
它的社区都是病态的胡说八道者
除了 rust 之外,什么都可以用
Zig 是治疗方法。
https://ziglang.org/