亿迅智能制造网
工业4.0先进制造技术信息网站!
首页 | 制造技术 | 制造设备 | 工业物联网 | 工业材料 | 设备保养维修 | 工业编程 |
home  MfgRobots >> 亿迅智能制造网 >  >> Industrial Internet of Things >> 嵌入式

功能安全的外星世界中的编译器

在各个领域,功能安全领域对开发人员提出了新的要求。功能安全的代码必须包括防御性代码,以抵御可能由各种原因导致的意外事件。例如,由于编码错误或宇宙射线事件导致的内存损坏可能导致根据代码逻辑“不可能”执行的代码路径。高级语言,尤其是 C 和 C++,包括数量惊人的特性,这些特性的行为并未由代码所遵循的语言规范规定。这种未定义的行为可能导致意外和潜在的灾难性结果,这在功能安全的应用程序中是不可接受的。由于这些原因,标准要求应用防御性编码,代码可测试,可以整理足够的代码覆盖率,并且应用程序代码可追溯到需求,以确保系统完全且唯一地实现它们。

代码还必须实现高水平的代码覆盖率,在某些领域(尤其是汽车领域),设计需要复杂的外部诊断、校准和开发工具是很常见的。出现的问题是,诸如防御性编码和外部数据访问等实践并不是编译器认可的世界的一部分。例如,C 和 C++ 都不允许内存损坏,因此除非在没有此类损坏的情况下可以访问旨在防止内存损坏的代码,否则在优化代码时可能会简单地忽略它。因此,防御性代码如果不想被“优化掉”,就必须在语法和语义上是可访问的。

未定义行为的实例也可能导致意外。建议简单地避免它们很容易,但通常很难识别它们。在它们存在的情况下,无法保证编译后的可执行代码的行为将符合开发人员的意图。对调试工具使用的数据的“后门”访问代表了另一种语言不允许的情况,因此可能会产生意想不到的后果。

编译器优化会对所有这些领域产生重大影响,因为它们都不属于编译器供应商的职责范围。优化可以导致明显健全的防御性代码在与“不可行性”相关联的地方被淘汰——也就是说,它存在于无法通过任何可能的输入值集进行测试和验证的路径上。更令人担忧的是,在构建系统可执行文件时,单元测试期间显示的防御性代码很可能会被消除。仅仅因为在单元测试期间已经实现了防御性代码的覆盖,因此并不能保证它存在于完整的系统中。

在功能安全这片陌生的土地上,编译器可能不合时宜。这就是为什么目标代码验证 (OCV) 代表了任何系统的最佳实践,因为失败会带来可怕的后果——事实上,对于任何只有最佳实践就足够好的系统。

编译前后

IEC 61508、ISO 26262、IEC 62304、MISRA C 和 C++ 等功能安全、安保和编码标准所倡导的验证和验证实践非常重视显示在基于需求的测试期间执行了多少应用程序源代码。

经验告诉我们,如果代码已被证明可以正确执行,那么该领域的失败概率就会大大降低。然而,由于这项值得称赞的努力的重点是高级源代码(无论使用什么语言),因此这种方法非常相信编译器能够创建准确再现开发人员内容的目标代码故意的。在最关键的应用程序中,这种隐含的假设是不合理的。

不可避免的是,目标代码的控制和数据流不会是源代码的精确镜像,因此证明所有源代码路径都可以可靠地执行并不能证明目标代码是同一件事.鉴于目标代码和汇编程序之间存在 1:1 的关系,源代码和汇编代码之间的比较是有说服力的。考虑图 1 中显示的示例,其中右侧的汇编代码是从左侧的源代码生成的(使用禁用优化的 TI 编译器)。


图1:右边的汇编代码是从左边的源代码生成的,显示了源代码和汇编代码之间的明显对比。 (来源:LDRA)

如后文所述,编译此源代码时,生成的汇编程序代码的流程图与源代码的流程图大不相同,因为 C 或 C++ 编译器遵循的规则允许它们以自己喜欢的任何方式修改代码,前提是二进制表现得“好像是一样的。”

在大多数情况下,该原则是完全可以接受的——但也有异常之处。编译器优化基本上是应用于代码内部表示的数学变换。如果假设不成立,这些转换就会“出错”——例如,代码库包含未定义行为的实例时经常出现这种情况。

只有在航空航天工业中使用的 DO-178C 将重点放在开发人员意图和可执行行为之间存在危险不一致的可能性上——即使如此,也不难找到具有明显潜力的变通方法的倡导者,使这些不一致不被发现。不管这种方法是可以原谅的,事实仍然是源代码和目标代码之间的差异可能对任何关键应用程序造成破坏性后果。

开发者意图与可执行行为

尽管源代码流和目标代码流之间存在明显差异,但它们并不是主要关注点。编译器通常是高度可靠的应用程序,虽然与任何其他软件一样可能存在错误,但编译器的实现通常会满足其设计要求。问题是这些设计要求并不总是反映功能安全系统的需求。

简而言之,可以假设编译器在功能上符合其创建者的目标。但这可能并不完全是我们想要或预期的,如下面的图 2 所示,其中包含一个使用 CLANG 编译器编译的示例。


图 2 显示了使用 CLANG 编译器的编译(来源:LDRA)

很明显,汇编代码中并没有表达对‘error’函数的防御性调用。

'state' 对象仅在初始化时在 'S0' 和 'S1' 情况下才会被修改,因此编译器可以推断给 'state' 的唯一值是 'S0' 和 'S1'。得出的结论是不需要“默认值”,因为“状态”将永远不会包含任何其他值,假设没有损坏——事实上,编译器做出了这个假设。

