堆栈大小估算

33
在使用C或C++编写的多线程嵌入式软件中,必须为线程提供足够的堆栈空间以允许其完成操作而不会溢出。在某些实时嵌入式环境中,正确调整堆栈大小非常关键,因为(至少在我使用过的一些系统中),操作系统将不会为你检测到这一点。
通常,在创建新线程(除主线程外)时指定堆栈大小(即在pthread_create()等函数的参数中)。通常,这些堆栈大小被硬编码为已知好的值,代码编写或测试时就确定了。
但是,对代码的未来更改往往会破坏基于硬编码堆栈大小的假设,然后有一天,您的线程进入其调用图的深层分支并溢出堆栈-导致整个系统崩溃或默默地损坏内存。
我曾亲眼见过这个问题,当代码在线程中执行声明结构实例时,堆栈会增加相应地,从而可能导致堆栈溢出。在已建立的代码库中,这可能是一个巨大的问题,其中添加字段到结构的全部影响可能无法立即知道(因为有太多的线程/函数来找到使用该结构的所有位置)。
由于“堆栈大小”问题的通常回应是“它们不具有可移植性”,因此让我们假设编译器、操作系统和处理器在此调查中都是已知的。同时,假设没有使用递归,因此我们不必处理“无限递归”情况的可能性。
有哪些可靠的方法来估计线程所需的堆栈大小?我更喜欢离线(静态分析)和自动化的方法,但所有想法都受到欢迎。

1
潜在的重复:http://stackoverflow.com/questions/924430/,https://dev59.com/jXRC5IYBdhLWcg3wKtz2 - sbi
1
作为这个领域的新手,我不得不问一下:最有可能的第一步难道不是消除结构体作为自动变量的使用吗?指针的大小不会因为它所指向的结构体的大小而改变。而且明确请求内存(而不是假设栈空间可用)将允许代码处理内存不可用的情况。 - mlibby
1
甚至更好的是,结构体应该只在堆栈上存储指向动态分配内存的指针。这样,你就可以同时获得最好的两种方式:因为它在堆栈上,所以具有自动生命周期管理;而且那些占用超过几个字节的东西都可以进行堆分配,以节省堆栈空间。 - jalf
@mcl:在C++中,你可以使用scoped_ptr,但在C语言中,你需要进行动态内存管理,这并没有什么帮助。我同意应该避免使用栈容器(数组),所以在C++中STL容器是有帮助的。 - stefaanv
显示剩余3条评论
10个回答

21

运行时评估

一种在线方法是使用某个值(例如0xAAAA或0xAA,取决于您的位宽)对整个堆栈进行标记。然后,您可以通过检查剩余标记量来确定堆栈过去最大增长了多少。

请访问此链接以获取带有插图的解释。

这种方法的优点是简单易行。缺点是无法确定在测试期间使用的堆栈大小不会超过最终堆栈大小。

静态评估

可以进行一些静态检查,我认为甚至存在一个被黑客修改过的gcc版本尝试执行此操作。唯一能告诉你的是,在一般情况下,静态检查非常困难。

还请参阅此问题


我在这个领域是新手,请问您能否再解释一下吗? - pdssn
我已经添加了一个链接到一个网站,该网站更详细地解释了动态方法。 - ziggystar

12
你可以使用静态分析工具,例如StackAnalyzer,如果你的目标符合要求。

+1 我会指出StackAnalyzer通过在二进制级别工作来解决“不可移植”的问题。 - Pascal Cuoq
2
如果你的处理器不受支持,那么你就会再次面临“不可移植”的问题。 - swegi

