【问题标题】:How to determine maximum stack usage in embedded system with gcc?如何使用 gcc 确定嵌入式系统中的最大堆栈使用量?
【发布时间】:2011-09-17 06:33:59
【问题描述】:

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

我被告知 gcc 编译器现在有一个 -fstack-usage 选项和 -fcallgraph-info 选项,它们可以以某种方式用于为我静态计算确切的“最大堆栈使用量”。 ("Compile-time stack requirements analysis with GCC" 由 Botcazou、Comar 和 Hainque 撰写)。

Nigel Jones 说递归在嵌入式系统中是一个非常糟糕的主意(“计算堆栈大小”,2009 年),所以我一直小心不要在这段代码中创建任何相互递归的函数。

另外,我确保我的中断处理程序在它们最后的从中断返回指令之前都不会重新启用中断,所以我不需要担心可重入中断处理程序。

没有递归或重入中断处理程序,应该可以静态确定最大堆栈使用量。 (因此How to determine maximum stack usage? 的大多数答案都不适用)。 我的理解是我(或者最好是我的 PC 上的一些代码,每次我重建可执行文件时都会自动运行)首先找到每个中断处理程序的最大堆栈深度,当它没有被更高优先级的中断中断时,以及最大值main() 函数未中断时的堆栈深度。 然后我将它们全部加起来以找到总(最坏情况)最大堆栈深度。当 main() 后台任务在被最低优先级中断中断时处于其最大深度时,会发生这种情况(在我的嵌入式系统中),并且当它被下一个最低优先级中断时,该中断处于其最大深度中断,等等。

我正在使用 YAGATO 和 gcc 4.6.0 来编译 LM3S1968 ARM Cortex-M3 的代码。

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

(有关针对 Keil 编译器的几乎相同问题,请参阅 How to determine maximum stack usage in embedded system?。)

【问题讨论】:

  • 还要注意,任何函数指针的使用都只会被动态分析捕获。
  • 要获取调用者和被调用者信息,您可以使用-fdump-ipa-cgraph。您引用的调用图选项不存在,afaik。
  • 在您从 ISR 返回之前重新启用中断不会阻止允许嵌套中断的系统重新进入。实现这一点的唯一方法是禁用 ISR 中的中断并从主代码重新启用它们。
  • @iheanyi:嗯?我非常小心 not 在从中断返回指令 (RETI) 之前重新启用中断,所以我不明白你的评论。 stackoverflow.com/questions/52886592/… ; infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0460d/… ;等暗示还有其他几种方法可以防止重新进入,这些方法不涉及在主代码中重新启用中断。如果某个中断处理程序在最终 RETI 之前从未重新启用中断,则永远不会重新进入(嵌套)该中断处理程序,对吗?
  • 大卫,重读你的问题,我知道我错了。假设您在进入 ISR 时禁用中断,在最终 RETI 之前重新启用将确保您不会损坏 ISR 中触及的任何数据。那时你是否重新进入 ISR 并不重要。

标签: gcc embedded code-analysis static-analysis


【解决方案1】:

GCC 文档:

-fstack-用法

根据每个函数使编译器输出程序的堆栈使用信息。转储的文件名是通过将 .su 附加到 auxname 来创建的。 auxname 从输出文件的名称生成,如果明确指定并且它不是可执行文件,否则它是源文件的基本名称。一个条目由三个字段组成:

  • 函数的名称。
  • 字节数。
  • 一个或多个限定符:静态、动态、有界。

限定符 static 意味着函数静态地操作堆栈:在函数进入时为帧分配固定数量的字节,并在函数退出时释放;没有在函数中以其他方式进行堆栈调整。第二个字段是这个固定的字节数。

限定符动态意味着函数动态地操作堆栈:除了上面描述的静态分配之外,堆栈调整是在函数体中进行的,例如在函数调用周围推送/弹出参数。如果限定符 bounded 也存在,则这些调整的数量在编译时是有界的,第二个字段是函数使用的堆栈总量的上限。如果不存在,则这些调整的数量在编译时不受限制,第二个字段仅代表有界部分。

我找不到任何对 -fcallgraph-info 的引用

您可能会从 -fstack-usage 和 -fdump-tree-optimized 创建您需要的信息

对于 -fdump-tree-optimized 中的每个叶子,从 -fstack-usage 获取其父节点并求和它们的堆栈大小数(请记住,此数字适用于任何具有“动态”但不是“有界”的函数),找到这些值的最大值,这应该是您的最大堆栈使用量。

【讨论】:

【解决方案2】:

以防万一没有人想出更好的答案,即使我没有使用这些选项和工具的经验,我也会在评论中发布我对您其他问题的看法:

GCC 4.6 添加了-fstack-usage 选项,它提供了逐个函数的堆栈使用统计信息。

如果您将此信息与cflow 或类似工具生成的调用图相结合,您可以获得您正在寻找的堆栈深度分析(可能很容易编写脚本来执行此操作)。让脚本读取堆栈使用信息并使用函数使用的堆栈加载函数名称映射。然后让脚本遍历cflow 图(可以是易于解析的文本树),将调用图中每个分支的每一行的堆栈使用情况相加。

因此,看起来这可以使用 GCC 完成,但您可能需要拼凑正确的工具集。

