编译器优化是否安全?

5
我最近在工作中发现,由于编译器存在漏洞的风险(我们主要使用gcc,但该政策也适用于其他编译器),因此在硬实时嵌入式系统中不使用编译器优化。显然,这项政策始于过去有人因优化器漏洞而遭受损失。我认为这种做法可能过于谨慎,因此我开始寻找与此问题有关的数据,但问题是我找不到任何详实的数据。
有人知道如何获取此类数据吗?可以使用gcc bugzilla页面生成有关编译器漏洞与优化级别的统计信息吗?是否可能获取到无偏倚的数据呢?

偶尔使用-O3会通过某些优化产生不正确的代码。除此之外,我认为没有仅从优化中出现的任何错误。 - Robert Rouhani
像你一样,我也有点沮丧。有两个问题的例子。从页面https://bugzilla.redhat.com/show_bug.cgi?format=multiple&id=734175 ,如果我点击重复链接https://bugzilla.redhat.com/show_bug.cgi?id=735304 ,我会得到一个访问被拒绝的消息。而一个已验证的错误http://gcc.gnu.org/bugzilla/show_bug.cgi?id=49915 4.1.2通过语句“不再得到上游支持”得到解决,这对我来说似乎很奇怪,因为4.1.2可能是$1B RHEL5的关键。 - Joseph Quinsey
只是提醒一下,我认为“偶尔-O3会产生错误的代码”并不公平。请参阅Ian的http://gcc.gnu.org/ml/gcc-help/2010-07/msg00190.html。人们认为是优化错误的问题通常是非标准代码。 - smparkes
7个回答

3

您正在假设编译器在没有优化的情况下是无缺陷的,只有优化才是危险的。然而,编译器本身是程序,很常见地会存在各种缺陷,无论是否使用某些功能。当然,这些功能可能会使编译器更好或更差。

另一篇回答中提到了LLVM,LLVM存在一个众所周知的优化缺陷,他们似乎对修复此问题没有任何兴趣。

while(1) continue;

"gets optimized out, just goes away...sometimes...and other similar but not completely infinite loops also disappear in the llvm optimizer. Leaving you with a binary that doesnt match your source code. This is one I know there are probably many more in both gcc and llvm's compilers."

"gets optimized out"指的是在优化器中被优化掉了, 有时会出现其他类似但不完全无限循环的情况也会在LLVM优化器中消失。这会留下一个与您的源代码不匹配的二进制文件。我知道在gcc和llvm编译器中可能有更多类似的情况。

"gcc is a monster that is barely held together with duct tape and bailing wire. It is like watching one of those faces of death movies or something like that once you have had those images in your head one time, you cant unwatch them, they are burned in there for life. So it is worth finding out for yourself how scary gcc is, by looking behind the curtain. But you might not be able to forget what you had seen. For various targets -O0 -O1 -O2 -O3 can and have all failed miserably with some code at some point in time. Likewise the fix sometime is to optimize more not less."

"gcc是一个几乎只能依靠胶带和铁丝钳勉强维持的怪物。它就像看那种死亡面孔电影或者类似的东西,一旦你在脑海中有了那些图像,你就无法忘记,它们会在你的脑海中烙印下来一辈子。因此,值得自己去了解一下gcc的恐怖之处,看看幕后的情况。但是也许您看到的东西无法从头脑中抹去。对于各种目标,“-O0”、“-O1”、“-O2”和“-O3”都可能在某个时间点上惨败于某些代码。同样,有时修复的方法是进行更多而不是更少的优化。"

"When you write a program the hope is the compiler does what it says it does, just like you hope your program does what you say it does. But it is not always the case, your debugging does not end when the source code is perfect, it ends when the binary is perfect, and that includes whatever binary and operating system you hope to target (different minor versions of gcc make different binaries, different linux targets react differently to programs)."

"编写程序时,希望编译器能够做到其所说的那样,就像您希望您的程序按照您的意思执行一样。但这并不总是成立,您的调试不会在源代码完美时结束,它将在二进制文件完美时结束,包括您希望针对的任何二进制文件和操作系统(不同版本的gcc会生成不同的二进制文件,不同的Linux目标对程序有不同的反应)。

"The most important advise is develop and test using the target optimization level. if you develop and test by always building for a debugger, well you have a created a program that works in a debugger, you get to start over when you want to make it work somewhere else. gcc's -O3 does work often but folks are afraid of it and it doesnt get enough usage to be debugged properly, so it is not as reliable. -O2 and no optimization -O0 get a lot of mileage, lots of bug reports, lots of fixes, choose one of those or as another answer said, go with what Linux uses. Or go with what firefox uses or go with what chrome uses."

"最重要的建议是使用目标优化级别开发和测试。如果您始终为调试器构建程序进行开发和测试,那么您已经创建了一个只能在调试器中工作的程序,当您想将其在其他地方运行时,就必须重新开始。gcc的

