文章目录
内存管理的概念
内存管理,是指软件运行时对计算机内存资源的分配和使用的技术。
- 主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。
- 内存管理的实现方法有很多种,他们其实最终都是要实现 2 个函数:malloc 和 free;
- malloc 函数用于内存申请,free 函数用于内存释放。
进程内存分区
不管什么样的操作系统、什么样的计算机架构,进程使用的内存都可以按照功能大致分成以下4个部分。
(1)代码区:这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指令并执行。
(2)数据区:用于存储全局变量等。
(3)堆区:进程可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区。动态分配内存和回收内存是堆区的特点。
(4)栈区:用于动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行。
进程执行过程
在Windows平台下,高级语言写出的程序经过编译链接,最终会变成所谓的PE文件。当PE文件被装载运行后,就成了所谓的进程。
- PE文件代码段中包含的二进制级别的机器代码会被装入内存的代码区(.text)
- 处理器将到内存的这个区域一条一条地取出指令和操作数,并送入算术逻辑单元进行运算;
- 如果代码中请求开辟动态内存,则会在内存的堆区分配一块大小合适的区域返回给代码区的代码使用;
- 当函数调用发生时,函数的调用关系等信息会动态地保存在内存的栈区,以供处理器在执行完被调用函数的代码时,返回母函数。
程序中所使用的缓冲区可以是堆区、栈区和存放静态变量的数据区。缓冲区溢出的利用方法和缓冲区到底属于上面哪个内存区域密不可分。
STM32 内存分区
STM32内存分为3块区域:全局/静态变量区、栈区、堆区。
- 其中全局/静态变量区用于存放全局/静态变量(包括指针变量)
- 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统自动释放。
- 栈区用于存放当前运行的函数及其中定义的局部变量和程序指针等
- 定义的局变量占用栈空间,当数据较多,栈空间使用完,则会占用堆空间,甚至其他全局变量空间,造成程序崩溃或数据错误(内存溢出错误)
- 堆区用于存放动态申请的内存,即malloc的内存。
- 文字常量区:常量字符串就是存放在这里的。
- 程序代码区:存放函数体的二进制代码。
内存首先存放/开辟全局变量区域,然后开辟栈区最后开辟堆区。
STM32F407内存结构
stm32F407存储器映像图:
STM32的内存(存储器)的地址空间大小为4G(0x0000 0000 ~ 0xFFFF FFFF),被分为8个block(block0~block7),每个block为512Mbyte
(出自STM32F407ZGT6数据手册P61)
- block 0共有512M其中Reserved即为保留区,即可以使用外部flash扩展这个块的存储空间,只要地址在范围内就可以。
- block 1名称就是SRAM,所以这个区域就是用来放置RAM。而图中的64KBSRAM就是stm32内置的RAM,MCU的基本配置就是得有CPU+RAM+外设。
- 可以看到这个块也有512MB,所以这个RAM也可以通过外部扩展至512MB,当然外部扩展的肯定不如内置的好用了。
- 它就是运行内存,就和电脑、手机上经常说的几G内存是同一个东西。它的作用就是把flash(这里只对于stm32来说)里正在运行的代码段(函数、变量等等)放进这个内存里,然后CPU对这里面的数据进行读写操作。
- 内有SARM1(112KB,0x2000 00000 ~ 0x2001 BFFF)和SRAM2(16KB, 0x2001 C000 ~ 0x2001 FFFF)两块连续的SRAM,可供所有的AHB 主控总线访问。
- 为什么是两块SRAM?这是因为主总线支持并发SRAM访问,提高执行效率。例如当 CPU 对 112 KB SRAM 进行读/写操作时,以太网MAC 可以同时对 16 KB SRAM 进行读/写操作;或者CPU和DMA可以同时访问不同的SRAM。
- MCU的堆、栈也是属于这片区域的,这里所说的堆栈和数据结构的堆栈概念不能混为一谈,当然也是类似的。
- 也就是说堆栈地址在0x20000000~0x2000FFFF范围内,通过查找.map文件关键字:__initial_sp可以找到栈地址。
- 下面倒数第3个Flash对应的地址是0x08000000~0x0807FFFF,计算得出这部分空间为:512KByte,这个地址就是用来存储代码的;
- 0x0800 0000开始的flash,是我们代码烧录的地方
- 从0x2000 0000开始的sram,s是我们程序执行的内存地址。
堆栈
堆栈是由栈(Stack)和堆(Heap)组成的,汇编中应用的 PUSH 和 POP 就是对 栈(Stack)的操作,其按照后进先出(LIFO-Last In First Out)的原理运作。
STM32堆栈
堆栈是一个特定的存储区或者寄存器。一般在内存总开辟一块区域作为堆栈,叫做软件堆栈;用寄存器构成的堆栈,叫硬件堆栈。大多数情况下,我们使用的都是软件堆栈。
-
普通单片机启动时,不需要用bootloader将数据 从ROM搬移到RAM。 但是STM32单片机需要。
- 这是因为RAM取数的速度是远高于ROM的,但是普通单片机因为本身运行频率不高,所以从ROM取指令慢并不影响。而STM32的CPU运行的频率高。
-
计算机系统的程序在存储状态时位于硬盘,执行的时候甚至会把上述的 RO 区域 (代码、只读数据) 加载到内存,加快运行速度,还有虚拟内存管理单元 (MMU) 辅助加载数据,使得可以运行比物理内存还大的应用程序。
- 而 STM32 没有 MMU,所以无法支持Linux 和 Windows 系统。
软件堆栈
32启动文件中的堆栈就是软件堆栈。
- 在存储空间上,堆栈属于RAM空间的一部分,堆栈用于函数调用、中断切换时保存和恢复现场数据。
- 这些存储单元的地址被记在了一个叫做堆栈指针(SP)的地方。
- 堆栈指针的最低两位永远是 0,这意味着堆栈总是 4 字节对齐的。
- 堆栈指针的最低两位永远是 0,这意味着堆栈总是 4 字节对齐的。
堆与栈区别
在mcu中,heap和stack的使用者是不同的。
-
stack(栈):由系统自动分配释放,存放的函数的参数值,局部变量的值。这个空间用户操作不了的。
-
heap(堆):由用户分配及释放,调用malloc 和free时操作的空间就是堆空间。
-
栈是从高到低分配,堆是从低到高分配。
-
栈区的首地址是编译器自动分配的,栈首地址=全局区域大小+栈大小(Stack_Size)。
-
程序中如果没有动态申请内存则堆区无用,可以不用管 Heap_Size
- 如果程序有动态申请内存,则注意Heap_Size(即堆的首地址)一定要大于 全局区+栈区 大小,否则堆区会被覆盖导致程序出错。
main.cpp
int a = 0; 全局初始化区
char *p1; 全局未初始化区
main()
{
int b; 栈
char s[] = "abc"; 栈
char *p2; 栈
char *p3 = "123456"; 123456\0在常量区,p3在栈上。
static int c =0; 全局(静态)初始化区
p1 = (char *)malloc(10); 堆
p2 = (char *)malloc(20); 堆
}
在keil编译后的控制台信息中:
从编译信息可以看出
我们的代码占用 FLASH 大小为:1892 字节(1556+336)
所用的 SRAM 大小为:1864 个字节(32+1832)
- Code:表示程序所占用 FLASH 的大小(FLASH)。
- RO-data:即 Read Only-data,表示程序定义的常量,如 const 类型(FLASH)。ROM,程序不能修改其内容。
- RW-data:即 Read Write-data,表示已被初始化的全局变量(可读写变量)(SRAM)。 RAM 区,应用程序可以更改其内容
- ZI-data:即 Zero Init-data,表示未被初始化的全局变量(可读写变量)(SRAM)
- 在 C 语言中,函数内部定义的局部变量属于栈空间,进入函数的时候从向栈空间申请内存给局部变量,退出时释放局部变量,归还内存空间。
- 使用 malloc 动态分配的变量属于堆空间。
- 在程序中的栈空间和堆空间都是属于ZI-data 区域的,这些空间都会被初始值化为 0 值。编译器给出的 ZI-data 占用的空间值中包含了堆栈的大小
最终,烧写时flash被占有的空间为:
falsh = Code + RO-Data + RW-Data
如果这些内容比 RT1052 芯片的 FLASH 空间大,程序就无法被正常保存了。
程序运行时ram被占有的空间为
ram = RW-Data + ZI-Data
STM32 的 RO 区域不需要加载到 SRAM,内核直接从 FLASH 读取指令运行。
是否需要掉电保存,是把 RW-data 与 ZI-data 区别开来的原因
- ZI 节的数据都被初始化为 0。在 RAM 创建数据的时候,默认值为 0,
- 但如果有的数据要求初值非 0,那就需要使用 ROM 记录该初始值,运行时再复制到 RAM。
启动文件中的stack_size是可以设定的。stm32的堆、栈设置可以在底层驱动文件startup_stm32f10x_hd.s(这个文件要看你所用MCU的型号,这个是大容量的)里设置。
- 一般程序,(在允许范围内)设置多少STACK,并不影响程序真实使用的RAM大小,(编译出来的HEX文件都是一样),程序还是按照它原本的状态使用RAM,把STACK设置为0,并不是真实地减少RAM使用。仅仅是欺骗一下编译器,让程序表面上看起来少用了RAM。只是在Debug调试时会提示错,栈溢出。
- 单片机代码在函数里定义一个大数组 int buf[8192],栈要是小于8192是会死的很惨。
- 设置一定size的STACK,也并不是真的就多使用了RAM,只是让编译器帮你检查一下,是否能够保证有size大小的RAM没有被占用,可以用来作为堆栈。
flash占用空间可以在最终生成的bin文件查看,刚好是13116字节大小。
ram占用空间在生成的.map文件中。
map文件中显示ram占用的size为0x19c0,转换为10进制为6592,而生成RW-Data + ZI-Data=180+6412=6592.
- data** data的空间累加即为RW-Data
- .bss+STACK** .bss的空间累加+STACK即为ZI-Data
堆栈位置
通过MAP文件可知
HEAP 0x200106f8 Section 512 startup_stm32f2xx.o(HEAP)
STACK 0x200108f8 Section 1024 startup_stm32f2xx.o(STACK)
__heap_base 0x200106f8 Data 0 startup_stm32f2xx.o(HEAP)
__heap_limit 0x200108f8 Data 0 startup_stm32f2xx.o(HEAP)
__initial_sp 0x20010cf8 Data 0 startup_stm32f2xx.o(STACK)
显然 Cortex-m3资料可知:__initial_sp是堆栈指针,它就是FLASH的0x8000000地址前面4个字节(它根据堆栈大小,由编译器自动生成)
堆栈调试
对于如下的空间占用
调试时会出现
MSP就是主堆栈指针,一般是我们复位之后指向的位置,复位执向的其实是栈顶。
MSP指向地址0x20000668是0x20000000偏移0x668而得来。具体哪些地方占用了RAM,可以参看map文件中:
栈顶长这样:
MSP(R13,主堆栈指针)和PC(R15,程序计数器)初始化流程图:
栈的管理
栈是一块连续的内存空间,由上往下增长,指针指向最下边的地址,即使用栈时地址是会越来越小的,如先声明的局部变量比后声明的地址要高;
- 栈是由程序(操作系统)自动分配,不会有内存碎片的问题;
- PUSH:为栈增加一个元素的操作叫做PUSH
- POP:从栈中取出一个元素的操作叫做POP
- TOP:标识栈顶位置,并且是动态变化的。每做一次PUSH操作,它都会自增1;相反,每做一次POP操作,它会自减1。
- BASE:标识栈底位置,BASE用于防止栈空后继续弹栈,一般情况下,BASE是不会变动的。
压栈
压栈(PUSH): SP 先自减 4,再存入新的数值。
出栈
先从 SP 指针处读取上一次被压入的值,再把 SP 指针自增 4。
虽然 POP 后被压入的数值还保存在栈中,但它已经无效了,因为为下次的 PUSH 将覆盖它的值!
双堆栈机制
- 堆栈分为主堆栈(MSP)和进程堆栈(PSP),由CONTROL寄存器(特殊功能寄存器)决定当前使用哪个堆栈指针。
-
当处理器处于线程模式时,控制寄存器决定使用的堆栈和软件执行的特权级别,并指示 FPU 状态是否为活动。
在 Cortex‐M4 的 handler 模式中, CONTROL[1](CONTROL寄存器的bit[1])总是 0 -
当 CONTROL[1]=0 时,只使用 MSP,此时用户程序和异常 handler 共享同一个堆栈。这也是复位后的缺省使用方式。
-
当 CONTROL[1]=1 时,线程模式将不再使用 PSP,而改用 MSP( handler 模式永远使用 MSP)。
注意,在这种情况下,进入异常时的自动压栈使用的是进程堆栈,进入异常 handler 后才自动改为 MSP,退出异常时切换回 PSP,并且从进程堆栈上弹出数据。
在特权级下,可以直接对 MSP 和 PSP 执行读/写操作,而不会混淆你所引用的R13。
栈的坏处
栈是固定且连续的一个大小,如果使用局部变量等超出了栈的大小则会造成内存溢出,而编译器通常是发现不了的,只有当程序运行到那个函数时才会发生的。这就会引入很难查找的bug。
堆的管理
malloc
当指针 p 调用 malloc 申请内存的时候,申请到的堆空间是不连续的,由于RAM中还存在局部变量,代码段和栈等等,所以动态分配的内存是取暂时空闲的内存而不是预先划出一块区域,这就是动态分配内存的好处。具体执行如下:
- 先判断 p 要分配的内存块数(m),然后从第 n 项开始,向下查找,直到找到 m 块连续的空内存块(即对应内存管理表项为 0)
- 然后将这 m 个内存管理表项的值都设置为 m(标记被占用),最后,把最后的这个空内存块的地址返回指针 p,完成一次分配。
- 注意,如果当内存不够的时候(找到最后也没找到连续的 m 块空闲内存),则返回 NULL 给 p,表示分配失败。
堆的增长方向是向上,所以malloc申请的地址也是越来越大的,前提是连续申请且在最后一次申请后再释放内存(free)。
代码:
char * Onebyte;//声明一个char指针
Onebyte = (char*)malloc(sizeof(Onebyte));//向堆中申请一个字节内存
//下面两句效果一样,若在函数里
char Onearr[10];//向栈中申请10个字节
char *Onearr = (char*)malloc(10);//向堆中申请10个字节
free
当 p 申请的内存用完,需要释放的时候,调用 free 函数实现。
- free 函数先判断 p 指向的内存地址所对应的内存块
- 然后找到对应的内存管理表项目,得到 p 所占用的内存块数目 m(内存管理表项目的值就是所分配内存块的数目)
- 将这 m 个内存管理表项目的值都清零,标记释放,完成一次内存释放。
代码
//对于上面malloc的使用
free(Onebyte);
free(Onearr);
堆的坏处
由于使用malloc申请内存时,不单只申请了所需的大小空间,还要额外暂用管理这部分空间的内存,而释放时又只释放申请的内存,所以使用堆会引入内存碎片。
- 当然如果不是在短时间内频繁的使用malloc申请和free释放内存,那么操作系统就有足够的时间来回收碎片空间。
- 另外如如果使用malloc申请的内存不规范使用,当释放内存后,没将指针地址清空,仍指向那个地址刚好是栈的地址,则会造成越界访问。
分块式内存管理
分块式内存管理由内存池和内存管理表两部分组成。
- 内存池被等分为 n块,对应的内存管理表,大小也为 n
- 内存管理表的每一个项对应内存池的一块内存。
内存管理表的项值代表的意义为:当该项值为 0 的时候,代表对应的内存块未被占用。当该项值非零的时候,代表该项对应的内存块已经被占用,其数值则代表被连续占用的内存块数。
-
比如某项值为 10,那么说明包括本项对应的内存块在内,总共分配了 10 个内存块给外部的某个指针。
-
内寸分配方向如图所示,是从顶→底的分配方向。即首先从最末端开始找空内存。当内存管理刚初始化的时候,内存表全部清零,表示没有任何内存块被占用。