堆栈大小估计

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

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

但是,未来对代码的更改通常会破坏硬编码堆栈大小所依据的假设,并且在一个重要的日子里,您的线程进入其调用图的较深分支之一并溢出堆栈 – 从而导致整个系统崩溃或者默默地腐蚀记忆。

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

由于对“堆栈大小”问题的通常响应是“它们不可移植”,因此我们假设编译器,操作系统和处理器都是此调查的已知数量。 我们也假设没有使用递归,所以我们没有处理“无限递归”场景的可能性。

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

运行时,评估

在线方法是使用特定值绘制整个堆栈,如0xAAAA(或0xAA,无论宽度是多少)。 然后,您可以通过检查多少绘画未受影响来检查堆栈在过去的最大增长量。

请查看此链接以获得有关插图的说明。

优点是它很简单。 缺点是您无法确定堆栈大小最终是否会超过测试期间使用的堆栈数量。

静态评估

有一些静态检查,我认为甚至存在一个黑客gcc版本试图这样做。 我能告诉你的唯一一点是静态检查在一般情况下很难做到。

还看看这个问题。

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

如果您想花大笔钱,可以使用商业静态分析工具,如Klocwork。 虽然Klocwork主要用于检测软件缺陷和安全漏洞。 但是,它还有一个名为“kwstackoverflow”的工具,可用于检测任务或线程中的堆栈溢出。 我正在使用我工作的嵌入式项目,并且我得到了积极的结果。 我不认为这样的工具是完美的,但我相信这些商业工具非常好。 我遇到的大多数工具都在与函数指针竞争。 我也知道许多像Green Hills这样的编译器供应商现在可以在他们的编译器中构建类似的function。 这可能是最好的解决方案,因为编译器非常了解做出有关堆栈大小的准确决策所需的所有细节。

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

这些类型的分析都将为您提供堆栈使用的最坏情况上限的近似值,这正是您想要了解的内容。 当然,专家(就像我合作的那些人)可能会抱怨你分配了太多的堆栈空间,但它们是恐龙,不关心良好的软件质量:)

另一种可能性,虽然它不计算堆栈使用量,但是使用处理器的内存管理单元(MMU)(如果有的话)来检测堆栈溢出。 我使用PowerPC在VxWorks 5.4上完成了这项工作。 这个想法很简单,只需将一页写保护的内存放在堆栈的最顶层。 如果溢出,将发生处理器执行,您将很快收到堆栈溢出问题的警报。 当然,它并没有告诉你需要多少增加堆栈大小,但如果您对调试exception/核心文件有好处,那么至少可以找出溢出堆栈的调用序列。 然后,您可以使用此信息来适当增加堆栈大小。

-djhaus

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

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

本质上,我们使用可执行代码的简单解析来确定每个函数的堆栈使用情况。 最重要的堆栈分配在每个函数的开始处完成; 请务必了解它如何通过不同的优化级别以及ARM / Thumb指令集等进行更改。还要记住,任务通常都有自己的堆栈,并且ISR通常(但不总是)共享一个单独的堆栈区域!

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

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

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

正如在这个问题的答案中所讨论的,一种常见的技术是使用已知值初始化堆栈,然后运行代码一段时间并查看模式停止的位置。

这不是一个离线方法,但在我正在处理的项目中,我们有一个调试命令,可以读取应用程序中所有任务堆栈的高水位线。 这将输出每个任务的堆栈使用情况表以及可用的余量。 在24小时运行后通过大量用户交互检查此数据可以让我们确信定义的堆栈分配是“安全的”。

这可以使用经过充分尝试的技术来填充已知模式的堆栈,并假设可以重写的唯一方法是通过正常的堆栈使用,尽管如果它是通过任何其他方式写入堆栈溢出是最不用担心!

我们试图在我的工作中在嵌入式系统上解决这个问题。 它变得疯狂,有太多的代码(我们自己和第三方框架)得到任何可靠的答案。 幸运的是,我们的设备是基于Linux的,所以我们回到了给每个线程2mb并让虚拟内存管理器优化使用的标准行为。

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

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

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

从某种意义上说,使用堆栈空间有点像分配内存,但没有(简单)方法来确定您的分配是否成功,因此不控制堆栈使用将导致永远难以找出系统再次崩溃的原因。 因此,例如,如果系统从堆栈为局部变量分配内存,则使用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()时。

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

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