针对这类环境,你绝不能仅停留在源代码上,需要将资金和时间投入到验证二进制文件中。每次更改二进制文件都需要重新开始验证。与其运行的硬件没有什么不同,你更换一个组件,加热一个焊点,就需要从头开始进行验证测试。也许唯一的区别是,在某些情况下,这些环境中的每个焊接点只允许最多进行一定数量的周期后,你就需要报废整个单元。但在软件中也可能存在这种情况,PROM 只能进行有限的烧录次数,PROM 引脚/孔只能进行有限的重做次数,否则就必须报废板子或整个单元。关闭优化器并找到更好、更稳定的编译器和/或编程语言。
如果这种硬实时环境在崩溃时不会影响人员或财产(除了它运行的设备),那就另当别论。也许它是一台蓝光播放器,会跳过一两帧或显示一些坏像素,没什么大不了的。打开优化器,大众对于这种质量水平不再关心,他们满足于 YouTube 质量的图像、压缩视频格式等。汽车必须关闭并重新启动才能使收音机或蓝牙工作,这对他们来说也没什么影响。打开优化器并声称在竞争对手之上获得了性能增益。如果软件太过错误,客户会绕过它或购买其他人的产品,当那个产品失败时,他们会回到你这里,购买你的新型号和更新的固件。他们会继续这样做,因为他们想要跳舞的肉饼,他们不想要稳定性或质量。那些东西太贵了。
你应该收集自己的数据,在你的环境中尝试使用软件优化器,并通过完整的验证套件测试产品。如果没有出现问题,那么说明当天针对该代码的优化器是可以的,或者测试系统需要更多的工作。如果你无法做到这一点,那么至少可以反汇编和分析编译器对你的代码所做的操作。我认为(并从个人经验中知道),gcc 和 llvm bug 系统确实存在与优化级别相关的错误,但这是否意味着你可以根据优化级别进行分类?不知道,这些都是开源、基本不受控制的接口,所以你不能依赖大众准确和完整地定义输入字段,如果 bug 报告表单上有一个优化器字段,它可能总是设置为表单/网页的默认值。你必须检查问题报告本身,看看用户是否遇到了与优化器相关的问题。如果这是一个对于公司而言是封闭系统的情况,在这种情况下,员工的绩效评估可能会因未按照正确的程序填写表格而受到负面影响,你将拥有更好的可搜索数据库来获取信息。

优化器确实会增加您的风险。假设编译器使用50%的代码来获得没有优化的输出,再使用10%的代码来获取-O1,则您的风险增加了,使用更多的编译器代码,出现错误的风险更大,并且输出结果可能更差。为了达到-O2和-O3,需要使用更多的代码。减少优化并不能完全消除风险,但可以降低出错的概率。


