堆栈大小估计
Posted
技术标签:
【中文标题】堆栈大小估计【英文标题】:Stack Size Estimation 【发布时间】:2010-12-17 21:31:01 【问题描述】:在多线程嵌入式软件(用 C 或 C++ 编写)中,必须为线程提供足够的堆栈空间,以允许它完成其操作而不会溢出。在某些实时嵌入式环境中,堆栈的正确大小至关重要,因为(至少在我使用过的某些系统中),操作系统不会为您检测到这一点。
通常,新线程(除了主线程)的堆栈大小是在创建线程时指定的(即在 pthread_create() 等的参数中)。通常,这些堆栈大小被硬编码为在最初编写或测试代码时已知良好的值。
但是,未来对代码的更改通常会破坏硬编码堆栈大小所基于的假设,并且有一天,您的线程进入其调用图的较深分支之一并溢出堆栈 - 导致整个系统或默默地破坏内存。
我个人在线程中执行的代码在堆栈上声明结构实例的情况下看到了这个问题。当结构被扩充以容纳额外的数据时,堆栈大小会相应地膨胀,可能会导致堆栈溢出。我想这对于已建立的代码库来说可能是一个巨大的问题,因为无法立即知道将字段添加到结构的全部效果(线程/函数太多,无法找到使用该结构的所有位置)。
由于对“堆栈大小”问题的通常回答是“它们不可移植”,因此我们假设编译器、操作系统和处理器都是本次调查的已知数量。我们还假设不使用递归,因此我们不处理“无限递归”场景的可能性。
有哪些可靠的方法可以估计线程所需的堆栈大小?我更喜欢离线(静态分析)和自动的方法,但欢迎所有想法。
【问题讨论】:
潜在重复:***.com/questions/924430, ***.com/questions/389219 作为这个领域的新手,我不得不问:最有可能的第一步不是消除使用结构作为自动变量吗?无论指针指向的结构做了多少,指针的大小都不会改变。并且显式请求内存(而不是假设堆栈空间可用)将允许代码处理内存不可用的情况。 或者更好的是,堆栈上的结构应该只存储指向动态分配内存的指针。这样您就可以两全其美:自动生命周期管理,因为它在堆栈上,并且所有占用超过几个字节的内容都可以堆分配以节省堆栈空间。 @mcl:在 C++ 中,您可以使用 scoped_ptr,但在 C 中,您将拥有动态内存管理,这无济于事。我确实同意应避免使用堆栈容器(数组),因此在 C++ STL 容器中有所帮助。 @mcl 在嵌入式软件中,显式请求(动态)内存(例如 malloc)会带来一系列问题,例如长时间运行时的堆碎片。在大多数情况下,任何此类分配内存的失败都将是产品的不可接受的失败,而不是可以有效地“由代码处理”的失败。但是仍然可以考虑静态分配内存而不是堆栈。但在许多系统中,就整体内存使用而言,从堆栈变量更改为静态变量实际上可能会让您落后。 【参考方案1】:不是免费的,但Coverity 对堆栈进行静态分析。
【讨论】:
堆栈分析是特定于平台的。 Coverity 可以在哪些平台上进行静态堆栈分析? 我不是专家,但是当超过一定大小的块被添加到堆栈以及总堆栈超过一定大小时,您应该告诉 Coverity 发出警告。我不认为他模仿实际的堆栈使用。我可能是错的。【参考方案2】:运行时评估
一种在线方法是使用特定值绘制完整的堆栈,例如 0xAAAA(或 0xAA,无论您的宽度是多少)。然后,您可以通过检查有多少绘画未被触及,来检查堆栈在过去最大增长了多少。
查看this 链接以获取带有插图的说明。
优点是简单。缺点是您无法确定您的堆栈大小最终不会超过测试期间使用的堆栈数量。
静态评估
有一些静态检查,我认为甚至存在试图执行此操作的被黑 gcc 版本。我唯一可以告诉你的是,在一般情况下,静态检查很难做到。
也可以看看this问题。
【讨论】:
我是这方面的新手,你能解释一下吗?? 我添加了一个网站链接,该网站更详细地解释了动态方法。【参考方案3】:正如this question 的回答中所讨论的,一种常见的技术是使用已知值初始化堆栈,然后运行代码一段时间并查看模式停止的位置。
【讨论】:
【参考方案4】:这不是离线方法,但在我正在处理的项目中,我们有一个调试命令,可以读取应用程序中所有任务堆栈的高水位标记。这会输出每个任务的堆栈使用情况和可用空间量的表格。在运行 24 小时并进行大量用户交互后检查这些数据,让我们确信定义的堆栈分配是“安全的”。
这使用了使用已知模式填充堆栈的久经考验的技术并假设可以重写的唯一方法是通过正常的堆栈使用,尽管如果它是通过任何其他方式写入堆栈溢出是您最不必担心的!
【讨论】:
24 小时运行的注意事项是 (1) 运行应该提供良好的代码覆盖率,和/或 (2) 未覆盖的代码不会使用更多的堆栈。 我同意这两个警告。在我的情况和产品中,这种覆盖范围是由真实和模拟的“用户”交互提供的。【参考方案5】:如果您的目标符合要求,您可以使用StackAnalyzer 等静态分析工具。
【讨论】:
+1 我会指出 StackAnalyzer 通过在二进制级别工作来解决“不可移植”问题。 好吧,如果你的处理器不受支持,你又遇到了“不可移植”的问题。【参考方案6】:在我的工作中,我们试图在嵌入式系统上解决这个问题。太疯狂了,有太多的代码(我们自己的和第 3 方的框架)无法得到任何可靠的答案。幸运的是,我们的设备是基于 Linux 的,所以我们回到了标准行为,即给每个线程 2mb 空间,并让虚拟内存管理器优化使用。
我们对此解决方案的一个问题是第 3 方工具之一在其整个内存空间上执行了mlock
(理想情况下是为了提高性能)。这导致其线程(其中 75-150 个)的每个线程的所有 2mb 堆栈都被分页。我们丢失了一半的内存空间,直到我们弄清楚并注释掉有问题的行。
旁注:Linux 的虚拟内存管理器 (vmm) 以 4k 块分配 RAM。当一个新线程为其堆栈请求 2MB 的地址空间时,vmm 将伪造的内存页面分配给除了最顶层之外的所有页面。当堆栈变成虚假页面时,内核会检测到页面错误并将虚假页面与真实页面交换(这会消耗另外 4k 的实际 RAM)。这样一个线程的堆栈可以增长到它需要的任何大小(只要它小于 2mb),并且 vmm 将确保只使用最少量的内存。
【讨论】:
【参考方案7】:如果您想花大价钱,可以使用像 Klocwork 这样的商业静态分析工具。虽然 Klocwork 的主要目的是检测软件缺陷和安全漏洞。但是,它还有一个名为“kw***”的工具,可用于检测任务或线程中的堆栈溢出。我正在使用我从事的嵌入式项目,并且取得了积极的成果。我不认为像这样的任何工具是完美的,但我相信这些商业工具非常好。我遇到的大多数工具都与函数指针有关。我也知道像 Green Hills 这样的许多编译器供应商现在在他们的编译器中构建了类似的功能。这可能是最好的解决方案,因为编译器非常了解准确决定堆栈大小所需的所有细节。
如果你有时间,我相信你可以使用脚本语言来制作你自己的堆栈溢出分析工具。该脚本需要识别任务或线程的入口点,生成完整的函数调用树,然后计算每个函数使用的堆栈空间量。我怀疑可能有可用的免费工具可以生成完整的函数调用树,这样应该会更容易。如果您知道生成每个函数使用的堆栈空间的平台的详细信息,则非常容易。例如,PowerPC 函数的第一条汇编指令通常是带有更新指令的存储字,该指令将堆栈指针调整为函数所需的数量。您可以直接从第一条指令获取大小(以字节为单位),这使得确定使用的总堆栈空间相对容易。
这些类型的分析都会为您提供堆栈使用的最坏情况上限的近似值,这正是您想知道的。当然,专家(就像我一起工作的那些)可能会抱怨你分配了太多的堆栈空间,但他们是不关心良好软件质量的恐龙:)
虽然它不计算堆栈使用量,但另一种可能性是使用处理器的内存管理单元 (MMU)(如果有的话)来检测堆栈溢出。我使用 PowerPC 在 VxWorks 5.4 上完成了这项工作。这个想法很简单,只需将一页写保护内存放在堆栈的最顶部。如果溢出,将发生处理器执行,并且您将很快收到堆栈溢出问题的警报。当然,它并没有告诉您需要增加多少堆栈大小,但是如果您擅长调试异常/核心文件,您至少可以找出溢出堆栈的调用序列。然后,您可以使用此信息来适当地增加您的堆栈大小。
-djhaus
【讨论】:
感谢您对 kw*** 的评论。我发现我的公司实际上有 kw*** 的许可证,所以我尝试在现有项目中使用它。不幸的是,经过数小时的试验,我发现 kw*** 不支持跟踪 C++ 虚函数调用,这使得它对于面向对象的 C++ 代码几乎毫无用处(在我看来)。见my post on the klocwork forums。也许有一天这会得到支持。不过,对于 C 语言来说,它仍然是一个简洁的工具。【参考方案8】:静态(离线)堆栈检查并不像看起来那么困难。我已经为我们的嵌入式 IDE (RapidiTTy) 实现了它——它目前适用于 ARM7 (NXP LPC2xxx)、Cortex-M3 (STM32 和 NXP LPC17xx)、x86 和我们内部的 MIPS ISA 兼容 FPGA 软核。
本质上,我们使用可执行代码的简单解析来确定每个函数的堆栈使用情况。最重要的堆栈分配是在每个函数开始时完成的;只需确保查看它如何随不同的优化级别以及(如果适用)ARM/Thumb 指令集等而改变。还要记住,任务通常有自己的堆栈,并且 ISR 通常(但不总是)共享一个单独的堆栈区域!
一旦您使用了每个函数,就很容易从解析中构建调用树并计算每个函数的最大使用量。我们的 IDE 为您生成调度程序(有效的精简 RTOS),因此我们确切知道哪些函数被指定为“任务”,哪些是 ISR,因此我们可以判断每个堆栈区域的最坏情况使用情况。
当然,这些数字几乎总是超过实际最大值。想想像sprintf
这样的函数,它可以使用很多 的堆栈空间,但会根据您提供的格式字符串和参数而有很大差异。对于这些情况,您还可以使用动态分析——在您的启动中用已知值填充堆栈,然后在调试器中运行一段时间,暂停并查看每个堆栈的多少仍然填充有您的值(高水印式测试) .
这两种方法都不是完美的,但是将两者结合起来可以让您很好地了解实际使用情况。
【讨论】:
【参考方案9】:除了已经提出的一些建议之外,我想指出,在嵌入式系统中,您通常必须严格控制堆栈的使用,因为您必须将堆栈大小保持在合理的大小。
从某种意义上说,使用堆栈空间有点像分配内存,但没有(简单的)方法来确定您的分配是否成功,因此不控制堆栈使用将导致永远难以弄清楚为什么您的系统再次崩溃。因此,例如,如果您的系统为堆栈中的局部变量分配内存,请使用 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() 时。
当然,您仍然会遇到问题,所以这只是帮助但不能解决您的问题。但是,它将帮助您估计将来的堆栈大小,因为一旦您为堆栈找到了合适的大小,并且如果您在一些代码修改后再次用完堆栈空间,您可以检测到许多错误或其他问题(调用堆栈太深)。
【讨论】:
那么您建议在嵌入式应用程序中使用堆而不是堆栈?【参考方案10】:不是 100% 肯定,但我认为这也可以做到。如果您有暴露的 jtag 端口,您可以连接到 Trace32 并检查最大堆栈使用量。尽管为此,您必须给出一个初始的相当大的任意堆栈大小。
【讨论】:
以上是关于堆栈大小估计的主要内容,如果未能解决你的问题,请参考以下文章