关于c ++:堆栈大小估计

Stack Size Estimation

在多线程嵌入式软件(用C或C ++编写)中,必须为线程提供足够的堆栈空间,以使其能够完成其操作而不会溢出。在某些实时嵌入式环境中,正确调整堆栈大小至关重要,因为(至少在我使用过的某些系统中),操作系统将无法为您检测到此问题。

通常,新线程(主线程除外)的堆栈大小是在创建线程时指定的(即在pthread_create()的参数等中)。通常,这些堆栈大小被硬编码为在最初编写或测试代码时已知良好的值。

但是,将来对代码的更改通常会打破硬编码堆栈大小所基于的假设,并且有朝一日,您的线程会进入其调用图的更深层分支之一并溢出堆栈-导致整个系统崩溃或默默地破坏内存。

在线程中执行的代码在堆栈上声明struct实例的情况下,我亲自看到了此问题。扩充结构以容纳其他数据时,堆栈大小会相应膨胀,从而可能导致堆栈溢出。我想这对于已经建立的代码库来说可能是个巨大的问题,因为无法立即知道向结构中添加字段的全部效果(太多的线程/函数无法找到使用该结构的所有位置)。

由于通常对"堆栈大小调整"问题的回答是"它们不是可移植的",因此我们假定编译器,操作系统和处理器在本次调查中都是已知数量。我们还假设不使用递归,因此我们不处理"无限递归"方案的可能性。

有什么可靠的方法可以估算线程所需的堆栈大小?我更喜欢离线(静态分析)和自动的方法,但是欢迎所有想法。


运行时评估

一种在线方法是用一定的值绘制整个堆栈,例如0xAAAA(或0xAA,无论您的宽度如何)。然后,您可以通过检查未更改的绘画数量来检查堆栈在过去最大增长了多少。

请查看此链接以获取带插图的解释。

好处是它很简单。缺点是无法确定测试期间堆栈大小最终不会超过已用堆栈的数量。

静态评估

有一些静态检查,我认为甚至存在尝试执行此操作的被黑gcc版本。我唯一可以告诉您的是,在通常情况下,静态检查非常困难。

也看看这个问题。


如果您的目标符合要求,则可以使用StackAnalyzer之类的静态分析工具。


如果您想花费大量资金,可以使用商业静态分析工具,例如Klocwork。尽管Klocwork主要旨在检测软件缺陷和安全漏洞。但是,它也有一个称为" kwstackoverflow"的工具,可用于检测任务或线程中的堆栈溢出。我正在使用我从事的嵌入式项目,并且取得了积极的成果。我认为这样的工具并不完美,但我相信这些商业工具非常好。我遇到的大多数工具都与函数指针作斗争。我也知道,像Green Hills这样的许多编译器供应商现在都在其编译器中内置了类似的功能。这可能是最好的解决方案,因为编译器对做出有关堆栈大小的准确决策所需的所有细节都非常了解。

如果有时间,我相信您可以使用脚本语言来制作自己的堆栈溢出分析工具。该脚本将需要标识任务或线程的入口点,生成完整的函数调用树,然后计算每个函数使用的堆栈空间量。我怀疑可能有可用的免费工具可以生成完整的函数调用树,因此应该使它更容易。如果您知道平台生成每个函数使用的堆栈空间的细节可能非常容易。例如,PowerPC函数的第一条汇编指令通常是带有更新指令的存储字,该指令通过功能所需的数量来调整堆栈指针。您可以从第一条指令开始获取字节大小,这使得确定使用的总堆栈空间相对容易。

这些类型的分析都将为您提供堆栈使用情况的最坏情况上限的近似值,而这正是您想知道的。当然,专家(例如与我一起工作的专家)可能会抱怨您分配了过多的堆栈空间,但是它们是恐龙,并不关心良好的软件质量:)

尽管它不计算堆栈使用情况,但另一种可能性是使用处理器的内存管理单元(MMU)(如果有)来检测堆栈溢出。我已经使用PowerPC在VxWorks 5.4上完成了此操作。这个想法很简单,只需将页面的写保护内存放在堆栈的顶部。如果您溢出,则会执行处理器,并且您将很快收到堆栈溢出问题的警报。当然,它并不能告诉您需要增加多少堆栈大小,但是如果您擅长调试异常/核心文件,则可以至少弄清楚溢出堆栈的调用顺序。然后,您可以使用此信息适当地增加堆栈大小。

