在嵌入式系统中,RAM 的大小是非常有限的。尤其是做器件选型时,更小 RAM 的芯片意味着更低的采购价格,产品才会更具竞争力,有更高的毛利。
在这样极致的压榨下,留给堆栈的空间更加少了。开发者不得不面对爆栈的巨大风险。每个软件工程师都想有一个工具能够帮助他们检验栈的使用情况,从而很好的评估风险。
人们寻常采用的方法是把栈里都先写满一个特定的值,比如 0xAA。随后在程序运行一段时间之后看看还剩多少 0xAA 没有被改掉。这种方法确实有一定的效果,但是显然还不够直观,又比较麻烦。尤其是当工程有不止一个栈的时候。
为此,新版本的 gcc 编译器提供了一个有用的编译选项-fstack-usage。使用这个选项后,编译器会额外产生有关栈使用情况的信息,而 MCUXpresso 可以整理这些信息,并将它们非常清晰地显示出来。
fstack-usage与Call Graph
先让我们看看 GNU 关于-fstack-usage 的说明:(https://gcc.gnu.org/onlinedocs/gcc/Developer-Options.html)
它以每个函数为基础,使编译器生成程序的堆栈使用信息。信息存放在后缀名为.su 的文件中。
下图是编译文件夹的内容,可以看到有一个同名的 su 文件。
这个文件的内容也很简单,它列 出 了 文 件名(system_MIMXRT1052.c), 函数的 行号和列号, 函数的名称, 如 SystemInit,堆栈的使用情况(8),以及如何分配(static)。
但是这样单个显示是没有什么参考价值的,我们需要的是整个工程的全景,要把所有的 su 文件整理出来。 MCUXpresso IDE v11 版本提供了这一功能。在 Image Info 窗口中有一个 Call Graph 标签。单击右上角的导入按钮就可以导入当前整个工程的 stack 使用情况。
前面带“>”的函数名称显示“根”函数:它们不能被从其他任何地方调用的。其中ResetISR就是 reset 入 口 函 数 , 而 exception handlers 里 面 都 是 中 断 服 务 程 序 。
HAL_UartReceiveBlocking()函数因为没有其它函数显式的调用它,所以也被认为是根函数。
这里我们可以看到这种分析的一个弱点,如果函数是通过函数指针的方式来调用那么该功能就无能为例了。但是使用者可以自己分析程序给续上。
如果函数是递归的,则用一个特殊的双箭头标记。成本估算将针对单级递归。
Full Cost 表示累积堆栈使用量(此函数加上所有被调用的)。
Local Cost 表示本层的堆栈使用量。
Depth 表示由该函数引起的调用级别数。
请注意Exception Handlers 这里,它集中了所有的中断服务程序。由于没有显式的调用,它们都是根函数,并且这里只统计非中断嵌套情况下的最大用量。所以如果允许中断嵌套, 那么对于栈的分配应该更加保守。
此外,如果函数是用汇编语言写的,那么工具是无法统计它们的栈使用情况, 一律会统计成‘4’。但如果调用到了其它函数,深度和 Full cost 还是会被统计的。
需要注意这个堆栈使用报告仅涵盖每个函数或调用树的堆栈使用情况。它们不包括异常处理程序所需的额外堆栈空间。所以最后的堆栈计算是 ResetISR 栈+中断栈,如果允许中断嵌套,那么整个中断嵌套最长的情况必须要被考虑。 该工具在基于 RTOS(例如 FreeRTOS)的系统中同样运行良好且开箱即用。因此,使用该工具,我们可以很好地估计每个任务堆栈的使用情况。栈计算可以使用下图,它来自 Joseph Yiu,一位来自 ARM 的大牛。
其它同栈保护有关的编译选项
GCC 除了提供 stack-usage 这个编译选项外,还有其它一些相关的选项可供选择。
- Wstack-usage
这是一个有用处的编译选项:-Wstack-usage。它能够在堆栈使用超过限制时产生 warning
信息。用法是:
-Wstack-usage=256
它表示如果栈使用量超过 256 时产生警告。这样就能更快速地知道哪个函数超了。只说它有些用处而不是非常有用是因为,只针对单个函数的堆栈用量, 不会按调用树累计被调用函数的堆栈总数。
- fstack-protector 这个选项的解释是: 产生额外的代码来检查缓冲区溢出,例如堆栈粉碎攻击。这是通过向具有易受攻击对象的函数添加保护变量来实现的。这包括调用 alloca 的函数,以及缓冲区大于 8 字节的函数。在输入函数时初始化保护,然后在函数退出时检查保护。如果保护检查失败,将打印错误消息,程序退出。
- fstack-protector-all
同-fstack-protector 基本相同, 区别是它为所有的函数都提供保护。
-fstack-protector和-fstack-protector-all在ebp和ip等信息的地址下面放一个保护数, 如果栈溢出 ,那么这个 32 位数会被修改,就会导致函数进入栈溢出错误处理函数。一旦检测到溢出就会调用__stack_chk_fail()函数。这个函数需要用户自己来写,比如可以打印一个报错信息,或者执行其它一些保护措施。 下图是在编译选项里加入-fstack-protector-all 后一个普通C函数的汇编内容。
当然,可以想见,如果每个函数都加这么一段,编译出来的二进制文件会大上许多,执行速度也会变慢一些。而如果仅仅使用-fstack-protector,则很少有函数会被保护。因为 alloca() 是在栈(stack)里面分配空间,而我们一般都是用 malloc()在堆(heap)里面分配。
小结
栈空间防溢出是软件设计中非常关键的问题。MCUXpresso IDE 的 Call Graph 窗口为开发者提供了很好的可视化统计表格, 非常便于对堆栈使用情况的评估。论程序有没有操作系统,它都非常有效。 而GCC编译器提供的其它一些选项,虽然也有用处,但是在嵌入式软件设计中还是用在 debug 阶段会更好一些。开发者还是应该尽量使用 call Graph 功能做到事先防范。
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !