C语言的本质
- 1、ARM架构与汇编
- 2、局部变量初始化与空间分配
- 2.1、局部变量的初始化
- 2.1、局部变量数组初始化
- 3、全局变量/静态变量初始化化与空间分配
- 4、堆空间
1、ARM架构与汇编
ARM简要架构如下:CPU,ARM(能读能写),Flash(能读,写比较麻烦)。
2、局部变量初始化与空间分配
2.1、局部变量的初始化
CPU寄存器如下:
CPU中的特殊寄存器
①SP
:栈空间地址指针
②LR
:返回地址
③PC
:保存Flash的代码段的值,执行那个机器码就保存哪个代码的对应的值
执行如下代码时,单片机内部是怎样执行操作的?
int main()
{volatile int a = 10;volatile int b = 20;a = a+b;return 0;
}
C语言代码被编译为单片机能识别的机器码后,烧录进入单片机的Flash的代码段。
如下为c代码转换的汇编码和机器码
0x08000014 B50C PUSH {r2-r3,lr}17: volatile int a = 10;
0x08000016 200A MOVS r0,#0x0A
0x08000018 9001 STR r0,[sp,#0x04]18: volatile int b = 20;
0x0800001A 2014 MOVS r0,#0x14
0x0800001C 9000 STR r0,[sp,#0x00]19: a = a+b; 20:
0x0800001E E9DD1000 LDRD r1,r0,[sp,#0]
0x08000022 4408 ADD r0,r0,r1
0x08000024 9001 STR r0,[sp,#0x04]21: return 0;
0x08000026 2000 MOVS r0,#0x0022: }
0x08000028 BD0C POP {r2-r3,pc}
常见的汇编指令:
PUSH:压栈,一般情况将CPU的寄存器压入RAM栈空间例如:PUSH {r2-r3,lr}。表示将lr,r3,r2压入栈空间
MOVS:赋值,给CPU的寄存器赋值例如:MOVS r0,#0x0A。表示给r0寄存器赋值0x0A
STR:写入数据,将CPU的寄存器数据写入栈空间里面例如:STR r0,[sp,#0x00]。表示将r0的数据写入地址为sp + 0x00的空间
LDRD:读取2个数据,将栈空间的数据读取到CPU的寄存器里面例如:LDRD r1,r0,[sp,#0]。表示将sp+0x00地址的数据读取到r0,将sp+0x04地址数据读取到r1
LDR:读取1个数据
ADD:做加法, 例如:ADD r0,r0,r1。表示将r0 = r0 + r1
SUB:做减法例如:SUB sp,sp,#0x68。表示将sp = sp - 0x68
POP:出栈,将CPU的寄存器退出栈空间,用于栈空间的释放。例如:POP {r2-r3,pc}。表示将r2,r3,pc对应的栈空间释放。
①PUSH {r2-r3,lr}
。表示依次将寄存器lr,r3,r2中的数据压入栈的空间里面。而压栈的同时,sp
也会随着压栈而改变。如下图所示
【注】lr
寄存器里面的数据是返回地址,即在执行main函数之前,将ENDP
的地址保存在lr中。
如图:PUSH {r2-r3,lr}
此汇编对应的机器码为0x08000014 B50C
,当单片机执行完此机器码后,lr,r3,r2
的寄存器的值被保存到RAM的栈区空间里面。而sp(栈空间地址光标)会指向地址0x2000 FFF4。
【注】此时的r2和r3寄存器的值为空。
②volatile int a = 10
对应的汇编:MOVS r0, #0x0A
。表示将0x0A移入r0寄存器
STR r0, [sp,#0x04]
。表示将r0的数据写入(sp + 0x04)的地址存储空间。sp = 0x2000 FFF4,则sp + 4 = 0x2000 FFF8。所以将r0的数据写入到栈空间的r3的位置。
【注】0x2000 FFF8为什么代表r3的位置,而不是代表r2的位置喃?一般情况下一个存储空间是以较小的那个地址表示的
③volatile int b = 20
对应的汇编:MOVS r0, #0x14
。表示将0x0A移入r0寄存器
STR r0, [sp,#0x00]
。表示将r0的数据写入(sp + 0x00)的地址存储空间。sp = 0x2000 FFF4,则sp + 0 = 0x2000 FFF4。所以将r0的数据写入到栈空间的r2的位置。
④a = a + b
对应的汇编:LDRD r1, r0, [sp,#0]
。从栈区读取2个数据到r0,r1寄存器中。读取的起始地址为sp + 0 = 0x2000 FFF4。(r0接收地址sp + 0x00空间的数据,r1接收地址sp + 0x04空间的数据)即将b/0x14读取到r0,将a/0x0A读取到r1。
ADD r0, r0, r1
。表示将r1的数据加上r0的数据赋值r0。即r0 = 0x14 + 0x0A = 0x1E
STR r0, [sp,#0x04]
。表示将r0的数据写入(sp + 0x04)的地址存储空间。sp = 0x2000 FFF4,则sp + 0x04 = 0x2000 FFF8。所以将r0的数据写入到栈空间的r3的位置。
最终调试结果如下:
⑤return 0;
对应的汇编:MOVS r0,#0x00
。表示将r0寄存器的数据清零。
⑥栈的回收
对应的汇编:POP {r2-r3,pc}
。从栈中恢复寄存器 r2、r3 和 pc所对应栈空间的值,并且会自动调整栈指针 sp。最终sp指向0x20010000。表示之前使用的栈空间被回收。
【注】①低标号寄存器在栈空间对应低地址。进栈出栈都是。所以r2在栈空间的下面。②压栈时,先压进去sp在向下移动;出栈时,先出栈,sp在向上移动。
2.1、局部变量数组初始化
执行如下代码时,单片机内部是怎样执行操作的?
int main()
{volatile int a = 10;volatile char b[100];b[99] = 20;return 0;
}
如下为c代码转换的汇编码和机器码
0x08000014 B09A SUB sp,sp,#0x6817: volatile int a = 10; 18: volatile char b[100];
0x08000016 200A MOVS r0,#0x0A
0x08000018 9019 STR r0,[sp,#0x64]19: b[99] = 20;
0x0800001A 2014 MOVS r0,#0x14
0x0800001C F88D0063 STRB r0,[sp,#0x63]20: return 0;
0x08000020 2000 MOVS r0,#0x00
SUB sp,sp,#0x68
。表示sp = sp - 0x68。则sp = 0x2000 FFFC - 0x68 = 0x2000 FF98。其中0x68 = 104。则表示在栈区开辟了104个字节
3、全局变量/静态变量初始化化与空间分配
#include "main.h"volatile int g_a = 123;//全局变量
int main()
{static volatile int g_b = 321;//静态变量volatile int a = 10;volatile int b = 20;a = a+b;g_b = g_a + g_b;return 0;
}
如上代码包含g_a全局变量,g_b静态变量。如下为c代码转换的汇编码和机器码
0x08000154 B50C PUSH {r2-r3,lr}7: volatile int a = 10;
0x08000156 200A MOVS r0,#0x0A
0x08000158 9001 STR r0,[sp,#0x04]8: volatile int b = 20;
0x0800015A 2014 MOVS r0,#0x14
0x0800015C 9000 STR r0,[sp,#0x00]9: a = a+b;
0x0800015E E9DD1000 LDRD r1,r0,[sp,#0]
0x08000162 4408 ADD r0,r0,r1
0x08000164 9001 STR r0,[sp,#0x04]10: g_b = g_a + g_b;
0x08000166 4804 LDR r0,[pc,#16] ; @0x08000178
0x08000168 6800 LDR r0,[r0,#0x00]
0x0800016A 4904 LDR r1,[pc,#16] ; @0x0800017C
0x0800016C 6809 LDR r1,[r1,#0x00]
0x0800016E 4408 ADD r0,r0,r1
0x08000170 4902 LDR r1,[pc,#8] ; @0x0800017C
0x08000172 6008 STR r0,[r1,#0x00]11: return 0;
0x08000174 2000 MOVS r0,#0x0012: }
0x08000176 BD0C POP {r2-r3,pc}
综上:并未有机器码和汇编代码来初始化全局变量和静态变量。那么在内存中他们是怎样被初始化赋值的喃?
答案:将全局变量和局部变量需要被初始化的值保存在Flash的数据段里面。有多少个数据,在数据段里面就有多少个数据
有了数据,那全局变量和局部变量的内存又在哪里喃?又怎样将数据给到全局变量和局部变量喃?
答案:全局变量和静态变量依旧保存在RAM的里面,但不在是栈区。全局变量/静态变量由编译器分配的存储空间,不再是像局部变量由代码指令分配。如下图所示:Linker(链接器):将0x0800 0000的空间与0x2000 0000的空间链接在一起。
如上图:R/O base:0x0800 0000。表示的是Flash的数据段的起始地址。
R/W base:0x0200 0000。表示的是RAM中保存全局变量和静态变量的起始地址。
综上:
①全局变量/局部静态变量赋值和栈里面的局部变量不同,全局变量是先占用低地址空间,而局部变量是先占用高地址空间。
②全局变量是通过copy函数,将Flash里面的数据复制到全局变量和静态变量的内存里面。
③当 main 函数执行完毕时,虽然栈上的局部变量会被销毁,但是全局变量不会受到影响。全局变量在整个程序运行期间都存在,直到程序退出时才会被操作系统回收
【注】copy函数在启动文件里面,由程序员编写,且在调用main函数之前。调用完copy函数后在执行main函数。全局变量在程序启动时分配内存和初始化值,并在整个程序运行期间都保持有效。
综上为有初始值的全局变量和静态变量的内存分配情况(简称为:RW段),那若没有初始值/初始化为0的全局变量。依然会在Flash的数据段将数据0保存起来吗?显然浪费内存空间。
答案:没有初始值和初始值为0的全局变量,在Flash的数据段里面并未保存数据。但是编译器会在RAM里面给这些变量分配存储空间(简称:ZI段)。在调用main函数之间,调用memset函数将这些变量的存储空间清零。
4、堆空间
综上:①RAM中存在栈区:用于存储局部变量、函数参数、返回地址等。栈内存是自动管理的,随着函数调用和返回而分配和释放。②RAM也存在全局变量/静态局部变量区域。③RAM还存在堆区:堆区由用户调用mallo函数分配和管理,调用free函数进行释放。
堆区的空间不能在栈区里面分配。因为栈区空间会随着函数的结束而释放,是用户不可控制的。而堆区是不会随着函数的结束而释放。除非main函数终止。
而堆空间可以是全局变量区域。因为都是不会随着函数的结束而释放。除非main函数终止。