你是否有关于LLVM优化无限循环的参考资料?而且,LLVM优化这个循环究竟是什么意思?它执行代码后面的代码吗? - CodesInChaos
如果我没记错的话,LLVM优化是被C标准允许的,因为它没有副作用。 - Maciej Piechotka
什么优化,我指出的那个错误有很大的副作用,它完全改变了代码的流程。 - old_timer
@dwelch:假设有一个函数 void foo(void) { long long exp=3; while(!FermatCounterexampleExistsWithExponent(exp) exp++; someGlobal = exp;},还有另一个调用了 foo() 的函数并且做了一些其他的事情。如果 Fermat... 方法没有副作用,编译器可以自由地将一个单独的线程分离出来处理 foo,只要它确保主线程在下一次访问 someGlobal 之前等待辅助线程完成。如果程序要做的所有有用的事情都可以在不实际访问 someGlobal 的情况下完成... - supercat
规范允许程序在循环未完成的情况下执行所有这些操作。虽然我认为编译器应该在看到没有break、不访问任何易失性变量、不写入任何指针且无法修改循环条件的while循环时发出警告,但我对规范说即使时间无限也不应被视为“副作用”并不反感。 - supercat

2

我没有任何数据(也没有听说过任何人有数据...),但是...

在选择禁用优化之前,我会选择要使用的编译器。换句话说,我不会使用我不能信任其优化的任何编译器。

Linux内核是使用 -Os 编译的。这比任何 bugzilla 分析对我更有说服力。

就个人而言,我会接受 Linux 所适用的 gcc 的任何版本。

另一个参考数据是,苹果一直在从 gcc 转向 llvm,有时使用 clang 有时不使用。llvm 在某些 C++ 方面存在问题,虽然 llvm-gcc 现在好多了,但似乎 clang++ 仍然存在一些问题。但这证明了一种模式:虽然据称 Apple 现在使用 clang 编译 OS X 和 iOS,但他们几乎不使用 C++ 和 Objective-C++。因此,对于纯 C 和 Objective-C,我会信任 clang,但我仍然不相信 clang++。


1
不幸的是,有人认为“如果它不是实时的,那么如果它崩溃了,它可能不是关键性问题”。我一直在寻找编译优化的关键系统示例(如果Linux使用-Os编译,则运行Linux的任何内容都可以)。像互联网上的根域名服务器或核心路由器之类的东西都很好。任何即使偶尔失败也会造成灾难性影响的事情越大越好。不幸的是,找到这样的系统的具体信息一直很难找到(可以理解)。 - nightrain
所有的Android系统都是运行在Linux上的,包括很多消费级路由器和PlayStation游戏机。"如果它不是实时的,那么它崩溃也不会太重要"这个定义相当不合理。因此,我可以看出这可能很难改变。可以说是不可能的。 - smparkes
5
我不禁想问:如果你没有使用优化,那么是否意味着你在手动优化代码?这肯定更容易出错。 - smparkes
3
我认为@smparkes的评论值得更多的关注。所有的程序都有一定可接受的最低速度。实现这种速度的一种方式是打开优化设置。另一种方式是重新编写代码以进行“手动优化”。如果只能选其一,我个人会选择打开优化设置。 - David Stone

2
使用编译器安全吗?
编译器的设计是将您的代码转换为另一种形式。它通常应该正确地进行转换,但像所有软件一样,可能会存在潜在的漏洞。因此,不,它不安全。
什么可以使代码变得安全?
测试/使用。
要显现出漏洞,其中包含漏洞的代码必须在特定配置中运行。对于任何非微不足道的软件,都几乎不可能证明不存在漏洞,然而大量测试和使用至少能够清除一些执行路径。
那么,我如何保证自己的安全呢?
好吧,通过使用其他人使用的相同路径。这给了你最好的机会,让路径是没有漏洞的,因为所有已经走过这里的人都在那里。
对于gcc,我会使用-O2或-Os(就像Linux一样),因为这些可能已经接受了大量的直接或间接审查。
你应该打开优化吗?
然而,将优化引入工具链是具有破坏性的。这不仅需要切换开关。您还需要进行大量测试,以确保在您的条件下不会发生任何不良情况。
更具体地说,编译器依赖未定义的行为来执行许多优化。如果您的代码从未暴露于优化,那么很可能会在这里和那里依赖此类未定义的行为,并且打开优化可能会暴露这些漏洞(而不是引入它们)。
但这并不比切换编译器更具破坏性。

1
在嵌入式系统中,经常需要通过写入寄存器来控制硬件。在C语言中,这非常容易,只需用寄存器地址初始化指针即可。
如果程序的其他部分没有读取或写入寄存器,很可能优化器会删除该赋值操作。从而破坏代码。
这个问题可以通过使用"volatile"关键字来解决。但不要忘记,优化器也会改变命令的顺序。因此,如果您的硬件期望按特定顺序写入寄存器,那么您可能会遇到问题。
优化器应该能够产生正确的结果,但是中间步骤可能会发生改变,这就是优化器可能会对您造成伤害的地方。

0

我不知道gcc的bug,但C编程语言与当前硬件不相符。请记住,它是在20世纪70年代设计的,当时甚至还不清楚2的补码算术是否会成为未来的趋势。好吧,在C中添加2个无符号整数。规范说明您不允许溢出。编译器可以假定在添加后清除进位标志,并基于此进行进一步的优化。您假设使用2的补码算术(这些天谁不会呢),然后嘭,您刚刚设置了进位标志。像这样的事情是安全问题的主要来源。我认为即使是低级别的代码,Java也可能更好,因为它定义得更好,而当前的HotSpot即时编译器生成的代码运行速度与C一样快。您还可以查看D编程语言,很可能它也被定义得很好。


具有讽刺意味的是,编译器编写者热衷于利用标准为奇怪的硬件所做的让步所带来的“优化机会”,而不是制定基于现代编译器实际需求的指令。 - supercat

0

关于编译器优化器错误的最新研究https://www.sciencedirect.com/science/article/abs/pii/S0164121220302740

是的,编译器也会出现错误,而优化器可能会给您的代码添加错误;-(

一个无用的解决方案是使用多个优化选项进行编译和测试,并比较程序结果。

随着程序规模和复杂性的增加,运行足够的测试以确保编译器不会使您的代码变得更加错误变得不可行。

可以进行风险评估,我是否正在处理性能至关重要的小型项目?还是我正在处理内核或其他关键应用程序?如果是前者,则您可能对编译器提供的所有优化感到满意。如果是后者,建议从“-O0”开始,然后是“-O1”,然后...测量代码的关键性能指标。您可能会发现,“-O2”及以上提供的好处微不足道,不值得冒险。


0
在我看来,除了调度和重排优化之外,大多数编译器优化对所有程序都是安全的。因为这种优化可能会改变原始程序的行为。
关于这个问题的数据,您可以查看: 编译器优化是否会引入错误?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接