如何确定最大堆栈使用量?

51

有哪些可用于确定嵌入式/内存受限系统最佳堆栈大小的方法?如果太大则浪费了本可以用在其他地方的内存。然而,如果太小,则会出现我们网站的名称...

试图启动事情:Jack Ganssle 在设计嵌入式系统的艺术中指出:“随着经验的增长,人们学会了计算堆栈正确大小的标准和科学方法:随机选择一个大小并希望好运。” 有没有人比这做得更好?

要求更具体的示例。那么,如何使用IAR嵌入式工作台工具链针对2 kB RAM的MSP430 MCU编写C程序,不使用操作系统?此IDE可以在使用JTAG调试器时显示堆栈内容和使用情况。


取决于您使用的芯片组/操作系统/编程语言。 - Ady
很高兴看到这个问题有一些答案,不像https://dev59.com/BEXRa4cB1Zd3GeqPrm6t那样没有回答。 - Constantin
2
我在提问时看到了那个问题,但认为嵌入式倾斜将它们分开了... - Judge Maygarden
2
Jack Ganssle说的不仅仅是那个。我认为那只是他的开场白。从他的第二版书的第250页中可以看出:“由于很少有程序员有合理的方法来确定最大堆栈要求,因此始终假设您的估计将是不正确的。对于系统中的每个堆栈,请确保初始化代码使用值0x55填充分配给堆栈的整个内存量。之后,在调试时,您可以查看堆栈,并通过在该区域中看不到0x55块来检测堆栈溢出...” - Craig McQueen
1
我知道他说的不仅仅是这些,但我试图以他的方式使用它:激发对该主题的兴趣。;) - Judge Maygarden
5个回答

35

确定最深的堆栈使用方法最普遍的方式是使用某些已知但不寻常的值初始化堆栈内存,然后定期(或在大型测试运行结束时)查看该模式停止的位置。

这正是IAR IDE确定堆栈使用量的方法。


真的,它会用0xCD填充堆栈。 - Judge Maygarden
我之前也使用过这种技术。诀窍是在 pre-main(或者在 main 中从当前栈帧之后的某个点)中将堆栈涂成极限颜色,然后从堆栈极限开始向后计数,直到发现被破坏的内存位置(不是 0xCD)。 - JeffV
你怎么实际做到这一点呢?我在https://dev59.com/s0rSa4cB1Zd3GeqPVVW1上问了一个关于常规PC的类似问题。 - JXG
8
对于一个 Windows 程序来说,这种方法不太可行;堆栈内存由操作系统管理,因此你没有很好的机会去调整堆栈。这种解决方案更适用于嵌入式系统,其中任务的堆栈通常由应用程序管理。我需要考虑如何在 Windows 上安全地实现这一点。 - Michael Burr
@Michael - IAR IDE和嵌入式系统采用的方法来确定使用的堆栈量很有趣。很好,你指出在Windows中,堆栈内存由操作系统管理,因此没有很好的机会来绘制/着色堆栈!+1。 - Karthik Balaguru
@Michael,Visual C++ 对你来说也会“绘制堆栈”吗? - Judge Maygarden

23
您在问题标签中打了静态分析的标签,但这是一个难以通过静态分析解决的问题。堆栈使用情况取决于程序的运行时配置文件,尤其是如果您正在使用递归或alloca,那么更加困难。考虑到这是一个嵌入式平台,我猜想运行像ps或top这样的工具并查看应用程序使用了多少堆栈也很困难。
一种有趣的方法是使用当前堆栈帧的地址来确定使用了多少堆栈。您可以通过获取函数参数或局部变量的地址来实现这一点。对于main函数和您认为使用最多堆栈的函数,请执行此操作。差异将告诉您应用程序所需的堆栈量。以下是一个示例(假设常规情况下堆栈从高到低增长)。
char *stack_top, stack_bottom;

int
main(int argc, char *argv[])
{
    stack_top = (char *)&argc;
    // ...
    printf("Stack usage: %d\n", stack_top - stack_bottom);
}

void
deeply_nested_function(void)
{
    int a;
    stack_bottom = (char *)&a;
    // ...
}

如果你的编译器允许你指定自定义函数序言(许多编译器都这样做,以便允许基于图形的程序分析),你甚至可以安排所有函数调用此类测量代码。然后,你的测量函数变成如下所示:

void
stack_measurement_function(void)
{
    int a;
    stack_bottom = min(stack_bottom, (char *)&a);
    // ...
}
我使用了类似于我所描述的方法来生成这些图表

