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函數終止。