我们正在编译一个嵌入式C++应用程序,它部署在电离辐射轰击的环境中的屏蔽设备中。我们正在为ARM使用GCC和交叉编译。在部署时,我们的应用程序会生成一些错误的数据,并且比我们希望的更频繁地崩溃。硬件是为这个环境而设计的,我们的应用程序已经在这个平台上运行了几年。

我们是否可以对代码进行更改,或对编译时进行改进,以识别/纠正由单个事件扰乱导致的软错误和内存损坏?是否有其他开发人员成功地减少了软错误对长时间运行的应用程序的有害影响?


当前回答

这是一个非常广泛的主题。基本上,您无法真正从内存损坏中恢复,但至少可以尝试立即失败。以下是您可以使用的一些技巧:

校验和常量数据。如果有任何配置数据长期保持不变(包括已配置的硬件寄存器),请在初始化时计算其校验和并定期验证。当您看到不匹配时,应该重新初始化或重置。冗余存储变量。如果你有一个重要的变量x,把它的值写在x1、x2和x3中,然后读为(x1==x2)?x2:x3。实施程序流程监控。将全局标志与从主循环调用的重要函数/分支中的唯一值进行异或。在接近100%测试覆盖率的无辐射环境中运行程序,应为您提供循环结束时标志的可接受值列表。如果看到偏差,则重置。监视堆栈指针。在主循环的开头,将堆栈指针与其预期值进行比较。偏差复位。

其他回答

首先,围绕失败设计应用程序。确保作为正常流程操作的一部分,它需要重置(取决于您的应用程序和软或硬故障类型)。这很难做到完美:需要某种程度的事务性的关键操作可能需要在组装级别进行检查和调整,以便关键点的中断不会导致不一致的外部命令。一旦检测到任何不可恢复的内存损坏或控制流偏差,就立即失败。如果可能,记录故障。

第二,如果可能,纠正腐败并继续下去。这意味着经常检查和修复常量表(如果可以的话,还包括程序代码);可能在每个主要操作之前或在定时中断上,并将变量存储在自动校正的结构中(同样在每个主要运算之前或在计时中断上,从3中获得多数票,如果是单个偏差,则进行校正)。如果可能,记录更正。

第三,测试失败。设置一个可重复的测试环境,随机翻转内存中的位。这将允许您复制损坏情况,并帮助围绕它们设计应用程序。

您可能还对有关算法容错的丰富文献感兴趣。这包括旧的赋值:编写一个排序,当恒定数量的比较将失败时(或者,更糟糕的版本,当失败的比较的渐近数量为n次比较的log(n)时),正确地对其输入进行排序。

开始阅读黄和亚伯拉罕1984年的论文《矩阵运算的基于算法的容错》。他们的想法隐约类似于同态加密计算(但实际上并不相同,因为他们正在尝试在操作级别进行错误检测/纠正)。

该论文的一个较新的后代是Bosilca、Delmas、Dongarra和Langou的“基于算法的容错应用于高性能计算”。

使用C语言编写在这种环境中表现稳健的程序是可能的,但前提是大多数形式的编译器优化都被禁用。优化编译器旨在用“更高效”的编码模式替换许多看似冗余的编码模式,并且可能不知道当编译器知道x不可能保持任何其他值时,程序员测试x==42的原因是因为程序员想要阻止执行某些代码,而x保持某个其他值——即使在这样的情况下,它保持该值的唯一方法是系统接收到某种电气故障。

将变量声明为易失性通常很有用,但可能不是万能药。特别重要的是,注意安全编码通常需要操作具有需要多个步骤来激活的硬件联锁,并且使用以下模式编写代码:

... code that checks system state
if (system_state_favors_activation)
{
  prepare_for_activation();
  ... code that checks system state again
  if (system_state_is_valid)
  {
    if (system_state_favors_activation)
      trigger_activation();
  }
  else
    perform_safety_shutdown_and_restart();
}
cancel_preparations();

如果编译器以相对文字的方式翻译代码,并且如果全部在prepare_for_activation()之后重复对系统状态的检查,系统可以对几乎任何可能的单一故障事件具有鲁棒性,甚至那些会任意破坏程序计数器和堆栈的程序。如果在调用prepare_for_activation()之后发生了一个小故障,这意味着激活是合适的(因为没有其他原因prepare_for_activation()将在故障发生之前被调用)。如果故障导致代码不正确地到达prepare_for_activation(),但如果没有后续故障事件,则代码将无法在未通过验证检查或先调用cancel_preparies的情况下到达trigger_activation()[如果堆栈出现问题,则在调用prepare_for_activation()的上下文返回后,执行可能会继续到trigger_active()之前的某个位置,但调用cancel_preparations(从而使后者的调用无害。

这样的代码在传统的C语言中可能是安全的,但在现代的C编译器中却不安全。这种编译器在这种环境中可能非常危险,因为它们努力只包含通过某种定义良好的机制可能出现的情况下相关的代码,并且其结果也将得到很好的定义。在某些情况下,旨在检测和清理故障的代码可能会使情况变得更糟。如果编译器确定尝试的恢复在某些情况下会调用未定义的行为,则可能推断在这种情况下不可能出现需要恢复的条件,从而消除了检查这些条件的代码。

你问的是一个非常复杂的话题——不容易回答。其他答案是可以的,但它们只涵盖了你需要做的所有事情的一小部分。

正如在评论中看到的,不可能100%解决硬件问题,但是使用各种技术很可能减少或解决这些问题。

如果我是你,我会创建最高安全完整性级别(SIL-4)的软件。获取IEC 61513文件(适用于核工业)并遵循该文件。

既然您专门要求软件解决方案,而且您使用的是C++,为什么不使用运算符重载来创建自己的安全数据类型呢?例如:

不要使用uint32_t(以及double、int64_t等),而是制作自己的SAFE_uint32-t,其中包含uint32/t的倍数(最小值为3)。重载您想要执行的所有操作(*+-/<<>>==!=等),并使重载的操作对每个内部值独立执行,即不要执行一次并复制结果。在之前和之后,检查所有内部值是否匹配。如果值不匹配,可以将错误的值更新为最常见的值。如果没有最常见的值,您可以安全地通知存在错误。

这样,即使ALU、寄存器、RAM或总线上发生损坏也无所谓,您仍然可以多次尝试并很好地捕获错误。然而,请注意,这只适用于您可以替换的变量-例如,堆栈指针仍然是易受影响的。

附带故事:我遇到了一个类似的问题,也是在一个旧的ARM芯片上。结果发现,这是一个使用旧版本GCC的工具链,与我们使用的特定芯片一起,在某些边缘情况下触发了一个错误,这会(有时)破坏传递到函数中的值。在将设备归咎于无线电活动之前,确保设备没有任何问题,是的,有时是编译器错误=)