在開源電子中看到一篇文章講的是棧增長和大端/小端問題。學C語言的時候,我們知道堆棧的區別:
1)棧區(stack):由編譯器自動分配和釋放,存放函數的參數值、局部變量的值等,其操作方式類似
於數據結構中的棧。
(2)堆區(heap):一般由程序員分配和釋放,若程序員不釋放,程序結束時可能由操作系統回收。分配
方式類似於數據結構中的鏈表。
(3)全局區(靜態區)(static):全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態
變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。程序結束後由系
統自動釋放。
(4)文字常量區:常量字符串就是存放在這裏的。
(5)程序代碼區:存放函數體的二進制代碼。
下面是原子哥的帖子:主要意思是要證明stm32是小端模式,堆從RAM的起始地址處(0x2000 0000)分配內存給全局變量和靜態變量,並且堆是向上增長,棧是向下增長。
在後續的討論中有人認爲全局變量和靜態變量是放在靜態區的,在編譯時分配內存,靜態區從RAM的起始地址分配內存,並且是連續的。接着纔是堆,堆上的內存是動態分配的,由malloc或者new申請。棧上存放局部變量,形參等。
在經過再三實驗, 原子哥最終認同了這種觀點,做了相應修改。
與之相關的討論有:
http://www.openedv.com/thread-24152-1-1.html;http://blog.csdn.net/slj_win/article/details/16906141 ;http://www.openedv.com/posts/list/26805.htm;
http://www.openedv.com/posts/list/26805.htm
原子哥:
1,首先來看:棧(STACK)的問題.函數的局部變量,都是存放在"棧"裏面,棧的英文是:STACK.STACK的大小,我們可以在stm32的啓動文件裏面設置,以戰艦stm32開發板爲例,在startup_stm32f10x_hd.s裏面,開頭就有:
Stack_Size EQU 0x00000800
表示棧大小是0X800,也就是2048字節.這樣,CPU處理任務的時候,函數局部變量做多可佔用的大小就是:2048字節,注意:是所有在處理的函數,包括函數嵌套,遞歸,等等,都是從這個"棧"裏面,來分配的.所以,如果一個函數的局部變量過多,比如在函數裏面定義一個u8 buf[512],這一下就佔了1/4的棧大小了,再在其他函數裏面來搞兩下,程序崩潰是很容易的事情,這時候,一般你會進入到hardfault....
這是初學者非常容易犯的一個錯誤.切記不要在函數裏面放N多局部變量,尤其有大數組的時候!
對於棧區,一般棧頂,也就是MSP,在程序剛運行的時候,指向程序所佔用內存的最高地址.比如附件裏面的這個程序序,內存佔用如下圖:
圖中,我們可以看到,程序總共佔用內存:20+2348字節=2368=0X940
那麼程序剛開始運行的時候:MSP=0X2000 0000+0X940=0X2000 0940.
事實上,也是如此,如圖:
圖中,MSP就是:0X2000 0940.
程序運行後,MSP就是從這個地址開始,往下給函數的局部變量分配地址.
再說說棧的增長方向,我們可以用如下代碼測試:
//保存棧增長方向
//0,向下增長;1,向上增長.
static u8 stack_dir;
//查找棧增長方向,結果保存在stack_dir裏面.
void find_stack_direction(void)
{
static u8 *addr=NULL; //用於存放第一個dummy的地址。
u8 dummy; //用於獲取棧地址
if(addr==NULL) //第一次進入
{
addr=&dummy; //保存dummy的地址
find_stack_direction (); //遞歸
}else //第二次進入
{
if(&dummy>addr)stack_dir=1; //第二次dummy的地址大於第一次dummy,那麼說明棧增長方向是向上的.
else stack_dir=0; //第二次dummy的地址小於第一次dummy,那麼說明棧增長方向是向下的.
}
}
這個代碼不是我寫的,網上抄來的,思路很巧妙,利用遞歸,判斷兩次分配給dummy的地址,來比較棧是向下生長,還是向上生長.
如果你在STM32測試這個函數,你會發現,STM32的棧,是向下生長的.事實上,一般CPU的棧增長方向,都是向下的.
2,再來說說,堆(HEAP)的問題.
全局變量,靜態變量,以及內存管理所用的內存,都是屬於"堆"區,英文名:"HEAP"
與棧區不同,堆區,則從內存區域的起始地址,開始分配給各個全局變量和靜態變量.
堆的生長方向,都是向上的.在程序裏面,所有的內存分爲:堆+棧. 只是他們各自的起始地址和增長方向不同,他們沒有一個固定的界限,所以一旦堆棧衝突,系統就到了崩潰的時候了.
同樣,我們用附件裏面的例程測試:
stack_dir的地址是0X20000004,也就是STM32的內存起始端的地址.
這裏本來應該是從0X2000 0000開始分配的,但是,我仿真發現0X2000 0000總是存放:0X2000 0398,這個值,貌似是MSP,但是又不變化,還請高手幫忙解釋下.
其他的,全局變量,則依次遞增,地址肯定大於0X20000004,比如cpu_endian的地址就是0X20000005.
這就是STM32內部堆的分配規則.
3,再說說,大小端的問題.
大端模式:低位字節存在高地址上,高位字節存在低地址上
小端模式:高位字節存在高地址上,低位字節存在低地址上
STM32屬於小端模式,簡單的說,比如u32 temp=0X12345678;
假設temp地址在0X2000 0010.
那麼在內存裏面,存放就變成了:
地址 | HEX |
0X2000 0010 | 78 56 43 12 |
CPU到底是大端還是小端,可以通過如下代碼測試:
//CPU大小端
//0,小端模式;1,大端模式.
static u8 cpu_endian;
//獲取CPU大小端模式,結果保存在cpu_endian裏面
void find_cpu_endian(void)
{
int x=1;
if(*(char*)&x==1)cpu_endian=0; //小端模式
else cpu_endian=1; //大端模式
}
以上測試,在STM32上,你會得到cpu_endian=0,也就是小端模式.
3,最後說說,STM32內存的問題.
還是以附件工程爲例,在前面第一個圖,程序總共佔用內存:20+2348字節,這麼多內存,到底是怎麼得來的呢?
我們可以雙擊Project側邊欄的:Targt1,會彈出test.map,在這個裏面,我們就可以清楚的知道這些內存到底是怎麼來的了.在這個test.map最後,Image 部分有:
==============================================================================
Code (inc. data) RO Data RW Data ZI Data Debug Object Name
112 12 0 0 0 427 led.o
72 26 304 0 2048 828 startup_stm32f10x_hd.o //啓動文件,裏面定義了Stack_Size爲0X800,所以這裏是2048.
712 52 0 0 0 2715 sys.o
348 154 0 6 0 208720 test.o//test.c裏面,stack_dir和cpu_endian 以及*addr ,佔用6字節.
384 24 0 8 200 3050 usart.o//usart.c定義了一個串口接收數組buffer,佔用200字節.
1800 278 336 20 2248 216735 Object Totals //總共2248+20字節
0 0 32 0 0 0 (incl. Generated)
0 0 0 2 0 0 (incl. Padding)//2字節用於對其
104 0 0 0 0 84 __printf.o
52 8 0 0 0 0 __scatter.o
26 0 0 0 0 0 __scatter_copy.o
28 0 0 0 0 0 __scatter_zi.o
48 6 0 0 0 96 _printf_char_common.o
36 4 0 0 0 80 _printf_char_file.o
92 4 40 0 0 88 _printf_hex_int.o
184 0 0 0 0 88 _printf_intcommon.o
0 0 0 0 0 0 _printf_percent.o
4 0 0 0 0 0 _printf_percent_end.o
6 0 0 0 0 0 _printf_x.o
12 0 0 0 0 72 exit.o
8 0 0 0 0 68 ferror.o
6 0 0 0 0 152 heapauxi.o
2 0 0 0 0 0 libinit.o
2 0 0 0 0 0 libinit2.o
2 0 0 0 0 0 libshutdown.o
2 0 0 0 0 0 libshutdown2.o
8 4 0 0 96 68 libspace.o //庫文件(printf使用),佔用了96字節
24 4 0 0 0 84 noretval__2printf.o
0 0 0 0 0 0 rtentry.o
12 0 0 0 0 0 rtentry2.o
6 0 0 0 0 0 rtentry4.o
2 0 0 0 0 0 rtexit.o
10 0 0 0 0 0 rtexit2.o
74 0 0 0 0 80 sys_stackheap_outer.o
2 0 0 0 0 68 use_no_semi.o
2 0 0 0 0 68 use_no_semi_2.o
450 8 0 0 0 236 faddsub_clz.o
388 76 0 0 0 96 fdiv.o
62 4 0 0 0 84 ffixu.o
38 0 0 0 0 68 fflt_clz.o
258 4 0 0 0 84 fmul.o
140 4 0 0 0 84 fnaninf.o
10 0 0 0 0 68 fretinf.o
0 0 0 0 0 0 usenofp.o
2118 126 42 0 100 1884 Library Totals //調用的庫用了100字節.
10 0 2 0 4 0 (incl. Padding) //用於對其多佔用了4個字節
1346 96 0 0 0 720 fz_ws.l
2118 126 42 0 100 1884 Library Totals
Code (inc. data) RO Data RW Data ZI Data Debug
3918 404 378 20 2348 217111 ELF Image Totals
3918 404 378 20 0 0 ROM Totals
Total RW Size (RW Data + ZI Data) 2368 ( 2.31kB) //總共佔用:2248+20+100=2368.
Total ROM Size (Code + RO Data + RW Data) 4316 ( 4.21kB)
通過這個文件,我們就可以分析整個內存,是怎麼被佔用的,具體到每個文件,佔用多少.一目瞭然了.
4,最後,看看整個測試代碼:
main.c代碼如下,工程見附件.
#include "sys.h"
#include "usart.h"
#include "delay.h"
#include "led.h"
#include "beep.h"
#include "key.h"
//ALIENTEK戰艦STM32開發板堆棧增長方向以及CPU大小端測試
//0,向下增長;1,向上增長.
static u8 stack_dir;
//0,小端模式;1,大端模式.
static u8 cpu_endian;
//查找棧增長方向,結果保存在stack_dir裏面.
void find_stack_direction(void)
{
static u8 *addr=NULL; //用於存放第一個dummy的地址。
u8 dummy; //用於獲取棧地址
if(addr==NULL) //第一次進入
{
addr=&dummy; //保存dummy的地址
find_stack_direction (); //遞歸
}else //第二次進入
{
if(&dummy>addr)stack_dir=1; //第二次dummy的地址大於第一次dummy,那麼說明棧增長方向是向上的.
else stack_dir=0; //第二次dummy的地址小於第一次dummy,那麼說明棧增長方向是向下的.
}
}
//獲取CPU大小端模式,結果保存在cpu_endian裏面
void find_cpu_endian(void)
{
int x=1;
if(*(char*)&x==1)cpu_endian=0; //小端模式
else cpu_endian=1; //大端模式
}
int main(void)
{
Stm32_Clock_Init(9); //系統時鐘設置
uart_init(72,9600); //串口初始化爲9600
delay_init(72); //延時初始化
LED_Init(); //初始化與LED連接的硬件接口
printf("stack_dir:%x\r\n",&stack_dir);
printf("cpu_endian:%x\r\n",&cpu_endian);
find_stack_direction(); //獲取棧增長方式
find_cpu_endian(); //獲取CPU大小端模式
while(1)
{
if(stack_dir)printf("STACK DIRCTION:向上生長\r\n\r\n");
else printf("STACK DIRCTION:向下生長\r\n\r\n");
if(cpu_endian)printf("CPU ENDIAN:大端模式\r\n\r\n");
else printf("CPU ENDIAN:小端模式\r\n\r\n");
delay_ms(500);
LED0=!LED0;
}
}
一、內存基本構成
可編程內存在基本上分爲這樣的幾大部分:靜態存儲區、堆區和棧區。他們的功能不同,對他們使用方式也就不同。
靜態存儲區:內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。它主要存放靜態數據、全局數據和常量。
棧區:在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。
堆區:亦稱動態內存分配。程序在運行的時候用malloc或new申請任意大小的內存,程序員自己負責在適當的時候用free或delete釋放內存。動態內存的生存期可以由我們決定,如果我們不釋放內存,程序將在最後才釋放掉動態內存。 但是,良好的編程習慣是:如果某動態內存不再使用,需要將其釋放掉,否則,我們認爲發生了內存泄漏現象。
按照這個說法,我在.s文件裏面設置了:
Heap_Size EQU 0x00000000
也就是,沒有任何動態內存分配。
這樣,內存=靜態存儲區+棧區了。
不存在堆!!!
因爲我沒有用malloc來動態分配內存。
因此,前面提到的一切堆區,其實就是靜態存儲區。
一、內存基本構成
可編程內存在基本上分爲這樣的幾大部分:靜態存儲區、堆區和棧區。他們的功能不同,對他們使用方式也就不同。
靜態存儲區:內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。它主要存放靜態數據、全局數據和常量。
棧區:在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。
堆區:亦稱動態內存分配。程序在運行的時候用malloc或new申請任意大小的內存,程序員自己負責在適當的時候用free或delete釋放內存。動態內存的生存期可以由我們決定,如果我們不釋放內存,程序將在最後才釋放掉動態內存。 但是,良好的編程習慣是:如果某動態內存不再使用,需要將其釋放掉,否則,我們認爲發生了內存泄漏現象。
按照這個說法,我在.s文件裏面設置了:
Heap_Size EQU 0x00000000
也就是,沒有任何動態內存分配。
這樣,內存=靜態存儲區+棧區了。
不存在堆!!!
因爲我沒有用malloc來動態分配內存。
因此,前面提到的一切堆區,其實就是靜態存儲區。
另外,經過測試,確實是這樣。
STM32的內存分配,應該分爲兩種情況。
1,使用了系統的malloc。
2,未使用系統的malloc。
第一種情況(使用malloc):
STM32的內存分配規律:
從0X20000000開始依次爲:靜態存儲區+堆區+棧區
第二種情況(不使用malloc):
STM32的內存分配規律:
從0X20000000開始依次爲:靜態存儲區+棧區
第二種情況不存在堆區。
所以,一般對於我們開發板例程,實際上,沒有所謂堆區的概念,而僅僅是:靜態存儲區+棧區。
無論哪種情況,所有的全局變量,包括靜態變量之類的,全部存儲在靜態存儲區。
緊跟靜態存儲區之後的,是堆區(如沒用到malloc,則沒有該區),之後是棧區。