C(或者任何)编译器的确定性性能
在从事最近的项目时,一位客户质量检查代表拜访了我,他问我一个我之前从未真正考虑过的问题:
How do you know that the compiler you are using generates machine code that matches the c code's functionality exactly and that the compiler is fully deterministic?
对于这个问题,我一无所获,因为我一直认为编译器是理所当然的。它接受代码并喷出机器代码。如何进行测试,证明编译器实际上没有添加我没有要求的功能?还是以与我期望的方式稍有不同的方式更危险地实现代码?
我知道这可能并不是每个人的问题,确实答案可能只是……"我们已经无所适从了"。但是,在嵌入式环境中工作时,我们会隐式信任编译器。我如何向自己和QA证明自己这样做是正确的?
解决方案
回答
尝试单元测试。
如果这还不够,请使用其他编译器并比较单元测试的结果。比较strace输出,在VM中运行测试,保留磁盘和网络I / O的日志,然后进行比较。
或者建议编写自己的编译器,并告诉他们这将花费多少。
回答
一切都归结为信任。客户是否信任任何编译器?使用它,或者至少比较我们和他们之间的输出代码。
如果他们不信任该语言,是否有该语言的参考实现?我们能否说服他们信任它?然后将与参考进行比较或者使用参考。
所有这些都假设我们确实验证了从供应商/提供者那里获得的实际代码,并且检查了编译器是否未被篡改,这应该是第一步。
无论如何,这仍然留下了一个问题,即如何在没有引用的情况下从头开始验证编译器。这当然看起来像是花了很多功夫,并且需要一种语言的定义,这种定义并不总是可用,有时是编译器。
回答
我们可以在任何级别上应用该论点:我们是否信任第三方库?我们相信操作系统吗?我们相信处理器吗?
一个很好的例子说明了为什么这可能是一个真正值得关注的问题,这是Ken Thompson如何将后门放入原始的" login"程序中,并修改了C编译器,以便即使重新编译登录后也仍然会得到后门。有关更多详细信息,请参见此帖子。
关于加密算法也提出了类似的问题-我们如何知道DES中没有后门供NSA窥探?
最后,我们必须决定是否对自己所构建的基础架构足够信任,而不必担心它,否则,我们必须开始开发自己的硅芯片!
回答
- 更改编译器的优化级别将更改输出。
- 对函数的少量更改可能会使编译器内联或者不再内联函数。
- 更改编译器(例如gcc版本)可能会更改输出
- 某些库函数可能是本征函数(即发出优化的程序集),而其他大多数则不是。
好消息是,对于大多数事情而言,它实际上并不重要。如果需要,我们可能需要考虑组装(例如在ISR中)。
回答
我们最容易证明的一点是,我们使用的是提供者X的不受干扰的编译器。如果他们不信任提供者X,那就是他们的问题(如果X值得信赖)。如果他们不信任任何编译器提供程序,那么它们完全是不合理的。
回答他们的问题:通过这些方式,我确保使用的X是不受干扰的编译器。 X享有盛誉,另外我还有一组不错的测试,它们表明我们的应用程序的运行符合预期。
其他所有东西都开始打开蠕虫的罐头。正如罗布所说,你必须停在某个地方。
回答
如果我们担心无法产生可见结果的意外机器代码,则唯一的方法可能是与编译器供应商联系以获得某种满足我们客户要求的认证。
否则,我们将对代码测试中的错误一无所知。
现代编译器的机器代码可能有很大的不同,对于微不足道的人来说是完全无法理解的。
回答
有时,当我们要求积极的优化水平时,行为确实会有所改变。
优化和浮点数?算了吧!
回答
我认为可以以某种方式将这个问题简化为"暂停问题"。
最明显的问题是,如果我们使用某种程序来分析编译器及其确定性,我们如何知道程序已正确编译并产生正确的结果?
但是,如果我们使用的是其他"安全"编译器,则不确定。我确定从头开始编写编译器可能会更容易。
回答
我们不确定该编译器是否会完全按照期望进行操作。原因当然是因为编译器是软件的一部分,因此容易受到错误的影响。
编译器编写者的优势是可以按照高质量的规范进行工作,而我们其他人则必须在前进的过程中弄清楚自己在做什么。但是,编译器规范也有错误,以及带有微妙交互的复杂部分。因此,弄清楚编译器应该做什么并不容易。
不过,一旦确定了语言规范的含义,就可以为每个细微差别编写良好,快速,自动化的测试。这就是编译器编写相对于编写其他类型的软件(在测试中)具有巨大优势的地方。每个错误都将成为一个自动测试用例,并且测试套件可以非常彻底。编译器供应商比我们有更多的预算用于验证编译器的正确性(我们已经有一份日常工作,对吗?)。
这对我们意味着什么?这意味着我们需要对编译器中各种错误的可能性持开放态度,但是我们自己找不到任何机会。
我会选择一个编译器供应商,该供应商不太可能很快就会倒闭,在其编译器中具有悠久的历史,并且已经证明了他们提供(修补)其产品的能力。编译器似乎会随着时间的推移变得更加正确,因此我选择了大约一两年的编译器。
将注意力集中在正确编写代码上。如果它简单明了,那么当我们遇到编译器错误时,我们将不必真的很难决定问题的根源。编写良好的单元测试,这将确保代码能够实现我们期望的功能。
回答
对于安全性至关重要的嵌入式应用程序,认证机构需要满足编译器的"使用证明"要求。通常需要满足某些要求(例如"工作时间"),并需要详细的文档证明。但是,大多数人不能或者不想满足这些要求,因为这可能非常困难,尤其是在第一个具有新目标/编译器的项目中。
基本上,另一种方法是根本不信任编译器的输出。除以后的功能测试外,还必须通过严格的一组静态分析,单元和覆盖率测试来弥补任何编译器甚至语言相关的缺陷(C-90标准的附录G,有人吗?)。
像MISRA-C这样的标准可以帮助将对编译器的输入限制为C语言的"安全"子集。另一种方法是将对编译器的输入限制为一种语言的子集,并测试整个子集的输出是什么。如果我们的应用程序仅由子集中的组件构成,则假定知道编译器的输出是什么。通常通过"编译器的资格认证"。
所有这一切的目的是能够回答质量保证代表的问题:"我们不仅依赖于编译器的确定性,而且这是我们证明它的方式……"
回答
通过测试就知道了。在测试时,我们正在同时测试代码和编译器。
我们会发现,我们或者编译器编写者出错的几率比如果我们使用某种汇编语言编写有问题的程序时出错的几率要小得多。
回答
对于大多数软件开发(例如桌面应用程序),答案可能是我们不知道和不在乎。
在安全关键系统(想想核电厂和商业航空电子设备)中,我们需要注意,监管机构会要求我们证明这一点。以我的经验,我们可以通过以下两种方式之一执行此操作:
- 使用合格的编译器,其中"合格"表示已按照监管机构制定的标准进行了验证。
- 执行目标代码分析。本质上,我们可以编译一段参考代码,然后手动分析输出以证明编译器未插入任何无法追溯到源代码的指令。
回答
有可用的编译器验证套件。
我记得的是"多年生"。
当我为嵌入式SOC处理器开发C编译器时,我们不得不针对该编译器和其他两个验证服(我忘记了它的名字)来验证编译器。合同中包括验证编译器是否达到与这些测试服的某种程度的一致性。
回答
即使是合格的或者经过认证的编译器也可能产生不良结果。保持代码简单,然后进行测试,测试和测试。手动查看或者遍历机器代码,同时不允许任何人为错误。请使用操作系统或者正在运行的任何环境(最好没有操作系统,只有程序)。
自从软件和编译器开始以来,在关键任务环境中已经解决了该问题。正如许多其他已答复的人也知道的那样。每个行业都有自己的规则,从经过认证的编译器到编程风格(我们必须始终以这种方式进行编程,而永远不要使用这种方式),大量的测试和同行评审。验证每个执行路径,等等。
如果我们不属于这些行业之一,那么我们将获得所得到的。 COTS硬件上的COTS操作系统上的商业程序。它将失败,这是保证。
回答
...you trust your compiler implicitly
第一次遇到编译器错误时,我们将停止这样做。 ;-)
但是最终这就是测试的目的。最初,漏洞是如何进入产品的,与测试体系无关,重要的是,它没有通过广泛的测试体系。
回答
How do you know that the compiler you are using generates machine code that matches the c code's functionality exactly and that the compiler is fully deterministic?
我们不这样做,这就是为什么要测试生成的二进制文件,以及确保确保提供与测试相同的二进制文件的原因。以及为什么在进行"次要"软件更改时,进行回归测试以确保所有旧功能都没有中断。
我认证的唯一软件是航空电子设备。 FAA认证不够严格,无法证明该软件可以正常运行,但同时又会迫使我们跳过一定的麻烦。诀窍是构造"过程",以使其尽可能地提高质量,同时避免不必要的不必要的跳跃。因此,我们所知道的一切都是毫无价值的,并且实际上不会发现错误,我们可能会感到疲倦。任何我们知道应该做的事情,因为它会发现FAA并未明确要求的错误,因此,最好的选择是乱说,直到听起来像是要向FAA /QA人员提供他们所要求的东西。
实际上,这还不像我所说的那样不诚实,总的来说,FAA更关心的是认真和自信,以确保我们正在努力做一份好工作,而不是真正在做什么。
回答
国防软件工程师杂志《 Crosstalk》中可能会找到一些智力弹药。这个问题是他们花很多时间醒来的事情。 http://www.stsc.hill.af.mil/crosstalk/2006/08/index.html(如果我可以从旧项目中找到旧笔记,我会回到这里...)
回答
我们永远无法完全信任编译器,即使是高度推荐的编译器也是如此。他们可能会发布包含错误的更新,并且代码也将进行编译。当使用越野车编译器更新旧代码,进行测试并发货时,让客户在3个月后遇到问题时,会给我们带来麻烦。
一切都回到测试中,如果我了解到一件事,那就是在进行任何不重要的更改后都要进行彻底测试。如果问题似乎无法找到,请查看编译的汇编器并检查其正在执行的操作。
有几次我在编译器中发现错误。曾经有一个bug,只有16位变量是头文件中定义的extern结构的一部分时,它会增加16位变量但没有进位。
回答
我们会收到Dijkstra所写的那本。
回答
选择一个经过正式验证的编译器,例如Compcert C编译器。
回答
如果我们担心编译器中的恶意错误,则建议(IIRC,某些项目的NSA要求)是编译器二进制文件在编写代码之前。至少那时我们知道没有人添加针对我们程序的错误。
回答
好吧..我们不能简单地说我们信任编译器的输出,特别是如果我们使用嵌入式代码。在使用不同的编译器编译完全相同的代码时,不难发现所生成的代码之间存在差异。之所以如此,是因为C标准本身太宽松了。在不违反标准的情况下,许多细节可以由不同的编译器以不同的方式实现。我们如何处理这些东西?我们尽可能避免依赖编译器的构造。我们可以通过选择用户cschol之前提到的Misra-C之类的C的安全子集来处理它。我很少需要检查由编译器生成的代码,但是有时这也发生在我身上。但是,最终,我们要依靠测试来确保代码的行为符合预期。
有更好的选择吗?有人声称有。另一种选择是用SPARK / Ada编写代码。我从来没有用SPARK编写过代码,但我的理解是,我们仍然必须将其与用C编写的处理"裸机"内容的例程链接起来。 SPARK / Ada的优点在于,我们绝对可以保证任何编译器生成的代码总是相同的。没有任何歧义。最重要的是,该语言允许我们使用关于代码的预期行为方式的注释来注释代码。 SPARK工具集将使用这些批注来正式证明所编写的代码确实确实完成了批注中所述的功能。因此,有人告诉我,对于关键系统,SPARK / Ada是一个不错的选择。我自己从来没有尝试过。