GNU 静态堆栈使用分析

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

堆栈溢出是一个大问题:如果我看到系统崩溃,通常第一件事就是尝试增加堆栈大小以查看问题是否消失。 GNU 链接器可以检查我的全局变量是否适合 RAM。但它不知道我需要多少堆栈。那么,如果有办法找出我需要多少堆栈,该有多酷?

GNU 静态堆栈使用分析

事实上,这可以通过 GNU 工具实现(例如,我将它与 GNU ARM Embedded (launchpad) 4.8 和 4.9 编译器一起使用 :-)。不过这个能力好像并不广为人知?

概述

我使用了很长时间的一种方法是:

  1. 用定义的模式填充堆栈的内存。
  2. 让应用程序运行。
  3. 检查调试器有多少堆栈模式已被覆盖。

效果很好。除了它是非常经验的。我需要的是来自编译器的一些数字以获得更好的视图。

在本文中,我介绍了一种使用 GNU 工具和 Perl 脚本来报告应用程序中堆栈使用情况的方法。

GNU -fstack-usage 编译器选项

GNU 编译器套件有一个有趣的选项: -fstack-usage

“使用 -fstack-usage 编译的单元将生成一个额外的文件,该文件指定每个函数使用的最大堆栈量。该文件与带有 .su 扩展名的目标对象文件具有相同的基本名称。” ( https://gcc.gnu.org/onlinedocs/gnat_ugn/Static-Stack-Usage-Analysis.html )

如果我将该选项添加到编译器设置,现在有一个 .su(堆栈用法)文件以及每个对象 (.o) 文件:

堆栈使用文件

这些文件是像这样的简单文本文件:


 main.c:36:6:bar    48    static
main.c:41:5:foo    88    static
main.c:47:5:main    8    static

它列出了源文件 (main.c)、函数的行 (35) 和列 (5) 位置、函数名称 (bar)、堆栈使用字节数 (48) 和分配(静态,这是正常情况)。

创建堆栈报告

虽然 .su 文件已经是基于文件/函数的重要信息来源,但如何将它们组合起来以获得完整的图片?我找到了 Daniel Beer 开发的 Perl 脚本 (avstack.pl)(参见 http://dlbeer.co.nz/oss/avstack.html )。

在原始脚本中,您可能需要调整 $objdump $call_cost 。使用 $objdump 我指定 GNU objdump 命令(确保它存在于 PATH 中)并且 $call_cost 是添加到每次调用成本中的常量值:


 main.c:36:6:bar    48    static
main.c:41:5:foo    88    static
main.c:47:5:main    8    static

使用目标文件列表调用 avstack.pl,例如


 main.c:36:6:bar    48    static
main.c:41:5:foo    88    static
main.c:47:5:main    8    static

:idea: 您需要列出所有目标文件,脚本没有使用目录中所有 .o 文件的功能。我通常将对 Perl 文件的调用放入一个批处理文件中,我从构建后步骤调用该文件(请参阅“ 在 Eclipse 中作为构建后步骤执行多个命令 ”)。

这会生成如下报告:


 main.c:36:6:bar    48    static
main.c:41:5:foo    88    static
main.c:47:5:main    8    static
  • 前面带 '>' 的函数名表示'根'函数:它们不是从其他任何地方调用的(也许我没有传递所有目标文件,或者真的没有被使用)。
  • 如果函数是递归的,则用 'R' 标记。成本估算将针对单个递归级别。
  • Cost 显示累积堆栈使用量(此函数加上所有被调用者)。
  • Frame 是 .su 文件中使用的堆栈大小,包括 $call_cost 常量。
  • 高度 表示由该函数引起的调用级别数。

注意 INTERRUPT 条目:它是中断所需的堆栈级别。该工具假定非嵌套中断:它将最坏情况下的中断向量 (IV) 堆栈使用情况计入峰值执行:


 main.c:36:6:bar    48    static
main.c:41:5:foo    88    static
main.c:47:5:main    8    static

什么算作中断例程是由Perl脚本中的这部分控制的,所以每个以__vector_开头的函数都被视为中断例程:


 main.c:36:6:bar    48    static
main.c:41:5:foo    88    static
main.c:47:5:main    8    static

汇编代码

如果我的项目中有内联汇编和汇编代码,那么编译器就无法报告堆栈使用情况。这些函数被报告为“零”堆栈使用:


 main.c:36:6:bar    48    static
main.c:41:5:foo    88    static
main.c:47:5:main    8    static

编译器会警告我:

此目标不支持堆栈使用计算

:idea: 我还没有找到在源代码中向编译器提供该信息的方法。

实时操作系统任务

对于基于 RTOS(例如 FreeRTOS)的系统中的任务,该工具运行良好且开箱即用。因此,使用该工具,我可以很好地估计每个任务堆栈的使用情况,但我需要将中断堆栈的使用情况计入该值:


 main.c:36:6:bar    48    static
main.c:41:5:foo    88    static
main.c:47:5:main    8    static

-Wstack-usage 警告

另一个有用的编译器选项是 -Wstack-usage 。使用此选项,只要堆栈使用量超过给定限制,编译器就会发出警告。

警告堆栈使用的选项

这样我就可以快速检查哪些函数超出了限制:

堆栈使用警告

概括

GNU 编译器套件带有非常有用的选项 -fstack-usage ,它为每个编译单元(源文件)生成列出堆栈使用情况的文本文件。这些文件可以进一步处理,我正在使用 Daniel Beer 创建的出色 Perl 脚本(谢谢!)。通过提供的工具和技术,我可以预先估计堆栈的使用情况。我知道这只是一个估计,递归只在最低级别计算,汇编代码不计算在内。我可能会扩展 Perl 文件以扫描文件夹中的所有目标文件,除非有人已经做过这个?如果是这样,请发表评论并分享 :-)。