ucos在s3c2410上運行過程整體剖析之基礎知識-c語言和堆棧
我們知道C語言是一種高級語言,所謂高級語言就是要經過翻譯才能在具體平臺上運行的程序。而編譯程序是一種比較繁瑣的程序,它要把高級語言編譯和鏈接后,成為能夠在具體平臺運行的程序。這其中有很多知識是和操作系統(tǒng)和具體硬件平臺相關的,如果你想弄清楚編譯程序請學習編譯原理,有一本書可以參考《linkers_and_loaders》。
我們這里只是說明一下C語言運行的環(huán)境以及和棧的關系。讓我們從匯編語言和底層硬件來了解C語言的一些概念和C語言是如何利用棧來進控制過程調用的。
先講一下棧:
棧是這樣一種結構:本事是一段連續(xù)的內存空間,怎么使用這樣一種內存空間才算是起到了棧的實際作用那,首先要規(guī)定這一段連續(xù)空間的基地址,然后就從這個地址開始依次放東西。取東西時也是從最上面的開始取。按照上面的方案管理這一段存儲空間,就是發(fā)揮了棧的作用。因為棧使用的頻率實在是太高了,所以在計算機匯編層次就有專門操作棧的指令。包括push(入棧)、pop(出棧)等。
其實棧又有一些邏輯上的分類:
根據先騰出空間再用還是先用再騰空間分為:
1,滿堆棧:即入棧后堆棧指針sp指向最后一個入棧的元素。也就是sp先減一(加一)再入棧。
2,空堆棧:即入棧后堆棧指針指向最后一個入棧元素的下一個元素。也就是先入棧sp再減一(或加一)。
根據從高地址開始用還是從低地址開始用分為:
1,遞增堆棧:即堆棧一開始的地址是低地址,向高地址開始遞增。就如同一個水杯(假設上面地址大)開口的是大地址,從杯底開始裝水。自己畫一畫圖就清楚了。我就偷懶一下不畫了。
2,遞減堆棧:即堆棧一開始的地址是高地址,向低地址開始遞增。就如同還是剛才說的那個水杯,現在開口的是小地址,從大地址開始用,往下走,相當于杯子口朝下。我們用的時候是把水往上一點點壓上去。呵呵呵,不過這樣的杯子就失去了用途。但在內存上還是可以的。
那么根據這兩種分類方法,我們就可以得到四種棧的類型,而ARM920T中使用的是遞減滿堆棧。
下面重點說明c語言運行時是怎么用棧來控制函數調用過程的。
大家想一想,我們寫c語言時用到函數調用,有時候還嵌套調用很多函數。還有有些函數還需要參數和返回值。怎么處理各個函數的參數和返回值,以及當每一個函數完成工作時該返回到那個地方。這些都是要解決的問題。當然最容易想到的也是必須做的是在進行調用跳轉之前,把我這個函數現有的狀態(tài)保存起來,保存什么那,調用函數返回后的下一條指令,還有我這個函數需要的哪些數據。還有就是保存這些信息到哪些地方哪?這些都是我們要解決的問題。還有就是你不光要保存這些信息,還要保存這些信息的順序。因為函數調用本身有順序,你像a調用b,b又接著調用c。在c執(zhí)行完后要返回到b,b執(zhí)行完再返回a。呵呵,有順序。
我們一一想辦法來解決,當然別人已經用棧的策略解決的很完美了,我們只是想一些更簡潔的最容易想起來的但是不完善的方法,也正說明了人家的策略是多么的優(yōu)秀。
關于調用函數的問題,我們可以把返回地址保存到一些地方,當然程序員知道在那?還知道順序,再根據順序返回就好了,但做這樣的工作太累了,除了寫程序還要記這些東西。哎肯定不好也不這樣做。關于傳參,有這樣可以考慮的,用專門規(guī)定好的寄存器來做傳參。行,但有缺陷,如果傳的參數很多或者是變化的,就不好用寄存器傳參了。而且我們有操作系統(tǒng)時往往要求編譯器產生的代碼具有可重入性,也就是保證代碼和數據的相對獨立性。一個函數被調用兩次,都有兩次的參數環(huán)境。到底現在我們是怎么做的那。答案是用棧。
怎么用,嘻嘻,下面一一道來:
函數在執(zhí)行一個函數調用調用時,用棧不僅保存函數的返回地址,并且一起把函數所需要的參數和返回值都保存在堆棧中。
也就是每一個函數都有一個這樣的棧,保存著一些信息。先說一下棧幀的概念,在函數調用過程中要保存的整個參數集合,包括返回地址稱作一個棧幀。
如上圖所示,我們以這個圖為例,分析一下棧在函數調用中的應用。函數p有兩個參數 x1、x2,函數p 調用函數q ,且有兩個參數。儲存在棧幀的第一幀是上一個棧幀的地址,當前棧幀的地址就是正在使用的棧幀的基值,使用這個指針能方便的找到函數所需的變量和參數等。那為什么每一針的第一個地址要保存上一個棧幀的地址那,和保存返回地址一個初衷,當你當前用的棧幀用完時,把在當前棧幀保存的上一個棧幀的地址取出就還原了上一個棧幀。
接著是返回地址,如果函數有返回值的話,返回值放在返回地址的下面。接著為函數所需要的變量申請空間。
下面說說函數p調用函數q時的具體情況。當執(zhí)行call q(y1)時,會為函數q創(chuàng)建一個新的棧幀,具體過程是:先保存丄一幀的地址,如果有返回值的話為返回值分配存儲空間,然后保存返回地址。然后為y1分配空間并把它初始化為調用q時給的參數。接著分配另一個參數的空間y2,這個參數用于在函數內部計算。
在任何狀態(tài)下,都有一個當前棧幀的指針fp,這個指針用來保存當前棧幀的地址,那這個值怎么保證是當前的那,先說q函數的吧,是把棧的指針sp先保存下來,然后接著保存fp。然后把fp的值改為sp-4 ,因為我們知道每個棧幀的第一個要保存的是fp。
其實整個過程是動態(tài)的,所謂我說是動態(tài)的是因為sp指針一直是快速移動的。所以要在每一幀開始的時候先把這個sp保存住。然后往減4的地址處放fp。當這個棧幀全彈出時,就把你保存的fp又恢復到原來你保存的fp了。就一直有fp代表當前棧幀底部。也是唯一一個不變的基地址,用它來找其他的變量。
好了,下面我們看一個在ARM下C語言寫的程序然后編譯成匯編語言分析其棧的應用。(在linux2.4內核下寫的c語言程序,用arm-linux-gcc3.4.1編譯器編譯)
C語言源程序如下:
#include
int max(int,int);
int main(int argc,char *argv[])
{
int a=3,b=5;
max(a,b);
return 0;
}
int max(int x,int y)
{
if(x>y)
return x;
else
return y
}
函數很簡單,在主函數里調用一個外部函數max用于兩個數中數值較大的那個數并返回。
下面看ARM的匯編是怎么實現的這些功能,以及這中間棧的使用情況。
.file "max.c"
.text
.align 2
.global main
.type main, %function
main:
@ args = 0, pretend = 0, frame = 16
@ frame_needed = 1, uses_anonymous_args = 0
mov ip, sp
stmfd sp!, {fp, ip, lr, pc}
sub fp, ip, #4
sub sp, sp, #16
str r0, [fp, #-16]
str r1, [fp, #-20]
mov r3, #3
str r3, [fp, #-24]
mov r3, #5
str r3, [fp, #-28]
ldr r0, [fp, #-24]
ldr r1, [fp, #-28]
bl max //開始調用max函數
mov r3, #0
mov r0, r3
sub sp, fp, #12
ldmfd sp, {fp, sp, pc}
.size main, .-main
.align 2
.global max
.type max, %function
max:
@ args = 0, pretend = 0, frame = 12
@ frame_needed = 1, uses_anonymous_args = 0
mov ip, sp
stmfd sp!, {fp, ip, lr, pc}
sub fp, ip, #4
sub sp, sp, #12
str r0, [fp, #-16]
str r1, [fp, #-20]
ldr r2, [fp, #-16]
ldr r3, [fp, #-20]
cmp r2, r3
ble .L3
ldr r3, [fp, #-16]
str





