? ? ?函數(shù)中的棧
棧是一種具有先入后出特性的數(shù)據(jù)結(jié)構(gòu),前面說(shuō)過(guò),這種特性常常用來(lái)幫住我們“原理返回”或者“保持原樣”。試想,當(dāng)我們第一次來(lái)到一個(gè)陌生的城市,走在陌生的街道上,尋找一個(gè)陌生的目標(biāo),最令我們有安全感的莫過(guò)于仔細(xì)記錄走過(guò)的每一個(gè)街道、穿過(guò)的每一個(gè)路口--這種安全感來(lái)源于潛意識(shí)里“萬(wàn)一找不到目的地就原路返回”的想法。記得20世紀(jì)90年代,有一首家喻戶曉的流行歌曲《星星點(diǎn)燈》中曾這樣唱到“星星點(diǎn)燈。。。為迷失的孩子,照亮來(lái)時(shí)的路”。
“找到來(lái)時(shí)的路”這種想法是人們基本的求生本能,對(duì)有人類編寫(xiě)的C語(yǔ)言編譯器來(lái)說(shuō),也是這樣--面對(duì)一層一層復(fù)雜嵌套關(guān)系的函數(shù)調(diào)用,編譯器總是試圖記錄下我們調(diào)用的過(guò)程,以便“找回回去的路”。棧就在這種場(chǎng)合中,得到了廣泛的應(yīng)用。
C語(yǔ)言支持函數(shù)的調(diào)用,這完全得益于棧式分配策略的使用。所謂棧式分配,拋去復(fù)雜的技術(shù)細(xì)節(jié),簡(jiǎn)單說(shuō)來(lái),就是將函數(shù)內(nèi)部使用的種種信息(例如,局部變量)在發(fā)生函數(shù)嵌套調(diào)用時(shí),壓入棧中“記錄下所走過(guò)的路”。這樣,當(dāng)調(diào)用的函數(shù)運(yùn)行結(jié)束需要返回時(shí),編譯器就能很容易從棧中找到“來(lái)時(shí)的路”。使用模擬的方法,我們來(lái)具體看看這一過(guò)程。
我們假設(shè):一個(gè)函數(shù)中所有牽涉到的局部信息都被包含在一個(gè)與函數(shù)同名的接節(jié)點(diǎn)中。當(dāng)我們?cè)谀骋粋€(gè)函數(shù)中發(fā)生了對(duì)另外一個(gè)函數(shù)的調(diào)用,就將本函數(shù)的局部信息壓入棧中--也就是將以該函數(shù)命名的結(jié)點(diǎn)壓入棧中:當(dāng)我們從某一函數(shù)中返回,就從棧中彈出一個(gè)結(jié)點(diǎn)。觀察一段代碼的函數(shù)調(diào)用情況,了解編譯器如何借助來(lái)實(shí)現(xiàn)函數(shù)的嵌套調(diào)用。
? ? ? 軟件堆棧和硬件堆棧
ICCAVR 使用兩個(gè)堆棧:一個(gè)用于子程序調(diào)用和中斷操作的硬件堆棧,一個(gè)用于傳遞參數(shù)、臨時(shí)變量和局部變量的軟件堆棧??梢允褂枚褩?a target="_blank">檢測(cè)函數(shù)檢測(cè)兩個(gè)堆棧是否溢出。
看其棧頂指針是否和CPU具有特殊的關(guān)聯(lián),有關(guān)聯(lián)者(如SP)“硬”,而無(wú)關(guān)聯(lián)者“軟”。
單片機(jī)在執(zhí)行調(diào)用子程序的指令時(shí),一般會(huì)把返回地址自動(dòng)存入堆棧,而沒(méi)有被單片機(jī)自動(dòng)入棧但是也需要保存的內(nèi)容比如狀態(tài)寄存器、通用寄存器等,就得通過(guò)PUSH等指令把它們?nèi)藶榈乇4娴蕉褩V?。自?dòng)入棧和“人為入?!笨赡苁褂玫氖且粋€(gè)堆棧指針。有的單片機(jī)可以分開(kāi),比如AVR,可以通過(guò)“ST -Y, R0”這樣的指令把R0存入軟件堆棧區(qū)(Y是由R28和R29兩個(gè)寄存器的值組成的16位指針),有的單片機(jī)缺少這樣的指令,就會(huì)把軟件堆棧和硬件堆棧放在一個(gè)??臻g,都使用SP,比如51.
硬件堆棧:或許也可以稱作系統(tǒng)堆棧,是位于片內(nèi)RAM區(qū)。有人說(shuō),只要能使用PUSH,POP指令的單片機(jī),都可以說(shuō)含有硬件堆棧。這樣的說(shuō)法我個(gè)人覺(jué)得不是很全面。通過(guò)指令進(jìn)行壓棧和出棧操作只是系統(tǒng)堆棧中的一種操做。系統(tǒng)堆棧還可以被隱含調(diào)用。例如,當(dāng)調(diào)用子程序時(shí),系統(tǒng)會(huì)主動(dòng)把返回地址壓入堆棧,并不需要用戶通過(guò)指令操作。通常,棧底設(shè)在內(nèi)存的高端,也就是把內(nèi)存的最高一段空間劃作棧區(qū)。這些都是向下生長(zhǎng)棧。棧指針可能是專用的寄存器,也可能借用一通用寄存器。也有單片機(jī)是在數(shù)據(jù)區(qū)里劃一塊作棧區(qū),可能是向上生長(zhǎng),也可能是向下生長(zhǎng)。
硬件堆棧:是通過(guò)寄存器SPH,SPL做為索引指針的地址,是調(diào)用了CALL,RCALL等函數(shù)調(diào)用指令后硬件自動(dòng)填充的堆棧!
軟件堆棧:是編譯器為了處理一些參數(shù)傳遞而做的堆棧,會(huì)由編譯器自動(dòng)產(chǎn)生和處理,可以通過(guò)相應(yīng)的編譯選項(xiàng)對(duì)其進(jìn)行編輯。
簡(jiǎn)單一點(diǎn)說(shuō),硬件堆棧主要做為地址堆棧用,而軟件堆棧主要會(huì)被分配成數(shù)據(jù)堆棧!
如果沒(méi)有硬堆棧,你可以選定一個(gè)寄存器作堆棧指針,通過(guò)軟件實(shí)現(xiàn)堆棧操作。移植μC/OS-II也不一定要硬堆棧。ARM 就很難說(shuō)它的堆棧是軟的還是硬的。32位的ARM指令中沒(méi)有PUSH、POP指令。ARM習(xí)慣上用R13作堆棧指針(SP),但用別的寄存器作堆棧指針也未常不可。ARM習(xí)慣上用LDM/STM(多寄存器加載/存儲(chǔ)指令)來(lái)操作堆棧,壓多少,按什么順序都能選擇。應(yīng)該說(shuō)ARM是軟硬結(jié)合的堆棧。
C代碼(AVR-GCC編譯,優(yōu)化等級(jí)-00):
#include 《avr/io.h》
int add(int a,int b)
{
int c;
c=a+b;
returnc;
}
int main(void)
{
inta=2,b=3,c=0;
c=add(a,b);
//c=sub(a,b);
}
匯編代碼:
(省略一些boot代碼)
。。。。。。。
00000054 《__ctors_end》:
54: 1124 eor r1,r1
56: 1fbe out 0x3f,r1 ;63
58: cfe5 ldi r28,0x5F ;95 //此處Y指針和SP都指到了SRAM最高端
5a: d4e0 ldi r29,0x04 ;4
5c: debf out 0x3e,r29 ;62
5e: cdbf out 0x3d,r28 ; 61
。。。
0000008e 《add》:
#include 《avr/io.h》
int add(int a,int b)
{
8e: cf93 push r28
90: df93 push r29 //保存了Y指針,此時(shí)SP已經(jīng)-2,這里再減2
92: cdb7 in r28,0x3d ;61 //重新定位Y指針跟SP一樣。
94: deb7 in r29,0x3e ;62
96: 2697 sbiw r28,0x06 ;6 //減掉6,即向下開(kāi)了6字節(jié)的區(qū)域,存放3變量
98: 0fb6 in r0,0x3f ;63
9a: f894 cli
9c: debf out 0x3e,r29 ;62
9e: 0fbe out 0x3f,r0 ; 63
a0: cdbf out 0x3d,r28 ;61
a2: 9a83 std Y+2,r25 ;0x02
a4: 8983 std Y+1,r24 ;0x01
a6: 7c83 std Y+4,r23 ;0x04
a8: 6b83 std Y+3,r22 ;0x03
int c;
c=a+b;
aa: 2981 ldd r18,Y+1 ;0x01
ac: 3a81 ldd r19,Y+2 ;0x02
ae: 8b81 ldd r24,Y+3 ;0x03
b0: 9c81 ldd r25,Y+4 ;0x04
b2: 820f add r24,r18
b4: 931f adc r25,r19
b6: 9e83 std Y+6,r25 ;0x06
b8: 8d83 std Y+5,r24 ;0x05
returnc;
ba: 8d81 ldd r24,Y+5 ;0x05
bc: 9e81 ldd r25,Y+6 ;0x06
be: 2696 adiw r28,0x06 ;6 //加了6個(gè)字節(jié)空間,Y指針恢復(fù)到減6之前
c0: 0fb6 in r0,0x3f ;63
c2: f894 cli
c4: debf out 0x3e,r29 ;62
c6: 0fbe out 0x3f,r0 ; 63
c8: cdbf out 0x3d,r28 ;61
ca: df91 pop r29
cc: cf91 pop r28
ce: 0895 ret //彈出堆棧中2個(gè)字節(jié)
000000d0 《main》:
}
int main(void)
{
d0: c9e5 ldi r28,0x59 ;89 //這4句給SP和Y指針重新賦值了,很明顯的在SP的
d2: d4e0 ldi r29,0x04 ;4 //上面還有6個(gè)字節(jié)(SRAM最大到045E),這6個(gè)字節(jié)
d4: debf out 0x3e,r29 ;62 //被存放了a,b,c三個(gè)變量(可以與上面理論對(duì)應(yīng))
d6: cdbf out 0x3d,r28 ;61 //通過(guò)Y指針來(lái)保存了這三個(gè)變量到這個(gè)區(qū)域
inta=2,b=3,c=0;
d8: 82e0 ldi r24,0x02 ;2
da: 90e0 ldi r25,0x00 ;0
dc: 9a83 std Y+2,r25 ;0x02
de: 8983 std Y+1,r24 ;0x01
e0: 83e0 ldi r24,0x03 ;3
e2: 90e0 ldi r25,0x00 ;0
e4: 9c83 std Y+4,r25 ;0x04
e6: 8b83 std Y+3,r24 ;0x03
e8: 1e82 std Y+6,r1 ;0x06
ea: 1d82 std Y+5,r1 ;0x05
c=add(a,b);
ec: 6b81 ldd r22,Y+3 ;0x03
ee: 7c81 ldd r23,Y+4 ;0x04
f0: 8981 ldd r24,Y+1 ;0x01
f2: 9a81 ldd r25,Y+2 ;0x02
f4: 0e 94 4700 call 0x8e《add》 //使用call時(shí)自動(dòng)將PC+2的地址壓到堆棧
f8: 9e83 std Y+6,r25 ;0x06
fa: 8d83 std Y+5,r24 ;0x05
//c=sub(a,b);
}
fc: 80e0 ldi r24,0x00 ;0
fe: 90e0 ldi r25,0x00 ;0
100: 0c 94 82 00 jmp 0x104《_exit》
00000104 《_exit》:
104: ffcf rjmp 。-2 ; 0x104《_exit》
r28和r29一起組成SP指針,Y指針可以作為間接尋址,很明顯的剛開(kāi)始的時(shí)候Y指針和SP都在045F這里,后來(lái)在高處開(kāi)了6個(gè)字節(jié)的空間來(lái)存放臨時(shí)變量,所以Y指針成了這個(gè)軟件堆棧的棧頂,在這個(gè)過(guò)程中都是使用Y和SP的配合來(lái)實(shí)現(xiàn)變量和數(shù)據(jù)的改變,以及恢復(fù),硬件堆棧和軟件堆棧在這里已經(jīng)不怎么區(qū)分了
棧是可變的,要留足夠的空間才行,如果沒(méi)有操作系統(tǒng)用的會(huì)很少,主要取決于函數(shù)的嵌套深度參數(shù)類型。
一般情況RAM存放三種類型的數(shù)據(jù):
1.全局變量
2.堆(典型的MALLOC函數(shù)調(diào)用),這個(gè)得看你用了還是沒(méi)有
3.棧,這個(gè)必然要用到的,有操作系統(tǒng)的話用的就更多了,每一個(gè)任務(wù)都會(huì)有一個(gè)棧,根據(jù)任務(wù)的函數(shù)嵌套程度可分配不同的大小。
具體要看什么編譯器了,所以首先要估計(jì)一下你的棧要用多少,然后,再計(jì)算一下你的全局變量有多少,最后定一下可能的動(dòng)態(tài)分配內(nèi)存(堆)有多少就可以了。
考慮到存儲(chǔ)器的大小的限制,我們?cè)诰帉?xiě)單片機(jī)的程序時(shí),一定要精打細(xì)算。一個(gè)程序,當(dāng)系統(tǒng)中使用了大量的中斷資源,并且允許了中斷嵌套的存在,那么在極端的情況下,中斷處理程序就很容易發(fā)生嵌套現(xiàn)象。此時(shí)適當(dāng)擴(kuò)大硬件堆棧的大小,支持較大的函數(shù)嵌套深度,往往能解決很多莫名其妙的跑飛問(wèn)題。與擁有豐富存儲(chǔ)器資源時(shí)的狀況不同,由于局部變量在函數(shù)發(fā)生嵌套時(shí),都要占用軟件堆棧空間,因此大量使用使用局部變量或者使用了占用空間頗為可觀的局部數(shù)組(也包括體積巨大的結(jié)構(gòu)體),在嵌套深度較大時(shí),都有可能造成向下生長(zhǎng)的軟件堆棧侵入其他存儲(chǔ)區(qū)域(詳細(xì)情形閱讀ICC的幫助文檔),導(dǎo)致某些變量意外修改、程序跑飛等現(xiàn)象。解決這一問(wèn)題的方法其實(shí)很簡(jiǎn)單,在某些局部變量占用空間較大的情況下,將其通過(guò)關(guān)鍵static聲明為靜態(tài)變量--這樣即保證了變量的局限性,又避免了將這些內(nèi)容壓入軟件棧中(靜態(tài)局部變量在存儲(chǔ)時(shí)和全局變量沒(méi)有本質(zhì)區(qū)別,采用的都是 靜態(tài)分配),只不過(guò)每次使用這些變量之前都要記得補(bǔ)充必要的初始化代碼。
評(píng)論