-djhaus


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


静态(脱机)堆栈检查并不像看起来那样困难。我已经为我们的嵌入式IDE(RapidiTTy)实现了它,目前可用于ARM7(NXP LPC2xxx),Cortex-M3(STM32和NXP LPC17xx),x86和我们内部兼容MIPS ISA的FPGA软核。

本质上,我们使用可执行代码的简单解析来确定每个函数的堆栈使用情况。最重要的堆栈分配是在每个功能开始时完成的;只需确保了解它如何随着不同的优化级别以及ARM / Thumb指令集等(如适用)而变化。还要记住,任务通常具有自己的堆栈,而ISR通常(但不总是)共享一个单独的堆栈区域!

一旦使用了每个函数,就很容易从解析中构建一个调用树并计算每个函数的最大使用率。我们的IDE为您生成了调度程序(有效的瘦RTOS),因此我们确切地知道哪些功能被指定为"任务",哪些是ISR,因此我们可以判断每个堆栈区域的最坏情况。

当然,这些数字几乎总是超过实际最大值。想想像sprintf这样的函数,该函数可能会占用大量堆栈空间,但根据您提供的格式字符串和参数而有很大的不同。对于这些情况,您还可以使用动态分析在启动时使用已知值填充堆栈,然后在调试器中运行一段时间,暂停并查看每个堆栈中仍有多少填充您的值(高水印样式测试)。

这两种方法都不是完美的,但是将两种方法结合起来可以使您对现实世界的使用情况有一个很好的了解。


我们尝试在嵌入式系统上解决此问题。太疯狂了,因为太多的代码(包括我们自己的代码和第三方代码)都无法获得可靠的答案。幸运的是,我们的设备是基于Linux的,因此我们回到了将每个线程分配2mb并让虚拟内存管理器优化使用的标准行为。

这个解决方案的一个问题是第三方工具之一在其整个内存空间上执行了mlock(理想情况下是为了提高性能)。这导致其线程中的每个线程(其中75-150个)的所有2mb堆栈都被分页。我们丢失了一半的内存空间,直到我们弄清楚它并注释了有问题的行。

旁注:Linux的虚拟内存管理器(vmm)以4k块分配RAM。当新线程为其堆栈请求2MB地址空间时,vmm会将伪内存页面分配给除最顶层页面之外的所有页面。当堆栈增长到伪造页面时,内核会检测到页面错误,并将伪造页面替换为真实页面(这会消耗另外4k的实际RAM)。这样,线程的堆栈可以增长到所需的任何大小(只要小于2mb),而vmm将确保仅使用最少的内存。


这不是脱机方法,但是在我正在处理的项目上,我们有一个调试命令,该命令读取应用程序内所有任务堆栈上的高水位线。这将输出一个表格,显示每个任务的堆栈使用情况和可用空间。在经过大量用户交互的24小时运行后检查此数据,使我们确信已定义的堆栈分配是"安全的"。

这项工作使用了一种久经考验的技术,即使用已知的模式填充堆栈,并假定可以重写的唯一方法是通过正常的堆栈使用,尽管如果通过其他任何方式写入,则堆栈溢出是最不用担心!


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


不能100%确定,但是我认为这也可以做到。如果暴露了jtag端口,则可以连接到Trace32并检查最大堆栈使用率。虽然如此,您将不得不给出一个初始的相当大的任意堆栈大小。


除了已经提出的一些建议外,我想指出的是,在嵌入式系统中,经常必须严格控制堆栈的使用,因为必须将堆栈的大小保持在合理的大小。

从某种意义上说,使用堆栈空间有点像分配内存,但是没有一种(简单的)方法来确定分配是否成功,因此,不控制堆栈使用率将导致人们永远难以弄清系统为何再次崩溃的原因。因此,例如,如果系统为堆栈中的局部变量分配内存,则可以使用malloc()分配该内存,或者如果不能使用malloc()编写自己的内存处理程序(这很简单)。

不,不:

1
2
3
4
void func(myMassiveStruct_t par)
{
  myMassiveStruct_t tmpVar;
}

是的是的:

1
2
3
4
5
void func (myMassiveStruct_t *par)
{
  myMassiveStruct_t *tmpVar;
  tmpVar = (myMassiveStruct_t*) malloc (sizeof(myMassicveStruct_t));
}

似乎很明显,但通常不是-尤其是当您不能使用malloc()时。

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