什么是堆栈溢出?如何分配堆栈空间大小?

描述

 

1、正文部分

1先说几句 

前些日子bug交流群里的小哥调试了一个堆栈溢出的bug,动不动数据就被篡改了,应该也是搞得焦头烂额,头皮发麻!当时bug菌看了下,于是抛出了自己的一些调试经验,一般这样的问题80%是越界和堆栈溢出造成的,没想到还真是堆栈溢出。

函数

函数

  所以对于一些问题的处理不仅仅是经验的积累,还需要多多交流!堆栈溢出问题bug菌和他算是“老朋友”了,所以非常想让相关文章跟大家见面,没想到这几天事情颇多,每天回家都没有太多的精力去更文,但是作为一名有态度的号主还是要坚持为大家带来点东西!

2理一理堆栈溢出

1堆栈名称

认识堆栈溢出首先我们要知道什么是" 堆栈 " ?  堆栈从名字上理解似乎是堆和栈的结合,而我们在数据结构中知道堆和栈是两种不同的数据结构,但这里的堆栈指的仅仅是栈,从英文名我们就可以知道 : 堆栈(stack)和堆(heap) , 至于把stack叫做堆栈是有一定的历史和翻译原因的,bug菌就不追溯了。   函数 对于栈,在bug菌的往期文章中也有提及,其实就是一种先进后出的数据结构;而在CPU层面有着堆栈寄存器,push和pop堆栈操作指令等等都是用于操作栈区的。 在C语言环境中栈是为了保存现场的信息,当程序需要执行函数调用,任务切换等等都会把相应的数据push到栈中,一旦回到原来函数和任务又会pop弹出之前的数据继续往下执行。 但栈是有具体大小的,一旦入栈的数据过多,就会导致罪恶的"堆栈溢出"问题。

2图解堆栈溢出

来我们首先看一个函数:

void RecvData(void);
{
    int Cnt;
    int Buff[6];
    ......
    do something...
}
这样的代码打死我也不敢相信会有什么大问题,然而一名经验老道、饱经bug洗礼的嵌入式程序员会自然而然的考虑是否有堆栈溢出的风险,如下图所示:  

 

函数

上图就不区分堆栈增长方向了,仅仅只是表述堆栈溢出现象,由于SP_end以外的内容未知,一般都由编译器分配决定,如果编译器把重要数据分配到此区域,一旦程序访问到Buff[3]往下的数据便会导致数据篡改,从而程序发生一些奇怪的行为,甚至奔溃。

那么很多朋友就会想,直接给这个任务或者系统分配一个1024或者4096个字节的堆栈,这总不会造成堆栈溢出了吧!我只想说:"你太秀了!"。

3如何分配堆栈空间大小

1堆栈内容

盲目的分配过大的堆栈空间,无非就是对资源的浪费。如果你的项目能够让你这样任性,那你们产品成本估算就真是个形式。所以合理的分配堆栈大小是非常重要的,首先我们得看看堆栈中主要放些什么 ?

局部变量的分配。

函数调用嵌套的返回地址等等数据的push,这个需要根据具体的CPU进行函数调用约定来进行分析。

函数的参数,因为有时候编译器为了增加执行效率会把相关参数放在寄存器中传递,但是毕竟这样的寄存器有限,过多的参数还是会通过堆栈来传递。

当我们触发中断CPU一般会自动把相应的信息压入堆栈中,从而保存中断现场。

对于RTOS进行任务切换、中断等过程中一般系统仅自动保存了部分寄存器等信息,而为了全面的保存好现场,还需要手动的压入一些其他的信息,比如stm32中的FPU相关寄存器信息等。

2计算最大堆栈空间难题

有了前面堆栈中放了些啥的分析,要确定堆栈的空间大小自然而然的就会想到把一个个加起来算堆栈最大暂用情况,算出该值以后预留一定的空间就再合适不过了。

现在对于比较强大的IDE,比如keil和IAR,都可以提供计算堆栈占用最大的情况,而对于我们采用函数指针这样的间接调用函数的方式或者是C嵌入式汇编等等,那IDE也无能为力。

更加可怕的是使用printf这种可变参数的函数,其堆栈的占用情况是根据参数的多少而动态变化的,其并不那么容易确定。

当然还有最让bug菌难以忘记的情况 : 递归 , 递归就是反复的函数调用,那么一系列的返回现场数据都会压入栈中,堆栈占用情况也是未知的,所以在嵌入式中使用递归一定要限制递归的深度,防止堆栈溢出。

4确定堆栈大小的好办法

既然正面计算堆栈占用最糟糕的情形如此麻烦,那我们从侧面出击,那就是我们常用的检测堆栈使用峰值法,实时的采集和输出堆栈的使用信息,我们根据堆栈的最大值*1.5倍的样子,基本上就可以把堆栈大小确定下来。

像目前的RTOS(如ucos、freertos等)都提供了对应的堆栈信息输出API,比如ucos中的OSTaskStkChk函数 : 

 

typedef struct os_stk_data 
{
    INT32U  OSFree;/* Number of free entries on the stack*/
    INT32U  OSUsed;/* Number of entries used on the stack                          */
} OS_STK_DATA;
......
INT8U OSTaskStkChk (
                    INT8U prio,
                    OS_STK_DATA *p_stk_data
                    );


 

通过调用该函数获得已经使用的和没有使用的堆栈大小,便可以获得堆栈的使用情况,如:

堆栈占用率 = (OSUsed/(OSUsed + OSFree)) * 100%

从而可以将该参数输出作为我们评估每个任务分配的堆栈是否合适,当然你需要让程序运行足够长的时间和尽量多的情况,从而获得最差的情况,再考虑预留>20%的空间,最终重新调整每个堆栈大小到合适状态。

版权声明:本文来源公众号最后一个bug

  审核编辑:汤梓红


 

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分