函数内部分配的buffer过大导致堆栈溢出

简介: 函数内部分配的buffer过大导致堆栈溢出

最近在调试一个模块的时候,先是IAR配置的环境,操作很正常,在keil简单移植操作以后老是在函数调用的时候,导致log实时数据出问题,找了好多天,修改了好几个版本,最后面终于确认在局部变量的内存溢出,具体细节是函数体内部分配一个2K+2byte的局部变量,超过启动文件分配的栈空间的大小导致了内存溢出,程序有时候会进入hardfault有时候正差运行但都伴随log数据失败。

溢出的原因我们找到了,但是为什么2k的buffer分配内存就会溢出呢,要知道手里用的MCU的FLASH有128k,RAM有16K大小,理论上使用是足够的

4edc953e2c684bbe819ffa954c899c08.png

这要从程序编译成代码的原理进行分析,程序的几个预编译、编译、汇编、链接几个步骤就不多做赘述,不了解的大家可以自行搜索,言归正传,程序在转为二进制的代码时候不仅在逻辑上语言类型上进行转换,在一些变量上也通过不同的修饰符来进行分配其在代码中或者在程序运行的时候所处的位置,这个时候出现了ROM区以及RAM区的变量。先用一个例子进行展示编译器如何识别定位变量的位置:

int *p1,*p2;
int  val_b = 2;   //全局变量 栈(程序自动分配 自动释放)
int  val_e ; // 未初始化的全局变量 栈
char s[]="abc";   //全局变量 栈
char *p3= "1234567";   //全局变量 在文字常量区ROM(FALSH)
const int  val_c = 3 ;  // const只读数据段 全局变量 ROM(FALSH)
static int val_a=0 ;   //全局变量 初始化的静态初始化区 栈
static int  val_d ;     // 未初始化的静态变量 栈
p1= (char *)malloc(10);   //全局变量 堆区(由程序员分配)
int main() //此处按照普通函数进行解释
{
    static int val_f = 5;  // 初始化的局部静态变量 栈
    static int val_g;      //未初始化局部静态变量 栈
    int    val_h = 6;      //初始化局部变量 栈
    int    val_i;          //未初始化局部变量 栈
    const  int val_j = 7;  //const局部变量 栈
    p2= (char *)malloc(10);   //局部变量堆区(由程序员分配)
  while(1)
  {
  }
  free(p2); 
    return 0;
}
free(p1);

除去ROM区变量剩下的都在RAM区,其余变量在芯片上电之后在RAM执行。

①static无论是全局变量还是局部变量都存储在全局/静态区域,在编译期就为其分配内存,在程序结束时释放,例如:val_a、val_d、val_f、val_g。

②const全局变量存储在只读数据段,编译期最初将其保存在符号表中,第一次使用时为其分配内存,在程序结束时释放,例如:val_c;const局部变量存储在栈中,代码块结束时释放,例如:val_j。

③全局变量存储在全局/静态区域,在编译期为其分配内存,在程序结束时释放,例如:val_b、val_e。

④局部变量存储在栈中,代码块结束时释放,例如:val_h、val_i。

注:当全局变量和静态局部变量未赋初值时,系统自动置为0

编译之后内存的情况

4edc953e2c684bbe819ffa954c899c08.png

(1)栈区(stack):由编译器自动分配和释放,存放函数的参数值、局部变量的值等,其操作方式类似于数据结构中的栈。

(2)堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。分配方式类似于数据结构中的链表。

(3)全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统自动释放。

(4)文字常量区:常量字符串就是存放在这里的。

(5)程序代码区:存放函数体的二进制代码。

单片机的堆和栈是分配在RAM里的,有可能是内部也有可能是外部,可以读写;栈:存函数的临时变量,即局部变量,函数返回时随时有可能被其他函数栈用。所以栈是一种分时轮流使用的存储区,编译器里定义的Stack_Size,是为了限定函数的局部数据活动的范围,超过这个范围,程序就会跑飞,也就是栈溢出;

