[东拼西凑]STM32单片机启动流程及RAM和Flash的配置关系和堆栈溢出现象

开头的话:
之前一直用现成的LED工程demo,改改就上,也没细究。直到做MQTT移植的时候,发现malloc始终出错,开始找问题,于是写本文。(前前后后摘抄、参考、改进本文,侵删)

一、STM32上电启动

BOOT1 BOOT0 启动方式
X 0 从STM32内置flash启动,JTAG或者SWD固化程序位置
1 1 从STM32内置SRAM启动,由于SRAM没有程序存储能力,这个模式一般用于程序debug
0 1 从STM32内置ROM启动,使用串口借助bootloader下载程序至flash,即ISP
  • 补充说明

Main Flash memory
从STM32内置的Flash启动

System memory
从系统ROM启动,这种模式启动的程序功能是由厂家设置的。一般来说,这种启动方式用的比较少。系统存储器是芯片内部一块特定的区域,STM32在出厂时,由ST在这个区域内部预置了一段BootLoader,也就是我们常说的ISP程序, 这是一块ROM,出厂后无法修改。一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的BootLoader中,提供了串口下载程序的固件,可以通过这个BootLoader将程序下载到系统的Flash中。但是这个下载方式需要以下步骤:

Step1:
将BOOT0设置为1,BOOT1设置为0,然后按下复位键,这样才能从系统存储器启动BootLoader
Step2:
最后在BootLoader的帮助下,通过串口下载程序到Flash中
Step3:
程序下载完成后,又有需要将BOOT0设置为GND,手动复位,这样,STM32才可以从Flash中启动可以看到, 利用串口下载程序还是比较的麻烦。可以参考原子哥一键下载电路,此处不贴了。

Embedded Memory
内置SRAM,用于快速的程序调试

----------Flash锁死解决办法
修改为BOOT0=1,BOOT1=0 即可从系统存储器ROM启动,通过JTAG或SWD重新烧写程序后,可将BOOT模式重新更换到BOOT0=0,BOOT1=X即可正常使用

二、keil编译信息

1. log信息

本文以STM32F407ZGT6为分析平台,写了一个最简单的程序,包含usart和delay
[东拼西凑]STM32单片机启动流程及RAM和Flash的配置关系和堆栈溢出现象_第1张图片
Code: 存储程序代码
RO: 存储常量
RW:存储初始化不为0的全局变量
ZI(zero initial):存储初始化为0或未初始化的全局变量

  • Flash存储项包括Code + RO + RW
    原因:
    Code 保存程序用
    RW 初始化不为零的变量值需要断电保存
    RO 常量值需要断电保存

  • Ram加载项包括RW + ZI
    RW 要开始运行程序了,全局变量必不可少,且变量不能总是从flash中读取,那样的话,值都和上电的一样了
    ZI 变量不能从flash读取,理由同上

2. map信息

看一下生成的map信息(双击Keil工程名)
[东拼西凑]STM32单片机启动流程及RAM和Flash的配置关系和堆栈溢出现象_第2张图片

看到当中有HEAP和STACK,最左列是Base Addr,次之是Size,可以看到
HEAP起始地址0x200000f0,size 0x00000200
STACK续上,起始地址0x200002f0,size 0x00000400

3.堆栈结构

