如何使用gcc确定嵌入式系统中的最大堆栈使用情况?

54

我正在编写嵌入式系统的启动代码——加载 main() 函数前的初始堆栈指针的代码——我需要告诉它我的应用程序将使用多少字节的堆栈(或者一些更大的保守估计值)。

有人告诉我,gcc 编译器现在有一个 -fstack-usage 选项和一个 -fcallgraph-info 选项,可以用来静态地计算出确切的 "最大堆栈使用量"。 ("使用 GCC 进行编译时堆栈需求分析" by Botcazou、Comar 和 Hainque)。

Nigel Jones 表示,在嵌入式系统中递归是一个非常糟糕的想法("计算你的堆栈大小" 2009),因此我在这段代码中小心不要制作任何相互递归的函数。

此外,我确保我的中断处理程序在最终的从中断返回指令之前不会重新启用中断,因此我不需要担心可重入的中断处理程序。

不使用递归或可重入中断处理程序,应该可以静态确定最大堆栈使用量。(因此大多数如何确定最大堆栈使用量?的答案并不适用。) 我的理解是,我(或更好的是,每次重新构建可执行文件时自动运行的PC上的某些代码)首先找到每个中断处理程序在没有被更高优先级的中断打断时的最大堆栈深度,以及主函数(main())在没有被打断时的最大堆栈深度。然后将它们全部加起来,以找到总的(最坏情况下的)最大堆栈深度。这在我的嵌入式系统中发生,当主背景任务(main())在被最低优先级中断打断时达到最大深度,并且该中断在被次低优先级中断打断时达到最大深度,依此类推。

我正在使用YAGARTO和gcc 4.6.0为LM3S1968 ARM Cortex-M3编译代码。

那么我如何使用gcc的-fstack-usage选项和-fcallgraph-info选项来计算最大堆栈深度呢?还是有其他更好的方法来确定最大堆栈使用量吗?

(请参见如何确定嵌入式系统中的最大堆栈使用量?,该问题与Keil编译器几乎相同。)


请注意,任何对函数指针的使用只能通过动态分析来捕获。 - Gabe
要获取调用者和被调用者信息,您可以使用-fdump-ipa-cgraph。据我所知,您提到的callgraph选项不存在。 - parvus
1
在允许嵌套中断的系统上,仅在从ISR返回之前重新启用中断无法防止重入。唯一的方法是在ISR内禁用中断,并从主代码重新启用它们。 - iheanyi
@iheanyi:嗯?在返回中断指令(RETI)之前,我非常小心地重新启用中断,所以我不理解你的评论。 https://stackoverflow.com/questions/52886592/avoiding-cortexm-interrupt-nesting; http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0460d/BEIDDFBB.html;等暗示有几种其他方法可以防止重入,而这些方法不涉及在主代码中重新启用中断。如果处理程序在最终的RETI之前从未重新启用中断,那么特定的中断处理程序永远不会被重新输入(嵌套),对吧? - David Cary
1
David,重新阅读你的问题后,我发现我错了。假设在进入ISR时禁用中断,在最终RETI之前重新启用中断可以确保您不会破坏ISR中涉及的任何数据。此时是否重新进入ISR并不重要。 - iheanyi
1
@Gabe 可能可以处理函数指针。这取决于用途。例如,如果它只是一个带有“设备A/B”或语言“A/B”的设备驱动程序或UI,则可以将最坏情况的指针纳入使用。您需要一个非常复杂的函数指针层次结构(可能需要一些伪函数式编程)才能通过分析无法完成此操作。 - artless noise
7个回答

28

GCC文档 :

-fstack-usage

使编译器按函数输出程序的栈使用信息。转储文件名通过向auxname添加 .su 生成。如果明确指定的输出文件不是可执行文件,则auxname生成自源文件的基本名称,否则生成自输出文件的名称。一个条目由三个字段组成:

  • 函数名。
  • 字节数。
  • 一个或多个限定符:static、dynamic、bounded。

静态限定符表示该函数在静态上操作栈:在函数进入时为帧分配一定数量的字节,在函数退出时释放它们;在函数中不进行其他栈调整。第二个字段是这些固定字节的数量。

动态限定符表示该函数在动态上操作栈:除静态分配外,函数体内还进行栈调整,例如在函数调用中推送/弹出参数。如果也有bounded限定符,则在编译时这些调整的数量受到限制,第二个字段是函数使用堆栈的总量的上限值。如果没有,则这些调整的数量在编译时没有限制,第二个字段仅表示受限部分。

我找不到有关-fcallgraph-info的任何参考。