5
如果你想花大量的钱,可以使用商用静态分析工具,如Klocwork。虽然Klocwork主要用于检测软件缺陷和安全漏洞,但它还有一个名为“kwstackoverflow”的工具,可用于检测任务或线程中的堆栈溢出。我在从事嵌入式项目时使用了它,并取得了积极的结果。我认为没有任何工具是完美的,但我相信这些商业工具非常好。我遇到的大多数工具都难以处理函数指针。我也知道许多编译器供应商(如Green Hills)现在将类似功能直接内置到他们的编译器中。这可能是最好的解决方案,因为编译器对于使准确决策所需的所有细节都非常熟悉。
如果你有时间,我相信你可以使用脚本语言制作自己的堆栈溢出分析工具。该脚本需要识别任务或线程的入口点,生成完整的函数调用树,然后计算每个函数使用的堆栈空间量。我猜测可能有免费的工具可以生成完整的函数调用树,因此应该会更容易。如果您知道平台的具体规格,则生成每个函数使用的堆栈空间量可能非常容易。例如,PowerPC函数的第一个汇编指令通常是用于调整堆栈指针所需的存储字与更新指令。您可以从第一条指令中获取大小(以字节为单位),这使得确定总堆栈使用空间相对容易。
所有这些类型的分析都将给您提供堆栈使用情况的最坏情况上限的近似值,这正是您想要了解的内容。当然,专家们(如我所在的那些人)可能会抱怨你分配了太多的堆栈空间,但他们是不关心良好软件质量的恐龙 :)
还有一种可能性,虽然它不能计算堆栈使用量,但可以使用处理器的内存管理单元(MMU)(如果有的话)来检测堆栈溢出。我在VxWorks 5.4上使用PowerPC做到了这一点。这个想法很简单,只需在堆栈的顶部放置一个页面的写保护内存。如果溢出,则会发生处理器异常,并且您将迅速被警告堆栈溢出问题。当然,它不会告诉您需要增加堆栈大小的数量,但如果您善于调试异常/核心文件,您至少可以找出导致堆栈溢出的调用序列。然后,您可以使用这些信息适当地增加堆栈大小。
-Djhaus

感谢您对kwstackoverflow的评论。我发现我的公司实际上拥有kwstackoverflow的许可证,所以我尝试在一个现有项目中使用它。不幸的是,在多个小时的实验后,我发现kwstackoverflow不支持跟踪C++虚函数调用,这使得它对面向对象的C++代码几乎没有用处(在我看来)。请参见我在klocwork论坛上的帖子。也许有一天会支持这个功能。但对于C语言仍然是一个很棒的工具。 - jeremytrimble

4

不是免费的,但Coverity可以对堆栈进行静态分析。


堆栈分析是特定于平台的。Coverity可以在哪些平台上进行静态堆栈分析? - Craig McQueen
我不是专家,但你应该告诉Coverity在堆栈中添加超过一定大小的块和总堆栈超过一定大小时发出警告。我不认为他模拟实际的堆栈使用情况。我可能错了。 - stefaanv

3
Static(离线)堆栈检查并不像看起来那么困难。我已经在我们的嵌入式IDE( RapidiTTy)中实现了它 - 目前它适用于ARM7(NXP LPC2xxx),Cortex-M3(STM32和NXP LPC17xx),x86和我们内部MIPS ISA兼容的FPGA软核。
基本上,我们使用可执行代码的简单解析来确定每个函数的堆栈使用情况。大多数重要的堆栈分配是在每个函数的开头完成的;只需确保查看它如何随着不同的优化级别以及(如果适用)ARM / Thumb指令集等而改变。还要记住,任务通常有自己的堆栈,而ISR通常(但并非总是)共享单独的堆栈区域!
一旦您获得了每个函数的使用情况,就很容易从解析中建立一个调用树,并计算每个函数的最大使用情况。我们的IDE为您生成调度程序(有效的轻量级RTOS),因此我们知道哪些函数被指定为“任务”,哪些是ISR,因此我们可以告诉每个堆栈区域的最坏情况使用情况。
当然,这些数字几乎总是高于实际最大值。想象一下像 sprintf 这样的函数,它可以使用大量堆栈空间,但根据您提供的格式字符串和参数而变化很大。对于这些情况,您还可以使用动态分析 - 在启动时用已知值填充堆栈,然后在调试器中运行一段时间,暂停并查看每个堆栈仍填充有多少您的值(高水位线样式测试)。两种方法都不完美,但结合使用将为您提供一个相当好的实际使用情况的图像。

