acm-header
登录

ACM通信

实践

最昂贵的单字节错误


最昂贵的单字节错误,插图

信贷:加里尼尔

回到顶部

信息技术(IT)推动和实施了现代西方式经济。因此,我们经常看到有关IT错误导致巨额资金的头条新闻。哪个IT或CS决策导致了最昂贵的错误?

不久前,许多权威人士还在大肆鼓吹索尼PlayStation Network问题的财务影响,但这样的事件在这里不算数。在我上学的时候,我和一位来自吉尼斯世界纪录他解释说,要想成为“真实的记录”,它不可能只是偶然;必须有人类意图的直接因果关系(例如,我们把26名高中生塞进音乐老师的大众甲壳虫车,然后关上车门)。

索尼(可能)没有意愿看看它可以在对安全不加注意的情况下造成多大的混乱,所以这个和其他类似的错误经济的例子是不合格的。另一个候选人可能是IBM选择比尔·盖茨而不是加里·基尔达尔为其个人电脑提供操作系统。这一决定造成的损害仍在以惊人的速度累积,StuxNet和对ISO标准化过程的OOXML歪曲,是损害传播范围的典范。但这并不是IT或CS的决定。就目前的历史来看,这是一个以基尔达尔不接受IBM保密要求为核心的商业决定。

一个更好的例子是MS-DOS决定发明自己的目录/文件名分隔符,使用反斜杠(\)而不是Unix使用的正斜杠(/)或DEC在其操作系统中使用的句点。除了实际伤害相对较小之外,这也不能算是一个好的例子,因为这并不是一个选择真正偏好的真正决定。IBM决定使用斜杠作为命令标志,取消了Unix作为先例,句点在文件名和文件名扩展名之间使用,使得不可能遵循DEC的例子。

太空探索的历史上有很多广为人知且代价高昂的错误,但有趣的是,我没有找到任何有效的候选者。Fortran语法错误和航天飞机的计算机同步错误不符合缺乏意图的条件。以英制单位运行项目的一部分,而以公制单位运行项目的另一部分,是一种“随机管理行为”,与CS或IT无关。

我所能想到的最佳选择是C/Unix/Posix使用以null结尾的文本字符串。选择真的很简单:C语言是应该将字符串表示为地址+长度元组,还是仅仅表示为带有标记结束的魔法字符(NUL)的地址?这是肯·汤普森、丹尼斯·里奇和布莱恩·克尼根三人组在20世纪70年代初的某一天做出的决定,他们有充分的自由选择。我没有发现任何关于这一决定的记录,我承认这是其候选资格的一个弱点:我没有证据证明这是一个有意识的决定。

然而,就我的研究来看,这个地址+长度格式是当时大多数编程语言的首选,而地址+魔术_记号笔Format主要用于汇编程序中。由于C语言是从汇编语言发展到可移植的高级语言,我很难相信Ken、Dennis和Brian没有考虑到这一点。

使用一个地址+长度格式会比格式多消耗一个字节的开销地址+魔术_记号笔格式,他们的PDP计算机的核心内存有限。换句话说,这可能是一个完美的典型和理性的IT或CS决策,就像我们每天都在做的许多类似的决策一样;但这一次的经济后果是非典型的。

硬件开发成本.最初,Unix对硬件和指令集设计的影响很小。提供字符串操作指令的cpu,例如Z-80和DEC vaxd,在更广泛的范围内都是这样做的adr +兰模型。然而,当Unix和C获得关注后,终止字符串作为优化目标出现在雷达上,CPU设计师开始添加指令来处理它们。一个例子是IBM在1992年添加到基于ES/9000 520的处理器中的逻辑字符串辅助指令。1

向CPU添加指令并不便宜,而且只有在有切实和可量化的金钱原因时才会这样做。

性能开销.IBM增加了操作以null结尾的字符串的指令,因为它的客户在处理这类字符串上花费了昂贵的CPU周期。但是,这一点信息并没有告诉我们,如果一个ptr +兰格式已经被使用。

对虚拟内存(VM)系统稍加思考就可以解决这个问题。优化已知长度的字节串的移动可以利用内存总线和缓存线的全宽度,而无需触及不属于源字符串或目标字符串的内存位置。

FreeBSD就是一个例子libc,那里的bcopy (3) / memcpy (3)实现将尽可能多地以“unsigned long”块(通常为32位或64位)移动数据,然后像注释中描述的那样,使用字节范围的操作“清除所有尾随字节”。2

但是,如果源字符串是NUL终止的,则试图以大于字节的单位访问它会有尝试读取NUL之后的字符的风险。如果NUL字符是VM页的最后一个字节,并且没有定义下一个VM页,这将导致进程死于不必要的“页面不存在”错误。

当然,在使用优化的代码路径之前,可以编写代码来检测拐角情况,但这为所有字符串移动增加了相对较高的固定成本,只是为了捕捉这种不太可能的拐角情况——无论如何都不是一个有利的权衡。

如果我们对弦有带外知识,事情就不一样了。

编译器开发成本.对于一个字符串,编译器通常知道的一件事是它的长度,特别是如果它是一个常量字符串。这允许编译器更快地发出调用memcpy (3)即使程序员使用strcpy (3)在源代码中。

