在嵌入式中的栈溢出
单片机栈溢出指程序调用栈(存储函数调用、局部变量等)超出预设内存空间,导致数据覆盖或程序崩溃的情况。减少栈空间的使用需求,可以从堆空间分配内存函数内部不要传递大量的数据,可以使用指针减少函数的调用层次,谨慎使用递归函数对于软件检测方法来说,有一定的缺点,需要进行轮询判断是否越过界限或者发生了读写、需要牺牲一定的内存空间作为越界区域。对于硬件检测方法来说,优点是可以及时响应异常,缺点就是。
什么是栈溢出
案例:
char test_char = 0; //已初始化的全局变量,存储在.data段
char i = 0; //已初始化的全局变量,存储在.data段
int main(void) {
char test_data[1648]; //函数内的局部变量,存储在栈空间
test_char = 1;
test_data[0] = 100;
while (1) {
i++;
}
}
这一段代码初看感觉没有什么大问题,但是如果考虑到堆栈情况,这段程序中变量test_char的值可能会在运行一段时间后发生改变,尽管程序中没有改变该变量的值。如果test_char变量恰好是比较主要的数据,那么对系统的破坏是很大的,甚至导致系统崩溃。
那么为什么会出现这种情况?首先要了解函数内的变量存储在哪一个地方。
由图可以知道,函数内的局部变量存储在栈空间中,对于栈空间的大小及RAM的分布情况,我们可以通过当前工程中的.map了解
Total RO Size (Code + RO Data) 780 ( 0.76kB)
Total RW Size (RW Data + ZI Data) 1648 ( 1.61kB)
Total ROM Size (Code + RO Data + RW Data) 796 ( 0.78kB)
如上图,可以看到当前RW就是1648个字节(RW区域存在RAM区),已经占据了整个RAM
找到.map文件查看RAM区的数据分布,如下
Execution Region RW_IRAM1 (Exec base: 0x20000000, Load base: 0x0800030c, Size: 0x00000670, Max: 0x0000c000, ABSOLUTE)
Exec Addr Load Addr Size Type Attr Idx E Section Name Object
0x20000000 0x0800030c 0x00000002 Data RW 11 .data main.o
0x20000002 0x0800030e 0x00000002 PAD
0x20000004 0x08000310 0x00000009 Data RW 300 .data stm32f1xx_hal.o
0x2000000d 0x08000319 0x00000003 PAD
0x20000010 - 0x00000060 Zero RW 1352 .bss c_w.l(libspace.o)
0x20000070 - 0x00000200 Zero RW 2 HEAP startup_stm32f103xe.o
0x20000270 - 0x00000400 Zero RW 1 STACK startup_stm32f103xe.o
可以看到,当前栈空间大小为0x400,即1024个字节,显然,在main函数内设置的大数组char test_data[1648]已经超过了这个范围,但是我们当前的编译器不会报错,所以导致系统存在隐含的问题。
所以为什么test_char这个全局变量被改变了呢
前面我们提到了全局变量存放在.data区域,根据上面的.map文件RAM区数据分布可以发现,对于目标main.o,我们定义了两个char类型的全局变量,存放在了.data区域,占用两个字节。(后面的PAD为自带的填充,目的是为了对齐四个字节)
那么RAM空间的数据如下图所示

可以看到,大数组的定义冲破了栈区践踏了变量test_char的内存空间,它们共用了一个内存空间,导致数组赋值的时候将全局变量的值也改变了。
【扩展】关于变量在内存中的存放顺序

【扩展】查看启动文件及.map文件

也可以通过双击工程文件名称打开.map文件

在这里可以查看栈大小,这里的栈顶地址即0x20003090 + 0x400 = 0x20003490
栈溢出的解决办法
1.改变变量存放空间
将变量添加static修饰,使其存储在静态变量段.data
将变量变为全局变量,使其存储在.data
2.增加栈空间的大小
修改Stack_Size的大小
栈溢出的检测手段
-
软件检测
软件预设区域
软件预设检测法就是在堆栈的末端规定一段内存区域,然后在程序运行前先采用特殊的数值对该部分内存区域进行填充,系统软件定时或者定期的比对这一部分规定的内存区域与所设置的特殊值是否一致,一旦检测不一致大概率说明堆栈已经使用到了末尾(这里为什么说大概率呢?因为你程序中起飞的指针也有可能篡改这一部分数据!),所以也就说明你之前分配的堆栈空间不够。

FreeRTOS中也是大概的思想来检测栈溢出
获取栈顶指针判断是否超过范围
栈顶指针的获取
/**
* @brief Return the Main Stack Pointer
*
* @return Main Stack Pointer
*
* Return the current value of the MSP (main stack pointer)
* Cortex processor register
*/
__ASM uint32_t __get_MSP(void)
{
mrs r0, msp
bx lr
}
根据前面.map文件查看的栈空间范围,通过判断获取栈顶指针的值是否在该范围即可
硬件检测
Stack Limit寄存器
ArmV8-M架构中包含了一种新的特殊寄存器Stack Limit寄存器,该寄存器可以用于检测Stack of Overflow
可以设置相关值SP_Limit来告诉硬件栈底位置,当SP小于该值时则说明发生了栈溢出,产生异常中断

MPU内存保护单元(Memory Protection Unit)
许多MCU集成了MPU内存保护单元,通过配置MPU属性,当SP访问到指定的MPU保护内存时,会触发中断错误

DWT数据监视点和跟踪单元(Data Watchpoint and Trace Unit)
DWT中存在最多4个可产生数据监视点、数据跟踪和多个概况计数器的硬件比较器。

通过配置比较器,可以实现监控特定数据地址,当发生读写操作时会产生异常中断,可以在异常中断处打印调试信息或者进行补救措施。
这里可以通过配置实现类似MPU的操作,保护特定的一段内存区域(配置COMP和MASK寄存器),当发生读写时就进入异常中断(配置FUNCTION寄存器)。
总结
单片机栈溢出指程序调用栈(存储函数调用、局部变量等)超出预设内存空间,导致数据覆盖或程序崩溃的情况。
减少栈溢出的方法:
-
减少栈空间的使用需求,可以从堆空间分配内存
-
函数内部不要传递大量的数据,可以使用指针
-
减少函数的调用层次,谨慎使用递归函数
栈溢出检测方法:
对于软件检测方法来说,有一定的缺点,需要进行轮询判断是否越过界限或者发生了读写、需要牺牲一定的内存空间作为越界区域。
对于硬件检测方法来说,优点是可以及时响应异常,缺点就是Stack Limit寄存器在ArmV8-M架构中才有,部分MCU没有MPU功能,而DWT的硬件比较器只有四个,对于RTOS多任务栈的情况要“精打细算”。

参考文章
【1】案例对应文章
【2】STM32 map文件解析_mdk .map文件的 pad是什么-CSDN博客
更多推荐




所有评论(0)