您可以通过使用-fstack-usage和-fdump-tree-optimized创建所需的信息。

对于-fdump-tree-optimized中的每个叶子,获取其父级并从-fstack-usage中对它们的堆栈大小数字求和(请记住,对于任何具有“dynamic”但不是“bounded”的函数而言,这个数字是不准确的),找到这些值的最大值,这将是您的最大栈使用情况。


关于 I can't find any references to -fcallgraph-info使用GCC进行编译时堆栈需求分析 描述了该选项。AdaCore ARM ELF GNAT Community gcc编译器支持 -fcallgraph-info - Chester Gillon
1
GCC文档中的开发人员选项页面现在描述了-fstack-usage和-fcallgraph-info两者。 - AJM

14

如果没有更好的答案,我将贴出我在您提出的另一个问题的评论中所写的内容,尽管我没有使用这些选项和工具的经验:

GCC 4.6 添加了 -fstack-usage 选项,可以按函数为基础提供堆栈使用统计信息。

如果将此信息与由 cflow 或类似工具生成的调用图结合起来,就可以获得您想要的堆栈深度分析(可以很容易地编写脚本来完成此操作)。让脚本读取堆栈使用信息并加载一个带有函数名称及其使用的堆栈的映射。然后让脚本遍历 cflow 图(可以是易于解析的文本树),为调用图中的每个分支添加与其相关联的每行代码的堆栈使用量。

因此,看起来可以使用 GCC 完成此操作,但您可能需要组合正确的一组工具。


如果有人阅读此内容并具有足够的声望进行一字符编辑,则此答案中存在http URL的https版本。 - AJM

8

虽然有点晚了,但是对于任何需要参考这个问题的人来说,使用fstack-usage和类似cflow的调用图工具来合并输出结果,在涉及到动态分配内存(即使是受限制的)时可能会出现非常不准确的情况,因为没有关于动态堆栈分配发生时间的信息。因此,无法确定将该值应用于哪些函数。

举一个简单的例子,如果fstack-usage的输出结果(简化后)如下:

main        1024     dynamic,bounded
functionA    512     static
functionB     16     static

一个非常简单的调用树是:

main
    functionA
    functionB

将这些内容简单合并的天真方法可能导致选择主要-> functionA作为最大堆栈使用路径,达到1536字节。但是,如果在调用functionB的条件块中直接将一个大的参数(如记录)推送到堆栈中,而这个堆栈分配是main() 中最大的动态堆栈分配,则真正的最大堆栈使用路径为 main -> functionB,达到1040字节。基于现有的软件设计,针对其他更受限制的目标,如果所有内容都通过堆栈传递,则累计误差可能会迅速导致您查看完全错误的路径,并声称极大地夸大了最大堆栈大小。
此外,在谈论中断时,根据您对“可重入性”的分类,可能会完全错过一些堆栈分配。例如,许多Coldfire处理器的第7级中断是边沿敏感的,因此忽略中断禁用掩码,因此如果使用信号量提前离开指令,则可能不认为它是可重入的,但初始堆栈分配仍将发生在检查信号量之前。
简而言之,使用此方法必须非常小心。

2
顺便提一下,在嵌入式工作中,我通常会避免在堆栈上传递过大的数据。 - Technophile
@Technophile 当然可以,而我的原始措辞在这方面并不好 - 任何在堆栈上进行的动态分配都会引入歧义,无论它是按值传递还是不传递。我(现在已经过时的)嵌入式系统经验是使用静态分配的堆和堆栈使用情况,但如果你想要对现有代码库进行任何形式的审计,这可能值得注意。 - user1171983
1
某人在编程时无意间设计出这种情况的可能性很小,但是在计算堆栈使用时却不了解如何加以考虑。随着应用程序规模越来越大,天真方法和“精确”计算之间的差距越来越小,同时,“精确”计算变得更加棘手。我无法想象一个人会仔细设计,而导致天真方法明显错误,但某种方式却无法考虑到设计如何影响堆栈使用情况。 - iheanyi
@iheanyi 印象是主观的。工程师能够确定信息是否与他们相关。这个例子只是对这种方法的注意事项进行了具体说明,而没有过度限制OP中的上下文。我不应该假设任何读者的开发实践会排除这个问题。如果你对这个回答有意见,而不是评论,我可能会在事后同意,但我没有发现你的评论的具体性质是建设性的或者与实际意图相关的。真的没有什么进一步讨论的必要了。 - user1171983
从我的角度来看,阅读答案时,您对读者的做法进行了假设并规定了一条行动路线,同时强烈暗示做出其他选择将是灾难性的。我的评论解释了我为什么不赞同这种做法。 - iheanyi
显示剩余3条评论

