编写程序a=a+1后,发生了什么
总述
该部分讲述的是程序在内部中是如何一步一步执行的。
“程序->编译->烧录Flash->CPU与内存的交互”的过程。
问题引出
当编写a=a+1后,a的值就发生了加1。这个过程分为三步:
1.读a
2.累加
3.写a
那么就存在了一些问题:
1.从哪里读,向哪里写?
2.怎么累加的?
3.是谁在控制这三步的执行?
问题分析
这里有3个部分:CPU、内存、Flash。下图为三者关系。
CPU内有存储单元,称作寄存器,如图中的R0~R15。有计算单元,如图中的ALU。
其中:R13(SP,栈寄存器)、R14(LR,返回地址)、R15(PC,程序计数器)为特殊寄存器。
内存中存放数据,每个数据都有自己的地址空间。
Flash中存放的是程序,用于控制CPU执行的动作。
当写完程序之后,用keil进行编译生成.hex文件,之后烧录.hex文件到单片机的Flash中。这时Flash中保留了用于控制CPU执行的汇编代码,上电后程序开始运行。
CPU从Flash中读出ADD指令,开始从内存进行读数据,并存入寄存器R0(假如存入R0,也可能是别的)。
CPU从Flash中读出LDR指令,开始对R0的数据进行累加,并存回寄存器R0(假如存入R0,也可能是别的)。
CPU从Flash中读出STR指令,开始将R0的数据存回到内存的相应地址位置。
就此,a=a+1;执行结束。
CPU如何与外设通信
通信结构
当CPU进行读操作时,CPU并不知道读的是谁,它所能做的事情只有两件:发送地址、接收数据。这就存在一个问题,CPU是如何判断此刻是读Flash还是读内存,还是读其他的外设。
具体的通信结构如下:
CPU与设备之间存在一个内存控制器。内存控制器可以接收CPU发送的地址,并判断这个地址是哪一个设备的地址范围,从而堆相应的设备进行使能,建立通信。
每一个设备的地址范围在芯片设计时就已经固定,这个范围可以通过芯片手册查询得到。
地址空间
地址空间要区别于地址范围。
地址范围指的是一个设备地址的范围,比如Flash的地址范围是0x08000000到0x0801FFFF。
地址空间指的是地址是由谁来发出,不同的地址空间,控制方不同。
具体区分图如下:
图中有两个地址空间:地址空间1是由CPU发出的地址信号,地址空间2是由SDIO发出的地址信号。
地址空间不同,同样的地址范围的含义也就不同。比如CPU发送一个地址0,含义是控制内存、Flash、SDIO的某些功能;而SDIO发送一个地址0,含义是控制SD卡的某些功能。
变量是如何存放的
变量的种类有3种:全局变量、局部变量、静态局部变量。
变量都存在于内存中,内存是一整块区域,这三种变量分别存放在不同的位置。
局部变量
函数中定义的变量称为局部变量,函数调用时,CPU分配空间给局部变量,这些空间是属于栈的空间,即:局部变量存储在栈中。函数执行之后,CPU会回收前面分配的空间,即:局部变量被释放。
那么,现在就存在了2个问题:什么是栈?CPU是怎么给局部变量分配空间的?
什么是栈
栈并不是系统自动分配的,而是由程序员决定它的起始位置。
当我们拿到一个内存时,可以知道该内存的起始地址、结束地址,通常结束地址就是栈的起始地址(这个地址是程序员写进去的,CPU不能自动识别结束地址是什么)。之后栈基于该地址向下生长,即:向低地址生长。
调用函数时,CPU做了什么
调用函数时,CPU会做两件事:
- 将返回地址入栈,这个返回地址指向的位置就是位置2。
- 给变量分配空间,先进行定义的先分配,地址为向下生长。
上述分析只是理论上如此,而实际上编译器会进行一些优化,所以可能有些空间并没有被分配。
int f1(){
volatile int a = 123;
volatile int b = 0;
volatile char buf[10];
b = f2(a);
return b; /* 位置2 */
}
int f2(volatile int a){
a++;
return a;
}
int main(){
f1();
return 0; /* 位置1 */
}
在上面例程中,栈分配的过程如下:
1、当CPU一上电时,有一个数值会被写入栈寄存器,这个值就是栈的起始位置
2、之后进入程序,当走到f1时,CPU会将返回地址入栈,并为局部变量分配空间。
这个返回地址指向的位置就是位置1。
分配空间时,谁先定义就先分配谁。因为栈是从高地址向低地址生长,因此a在高位,之后b变量,之后buf变量。这里要注意的是,不是buf[0]在高位,而是buf[9]在高位。
分配完成空间之后,f1的栈空间就已经确定了,此时SP已经指向了buf[0]位置。(在下图)
3、之后f1()中调用了f2(),CPU与之前一样,分配f2的栈空间。
具体的栈空间分配如下:
返回函数时,CPU做了什么
同样以上述代码为例。当程序执行到f2()的return后,CPU会将原来依次出栈原来分配的空间,这样,SP会逐渐向高地址移动,最终回到起始地址。这样栈空间就被释放,再次调用函数时,CPU就会从起始位置依次分配空间。
这里要注意的是,空间释放并不是值清零。当释放之后,a空间仍保持原来的值,直到有新的变量占用了这个空间,重写了这个空间的值,此时a原来的值才被破坏。
全局变量、静态局部变量
- 全局变量、静态局部变量的初始值存放在flash的数据段中。
- 全局变量、静态局部变量的内存位置由链接器决定。
整体过程如下图:
情况1:有初始值
全局变量与静态局部变量的赋值发生在main之前,CPU读取flash代码段中自己编写的汇编copy函数,将flash中存储的全局变量和静态局部变量的初值拷贝到链接器指定的内存位置,生长方向是从低地址向高地址生长。
情况2:无初始值、初始值为0
CPU读取flash代码段中自己编写的汇编清0函数,在有初值的全局变量、静态局部变量之后将0写入相应内存,生长方向是从低地址向高地址生长。
堆是什么东西
堆就是内存中一块由malloc、free来管理的空闲空间,这个空间既不会与栈的空间重合,也不会与全局变量、静态局部变量的空间重合。
堆可以从高地址向低地址增长,也可以从低地址向高地址增长。
具体结构如下: