【问题标题】:Detect declaration of array larger than free memory检测大于可用内存的数组声明
【发布时间】:2020-03-16 17:39:39
【问题描述】:

有没有办法检测数组(或任何变量)何时会大于系统中的可用内存量?其次,如果声明了这样一个变量,会发生什么?

一些背景知识:我使用的是具有 20KB RAM 的嵌入式 ARM 设备;它没有运行 RTOS。我正在尝试将传感器数据读取到数组中,但是可以读取大量传感器数据。如果我使用 malloc 分配内存,我可以检查 return != NULL 以查看堆上是否有足够的空间可用(尽管this question 似乎表明这可能是乐观的)。但是,我不确定当声明一个大于可用内存的数组时会发生什么。

显而易见的解决方案是按照链接问题的已接受答案所述执行操作:预先为数据分配所有内存。但是如果动态声明数组,如何判断系统是否内存不足,如果没有会发生什么?

编辑:一些示例,以说明我所指的内容。 如果像这样定义一个数组会发生什么:

void breaking_things(){
    uin8_t contrived_example[30000] = {0};
}

这在我只有 20KB 可用空间的系统上是不可能的,但是会发生什么。

再举一个例子:

void breaking_things(){
   uin8_t contrived_example0[7000] = {0};
   uin8_t contrived_example1[7000] = {0};
   uin8_t contrived_example2[7000] = {0};
}

【问题讨论】:

  • 你能解释一下你是如何分配数组的吗?数组是分配有malloc(似乎不是这种情况),分配为静态内存还是分配为堆栈内存,这会有所不同。一个简单的代码示例可能会有所帮助。
  • 在小型嵌入式系统中使用动态分配通常是一种不好的形式。计算出设备中有多少内存,将其全部分配给一个大的静态数组,然后从中取出你需要的东西(可能使用简单的标记释放分配器之类的东西)。
  • 如果您将数组定义为全局数组,那么链接器会告诉您您的 BSS 部分与其他部分重叠。当然,假设一个正确的链接器脚本......如果它是本地的(在堆栈上)那么......那么这是一个坏主意。
  • 正如我所说,最实用的方法是定义一个静态(全局)数组,让链接器担心空间。您的示例溢出了堆栈,这几乎无法检测到(直到为时已晚)。
  • 在我看来,这条路充满了危险。有不同的方法可以检查剩余堆栈大小,但它们要么依赖于平台,要么非常脆弱。我个人不会去那里。但例如:Is it possible to predict a stack overflow in C on Linux?

标签: c arm embedded malloc


【解决方案1】:

就 C 语言而言,尝试分配不适合内存的局部或全局变量具有未定义的行为,这意味着任何事情都可能发生。

实际上,根据您的工具链、您的 MMU/MPU 设置(如果有)以及确切的内存布局,结果可能是在为堆栈保留的内存区域之外写入会覆盖内存中该位置包含的任何内容,导致“有趣”的结果,或某种与记忆有关的故障。你肯定不想覆盖其他内存,而且内存故障很难恢复,所以你应该确保不会发生这种情况。

主要的嵌入式工具链可以计算程序的最大堆栈使用量。更准确地说,他们计算出一个最坏情况的近似值,这对于你通常在小型嵌入式系统上运行的那种程序来说已经足够好了。这仅适用于程序不使用函数指针、递归、可变长度数组和alloca 等动态特性的情况,所以不要这样做。

例如,使用 GCC,编译器可以告诉您每个函数的堆栈使用情况,并结合控制流图您可以确定程序堆栈使用的上限。见How to determine maximum stack usage in embedded system with gcc?

另请参阅Stack Size Estimation,其中提到了一些独立于编译器工作的工具。

除了main(如果适用)之外,请记住考虑中断例程。

如果您使变量成为永久变量(静态存储持续时间),即如果它是一个全局变量或带有 static 限定符的局部变量,您的链接器应该会告诉您是否用完 BSS。它更简单,但是在程序的一部分(例如,在启动期间),您无法将该内存用于不同的目的。您可以通过将全局变量设置为union 来重新获得这种能力,但这很危险,因为编译器不会保护您免于在错误的时间使用错误的联合成员。