由编译器进行的更深入的代码检查允许更高级的优化,其中一些非常聪明,但只有在有人为编译器编写代码来进行优化的情况下才可以。编译器优化的开发历来既不容易也不便宜,但显然苹果希望这种情况会随着低级虚拟机(LLVM)而改变,在那里优化器似乎出现了在格罗斯

重载编译器优化的缺点,尤其是在大规模操作中采用源代码的整体视图并重新排列的优化,程序员必须非常小心地让源代码精确地指定他或她的完整意图。一名在凸C3800系列超级计算机上与编译器一起工作的程序员说,他的经验是“必须把编译器当成我前妻的律师来编程”。

安全成本.即使您的编译器没有恶意,编写源代码也应该防止攻击,而以null结尾的字符串在这方面的记录很糟糕。绝对安全灾难如(3)“假设缓冲区足够大”,这是一个“我们相对可控”的问题。3.

然而,要控制它,需要向编译器添加一些内容,这些内容会在(3)函数被调用。尽管对字符串缓冲区的关注已经有15年了,但过度或不足的字符串缓冲区仍然是犯罪分子首选的攻击向量,而且往往会得到回报。

在所有级别都增加了减轻这些风险的措施。cpu的内存管理硬件中加入了久违的无执行位;操作系统和编译器增加了地址空间随机化,这通常以较高的性能成本为代价;程序的静态和动态分析耗费了无数的时间,试图找出拜占庭式诊断是真实的bug还是聪明的编程。

然而,如果索尼的问题被发现始于缓冲区溢出或错误的null终止假设,绝对没有人会感到惊讶。

回到顶部

Slashdot的感觉
预防部分

我们从错误中吸取教训,所以让我郑重声明,在有人想出一个朗朗上口但完全具有误导性的互联网标题为这篇文章之前,Ken, Dennis和Brian绝对不可能预见到他们30年前的选择的全部后果,他们当时否认了所有担保。据我所知,至少过了15年,才有人意识到为什么这个微妙的决定是一个坏主意,而我自己的it决策很少(如果有的话)能坚持这么久。

换句话说,肯、丹尼斯和布莱恩做了正确的事。

回到顶部

但这并不能解决问题

对于很多人来说,C语言是一种死语言,而${lang}是一种未来的语言,因为${lang}的临时值不断变化。现实情况是,现在所有其他语言都直接或间接地建立在Posix API和以null结尾的C字符串之上。

当您的Java、Python、Ruby或Haskell程序打开一个文件时,它的运行时环境将文件名作为一个以null结尾的字符串传递给(3)打开,以及何时解决www.eqigeno.com对于IP号码,它将主机名作为一个以空结尾的字符串传递给getaddrinfo (3).只要你一直这样做,你就保留了在PDP/11上运行程序的所有优点,以及在其他任何设备上运行程序的所有缺点。

我可以在这里写一个稻草人API提案,提出表示、操作和错误处理策略,我很确定这将是一个美好下午的绝佳浪费。经验表明,这样的建议不会有什么结果,因为与PDP/11的向后兼容性和编写的有限数量的程序比在未来以有效和安全的方式编写潜在的无限数量的程序要重要得多。

因此,肯、丹尼斯和布莱恩决定的代价将不断累积,就像几个世纪以来几乎掩埋了古罗马纪念碑的灰尘一样。

ACM队列的q戳相关文章
queue.acm.org

大型多人中间件
Michi亨宁
http://queue.acm.org/detail.cfm?id=971591

Linux安全的七宗罪
鲍勃Toxen
http://queue.acm.org/detail.cfm?id=1255423

B.Y.O.C.(1342次,还在增加)
Poul-Henning坎普
http://queue.acm.org/detail.cfm?id=1944489

回到顶部

参考文献

1.计算机商业评论》(harvard Business Review)。高端ES/9000的分区和Escon增强(1992年);http://www.cbronline.com/news/ibm_announcements_71

2.ViewVC。的内容/头/ lib / libc / / bcopy.c字符串(2007);http://svnweb.freebsd.org/base/head/lib/libc/string/bcopy.c?view=markup

3.维基百科。救生艇素描(2011);http://en.wikipedia.org/wiki/Lifeboat_sketch

回到顶部

作者

Poul-Henning坎普phk@FreeBSD.org)已经编写了26年的计算机程序,是其背后的灵感来源bikeshed.org.他的软件在开源和商业产品中被广泛采用,作为“幕后”的构建模块。他最近的项目是Varnish HTTP加速器,用于加速像Facebook这样的大型网站。


©2011 acm 0001-0782/11/0900 $10.00

如果您不是为了盈利或商业利益而制作或分发本作品的部分或全部,并在第一页注明本通知和完整引用,则允许您免费制作本作品的部分或全部数字或纸质副本,供个人或课堂使用。本作品的组成部分必须由ACM以外的其他人享有版权。信用文摘是允许的。以其他方式复制、重新发布、在服务器上发布或重新分发到列表,需要事先获得特定的许可和/或费用。请求发布的权限permissions@acm.org或传真(212)869-0481。

