嵌入式软件只做静态堆栈分析,还不够呀?

张开发
2026/4/19 6:04:31 15 分钟阅读

分享文章

嵌入式软件只做静态堆栈分析,还不够呀?
正文大家好我是bug菌~到了一年一度的公司风向标会议各种做调研、做方案、做报告那是忙得一个不可开交其中各个部门提得最多的还是AI在部门工作中的加持下所预计会带来的收益但是也是想了下既然大家都提AI那我就写一个关于AI安全相关的主题来展开吧其实每年都加都提了非常多的方案和所谓的风口不过都推进得非常缓慢这会议就当做公司的仪式感吧。那么这篇文章呢不是谈AI还是开发中的一些小思考吧玩C或者C都离不开对堆栈的理解那么堆栈究竟要配置多大其实一直也是一个经验性的问题堆栈小了导致所谓的爆栈溢出引发 HardFault、随机死机栈太大了又会资源增加成本。所以你特别像知道你的嵌入式程序每个人物所需要的最大堆栈是多少然后在最大堆栈的基础上预留一点点就可以了。然而大部分的工具都只能分析编译后拿到静态的堆栈使用数据而无法知道程序运行起来真实的堆栈消耗那这不就矛盾了吗好吧一步一步来我们先看看堆栈的静态分析都干了些啥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 288 Maximum 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语言进阶专辑☞“bug说”专辑☞专辑|Linux应用程序编程大全☞专辑|学点网络知识☞专辑|手撕C语言☞专辑|手撕C语言☞专辑|经验分享☞专辑|电能控制技术☞专辑 | 从单片机到Linux

更多文章