今天搞的一個stm32 的程序發生了錯誤。全局變量遭到了局部變量的篡改。新手感覺很奇特。
看了一些資料,發現時棧區設置太小所導致的,全局變量向上生長,棧區向下生長。stm32的棧頂是程序自動生成的(暫時是這麼認爲的,有待進一步確定),程序會地洞生成棧頂。並且棧底和全局變量區是緊挨的,因此如果棧溢出的話,會直接將全局變量去的地址拿來自己用,於是全局變量區的地址和棧區的地址重合,導致全局變量遭到局部變量篡改的錯誤。
看看下面一些專業的解釋會更清晰!
對於單片機這種封閉代碼的運行平臺,內存分配有2個大方向,一個是靜態變量,一個是動態變量,具體到作用域,又分爲局部變量和全局變量.
全局靜態變量:不管是否調用,它都在那裏,比如LZ示例<test.c>的 line:11 和 line:15,注意這裏加了<static>關鍵字,指明這個變量是並不是真正意義的全局變量,只是在這個文件的所有位置<聲明位置以後的所有位置>可用.
局部靜態變量:和全局靜態變量類似,也是不管拉不拉屎先佔坑的貨,比如LZ示例<test.c>的 line:23 .特點是加了關鍵字<static>,意思是在這個位置,它是唯一的.在<find_stack_direction>函數裏使用了遞歸,但局部靜態變量是不在遞歸裏重新分配空間的,原子也是通過這個方式來判斷兩次進入之間的地址關係.
局部動態變量:這個是最常見的,比如LZ示例<test.c>的 line:24,在這個示例裏,每次聲明<神燈啊神燈>,結果出來的都是新的神燈,許了願就溜掉,是這種變量的特點.它不會記得它曾經是什麼.注意,由於每次都喝了孟婆湯,有經驗的碼農會在召喚時默認賦一個初值,避免出現不可預料的使用.
全局動態變量:存在嗎?全局可見但又可以踢掉的奇葩嗎?抱歉,這句話對<全局>是個誤解.<全局>的意思是變量本身沒有編譯器指定的生命週期,也就是<作用域>,但還有代碼指定的生命週期.在LZ的示例裏,<堆>就是這麼一個東西,代碼說<你在>就在,<你不在>就不在.申請了堆後,只要誰(任何位置的代碼)知道這個位置是可以用的,誰都可以用(**具有進程內存保護的平臺除外**),即使申請空間的變量<掛了>,這個空間也一直存在,直到有代碼把它<銷燬>掉.
順便推銷老帖http://www.openedv.com/posts/list/19693.htm
修改+註釋.
**新的linux把uclinux統一了,不知道是否在單片機實現進程內存保護,同求證.不過這也不在<封閉代碼平臺>這個前提下了.
一、內存基本構成
可編程內存在基本上分爲這樣的幾大部分:靜態存儲區、堆區和棧區。他們的功能不同,對他們使用方式也就不同。
靜態存儲區:內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。它主要存放靜態數據、全局數據和常量。
棧區:在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。
堆區:亦稱動態內存分配。程序在運行的時候用malloc或new申請任意大小的內存,程序員自己負責在適當的時候用free或delete釋放內存。動態內存的生存期可以由我們決定,如果我們不釋放內存,程序將在最後才釋放掉動態內存。 但是,良好的編程習慣是:如果某動態內存不再使用,需要將其釋放掉,否則,我們認爲發生了內存泄漏現象。
按照這個說法,我在.s文件裏面設置了:
Heap_Size EQU 0x00000000
也就是,沒有任何動態內存分配。
這樣,內存=靜態存儲區+棧區了。
不存在堆!!!
因爲我沒有用malloc來動態分配內存。
因此,前面提到的一切堆區,其實就是靜態存儲區。
棧增長和大端/小端問題是和CPU相關的兩個問題.
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 部分有:
==============================================================================
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
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;
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;
}
}
測試結果如圖:
4、內存基本構成
可編程內存在基本上分爲這樣的幾大部分:靜態存儲區、堆區和棧區。他們的功能不同,對他們使用方式也就不同。
靜態存儲區:內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。它主要存放靜態數據、全局數據和常量。
棧區:在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。
堆區:亦稱動態內存分配。程序在運行的時候用malloc或new申請任意大小的內存,程序員自己負責在適當的時候用free或delete釋放內存。動態內存的生存期可以由我們決定,如果我們不釋放內存,程序將在最後才釋放掉動態內存。 但是,良好的編程習慣是:如果某動態內存不再使用,需要將其釋放掉,否則,我們認爲發生了內存泄漏現象。
按照這個說法,我在.s文件裏面設置了:
Heap_Size EQU 0x00000000
也就是,沒有任何動態內存分配。
這樣,內存=靜態存儲區+棧區了。
不存在堆!!!
因爲我沒有用malloc來動態分配內存。
因此,前面提到的一切堆區,其實就是靜態存儲區。