【进阶】" 堆栈溢出 ",也就这么回事!
1、聊一聊
虽然没有第13个月,但和大家有着无数个明年,就用一首《飞鸟和蝉》告别2020年的一切吧!
2、正文部分
1
先说几句
2
理一理堆栈溢出
1
堆栈名称
2
图解堆栈溢出
void RecvData(void);
{
int Cnt;
int Buff[6];
......
do something...
}
上图就不区分堆栈增长方向了,仅仅只是表述堆栈溢出现象,由于SP_end以外的内容未知,一般都由编译器分配决定,如果编译器把重要数据分配到此区域,一旦程序访问到Buff[3]往下的数据便会导致数据篡改,从而程序发生一些奇怪的行为,甚至奔溃。
那么很多朋友就会想,直接给这个任务或者系统分配一个1024或者4096个字节的堆栈,这总不会造成堆栈溢出了吧!我只想说:"你太秀了!"。
2
如何分配堆栈空间大小
1
堆栈内容
局部变量的分配。
函数调用嵌套的返回地址等等数据的push,这个需要根据具体的CPU进行函数调用约定来进行分析。
函数的参数,因为有时候编译器为了增加执行效率会把相关参数放在寄存器中传递,但是毕竟这样的寄存器有限,过多的参数还是会通过堆栈来传递。
当我们触发中断CPU一般会自动把相应的信息压入堆栈中,从而保存中断现场。
对于RTOS进行任务切换、中断等过程中一般系统仅自动保存了部分寄存器等信息,而为了全面的保存好现场,还需要手动的压入一些其他的信息,比如stm32中的FPU相关寄存器信息等。
2
计算最大堆栈空间难题
有了前面堆栈中放了些啥的分析,要确定堆栈的空间大小自然而然的就会想到把一个个加起来算堆栈最大暂用情况,算出该值以后预留一定的空间就再合适不过了。
现在对于比较强大的IDE,比如keil和IAR,都可以提供计算堆栈占用最大的情况,而对于我们采用函数指针这样的间接调用函数的方式或者是C嵌入式汇编等等,那IDE也无能为力。
更加可怕的是使用printf这种可变参数的函数,其堆栈的占用情况是根据参数的多少而动态变化的,其并不那么容易确定。
当然还有最让bug菌难以忘记的情况 : 递归 , 递归就是反复的函数调用,那么一系列的返回现场数据都会压入栈中,堆栈占用情况也是未知的,所以在嵌入式中使用递归一定要限制递归的深度,防止堆栈溢出。
3
确定堆栈大小的好办法
既然正面计算堆栈占用最糟糕的情形如此麻烦,那我们从侧面出击,那就是我们常用的检测堆栈使用峰值法,实时的采集和输出堆栈的使用信息,我们根据堆栈的最大值*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%的空间,最终重新调整每个堆栈大小到合适状态。
3、结束语
推荐好文 点击蓝色字体即可跳转
☞ 【硬壳】C程序里面嵌点"机器码"玩一玩"(小知识揭露大道理)