Stack_Size不影响Hex,更不影响Hex怎么运行的,只是在Debug调试时会提示错。栈溢出也有是超过了边界进行所以单片机代码在函数里定义一个大数组 int buf[2048],栈要是小于2K= 2048=0x400会导致内存溢出,导致很多不可知的bug,例如溢出到其他外设的寄存器进行操作。

堆:存的是全局变量,这变量理论上是所有函数都可以访问的,全局变量有的有初始值,但这个值不是存在RAM里的,是存在Hex里,下载到Flash里,上电由代码(编译器生成的汇编代码)搬过去的。上电的时候就分配一块很大内存大的RAM(Heap_Size),作为(malloc_init),别人用只能通过 (malloc)去分配,用完还得(free),否则一不小心内存都使用光了。所以一旦有需要分配很大的内存是编译器里必须定义Heap_Size,否则malloc 也没有实际效果。

从这里可以想到函数体内部分配临时变量超过限制,可能需要调整 STACK SIZE。

而ST系列的堆栈一般都在在头文件或者在IDE进行配置(IAR),本人习惯在在头文件去看堆栈的大小,一般在在STM32Fxxxxx.s文件中(对应的头文件)

修改Stack_Size EQU 0x00000200 (keil 默认1k)

修改Heap_Size EQU 0x00000400 (keil 默认2k)

在IDE开发的时候我们有一处可以很方便的看到我们程序使用内存空间的大小

就是在下方的编译窗口信息。

eg:

keil

4edc953e2c684bbe819ffa954c899c08.png

IAR

4edc953e2c684bbe819ffa954c899c08.png

eclipse

4edc953e2c684bbe819ffa954c899c08.png

其中Code 或者 .text为程序代码部分

RO-data 表示 程序定义的常量 const temp;

RW-data 或者 .data表示 已初始化的全局变量

ZI-data 或者 .bss表示 未初始化的全局变量

Total RO Size (Code + RO Data)

Total RW Size (RW Data + ZI Data)

Total ROM Size (Code + RO Data + RW Data)

**这块map文件详解以及数据分配地方的详解推荐大家看哈[strongerhuang的CSDN或者公众号]**讲的很详细。

也可以关注公众号嵌入式Linux,这些大佬都写的很详细。

4edc953e2c684bbe819ffa954c899c08.png

前情提要就是这些帮助大家理解问题

我也是通过看文章以及和几位大佬聊天测试之后发现了问题:

通过以上了解我用了三种方法来解决我的问题。

1. 通过const 修饰,将分配的内存置于FLASH空间执行

2. 扩大堆区分配最小值实现

3. 扩大栈区分配最小值实现

注释:前两种都是在RAM分配实现,后一种是在FLASH空间实现(本次解决问题的测试环境都在keil下进行)

4edc953e2c684bbe819ffa954c899c08.png

初始化程序什么都没有操作的情况下 程序容量大小

第一种方法: 我还是依旧在函数内部定义一个buffer,首先判断buffer被分配到哪个区(在ROM 区)

4edc953e2c684bbe819ffa954c899c08.png4edc953e2c684bbe819ffa954c899c08.png

发现RO Data明显增加2K,所以确认是在ROM区。放置于ROM(FLASH)区一般要需要程序读写要求不高的数据,因为成本的问题,一般MCU 的ROM(FLASH)区远大于RAM,放置于FLASH基本不需要担心溢出等的问题。由于我的数据是固定的,所以我用const也可以进行成功操作。

但是切记放于ROM代码只读不可写,如果你的变量需要读写操作,此种操作是不适合的,可以看接下来的其他两种实现方法。

设置完也可以在调试call+stack或者watch窗口看数据的地址:

4edc953e2c684bbe819ffa954c899c08.png

