开头的话:
之前一直用现成的LED工程demo,改改就上,也没细究。直到做MQTT移植的时候,发现malloc始终出错,开始找问题,于是写本文。(前前后后摘抄、参考、改进本文,侵删)
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即可正常使用
本文以STM32F407ZGT6为分析平台,写了一个最简单的程序,包含usart和delay
Code: 存储程序代码
RO: 存储常量
RW:存储初始化不为0的全局变量
ZI(zero initial):存储初始化为0或未初始化的全局变量
Flash存储项包括Code + RO + RW
原因:
Code 保存程序用
RW 初始化不为零的变量值需要断电保存
RO 常量值需要断电保存
Ram加载项包括RW + ZI
RW 要开始运行程序了,全局变量必不可少,且变量不能总是从flash中读取,那样的话,值都和上电的一样了
ZI 变量不能从flash读取,理由同上
看到当中有HEAP和STACK,最左列是Base Addr,次之是Size,可以看到
HEAP起始地址0x200000f0,size 0x00000200
STACK续上,起始地址0x200002f0,size 0x00000400
这里引出了一个问题,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)
自0x20000000起,分别是静态存储区,HEAP和STACK。不妨以STM32F407ZGT6为例子看程序启动,如下图
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的内存内容,严重的直接程序崩溃)
再说flash,看配置表
onchip ROM 可以看出,自0x08000000起,大小为0x00100000,即1MB,
onchip RAM1自0x20000000起,大小为0x00020000,即128KB,RAM2自0x10000000起,大小为0x00010000,即64KB,合计192KB
STM32上电启动后,根据BOOT0和BOOT1选择flash启动还是ram启动。如前所述,最常见还是flash启动。启动后,搬运RW到RAM,但不会搬运Code,即STM32每条指令都是从flash读取执行。当然,为了提高指令加载速度,也可以一次性加载到RAM,但不这么做,因为RAM本来就小,不值得。
另外,一般程序复位、IAP都是将指针指向0x08000000,实现重新加载。(STM32的IAP可参考链接文章)
debug启动后,PC指向0x0800091C,即指向main函数。其实这并不是完整的启动过程,
在s文件183行打断点,可以看到先运行了SystemInit(PC指向0x0800019E),然后才执行main
全面的启动运行流程图如下(图片来自https://blog.csdn.net/menghuanbeike/article/details/78866013):
先上代码,
#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
43行for循环断点,watch memory发现
在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,敲重点)
即:
地址 | 归属 |
---|---|
0x2000_026F | 0x22终止地址 |
0x2000_01a8 | 0x22起始地址 |
共计200个字节
地址 | 归属 |
---|---|
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,这下不得了,直接覆盖堆区跑到静态存储区了,看下面的断点运行
地址 | 归属 |
---|---|
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多大空间?
先上代码
#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字节
地址 | 归属 |
---|---|
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不了
后续再解决吧,时间不早了,得干活了