【讨论】:

    【解决方案3】:

    很晚了,但是对于任何看到这个的人来说,给出的答案涉及结合 fstack-usage 的输出和 cflow 等调用图工具可能最终对于任何动态分配都非常不正确,甚至是有界的,因为没有关于何时的信息发生动态堆栈分配。因此,不可能知道您应该将该值应用于哪些功能。作为一个人为的例子,如果(简化的)fstack-usage 输出是:

    main        1024     dynamic,bounded
    functionA    512     static
    functionB     16     static
    

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

    main
        functionA
        functionB
    

    将这些组合起来的天真的方法可能会导致 main -> functionA 被选为最大堆栈使用的路径,为 1536 字节。但是,如果 main() 中最大的动态堆栈分配是在调用 functionB 的条件块中将一个大参数(如记录)直接推送到 functionB() 的堆栈上(我已经说过这是人为的),那么真的是 main -> functionB 是最大堆栈使用的路径,为 1040 字节。根据现有的软件设计,以及其他通过堆栈上所有内容的更受限制的目标,累积错误可能会很快导致您查看完全错误的路径,声称显着夸大了最大堆栈大小。

    此外,根据您在谈论中断时对“可重入”的分类,可能会完全错过一些堆栈分配。例如,许多 Coldfire 处理器的 7 级中断是边沿敏感的,因此忽略了中断禁用掩码,因此如果使用信号量提前离开指令,您可能不会认为它是可重入的,但初始堆栈分配仍然会发生在之前检查信号量。

    简而言之,您在使用这种方法时必须非常小心。

    【讨论】:

    • 顺便说一句,在嵌入式工作中,我通常会避免在堆栈上传递大量数据。
    • @Technophile 当然,我原来的措辞在这方面不是很好 - 堆栈上的任何动态分配都会引入歧义,无论它是否按值传递。我(现在已经过时)在嵌入式系统方面的经验是静态分配的堆和堆栈的使用,但如果您想对现有代码库执行任何类型的审计,可能值得注意。
    • 这太做作了。要使这种情况滚雪球到极端错误,就需要一种具有交替函数调用的深层嵌套的设计,其中一个条件分配非常少的内存并调用具有“大”堆栈使用量的函数,而相反的条件分配大量内存并调用使用“小”堆栈使用的函数。不仅如此,设计还以某种方式确保这不是随机的,而是从“小”使用函数的路径永远不会导致使用“大量”堆栈内存的函数。
    • 有人会在他们的代码中意外设计这个,但在计算堆栈使用量非常小的时候却不知道如何解释它的可能性。您的应用程序变得越大,幼稚方法和“精确”计算之间的差异就越小。此外,计算“精确”变得更加棘手。我看不出人们怎么会经历那种会导致天真的方法大错特错的严格设计。 . .并且不知何故无法解释设计将如何影响堆栈使用。
    • @iheanyi 你没抓住重点,花了两个cmets来做。也就是说,您需要了解任何方法的局限性,然后才能评估其在您的情况下的有用性。 “人为”的例子(我自己的话,五年前)显然是说明性的,而不是详尽的。考虑到代码库、库和用例(例如学术与监管),这些问题是否相关或重要最好由这些工程师决定。也就是说,我当然不想使用工程师编写的静态分析工具,他们以你描述的方式手动消除了覆盖范围的差距。
    【解决方案4】:

    我最终编写了一个 python 脚本来实现 τεκ 的 answer。代码太多在这里贴,不过可以在github上找到

    【讨论】:

      【解决方案5】:

      我不熟悉-fstack-usage-fcallgraph-info 选项。但是,始终可以通过以下方式找出实际的堆栈使用情况:

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

      【讨论】:

      • OP 正在专门寻找一种基于静态分析而不是运行时实验来计算最坏情况的方法。
      • @Michael:好吧,OP 写道或者有没有更好的方法来确定最大堆栈使用量? 当然,古老的方法会产生有用的答案。这可能比分析代码路径更容易。
      • 运行时分析的问题在于,它只有在可能的路径数量非常少的情况下才有效。大多数非平凡的程序有数十亿,如果不是无限多的话。
      • @Gilles Billions 在为具有 64K RAM 的嵌入式系统编写的程序中?
      • @endolith 数十亿个状态可以在 32 的 RAM 中进行编码。
      【解决方案6】:

      通常有两种方法 - 静态和运行时。

      静态:使用-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 并将其添加到结果中。

      【讨论】:

        【解决方案7】:

        通常,您需要将调用图信息与-fstack-usage 生成的 .su 文件结合起来,以从特定函数开始找到最深的堆栈使用情况。从main() 或线程入口点开始,将为您提供该线程的最坏情况。

        使用来自here 的 Perl 脚本,已按照 here 的讨论为您完成了创建此类工具的工作。

        【讨论】:

        • 这个问题被引用为最近的一个问题stackoverflow.com/questions/67397737/… 的重复,但它是旧的,并且接受的答案可以说是一个不完整的或理论上的解决方案。我已将此信息添加为更完整和实用的解决方案(最初提出问题时不存在的解决方案)。
        猜你喜欢
        • 2011-09-16
        • 2010-09-28
        • 2012-07-26
        • 2010-11-14
        • 2021-07-21
        • 1970-01-01
        • 1970-01-01
        • 2014-01-28
        • 2013-05-30
        相关资源
        最近更新 更多