数字图书馆是由计算机协会出版的。版权所有©2011 ACM股份有限公司


评论


赛斯Powsner

啊…这让我想起了美好的回忆:热熔烙铁、冷的散热器油脂、电路板上的玻璃纤维碎片。

一个模糊的回忆是pdp - 11mov指令设置了条件代码。0 / null终止的循环只是两条指令:MOV自动增量寻址和BNE(分支不相等)。不要回忆任何两个使用长度/计数的指令循环。

另一个模糊的回忆是Jensen & Wirth在他们那个时代的书中评论了行尾/字符串标记的问题。这可能是在他们描述文本文件io。

我倾向于相信汤普森、里奇和克尼根,如果他们说他们很少考虑这个问题。在PDP-11汇编程序中编程让我清楚地知道了这个选择,尤其是字符串或搜索列表大于255时。

你的观点还是非常非常好。


亨利Ledgard

这是一篇整洁的文章。作者有很好的谈话风格和一套深思熟虑的问题来引起我们的注意。

我一直认为Java中的字符串很奇怪,并且不知道为什么它们不被当作原始类型。此外,所谓的空字符本身是奇数。

回到作者的主要观点。我想我总是把这些事情视为理所当然,从来没有真正质疑过它们。

同样地,我认为Java和c++都是古老的语言,并且想知道为什么我们不能找到一种更现代、更易读、更简单的通用编程语言。


CACM管理员

以下信件刊登在2012年4月的《中华美术学会编辑函》(//www.eqigeno.com/magazines/2012/4/147353)上。
——CACM管理员

在《最昂贵的一个字节错误》(2011年9月)中,pple - henning Kamp提到了Ken Thompson, Dennis Ritchie和Brian Kernighan的“动态三人组”所做的决定,他们设计C语言来表示以null结尾的字符串,而不是通过之前的长度计数来测量它们。坎普说,他没有找到有关这一决定的记录,也没有证据证明这是一个有意识的决定。然而,他推测,在内存是一种昂贵的资源的时候,这可能是出于节省内存的愿望。

我完全同意坎普的决定,不管是否出于良心,这都是一个错误,而且最终代价高昂。在20世纪70年代我自己的C语言编程中,我发现它经常让我感到沮丧,但我相信我理解它的动机:C语言在设计时就考虑到了PDP-11指令集。著名的C俏皮话

While (*s++ = *t++);

将s处的字符串复制到t处的字符串。但是在时间的迷雾中被忽略的事实是,它被编译成只有两个PDP-11指令的循环,其中寄存器R0包含s的位置,寄存器R1包含t的位置:

一个mov (@R0) + + + + (@R1)
bne非零和的测试结果
分支

这样简洁的代码非常诱人,我记得在讨论c时经常提到。前面的长度计算至少需要三个指令。

但是,即使在这种级别的编码中,移动字符串的代码的经济性也要付出很高的代价:必须搜索终止符null来确定字符串的长度;连接字符串时也要付出这种代价。潜在的缓冲区溢出导致的安全问题在当时几乎无法预料,但是,即使在那时,这样的计算成本也是显而易见的。

“x射线将被证明是一场骗局”:开尔文勋爵,1883年。即使是最杰出的科学家有时也会犯错。

保罗·w·亚伯拉罕
迪尔菲尔德中学,马


CACM管理员

以下信件刊登在2011年12月的《编辑来信》(//www.eqigeno.com/magazines/2011/12/142534)上。
——CACM管理员

在Poul-Henning Kamp的文章《最昂贵的单字节错误》(2011年9月)中,Ken、Dennis和Brian在使用以null结尾的文本字符串时确实选择了错误吗?我认为他们当时和现在的选择都是正确的。在过去的30年里,C语言正在消亡,而没有人使用PL/I、Algol或Pascal来完成实际工作的原因是,C语言使人们有可能用几行直观的代码完成很多事情,尽管它只需要很少的内存或CPU能力。搜索和比较以null结尾的字符串可以用这样短的代码段完成;程序员几乎不需要标准库,代码可以编译成几个PDP-11机器指令。在任何语言中,无法检查不受信任的数据都是致命的。

C允许有能力的程序员快速编写简单的代码,简单的代码往往比复杂的代码bug更少,可读性更强。对于那些仍然想使用地址+长度字符串的程序员来说,这样的使用只需要几行就可以完成。当然,还有用于测量字符串长度的strlen()函数和用于限制从文件读入字符串的字符数量的fgets()函数。

当然,如果已知字符串长度,在更新的硬件上复制大型字符串可以运行得更快。这是一种权衡,如果需要,程序员可以在C语言中使用地址+长度字符串,甚至字对齐。对于其他人来说,如果不急于求成的话,总是有“带训练轮的C语言”,也就是Pascal或Java。

优秀的程序员编写安全的代码;糟糕的程序员会编写不安全、bug百出的代码。好的实践比“神奇”的语言特性更有价值。我所知道的最大的Java应用程序也是我所知道的bug最多的应用程序。

鲍勃Toxen
亚特兰大,乔治亚州


显示所有4评论

Baidu
map