2

如在这个问题的答案中所讨论的那样,一种常见的技术是用已知的值初始化栈,然后运行代码一段时间,看看模式停在哪里。


2
这不是一种离线方法,而是在我正在工作的项目中,我们有一个调试命令,读取应用程序内所有任务堆栈的高水位标记。这会输出每个任务的堆栈使用情况以及可用空间量的表格。在与大量用户交互运行24小时后检查这些数据,可以让我们对定义的堆栈分配“安全性”更有信心。
这种方法使用了众所周知的技术,即使用已知模式填充堆栈,并假定唯一可能重写它的方式是正常的堆栈使用。但如果它被任何其他方式写入,那么堆栈溢出只是您所担心的最小问题!

24小时运行的注意事项是:(1)运行应该提供良好的代码覆盖率,和/或者(2)未覆盖的代码不会使用大量的堆栈空间。 - Craig McQueen
我同意这两个警告。在我的情况和产品中,这种覆盖范围是由“用户”交互(真实和模拟)提供的。 - uɐɪ

2
我们曾试图在我们公司的嵌入式系统上解决这个问题。然而,由于代码量太大(包括我们自己的代码和第三方框架),我们无法得到可靠的答案。幸运的是,我们的设备基于Linux,所以我们采用了标准行为,即给每个线程2MB并让虚拟内存管理器优化使用。
我们对这种解决方案唯一的问题是,其中一个第三方工具对其整个内存空间执行了mlock操作(理想情况下是为了提高性能)。这导致它的线程的所有2MB堆栈(75-150个)被分页。我们失去了一半的内存空间,直到我们找到问题所在并注释掉了有问题的代码行。
顺便说一下:Linux的虚拟内存管理器(vmm)以4k块分配RAM。当新线程请求2MB的地址空间用于其堆栈时,vmm会为除最顶部页面外的所有页面分配虚假的存储页。当堆栈扩展到虚假页面时,内核检测到页面错误并将虚假页面与实际页面交换(这消耗了另外4k的实际RAM)。这样,线程的堆栈可以增长到任何需要的大小(只要小于2MB),并且vmm将确保只使用最少量的内存。

0
除了已经提出的一些建议,我想指出嵌入式系统中通常需要严格控制堆栈使用,因为您必须将堆栈大小保持在合理的范围内。
从某种意义上说,使用堆栈空间有点像分配内存,但没有(容易的)方法确定您的分配是否成功,因此不控制堆栈使用会导致永远无法弄清楚为什么系统再次崩溃。例如,如果您的系统从堆栈中为局部变量分配内存,则可以使用malloc()分配该内存,或者如果无法使用malloc(),则编写自己的内存处理程序(这是一个足够简单的任务)。
不行:
void func(myMassiveStruct_t par)
{
  myMassiveStruct_t tmpVar;
}

是的-是的:

void func (myMassiveStruct_t *par)
{
  myMassiveStruct_t *tmpVar;
  tmpVar = (myMassiveStruct_t*) malloc (sizeof(myMassicveStruct_t));
}

看起来很明显,但通常并不是这样 - 特别是当你不能使用malloc()时。

当然,你仍然会遇到问题,所以这只是一些帮助,但并不能解决你的问题。然而,它将帮助你估计未来的堆栈大小,因为一旦你找到了适合你的堆栈大小,并且如果你在一些代码修改后再次耗尽堆栈空间,你可以检测到许多错误或其他问题(例如太深的调用堆栈)。


所以您建议在嵌入式应用程序中使用堆而不是栈? - ziggystar

0

不是100%确定,但我认为这也可以完成。如果您有一个jtag端口暴露,您可以连接Trace32并检查最大堆栈使用量。虽然对于此,您将不得不提供一个相当大的任意堆栈大小。


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