ST一般0x08000000这样0x8打头的地址为FASH区(ROM),0x20000000 这样0x2打头的地址为RAM区,不同的芯片请看对应的数据手册

4edc953e2c684bbe819ffa954c899c08.png

第二种方法: 通过在堆区进行设置大小,从而使空间足够用(RAM区)

4edc953e2c684bbe819ffa954c899c08.png

发现其中ZI-data明显变大置于RAM 区的空间

记得前面提到过,在stm32的启动文件中有堆栈大小的分配,但是默认比较小一个1k一个2k,当我们分配2K大小的一个buffer的时候空间是不够的,所以我们要去把堆的空间变大。

启动文件的堆栈默认值:

4edc953e2c684bbe819ffa954c899c08.png

此次需要malloc一个2k 的内存,属于heap区,而初始化默认的堆区的分配大小只有1K,所以需要需要进行安排需要修改,此处堆区所需的内存大家可以设置稍微大些,不要刚刚好,也容易出现问题。

4edc953e2c684bbe819ffa954c899c08.png

第三种方法:通过在栈区进行设置大小,从而使空间足够用(RAM区)

4edc953e2c684bbe819ffa954c899c08.png4edc953e2c684bbe819ffa954c899c08.png

同样的char定义一个buffer之后可以看到RAM空间变大,而定义的变量属于栈区所以我们需要增加栈的空间分配。

此次需要一个2k 的buffer,属于stack区,而初始化默认的堆区的分配大小只有2K,所以需要需要进行安排需要修改,此处栈区所需的内存大家也可以设置稍微大些

按照空间大小修改之后情况:

4edc953e2c684bbe819ffa954c899c08.png

程序成功运行,希望对大家有用,谢谢。

目录
相关文章
|
4月前
|
人工智能 Java 5G
常见的Java内存溢出情况和实例
常见的Java内存溢出情况和实例
|
1月前
|
缓存
非连续内存分配
非连续内存分配
17 0
|
9月前
|
Java
16-内存分配与回收策略-对象优先分配Eden+大对象进老年代
大多数情况下, 对象在新生代Eden区中分配。 当Eden区没有足够空间进行分配时, 虚拟机将发起一次Minor GC。HotSpot虚拟机提供了-XX: +PrintGCDetails这个收集器日志参数, 告诉虚拟机在发生垃圾收集行为时打印内存回收日志, 并且在进程退出的时候输出当前的内存各区域分配情况。 在实际的问题排查中, 收集器日志常会打印到文件后通过工具进行分析 。
80 0
16-内存分配与回收策略-对象优先分配Eden+大对象进老年代
|
安全 C++
C++指针的内存分配与内存安全
C++指针的内存分配与内存安全
106 0
|
存储 缓存 算法
【JVM深度解析】对象的分配策略栈上分配与TLAB
JVM是如何自动进行内存管理的呢?本文详细对象的分配策略,栈上分配与TLAB,相信相信大家看完已经掌握JVM是如何管理,本文适合点赞+收藏。
【JVM深度解析】对象的分配策略栈上分配与TLAB
|
监控 算法 Java
请问什么时候对象分配会不在 TLAB 内分配
请问什么时候对象分配会不在 TLAB 内分配
请问什么时候对象分配会不在 TLAB 内分配
|
缓存 算法 Java
JVM05_堆的概述、内存结构、复制算法、Minor|Major|Full GC、私有区域TLAB、对象分配、参数总结、逃逸分析、栈上分配、锁消除、标量替换(二)
⑤. 复制算法 ⑥. Minor GC | Major GC | Full GC ⑦. 针对不同年龄阶段的对象分配原则 ⑧. TLAB(Thread Local Allocation Buffer)
110 0
JVM05_堆的概述、内存结构、复制算法、Minor|Major|Full GC、私有区域TLAB、对象分配、参数总结、逃逸分析、栈上分配、锁消除、标量替换(二)