
正文
1
堆栈静态分析
当我们的嵌入式程序还没有运行的时候,我们能拿到的无非是程序的源码、函数的调用关系、编译后的二进制,当然这里面也包含对堆栈的操作。
有了这些信息的话,静态堆栈分析就能去做一些事情了,最直接的就是单函数栈帧的计算,编译器可以准确计算出每个独立函数的栈帧大小,这部分是完全确定的:
函数内的局部变量、数组、结构体的总大小
函数调用时需要保存的 CPU 寄存器(如 ARM Cortex-M 的 R4-R11 等)
函数参数、返回地址的存储开销
栈对齐所需的填充字节
比如 GCC 编译器提供的-fstack-usage选项,GCC会为每个编译单元(.c / .cpp)生成一个对应的 .su 文件(Stack Usage 文件)。该文件记录了该编译单元内每一个函数的堆栈帧大小信息,类似于这样的格式:

其实这里的:
static 表示函数的堆栈帧大小完全在编译时确定,全部是静态分配的局部变量、保存的寄存器、参数区域等。
dynamic 表示函数中使用了运行时动态栈分配,比如 alloca 或可变长度数组(VLA),因此报告中的数值只包含静态部分,实际运行时会动态增加。
当然前面只是简单的单函数栈帧的分析,相对全面一点是静态调用图的理论栈深估算。
静态分析工具会扫描所有确定的函数调用关系,构建调用树(Call Graph),然后找到最深的那条调用路径,把路径上所有函数的栈帧大小累加起来,得到一个理论上的最大栈深。

比如 IAR、Keil 等 IDE 的静态栈分析功能,就是通过这个逻辑,在 map 文件中输出类似这样的报告:
**************************************************************************** STACK USAGE***Call Graph Root Category Max Use Total Use------------------------ ------- ---------Program entry 288 288Maximum call chain 288 bytes"__iar_program_start" 4"_main" 8"_printf" 8"__PrintfFullNoMb" 152"__LdtobFullNoMb" 802
堆栈动态分析
没办法,动态堆栈信息还必须得程序跑起来才能获取,因为静态分析的所有结论,都建立在一个假设上:程序的执行路径、调用关系、触发时机都是编译期可预测的。但在嵌入式系统实际运行的运行过程中,这个前提似乎很不全面。
1、动态调用:编译期根本不知道你会调用谁
嵌入式代码中充满了大量的间接调用:
函数指针:比如状态机的跳转表、驱动的回调函数,编译期根本不知道这个指针最终会指向哪个函数;
回调函数:比如外设中断的回调、RTOS 的定时器回调,调用关系是运行时注册的;
这就导致前面我们分析的静态分析的调用图没法一层一层往下调用,这也就是我常常说的“断链”,当然如果编译器足够聪明,把所有路径的栈开销都加起来,找到最深的栈,否则就会导致漏算,bug菌觉得当你程序比较大的时候,编译器去跟你这样分析也是非常吃力的。
2、谈到堆栈必定要聊递归调用
因为递归调用尝尝是导致爆栈的元凶,因为递归的深度完全取决于输入数据:比如快速排序的递归深度,取决于输入数组的有序程度;通常静态分析都是直接忽略递归的,要么你手动指定一个最大递归深度。
3、异常堆栈的隐形栈开销
大家都知道中断是异步的!它随时可能打断当前正在执行的代码,不管你现在的函数调用到了哪一层。

比如说我们的主程序正在执行最深的调用链,已经用了 2KB 的栈空间;这时候一个高优先级中断触发了,CPU 立刻跳转到中断服务程序;中断服务程序自己又调用了 FFT 函数,又用了 1.5KB 的栈;如果你的总栈空间只有 3KB,直接就溢出了~
所以中断随时可能插队,把两个栈开销叠加起来,这个叠加效应只有运行时才能测到。再来个中断嵌套:如果你的系统支持中断嵌套,那可能一个中断里又来另一个中断,栈开销会层层叠加。
4、RTOS多任务更复杂
大家都知道RTOS任务的栈都是独立的、动态的,高优先级任务随时可以抢占低优先级任务;而且中断的栈开销,是随机扣在某个任务的栈上的!
总的来说动态堆栈就还是在程序跑起来的复杂工况下去测试吧。
3
运行中的堆栈检测
堆栈的静态分析还是有一些局限,进行堆栈的动态分析也就是我们常说的“栈水印“(High Watermark)方法进行检测:

以前的文章有写,再翻一翻:
一种省内存的MCU堆栈溢出检测方法
【进阶】" 堆栈溢出 ",也就这么回事!
大致就是:做栈标记->暴力测试->看水位,比如 IAR 的 C-SPY 调试器、FreeRTOS 的uxTaskGetStackHighWaterMark()函数,也基本都是这个原理。
bug菌做一些稳定性、可靠性要求较高的产品,基本上都是:
1、先用静态分析做第一轮评估,设置初始的堆栈大小;
2、然后各种工况下做长时间的压力测试,用动态分析拿到真实的栈峰值;最后
3、最后预留至少 20%~40% 的安全余量,确保极端情况下也不会溢出。
没错就是这么稳~
最后
好了,今天就跟大家分享这么多了,如果你觉得有所收获,一定记得点个赞,标个星~
唯一、永久、免费分享嵌入式技术知识平台~
推荐专辑点击蓝色字体即可跳转
☞ MCU进阶专辑

☞ 专辑|手撕C语言
☞ 专辑|经验分享

夜雨聆风