不要动辄滚粗,先看堆栈是否溢出
中国人是惯于精打细算的,鲁迅先生说:“时间是海绵里的水,挤一挤总会有的!”
本文引用地址:http://www.amcfsurvey.com/article/201910/406345.htm领导说,鲁迅说得对!
于是,领导们经常带着期盼的神情,忽悠苦逼的软件工程师:“再多想想办法吧,嗯,MCU的主频是低了点,RAM资源是少了些,但是,考虑一下成本,MCU还是尽量不要换的吧?方法总比困难多,看着RAM资源好像不大够,但是换个实现方式,还可以再挤一挤的吧?鲁迅先生曾说......”
好吧,领导们肯定读过华严经,深谙佛菩萨“螺蛳壳里做道场”的本事:大即是小,小即是大,大小无二无别!嫌功能太多,RAM资源太少,多少算多呀?为啥子要生出那么多分别心撒?
可是,领导们可能不知道,鲁迅先生写错了字不叫错别字,叫“通假字”,我们写错了就是实实在在的“错别字”,而且佛菩萨的境界也是“非汝边事”。所以,用小马拉大车,在资源一般般的MCU中塞入尽可能多的代码,实现那么多功能,还能让这些模块配合无间地亲密运转,实在不是我等的境界了!
这不,同事小王又找我来诉苦了。
1
“马步君,救救我吧。快要被领导折磨疯了,这就是个16位的单片机,RAM总共512个字节。留给堆栈256个字节,剩下的就只有256字节了,哪能实现那么多功能呢?可是领导说干嘛留给堆栈256个字节,堆栈留少一点RAM不就够用了吗?”
说着说着,小王愈加地愤愤不平了:“明明有个管脚兼容的芯片,RAM有1k字节,但是领导就是不让换。说让堆栈留少一点,哼,他知道个屁!滚粗,堆栈不够的话系统会跑飞的呀!”
看着小王蜡黄黄的脸蛋和红通通的眼睛,我心下有些不忍:‘万般皆苦,做人最苦,难怪如来说为可怜愍者呀!’可是,领导说的也不无道理,对于堆栈该设置多少,很多人都是稀里糊涂,又有多少人能够弄得明白呢?于是我竟而给领导辩护了起来:“也许领导说得对吧,因为你确实不知道该给堆栈留多少空间吧?”
我一边小心翼翼地说着,一边看着小王的脸慢慢地耷拉了下来,甚而就要拉到地上了。于是我赶忙提起万般的精力找补一番,给他讲起MCU中RAM资源和堆栈分配的矛盾性来:
“RAM资源确实很重要,领导的意思应该是说你对它的分配要照顾到应用、系统堆栈两方面的需求,不可有所偏颇。
在MCU的地址空间中,RAM是连续分配一段线性地址空间,应用中用到的全局变量、中断和系统调用用到的栈、动态分配用到的堆都要分配在这段有限的线性空间内,当然你可以选择不用‘堆’。不过,如果有所富余,或者确实需要,你还得把存在程序存储空间中的一段代码复制到RAM空间内运行,以加快程序的运行速度,提高系统实时性。
所以,RAM资源确实是有限,不可能也不应该盲目得为堆栈分配太大的尺寸。不过话又说回来,如果堆栈设置地过小也不行,因为设置过小的话,一旦程序设计得不合理就很容易出问题。比如在函数调用中子函数中的局部变量太多、中断优先级设置得不合理导致高低中断间的嵌套、中断ISR程序过长导致本中断被嵌套,或者出现函数调用层次过深等程序设计不当之处都可能导致堆栈溢出,改变临近堆栈的RAM空间中的内容,从而造成程序运行异常,发生故障甚至导致重大事故。
从这个角度来说,在一定程度上,堆栈设置得大一些,有利于弥补程序设计的缺陷。话再说回来,程序设计地很完美,就不需要设置那么大的堆栈。归结到底,这就是个平衡木啊!”
跟小王进行了这段科普后,他着实有些懵圈了。于是我把他晾在一边,忙活起了自己的事儿。
我想,上面那番话够他消化一段时间的了。
2
快到饭点了,办公室里突然热闹了起来,有人在大声讲电话,有人被踩了尾巴似的叫上一声,然后戛然而止,就像被一把剪刀剪断了声线一般,有人开始四处走动串联,但是我却感到背后有一种异样的寂静!果然,一回头,小王又找上门来了。
“马步君,你刚才说的是不错,堆栈不能设置得过大,也不能设置得过小,可是这好像等于什么也没有说一样嘛。归根到底,我该怎么设置堆栈的大小呢?”缓过神来的小王,突然发现我只是专业性地描述了问题,却没有给出问题的答案。
“孺子可教也,”小王的发问让我不禁有些凛然,我一边向他投去赞赏的目光,一边心下思忖该怎么样回答。思量片刻,我又开启了说教模式:
“可以通过静态分析的方式确定堆栈空间的尺寸。你需要根据源程序中每个函数的局部变量大小确定每个函数的堆栈使用量,然后根据编译器生成的函数调用列表为每个函数建立调用树,检查每棵调用树,确定从树根到树叶的调用路径的堆栈使用量,从中选出最大堆栈使用量,同时,还要仔细分析系统用到的所有中断,确定中断服务程序的堆栈使用量。”
看着他再次陷入懵圈状态,我满意地点了点头,鼓起腮帮子继续说教,
“但是,除了咱们自己写的程序,你所调用的C标准库函数以及大值整数的乘除、浮点运算等对应的运行库函数也会消耗堆栈,它们的堆栈使用量具体是多少我也不是很清楚,但是应该可以查得到。讲到这里你也看到了,这种静态分析方式对开发者的技术水平、对产品代码的理解程度要求非常高,得到的数据并不完善,而且这种方式依赖于具体的应用和源程序实现方式,所以,好麻烦!”
被说到怀疑人生的小王再次锁紧了眉头,抿着嘴唇一言不发,他在想什么我不清楚,但是我想:“按照我刚才的说法,我不是也不知道该怎么设置堆栈大小的嘛?哎,做人难,做嵌入式软件工程师更难啊!”
3
我本以为这件事到此结束了,没曾想吃完饭后,小王又找上门来了,“马步君,你刚才说了,通过静态分析判断堆栈使用量对程序员要求很高,而且不通用,那么,有没有一种动态的判断方式呢?”
“当然有了,可以在链接文件中,对RAM的空间分配做手脚。”我再次侃侃而谈起来,这边厢我吐沫飞溅,那边厢小王两眼放光。各位看官且先不要觉得笔者的思维实在敏捷、脑路不得了的灵光,而对笔者投来钦敬的目光。实际上,就在吃饭的空当,我就在苦苦地思索,到底该怎样,堆栈的空间分配才算适当。
如果不在链接文件中做任何设置,RAM就是堆栈区+全局变量区,这样一来,堆栈区以下便是全局变量区,堆栈的生长方向为自上而下,即向着RAM地址减小的方向增长,堆栈溢出时改变全局变量的值,可是很多情况下,你根本意识不到程序溢出,只有在特殊的触发条件下程序运行某个功能时,你才可能意识到不对劲。
所以,为了第一时间就检查到堆栈溢出,要加入一个紧邻堆栈区的新区,这个新区叫‘堆栈溢出缓冲区’。想一想哈,这时堆栈溢出时就会改变‘堆栈溢出缓冲区’的数据,只要我上电初始化时将‘堆栈溢出缓冲区’初始化为固定数据,然后定期查询这个新区中的数据,就能判断堆栈是否溢出,而且可以判断这一段时间内的最大堆栈使用量。”
我缓缓着解释着自己的思路,等着小王慢慢跟上来。过了一会儿,小王又猝不及防地发问了:“如果堆栈设置地比较大,不会发生溢出,那这个‘堆栈溢出缓冲区’也起不到什么作用,只会白白浪费RAM资源啊!”
好吧,我承认,当时确实被他问住了,但是,既然之前的思路已经打开,再打个小补丁就不算什么难事了。我思量片刻,就给出了让他满意的答案:
“可以在MCU上电初始化时,将堆栈区和堆栈溢出缓冲区的数据全部初始化为一个固定数据,比如0xa5,将最大堆栈使用量记为stack_max,然后用一个周期定时器定时读取堆栈溢出缓冲区和堆栈区的数据,就可以判断堆栈设置是否过大。
而且,第二次读取这两个区的数据时,从stack_max个数据后开始读取即可,比如上周期统计到堆栈用到100个字节,stack_max=100,下个周期从第101个字节开始读起就可以了。
如果你开始设置堆栈为384个字节,跑了一天后,发现stack_max=210,那就把堆栈设置为256就可以了。这样就能解决你的问题-科学合理地缩小堆栈分配了!”
4
过了几天,小王终于发现,‘原来’自己的程序用到的堆栈从来都不会超过130个字节,于是他乖乖地改小了堆栈,把空出来的100来个字节都分给了全局变量,RAM一下子绰绰有余了,他很开心地对我说:看来还是不要动辄滚粗,要先看看堆栈是否真的溢出!
评论