编译器还决定,因为实际对象(13 和 23)的值不在数字上下文中使用,它将简单地使用 0 和 1 的值在状态之间切换,然后使用互斥“或”来更新状态值。二进制遵守“好像”的义务,代码快速而紧凑。在其职权范围内,编译器做得很好。

这种行为对使用链接器内存映射文件间接访问对象的“校准”工具以及通过调试器直接访问内存有影响。同样,此类考虑因素不属于编译器的职责范围,因此在优化和/或代码生成期间不会考虑。

现在假设代码保持不变,但它在呈现给编译器的代码中的上下文略有变化,如图 3 所示。


图 3:代码保持不变,但其在呈现给编译器的代码中的上下文略有变化。 (来源:LDRA)

现在有一个附加函数,它以整数形式返回状态变量的值。这次提交给编译器的代码中的绝对值 13 和 23 很重要。即便如此,这些值也不会在更新函数(保持不变)中进行操作,并且只在我们新的“f”函数中可见。

简而言之,编译器继续(正确地)对 13 和 23 的值应该在哪里使用进行价值判断——而且它们绝不适用于它们可能适用的所有情况。

如果更改新函数以返回指向我们的状态变量的指针,则汇编代码会发生很大变化。因为现在有可能通过指针访问别名,编译器不能再推断状态对象发生了什么。如下图 4 所示,不能断定 13 和 23 的值不重要,因此现在在汇编程序中明确表示它们。


图 4:如果更改新函数以返回指向我们的状态变量的指针,则汇编代码会发生重大变化。不能断定 13 和 23 的值不重要,因此它们现在在汇编程序中明确表示(来源:LDRA)。

对源代码单元测试的影响

现在考虑在一个虚构的单元测试工具的上下文中的示例。由于需要使用工具来访问被测代码,状态变量的值被操纵,因此默认值不会“优化掉”。这种方法在测试工具中是完全合理的,它没有与源代码其余部分相关的上下文,并且需要使所有内容都可以访问,但作为副作用,它可以掩盖编译器对防御性代码的合法省略。

编译器识别出通过指针将任意值写入状态变量,并且再次无法断定 13 和 23 的值不重要。因此,它们现在在汇编器中明确表达。在这种情况下,不能断定 S0 和 S1 代表状态变量的唯一可能值,这意味着默认路径可能是可行的。如图 5 所示,状态变量的操作达到了它的目的,现在在汇编程序中对错误函数的调用是显而易见的。


图 5:状态变量的操作达到了它的目的,现在在汇编程序中对错误函数的调用是显而易见的。 (来源:LDRA)

然而,这种操作不会出现在将在产品中提供的代码中,因此对 error() 的调用在整个系统中并不真正存在。

目标代码验证的重要性

为了说明目标代码验证如何帮助解决这个难题,请再次考虑第一个示例代码片段,如图 6 所示:


图 6:这说明了目标代码验证如何帮助解决错误调用不在完整系统中的问题。 (来源:LDRA)

这段 C 代码可以证明通过一次调用就实现了 100% 的源代码覆盖率,因此:

f_while4(0,3);

代码可以重新格式化为每行一个操作,并在流程图上表示为“基本块”节点的集合,每个节点都是一个直线代码序列。基本块之间的关系用节点之间的有向边表示在图7中。


图 7:这显示了使用节点之间的有向边的基本块之间的关系。 (来源:LDRA)

代码编译后,结果如下图(图8)。流程图的蓝色元素表示未被调用 f_while4(0,3) 执行的代码。

通过利用目标代码和汇编代码之间的一对一关系,该机制暴露了未执行的目标代码部分,促使测试人员设计额外的测试并实现完整的汇编代码覆盖——从而实现目标代码验证。


图 8:这显示了代码编译时的结果。流程图的蓝色元素表示调用 f_while4(0,3) 尚未执行的代码。 (来源:LDRA)

显然,目标代码验证无法阻止编译器遵循其设计规则并无意中规避开发人员的最佳意图。但它可以而且确实让粗心的人注意到任何此类不匹配。

现在在前面的“调用错误”示例的上下文中考虑该原则。当然,完整系统中的源代码将与在单元测试级别验证的源代码相同,因此对其进行比较将一无所获。但是,将目标代码验证应用于完整的系统对于确保基本行为按照开发人员的意图表达是非常宝贵的。

任何世界的最佳实践

如果与单元测试相比,编译器在测试工具中处理代码的方式不同,那么源代码单元测试覆盖率是否值得?答案是肯定的“是”。许多系统已经根据此类工件的证据进行了认证,并且在服务中被证明是安全可靠的。但是对于所有部门中最关键的系统,如果开发过程要经受最详细的审查并坚持最佳实践,那么源级单元测试覆盖率必须由 OCV 补充。假设它满足其设计标准是合理的,但这些标准不包括功能安全考虑。目标代码验证目前代表了功能安全领域最可靠的方法,其中编译器行为符合标准,但可能会产生重大的负面影响。


嵌入式

  1. 电气安全的重要性
  2. 纺织染料的世界
  3. 酸性染料在织物世界中的应用
  4. 染料世界一览
  5. 数字世界中的维护
  6. 安全篮的多种用途
  7. 快速发展的仿真世界
  8. 世界制造业之都
  9. 5 条最重要的起重机安全提示
  10. 摩擦材料在安全系统中的重要性
  11. 工厂安全:持续改进的源泉
  12. G码和M码的区别