这里引出了一个问题,STM32内部的堆栈结构,先看STM32内存地址映射(图片来自:https://blog.csdn.net/qq_15232177/article/details/73336374)
在这里插入图片描述
本质上,STM32对外设的操作都是对地址的操作,RAM以0x20000000起始,实际大小取决于芯片系列。内置flash以0x08000000起始,实际大小取决于芯片系列。芯片启动程序就是从0x08000000开始

继续说Ram,见下图(图片改编自:https://blog.csdn.net/qlexcel/article/details/78916934)
[东拼西凑]STM32单片机启动流程及RAM和Flash的配置关系和堆栈溢出现象_第3张图片
自0x20000000起,分别是静态存储区,HEAP和STACK。不妨以STM32F407ZGT6为例子看程序启动,如下图
[东拼西凑]STM32单片机启动流程及RAM和Flash的配置关系和堆栈溢出现象_第4张图片
debug刚启动,SP指向0x200006F0,即栈顶。当程序运行后,MSP就是从这个地址开始,往下给函数的局部变量分配地址。另外,STM32栈增长方向为向下,堆增长方向为向上。
插一段比较巧的代码,借助递归函数检查栈的增长方向:

void find_stack_direction(void)
{
    static u8 *addr=NULL;   //用于存放第一个dummy的地址。
    u8dummy;                  //用于获取栈地址 
    if(addr==NULL)         //第一次进入
   {                          
        addr=&dummy;       //保存dummy的地址
        find_stack_direction (); //递归 
    }
	else               //第二次进入 
  	{  
       //第二次dummy的地址大于第一次dummy,那么说明栈增长方向是向上的. 
       if(&dummy>addr)
       		stack_dir=1; 
        else
        //第二次dummy的地址小于第一次dummy,那么说明栈增长方向是向下的. 
        	stack_dir=0;            
 	}
} 

言归正传,堆栈区别:
RAM分为静态常量区、栈区和堆区

  • STACK
    存放函数内局部变量,形参,函数运行结束后自动释放

  • HEAP
    存放由malloc/new开辟的空间,自由使用,但必须通过free/delete释放

  • 静态常量区
    内存在程序编译的时候已经分配好,主要存放全局数据(初始化&未初始化)和常量

从堆栈空间分布即可看出,由于堆栈增长方向相反,因此,存在堆栈干扰的情况。(当malloc很大的区域时,或者局部变量定义大数组时。比如,malloc一个正常堆范围的内存,同时某个函数内部定义了大数组,有可能改动数组就影响到malloc的内存内容,严重的直接程序崩溃)

4. flash

再说flash,看配置表
onchip ROM 可以看出,自0x08000000起,大小为0x00100000,即1MB,
onchip RAM1自0x20000000起,大小为0x00020000,即128KB,RAM2自0x10000000起,大小为0x00010000,即64KB,合计192KB
[东拼西凑]STM32单片机启动流程及RAM和Flash的配置关系和堆栈溢出现象_第5张图片
STM32上电启动后,根据BOOT0和BOOT1选择flash启动还是ram启动。如前所述,最常见还是flash启动。启动后,搬运RW到RAM,但不会搬运Code,即STM32每条指令都是从flash读取执行。当然,为了提高指令加载速度,也可以一次性加载到RAM,但不这么做,因为RAM本来就小,不值得。
另外,一般程序复位、IAP都是将指针指向0x08000000,实现重新加载。(STM32的IAP可参考链接文章)

三、关于STM32启动过程的一点记录

debug启动后,PC指向0x0800091C,即指向main函数。其实这并不是完整的启动过程,
在s文件183行打断点,可以看到先运行了SystemInit(PC指向0x0800019E),然后才执行main
[东拼西凑]STM32单片机启动流程及RAM和Flash的配置关系和堆栈溢出现象_第6张图片
全面的启动运行流程图如下(图片来自https://blog.csdn.net/menghuanbeike/article/details/78866013):
[东拼西凑]STM32单片机启动流程及RAM和Flash的配置关系和堆栈溢出现象_第7张图片

四、正题

1. 堆栈溢出问题

先上代码,

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "stdlib.h"
volatile char vgdata[200] = {0};

int main(void)
{ 
	int i = 10;
	char* p = (char*)malloc(200);
	char idata[1392] = {0};
	
	for(i = 0;i < 200;i++)
	{
		*(p+i) = 0x22;
	}
	
	for(i = 0;i < 200;i++)
	{
		vgdata[i] = 1;
	}
	vgdata[0] = 0x66;
	vgdata[1] = ((uint32_t)(&i) &0xff000000) >> 24;
	vgdata[2] = ((uint32_t)(&i) &0x00ff0000) >> 16;
	vgdata[3] = ((uint32_t)(&i) &0x0000ff00) >> 8;
	vgdata[4] = ((uint32_t)(&i) &0x000000ff);
	
	vgdata[5] = ((uint32_t)(&p) &0xff000000) >> 24;
	vgdata[6] = ((uint32_t)(&p) &0x00ff0000) >> 16;
	vgdata[7] = ((uint32_t)(&p) &0x0000ff00) >> 8;
	vgdata[8] = ((uint32_t)(&p) &0x000000ff);
	
	vgdata[9] = ((uint32_t)(idata) &0xff000000) >> 24;
	vgdata[10] = ((uint32_t)(idata) &0x00ff0000) >> 16;
	vgdata[11] = ((uint32_t)(idata) &0x0000ff00) >> 8;
	vgdata[12] = ((uint32_t)(idata) &0x000000ff);
	
	for(i = 0;i < 1392;i++)
	{
		idata[i] = 0x44;
	}

	idata[1391] = 0x99;
	while(1);  
}

全局区定义字符数组,大小200。main函数什么也没做,

首先,malloc 200字节大小的空间,并将首字节赋值2
然后,初始化全局数组vgdata,并将首字节赋值0x66,顺次存入i,p,idata地址,其余赋值1
最后,定义1392字节大小的局部数组,并赋值0x44
末尾,局部数组最后一个值改成0x99

生成的map文件如下:
[东拼西凑]STM32单片机启动流程及RAM和Flash的配置关系和堆栈溢出现象_第8张图片

43行for循环断点,watch memory发现
[东拼西凑]STM32单片机启动流程及RAM和Flash的配置关系和堆栈溢出现象_第9张图片
在0x2000_0000行,第17个数开始(即0x2000_0010)顺序写入0x66,变量i地址,指针p地址,数组idata地址,0x01 ...,直至(0x2000_00CC + 0X0B),共计200个字节
在0x2000_0198行,第17个数有02写入,其地址为0x2000_0198 + 0x10 = 0x2000_01a8
即在堆地址0x2000_01a0基础上偏移8,敲重点

即:

  • malloc空间范围
地址 归属
0x2000_026F 0x22终止地址
0x2000_01a8 0x22起始地址

共计200个字节

  • 全局变量vgdata范围
地址 归属
0x2000_00D8 0x01终止地址
0x2000_0010 0x66起始地址

共计200个字节
着重注意一下:

变量i地址是 20 00 05 9C,在上图底部红色标出,其值为C8 00 00 00,由于STM32为小端模式,故而低字节在低地址,因此数据为0x0000_00C8 = 200,第一个for循环结束,i = 200 对的上

指针p地址是20 00 05 98,同样从上图可知,其值为A8 01 00 20,小端的原因,数据为0x2000_01A8,即malloc后写入0x22的首地址,对的上

数组idata地址是20 00 00 28,这下不得了,直接覆盖堆区跑到静态存储区了,看下面的断点运行

49行while(1)断点,
[东拼西凑]STM32单片机启动流程及RAM和Flash的配置关系和堆栈溢出现象_第10张图片

  • 局部变量idata范围
地址 归属
0x2000_0597 0x99终止地址
0x2000_0028 0x44起始地址

共计1392个字节

问题出现:自0x2000_0028起,原数据全被数据0x44覆盖,直至0x2000_0597的0x99。不仅malloc开辟的空间数据丢失,连全局静态存储区的数据也丢失。这就是堆栈溢出,相当危险!!!

  • 至此,总结如下

由前述堆栈结构图知,栈向下,堆向上。malloc开辟空间不足,会返回NULL,不会向上影响栈区,没有什么致命伤。但是,STACK 就不一样,当需要临时变量比较大时,系统从当前栈指针开始向下开辟空间,这就可能污染到HEAP和静态区,造成堆栈溢出

针对上述问题,本质上还是栈空间大小不合适导致,增大startup_stm32f40_41xxx.s文件中的Stack_Size EQU 0x00000200即可解决。同样,如果malloc失败,即堆空间不足,亦可增大Heap_Size EQU 0x00000200解决。那在确定的HEAP size下,究竟能malloc多大空间?

2. malloc到底能分配多大空间

先上代码

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "stdlib.h"
volatile char vgdata[200] = {0};

int main(void)
{ 
	int i = 10;
	char* p = (char*)malloc(0x200 - 12);
	char idata[1392] = {0};
	
	for(i = 0;i < (0x200 - 12);i++)
	{
		*(p+i) = 0x22;
	}
	
	for(i = 0;i < 200;i++)
	{
		vgdata[i] = 1;
	}
	vgdata[0] = 0x66;
	vgdata[1] = ((uint32_t)(&i) &0xff000000) >> 24;
	vgdata[2] = ((uint32_t)(&i) &0x00ff0000) >> 16;
	vgdata[3] = ((uint32_t)(&i) &0x0000ff00) >> 8;
	vgdata[4] = ((uint32_t)(&i) &0x000000ff);
	
	vgdata[5] = ((uint32_t)(&p) &0xff000000) >> 24;
	vgdata[6] = ((uint32_t)(&p) &0x00ff0000) >> 16;
	vgdata[7] = ((uint32_t)(&p) &0x0000ff00) >> 8;
	vgdata[8] = ((uint32_t)(&p) &0x000000ff);
	
	vgdata[9] = ((uint32_t)(idata) &0xff000000) >> 24;
	vgdata[10] = ((uint32_t)(idata) &0x00ff0000) >> 16;
	vgdata[11] = ((uint32_t)(idata) &0x0000ff00) >> 8;
	vgdata[12] = ((uint32_t)(idata) &0x000000ff);
	
	for(i = 0;i < 1392;i++)
	{
		idata[i] = 0x44;
	}

	idata[1391] = 0x99;
	while(1);  
}

HEAP和STACK都取0x200
经实验,malloc能开辟的最大空间是(0x200 - 12),问题来自两个方面

malloc开辟空间的指针p,指向的是HEAP地址偏移8个字节
malloc最大的范围只能到堆结束位置之前4个字节
综上,偏差12字节

[东拼西凑]STM32单片机启动流程及RAM和Flash的配置关系和堆栈溢出现象_第11张图片

  • malloc最大空间测试
地址 归属
0x2000_039F HEAP终止地址
0x2000_039B malloc 0x22终止地址
0x2000_01A8 malloc 0x22起始地址
0x2000_01A0 HEAP起始地址

就算精简如下,也还是前差8字节,后剩4字节,此是问题1

int main(void)
{ 
	int i = 10;
	char* p = (char*)malloc(0x200 - 12);
	
	for(i = 0;i < (0x200 - 12);i++)
	{
		*(p+i) = 0x22;
	}
	while(1)
}

但另一方面,如果我把idata大小改成1393,malloc直接GG,1个都分配不了,此是问题2

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "stdlib.h"
volatile char vgdata[200] = {0};

int main(void)
{ 
	int i = 10;
	char* p = (char*)malloc(1);
	char idata[1393] = {0};
	
	for(i = 0;i < (1);i++)
	{
		*(p+i) = 0x22;
	}
	
	for(i = 0;i < 200;i++)
	{
		vgdata[i] = 1;
	}
	vgdata[0] = 0x66;
	vgdata[1] = ((uint32_t)(&i) &0xff000000) >> 24;
	vgdata[2] = ((uint32_t)(&i) &0x00ff0000) >> 16;
	vgdata[3] = ((uint32_t)(&i) &0x0000ff00) >> 8;
	vgdata[4] = ((uint32_t)(&i) &0x000000ff);
	
	vgdata[5] = ((uint32_t)(&p) &0xff000000) >> 24;
	vgdata[6] = ((uint32_t)(&p) &0x00ff0000) >> 16;
	vgdata[7] = ((uint32_t)(&p) &0x0000ff00) >> 8;
	vgdata[8] = ((uint32_t)(&p) &0x000000ff);
	
	vgdata[9] = ((uint32_t)(idata) &0xff000000) >> 24;
	vgdata[10] = ((uint32_t)(idata) &0x00ff0000) >> 16;
	vgdata[11] = ((uint32_t)(idata) &0x0000ff00) >> 8;
	vgdata[12] = ((uint32_t)(idata) &0x000000ff);
	
	for(i = 0;i < 1393;i++)
	{
		idata[i] = 0x44;
	}

	idata[1392] = 0x99;
	while(1);  
}

看地址数据发现,虽然给idata分配1393字节,但系统为了4字节对齐,实际分配了1396字节

但前述的问题1与问题2现象比较奇怪:

问题1,前8后4,整个工程也没找到第二处malloc,这个12字节哪去了?
问题2,main是先malloc,后定义栈数组,改数组大小怎么会导致malloc失败,而且是一个都malloc不了

后续再解决吧,时间不早了,得干活了

你可能感兴趣的:(Embedded,IOT)