我是老温,一名热爱学习的嵌入式工程师,关注我,一起变得更加优秀!
各位嵌入式底层牛马,开工大吉啦!~
(记错上班时间了,都不知道什么时候才能改掉爱上班的老毛病,嗐~)
在嵌入式软件开发里面,特别是刚入门不久的嵌入式工程师,栈溢出是最常见并且极具破坏力的内存操作错误之一。
今天,我们就来聊一下嵌入式软件栈溢出的成因和风险,以及在写bug的过程中如何规避栈溢出。
一、什么是嵌入式软件的栈溢出?
嵌入式软件系统的内存,通常会被规划出堆(Heap)和栈(Stack)这两个核心区域,比如,我们使用malloc和free函数的时候会使用堆(Heap)内存空间,而对于函数传参或局部变量,会使用栈(Stack)内存空间。
栈是一种“后进先出(LIFO)”的内存数据结构,由系统自动分配与释放,主要是用于存储函数的参数、局部变量、函数返回值等临时数据。
嵌入式软件系统的栈空间大小,通常是固定且有限的(一般几KB到几十KB不等),当我们在编程的时候,试图使用的栈空间超过了系统额定的栈空间大小的时候,就会发生“栈溢出”,也就是 Stack Overflow。
二、什么情况下会出现栈溢出?
区别于PC端软件,嵌入式软件的内存资源比较有限,栈空间的使用通常需要谨慎且严格控制,因此,栈溢出的问题在嵌入式编程里面更容易触发,并且触发之后的后果更加严重。
(1)局部变量占用过大的栈空间:在栈上面定义超大的数组、结构体等数据类型,这样会直接耗尽栈空间。比如在函数内部定义char buffer[10240]数组,若系统的栈空间只有8KB,则会立刻导致栈溢出。
(2)函数递归调用且没有终止条件:在软件编程的时候进行函数递归调用,并且没有正确设置退出条件,函数内部的传参和局部变量就会不断压入函数栈帧,当嵌套层数超出栈容量,就会导致栈溢出。
(3)函数嵌套调用过深:多层函数在嵌套调用的时候,每个函数就会调用栈帧空间,当嵌套的层数超过栈容量的时候,就会导致栈溢出。
(4)栈空间分配过小:嵌入式系统在初始化的时候,分配到的栈内存空间不足以支撑程序的正常运行,即使出现常规操作,也可能会导致出现栈溢出。
三、栈溢出的风险与弊端。
当嵌入式软件系统发生栈溢出的时候,其危害通常都会大于PC端软件系统,通常会导致以下风险弊端:
(1)程序崩溃与系统死机:栈溢出会覆盖函数的返回地址,从而导致程序函数跳转到错误的地址执行,会直接引发程序崩溃和系统复位。
(2)数据篡改与功能异常:溢出的栈内存数据,会对相邻内存区域的关键数据进行覆盖(比如寄存器值或全局变量等等),从而导致外设控制异常或数据计算错误,从而导致设备运行异常或崩溃。
(3)系统安全问题:恶意攻击者可能会利用栈溢出注入恶意代码并运行,从而篡改程序的执行流程,窃取设备数据或控制设备,这样对于联网的嵌入式设备而言风险极高。
(4)调试难度非常大:嵌入式软件栈溢出之后,导致的程序运行错误并没有固定的表现形式,往往容易在非溢出代码的地方产生运行异常,这会增加排查的难度。
四、代码示例,栈溢出的触发与规避
我们可以通过以下几个代码示例,来说明一下嵌入式软件有哪几种栈溢出情况,以及如何进行规避。
示例1:超大局部数组。
#include <stdio.h>// 嵌入式系统栈空间通常为8KB(8192字节),该函数声明的数组超出栈容量void stack_overflow_demo1() {// 声明10KB的局部数组,直接占用栈空间超出上限char large_buffer[10240];// 写入数据时触发栈溢出for (int i = 0; i < 10240; i++) {large_buffer[i] = 'a';}printf("数组赋值完成n"); // 大概率无法执行到此处}int main() {stack_overflow_demo1();return 0;}
示例2:无终止递归调用。
#include <stdio.h>// 无退出条件的递归函数,不断压栈导致溢出void recursive_overflow(int count) {int local_var = count; // 每次递归都会创建局部变量,占用栈帧// 无终止条件,递归会无限进行recursive_overflow(count + 1);}int main() {recursive_overflow(0);return 0;}
示例3:栈溢出的规避方案。
#include <stdio.h>#include <stdlib.h> // 包含malloc/free函数// 方案1:使用堆内存替代大局部数组void avoid_overflow1() {// 堆内存(malloc)不受栈空间限制,按需分配char *large_buffer = (char *)malloc(10240 * sizeof(char));if (large_buffer == NULL) { // 检查内存分配是否成功,避免空指针printf("内存分配失败n");return;}for (int i = 0; i < 10240; i++) {large_buffer[i] = 'a';}free(large_buffer); // 手动释放堆内存,避免内存泄漏large_buffer = NULL; // 清空指针,防止野指针}// 方案2:递归添加终止条件,避免无限递归void safe_recursive(int count, int max_count) {if (count >= max_count) { // 终止条件:达到最大递归次数则退出return;}int local_var = count;safe_recursive(count + 1, max_count); // 可控的递归深度}int main() {avoid_overflow1();safe_recursive(0, 100); // 限制递归深度为100,避免栈溢出return 0;}
五、栈溢出通用的规避方法
嵌入式软件工程师在进行软件编程的时候,就要开始注意规划栈空间的使用,通常使用以下几种通用方法来规避栈溢出。
(1)合理规划栈空间:根据程序的实际需求来配置栈空间的大小,避免配置过小而不够用,但同时也需要注意避免配置过大,挤占堆内存或其他内存区域,栈内存空间的分配需要合理。
(2)栈空间减少大对象存储:不要在函数内部定义大数组或大结构体,要将这些大内存对象放在堆内存(malloc/free分配)或全局/静态变量区域,栈空间仅仅存储必要的小变量。
(3)控制好函数的调用深度:需要时刻注意避免函数过深的嵌套调用,如果使用了递归函数,要设置明确的终止条件,并且限制其递归的深度。
(4)代码审查与静态分析:使用静态代码分析工具(比如Cppcheck、Lint)检测栈溢出的风险,重点检查一下超大型局部变量以及无终止条件的递归调用等场景。
(5)运行时监控:在关键的函数内部添加栈空间使用量监测,用来实时监控栈的剩余内存空间,这样可以提前预知栈溢出风险。
六、总结
嵌入式软件栈溢出的核心原因,通常是栈空间的使用超过了系统分配的固定上限,常见于函数内部定义超大局部变量、函数无限递归调用,等场景。
栈溢出通常都会导致程序运行崩溃、数据被篡改、以及系统安全等问题,在内存资源受限的嵌入式设备里面,危害极大。
有效规避栈溢出的关键,是需要嵌入式软件工程师合理规划栈空间,减少栈上大对象存储,控制好函数的调用深度,并且通过静态分析和运行时监控,进行提前防范。
祝各位嵌入式软硬件牛马工程师,新的一年,硬件无误,软件无bug!
203