扫码加入

  • 正文
  • 相关推荐
申请入驻 产业图谱

嵌入式牛马开始写bug,一不小心就栈溢出了!

1小时前
203
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

我是老温,一名热爱学习的嵌入式工程师,关注我,一起变得更加优秀!

各位嵌入式底层牛马,开工大吉啦!~

(记错上班时间了,都不知道什么时候才能改掉爱上班的老毛病,嗐~)

嵌入式软件开发里面,特别是刚入门不久的嵌入式工程师,栈溢出是最常见并且极具破坏力的内存操作错误之一。

今天,我们就来聊一下嵌入式软件栈溢出的成因和风险,以及在写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!

 

相关推荐