7

最终我编写了一个Python脚本来实现τεκ的答案。这段代码太长,无法在此处发布,但可以在GitHub上找到。


4

我不熟悉-fstack-usage-fcallgraph-info选项。但是,我们总是可以通过以下方式确定实际堆栈使用情况:

  1. 分配足够的堆栈空间(对于此实验),并将其初始化为易于识别的内容,例如0xee
  2. 运行应用程序并测试其所有内部路径(通过输入和参数的所有组合)。让它运行超过足够长的时间。
  3. 检查堆栈区域,查看使用了多少堆栈。
  4. 将其作为堆栈大小,再加上10%或20%以容忍软件更新和罕见情况。

3
OP 特别寻找一种基于静态分析计算最坏情况的方法,而不是通过运行时实验来进行计算。 - Michael Burr
4
@Michael:好的,原帖提到:“或者有更好的方法来确定最大堆栈使用量吗?”肯定传统的方法可以得出有用的答案。这可能比分析代码路径更容易。 - wallyk
3
运行时分析的问题在于,它只在可能路径非常少的情况下有效。大多数复杂程序有数十亿甚至无限多个可能路径。 - Gilles 'SO- stop being evil'
3
只需32位RAM就能编码数十亿个状态。 - Gilles 'SO- stop being evil'
1
@endolith StackOverflow旨在成为一个通用资源,最好的答案是广泛有用的。存在超过64K的嵌入式系统;我目前正在处理一个带有RTOS和十几个线程的系统。 - Technophile
显示剩余6条评论

2
一般来说,您需要将调用图信息与由-fstack-usage生成的.su文件相结合,以查找从特定函数开始的最深堆栈使用情况。从main()或线程入口点开始,然后可以给出该线程的最坏情况使用情况。
幸运的是,已经为您创建了这样的工具,如此处所讨论的那样,使用此处的Perl脚本。

这个问题被引用为最近一个问题的重复 https://stackoverflow.com/questions/67397737/how-to-know-limit-static-stack-size-in-c-program-with-gcc-clang-compiler,但它已经过时了,而且被接受的答案可能是不完整或理论上的解决方案。我添加了这些信息作为更完整和实用的解决方案(在最初提出问题时不存在的解决方案)。 - Clifford

2
一般有两种方法——静态和运行时。
静态方法:使用-fdump-rtl-expand -fstack-usage编译项目,并从*.expand脚本获取每个函数的调用树和堆栈使用情况。然后迭代所有叶子节点,并计算每个叶子节点的堆栈使用情况并获得最高堆栈使用情况。接着将该值与目标上可用内存进行比较。这种方法在静态情况下运作,不需要运行程序。但是对于递归函数无法适用。无法处理VLA数组。如果sbrk()操作的是链接器部分而不是静态预分配缓冲区,则无法考虑动态分配,后者可能会自行增长。我在我的代码库中有一个脚本,stacklyze.sh,用它来探索这种选项。
运行时方法:在每个函数调用之前和之后检查当前的堆栈使用情况。使用-finstrument-functions编译代码。然后在代码中定义两个函数,大致应该获取当前的堆栈使用情况并对其进行操作。
static unsigned long long max_stack_usage = 0;

void __cyg_profile_func_enter(void * this, void * call) __attribute__((no_instrument_function)) {
      // get current stack usage using some hardware facility or intrisic function
      // like __get_SP() on ARM with CMSIS
      unsigned cur_stack_usage = __GET_CURRENT_STACK_POINTER() - __GET_BEGINNING_OF_STACK();
      // use debugger to output current stack pointer
      // for example semihosting on ARM
      __DEBUGGER_TRANSFER_STACK_POINTER(cur_stack_usage);
      // or you could store the max somewhere
      // then just run the program
      if (max_stack_usage < cur_stack_usage) {
            max_stack_usage = max_stack_usage;
      }
      // you could also manually inspect with debugger
      unsigned long long somelimit = 0x2000;
      if (cur_stack_usage > somelimit) {
           __BREAKPOINT();
      }
}
void __cyg_profile_func_exit(void * this, void * call) __attribute__((no_instrument_function)) {
      // well, nothing
}

每次函数调用前后都可以检查当前的堆栈使用情况。由于函数在使用堆栈之前被调用,因此该方法不考虑当前函数堆栈的使用情况 - 因为只有一个函数并且没有太多操作,可以通过获取函数并使用 -fstack-usage 获取堆栈使用情况并将其添加到结果中来缓解这种情况。


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