【讨论】:

  • 函数指针和中断在所有嵌入式系统中几乎都是标准的,因此用于计算堆栈使用的上述工具链功能不是很有用或可靠。
  • 为了节省内存而使用union 是非常糟糕的建议——例如,MISRA-C 禁止这种做法。有很多更好的方法可以优化内存消耗,最显着的是微观管理整数变量的大小。
  • @Lundin 函数指针用于除中断处理程序之外的任何内容?我曾在有效利用构建时堆栈使用分析的系统上工作,并主要禁止使用递归和函数指针(除非需要在启动时注册异常处理程序),以便这种分析能够奏效。您只需要考虑所有入口点。
  • 除了中断/向量表外,还用于回调、状态机、引导加载程序等。是的,它们可能会搞砸堆栈使用的静态分析,但中断也是如此。
  • @M.M 嗯,我以为这会达到实现限制,但检查后我找不到这样的限制。不过,这将是标准中的一个非常糟糕的遗漏。程序#include <stdint.h> int main(void) { static char a[SIZE_MAX]; char b[SIZE_MAX]; return 0; } 真的严格符合吗? (PS 要求引用一个看起来合理的非正式声明会显得相当被动攻击性。如果您得出结论认为该行为实际上是已定义的,这会很奇怪,因为几乎每个实现都拒绝它,请分享您的智慧而不是囤积它。)
【解决方案2】:

gcc 提供标志-fstack-usage-Wstack-usage,它们将输出每个函数的堆栈使用情况并警告过多,这可以作为寻找有堆栈溢出风险的函数的起点。但不会帮助您处理以许多小块溢出堆栈的函数调用深度。

一种可能的方法是在您的硬件上计算出堆栈末尾的地址,这样您就可以有一个调试宏来告诉您还剩多少堆栈。当然,你不能在马跑完后关门——如果你有一个需要使用大量堆栈的函数,那么你需要在调用该函数之前进行堆栈检查;不在函数的开头(通常堆栈在函数入口处被消耗,而不是在执行到达大缓冲区的声明时)。

理想情况下,您的代码设计方式应该是所有可能的代码路径都是已知的,并且您可以理解它。 clang 能够生成一个调用图,显示所有相互调用的函数——如果你的代码一团糟,那么这看起来就像猫进了羊毛篮,但如果不是,那么你可以将堆栈使用与每个函数相关联并工作计算出任何代码路径的理论最大可能堆栈使用量。

可能有一些商业工具可以自动完成所有这些工作,尽管 IDK 是这样的。

【讨论】:

  • 编译器选项和调用图不是很有用,因为这类系统有很多中断,无法静态确定堆栈使用情况。它必须在运行时进行检查。
  • @Lundin 中断不应使用任何大量堆栈(为了便于移植,它们除了设置原子标志之外什么都不做)
  • 重要的不是数量,而是在您已经认为的最大使用量之上增加的堆栈峰值使用量。由于程序员未能考虑最坏的中断情况而导致的堆栈溢出是嵌入式系统中众所周知的问题。
【解决方案3】:

这个问题有点令人困惑,因为您永远不应该在堆栈上分配这么大的数组,尤其是在嵌入式系统中。如果您在 20kib 系统上声明一个大小为 30kb 的本地数组,您只需在运行时通过堆栈溢出终止堆栈。

您只能通过程序员知识和代码审查来保护自己免受堆栈溢出的影响,尽管一些工具链提供了测量堆栈使用情况的方法,并且一些 MCU 会在堆栈溢出时给出有意义的错误,例如软件中断/异常。还有一种从调试器测试堆栈使用的手动方法,通过用一些无意义的值(如 0xAA)填充整个堆栈,然后以最大的代码覆盖率执行程序,然后分析内存映射以查看堆栈中的距离仍然可以找到 0xAA。

但是如果动态声明数组,如何判断系统是否内存不足

通过检查malloc 的结果。但这在您的情况下不是问题,因为您永远不应该在 20kib 裸机系统上使用动态内存。 Because it doesn't make any sense to do so.

应该做的是声明具有静态存储持续时间的数组,方法是使其成为static 和/或将其移动到文件范围。在这种情况下,如果使用太多内存,就会出现链接器错误。链接器会发出“.bss 部分内存不足”或类似情况。

【讨论】:

    猜你喜欢
    • 2023-04-09
    • 2012-10-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-29
    • 2013-06-30
    相关资源
    最近更新 更多