处理嵌入式系统中的堆栈溢出问题

8
在嵌入式软件中,如何以通用的方式处理堆栈溢出? 我遇到一些处理器,像最近的AMD处理器一样以硬件方式进行保护。 维基百科上有一些技术,但这些是真正实用的方法吗?
是否有人可以提供一个清晰的建议方法,在今天的32位嵌入式处理器中适用于所有情况?

1
具体是哪个嵌入式处理器?实际上并没有通用的解决方案,因为每个处理器都有很大的不同。 - Matthew Iselin
处理器无关的算法或技术。 黑客在PC环境中使用的技术,同样可以通过玩弄指针并在嵌入式软件上崩溃您的系统来应用。 它归结为如何保护堆栈区域中的返回地址。 - Sri900
1
你所说的"堆栈溢出"是指当调用栈过大而耗尽堆栈内存时吗?比如递归函数嵌套太深导致的层级过多?保护返回地址更多地是为了防止堆栈缓冲区溢出,而不是"堆栈溢出"。 - Sean A.O. Harney
4个回答

13

最好的情况是你的代码使用静态堆栈(没有递归调用)。然后您可以通过以下方式评估最大堆栈使用量:

  1. 静态分析(使用工具)
  2. 在使用完整的代码覆盖率运行代码时测量堆栈使用量(或尽可能高的代码覆盖率,直到您有合理的信心确定了堆栈使用范围,只要您很少运行的代码不比正常执行路径使用更多的堆栈)

但即使这样,如果可能的话,您仍希望有一种方法来检测处理堆栈溢出,以增强鲁棒性,尤其是在项目的开发阶段。一些检测溢出的方法:

  1. 如果处理器支持内存读取/写入中断(即内存访问断点中断),则可以配置它指向堆栈区域的最远端。
  2. 在内存映射配置中,设置一个小的(或大的)RAM块作为“堆栈警卫”区域,并填充已知值。在嵌入式软件中,定期(尽可能经常)检查此区域的内容。如果它改变了,就假设有堆栈溢出。

一旦检测到,您就需要处理它。我不知道有多少种方法可以优雅地从堆栈溢出中恢复代码,因为一旦发生,您的程序逻辑几乎肯定会失效。所以你能做的只有:

  1. 记录错误
    1. 记录错误非常有用,否则症状(意外重新启动)可能很难诊断。
    2. 注意:日志记录例程必须能够在损坏的堆栈情况下可靠运行。该例程应该是简单的。例如,在堆栈被破坏的情况下,您可能不能使用您的精巧的EEPROM写入后台任务尝试写入EEPROM。也许只需将错误记录到专门保留此目的的结构中,在非初始化的RAM中进行检查,然后可以在重启后进行检查。
  2. 重新启动(或者如果错误多次重复出现,则关闭)
    1. 可能的替代方案:如果您使用的是RTOS,并且系统设计使栈破坏得到隔离,所有其他任务都能够处理该任务的重新启动,则可以仅重新启动特定任务。这需要进行一些严肃的设计考虑。

有没有一种方法可以通过包装每个函数调用并在包装器中检查堆栈指针来测量(非静态)堆栈使用情况?我担心堆栈溢出会掉入bss或数据中并进行更改,而不会弄乱“堆栈保护”。 - CodePoet
@CodePoet:我猜你的意思是以一种不需要修改源代码中每个函数调用的方式来实现。这必须由编译器支持,而我不知道在大多数情况下是否存在这样的钩子。(我认为原则上你会想要包装函数,而不是函数调用,因为调用者无法预测被调用者的堆栈使用情况。) - Craig McQueen
@CodePoet:我认为一个更简单的解决方案是有一个更大的堆栈保护区: 你可以将其任意扩大 (例如100字节), 这将使得在不触碰堆栈保护区且进入bss/data的情况下跳过它的可能性降低。需要注意的主要问题是大型本地变量数组, 可能会允许堆栈像你所说的那样"跳过"堆栈保护区。 - Craig McQueen

2

嵌入式堆栈溢出可能是由于递归函数失控引起的,也可能是由于指针误用(虽然这可以被认为是另一种类型的错误),以及使用不足的堆栈进行正常系统操作。换句话说,如果您不对堆栈使用情况进行分析,那么它可能会在缺陷或错误情况之外发生。

在您能够“处理”堆栈溢出之前,您必须先确定它。一个好的方法是在初始化期间加载堆栈,并监视运行时消失的模式数量。通过这种方式,您可以确定堆栈达到的最高点。

模式检查算法应该按照堆栈增长的相反方向执行。因此,如果堆栈从0x1000增长到0x2000,则您的模式检查可以从0x2000开始以提高效率。如果您的模式是0xAA,而0x2000处的值包含除0xAA之外的其他内容,则说明您可能已经溢出了一些内容。

您还应考虑在堆栈之后立即放置一个空的RAM缓冲区,以便在检测到溢出时可以关闭系统而不会丢失数据。如果您的堆栈紧随堆或SRAM数据,则识别溢出将意味着您已经遭受了破坏。您的缓冲区将为您提供更长时间的保护。在32位微控制器上,您应该有足够的RAM来提供至少一个小缓冲区。


使用自动变量(特别是大数组)和中断也会增加堆栈使用。后者尤其难以事先评估,因此可能需要像 Craig McQueen 的回答中所提到的那样运行您的代码一段时间。 - Steve Melnikoff
非常正确 - 此外,手写中断序言会帮助你控制每个中断上下文切换需要推送到堆栈的数据量。一些微控制器甚至允许禁用嵌套软件中断,这将使您能够进一步计算由于中断所导致的最坏情况栈使用情况。 - dls
...并避免嵌套中断可能引起的所有其他问题! - Steve Melnikoff

2
如果您使用带有内存管理单元(MMU)的处理器,硬件可以在最小化软件开销的情况下为您执行此操作。大多数现代32位处理器都配备了它们,越来越多的32位微控制器也具备了它们。
在MMU中设置一个将用于堆栈的内存区域。它应由两个内存区域包围,其中MMU不允许访问。当您的应用程序运行时,一旦溢出堆栈,您将接收到一个异常/中断。
由于您在错误发生时立即得到异常,因此您确切地知道在应用程序中堆栈出问题的位置。您可以查看调用堆栈,以准确了解如何到达当前位置。这比尝试在发生问题之后很久才找出问题要容易得多。
我已经在PPC和AVR32处理器上成功使用过。开始使用MMU时,您可能会觉得这是浪费时间,因为多年来您一直没有用它,但是一旦您看到异常发生在内存问题确切发生的地方的优势,您就绝不会退缩。如果禁止访问RAM底部的内存,MMU还可以检测零指针访问。
如果您正在使用RTOS,则MMU会保护其他任务的内存和堆栈,一个任务的错误不应影响它们。这意味着您还可以轻松重新启动您的任务,而不影响其他任务。
除此之外,带有MMU的处理器通常还具有大量RAM,因此您的程序更不可能溢出堆栈,并且您不需要微调所有内容以使应用程序以小内存占用正确运行。
另一种选择是使用处理器调试工具,在堆栈末尾的内存访问上引发中断。这可能非常特定于处理器。

1

当调用栈过大时,例如递归函数嵌套层数太多时,会发生堆栈溢出。

通过在栈后放置已知数据来检测堆栈溢出,如果栈增长过多并覆盖了它,就可以检测到它。

有一些静态源代码分析工具,如GnatStack、AbsInt的StackAnalyzer和Bound-T,可以用于确定或猜测最大运行时堆栈大小。


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