那么,运行时测试是答案吗?选择一个大堆栈,然后在确定上限后缩小它? - Judge Maygarden
3
您在回答中提到“这是一个难以通过静态分析解决的问题”,因此我想提及http://www.absint.com/stackanalyzer/。 - Pascal Cuoq

5
通过一个好的源代码静态分析工具,你可以为你的应用程序生成一个调用图。有了这个,再加上你的编译器产生的局部变量/临时变量的数量估计,你就可以直接计算出对堆栈的保守需求估计。
我所说的“好”的分析工具是指可以读取所有涉及到的编译单元,可以确定直接函数调用,可以确定在编译单元中的间接指针,可以在整个系统范围内计算保守的指向分析,可以构建考虑指向分析的调用图。这排除了很多工具,这就是为什么人们会看到一些特别的方法,比如“在运行时填充堆栈并观察发生了什么”。你还需要估计编译器将要放置在堆栈中的堆栈需求;你可以通过简单地知道所有类型的存储需求大小来近似估计大部分内容,对于嵌入式系统C程序来说,这通常相当容易确定。最后,你需要相信你的应用程序没有递归调用,或者该工具已经了解了最深层次的递归(可能是由你告诉它的)。
DMS软件重构工具包符合C程序的所有要求。请参见http://www.semanticdesigns.com/Products/DMS/DMSToolkit.html。您仍然需要配置它以通过爬取调用图并使用各种大小估计来计算堆栈需求。
如果您想获得快速答案,请使用堆栈填充技巧。如果您想要一个在每次源代码更改后都可以重新计算的答案,您将需要静态分析方法。

2
我正在解决这个问题——即栈大小的分析计算。显然,这将是一个高度递归的代码片段,因为函数调用可能会有一个或多个索引数组作为其参数之一,而其中一个或多个数组索引可能涉及函数调用!
然而,有几点认识可以减轻复杂性:
1. 使用高级语言编译器时,每个语句/代码行执行结束时的堆栈指针应该与开始时相同。 (至少这是一个好的规则,否则你会遇到问题!)
2. 每次从函数或子程序调用返回后,堆栈指针应该与调用前相同。因此,最大堆栈大小是程序中所有语句中达到峰值堆栈大小的最大值。 (至少这是一个好的规则,否则你会遇到问题!)
当然,语句可以包括我上面提到的递归问题,但至少找到整个程序所需的最大堆栈大小要求的问题就转化为找到每个语句的最大堆栈大小要求,然后选择其中的最大值。
在所有被调用的函数都被编译之前,这个过程是无法完成的。因此,我为每个已编译模块生成一个文件,记录每个语句的若干堆栈大小(基本上是每个函数调用之前达到的峰值和每个函数调用之前的值(不包括由函数调用引起的尚未知的堆栈大小增加),以及涉及的函数名称)。然后,一旦所有函数都被编译,我使用一个递归例程对这些文件进行回顾性处理,以确定峰值堆栈大小。
幸运的是,除了递归例程之外,最大可能的堆栈大小要求不取决于程序流程,尽管在典型的流程中(这是数据相关的)可能永远不会达到这个最大可能的堆栈大小。
例如:假设函数1调用函数2,并且两者的程序流程都取决于数据值X。假设有一系列X导致函数1执行其最坏语句,其中包括对函数2的调用,但对于相同的X范围,函数2不会执行其最坏情况的语句。由于我们同时使用了函数1和函数2的最坏情况来计算最大可能的堆栈大小,因此我们可能高估了堆栈大小。至少我们保持了安全。
我喜欢为中断例程分配它们自己的堆栈空间,如果需要的话可以使用操作系统堆栈,这样它们就不会增加程序堆栈要求,除了从中断返回的地址。

-12
  • 永远不要使用递归或递归算法(注意正则表达式库)。
  • 不要使用数组,总是使用malloc()。
  • 不要使用alloca(),一些编译器甚至在这个函数中存在错误。

然后通过手动检查代码部分,寻找堆栈使用最高的地方(记住我说过没有数组)

  • 在代码本身的疑似高点处检查堆栈使用情况,并记录到调试器接口中。
  • 基于估计的堆栈使用情况设置上限,例如限制服务器连接数。

14
这个问题被标记为嵌入式系统相关,因此您建议“永远不要使用数组,而应始终使用malloc()”特别不合适。但在任何硬件平台上,我认为这是一个不好的通用建议。为什么你总是只从堆中分配内存呢?请解释一下原因。 - Morten Jensen
3
很难想象为什么你还没有删除这个回答。 - phonetagger
1
不要提供毫无理由的可怕、教条式的建议。 - Craig Barnes

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