ucos在s3c2410上運行過程整體剖析之基礎知識-c語言和堆棧

來源:互聯網
上載者:User

 

我們知道C語言是一種進階語言,所謂進階語言就是要經過翻譯才能在具體平台上啟動並執行程式。而編譯器是一種比較繁瑣的程式,它要把進階語言編譯和連結後,成為能夠在具體平台啟動並執行程式。這其中有很多知識是和作業系統和具體硬體平台相關的,如果你想弄清楚編譯器請學習編譯原理,有一本書可以參考《linkers_and_loaders》。

 

我們這裡只是說明一下C語言啟動並執行環境以及和棧的關係。讓我們從組合語言和底層硬體來瞭解C語言的一些概念和C語言是如何利用棧來進控制程序呼叫的。

先講一下棧:

棧是這樣一種結構:本事是一段連續的記憶體空間,怎麼使用這樣一種記憶體空間才算是起到了棧的實際作用那,首先要規定這一段連續空間的基地址,然後就從這個地址開始依次放東西。取東西時也是從最上面的開始取。按照上面的方案管理這一段儲存空間,就是發揮了棧的作用。因為棧使用的頻率實在是太高了,所以在電腦彙編層次就有專門操作棧的指令。包括push(入棧)、pop(出棧)等。

其實棧又有一些邏輯上的分類:

根據先騰出空間再用還是先用再騰空間分為:

1,滿堆棧:即入棧後堆棧指標sp指向最後一個入棧的元素。也就是sp先減一(加一)再入棧。

2,空堆棧:即入棧後堆棧指標指向最後一個入棧元素的下一個元素。也就是先入棧sp再減一(或加一)。

根據從高地址開始用還是從低地址開始用分為:

1,遞增堆棧:即堆棧一開始的地址是低地址,向高地址開始遞增。就如同一個水杯(假設上面地址大)開口的是大地址,從杯底開始裝水。自己畫一畫圖就清楚了。我就偷懶一下不畫了。

2,遞減堆棧:即堆棧一開始的地址是高地址,向低地址開始遞增。就如同還是剛才說的那個水杯,現在開口的是小地址,從大地址開始用,往下走,相當於杯子口朝下。我們用的時候是把水往上一點點壓上去。呵呵呵,不過這樣的杯子就失去了用途。但在記憶體上還是可以的。

那麼根據這兩種分類方法,我們就可以得到四種棧的類型,而ARM920T中使用的是遞減滿堆棧。

下面重點說明c語言運行時是怎麼用棧來控制函數調用過程的。

大家想一想,我們寫c語言時用到函數調用,有時候還嵌套調用很多函數。還有有些函數還需要參數和傳回值。怎麼處理各個函數的參數和傳回值,以及當每一個函數完成工作時該返回到那個地方。這些都是要解決的問題。當然最容易想到的也是必須做的是在進行調用跳轉之前,把我這個函數現有的狀態儲存起來,儲存什麼那,調用函數返回後的下一條指令,還有我這個函數需要的哪些資料。還有就是儲存這些資訊到哪些地方哪?這些都是我們要解決的問題。還有就是你不光要儲存這些資訊,還要儲存這些資訊的順序。因為函數調用本身有順序,你像a調用b,b又接著調用c。在c執行完後要返回到b,b執行完再返回a。呵呵,有順序。

我們一一想辦法來解決,當然別人已經用棧的策略解決的很完美了,我們只是想一些更簡潔的最容易想起來的但是不完善的方法,也正說明了人家的策略是多麼的優秀。

關於調用函數的問題,我們可以把返回地址儲存到一些地方,當然程式員知道在那?還知道順序,再根據順序返回就好了,但做這樣的工作太累了,除了寫程式還要記這些東西。哎肯定不好也不這樣做。關於傳參,有這樣可以考慮的,用專門規定好的寄存器來做傳參。行,但有缺陷,如果傳的參數很多或者是變化的,就不好用寄存器傳參了。而且我們有作業系統時往往要求編譯器產生的代碼具有可重新進入性,也就是保證代碼和資料的相對獨立性。一個函數被調用兩次,都有兩次的參數環境。到底現在我們是怎麼做的那。答案是用棧。

怎麼用,嘻嘻,下面一一道來:

 

函數在執行一個函數調用調用時,用棧不僅儲存函數的返回地址,並且一起把函數所需要的參數和傳回值都儲存在堆棧中。

 

也就是每一個函數都有一個這樣的棧,儲存著一些資訊。先說一下棧幀的概念,在函數調用過程中要儲存的整個參數集合,包括返回地址稱作一個棧幀。

 

如所示,我們以這個圖為例,分析一下棧在函數調用中的應用。函數p有兩個參數 x1、x2,函數p 調用函數q ,且有兩個參數。儲存在棧幀的第一幀是上一個棧幀的地址,當前棧幀的地址就是正在使用的棧幀的基值,使用這個指標能方便的找到函數所需的變數和參數等。那為什麼每一針的第一個地址要儲存上一個棧幀的地址那,和儲存返回地址一個初衷,當你當前用的棧幀用完時,把在當前棧幀儲存的上一個棧幀的地址取出就還原了上一個棧幀。

接著是返回地址,如果函數有傳回值的話,傳回值放在返回地址的下面。接著為函數所需要的變數申請空間。

下面說說函數p調用函數q時的具體情況。當執行call q(y1)時,會為函數q建立一個新的棧幀,具體過程是:先儲存丄一幀的地址,如果有傳回值的話為傳回值分配儲存空間,然後儲存返回地址。然後為y1分配空間並把它初始化為調用q時給的參數。接著分配另一個參數的空間y2,這個參數用於在函數內部計算。

在任何狀態下,都有一個當前棧幀的指標fp,這個指標用來儲存當前棧幀的地址,那這個值怎麼保證是當前的那,先說q函數的吧,是把棧的指標sp先儲存下來,然後接著儲存fp。然後把fp的值改為sp-4 ,因為我們知道每個棧幀的第一個要儲存的是fp。

其實整個過程是動態,所謂我說是動態是因為sp指標一直是快速移動的。所以要在每一幀開始的時候先把這個sp儲存住。然後往減4的地址處放fp。當這個棧幀全彈出時,就把你儲存的fp又恢複到原來你儲存的fp了。就一直有fp代表當前棧幀底部。也是唯一一個不變的基地址,用它來找其他的變數。

好了,下面我們看一個在ARM下C語言寫的程式然後編譯成組合語言分析其棧的應用。(在linux2.4核心下寫的c語言程式,用arm-linux-gcc3.4.1編譯器編譯)

C語言來源程式如下:

#include<stdio.h>

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   r3, [fp, #-24]

       b     .L2

.L3:

       ldr   r3, [fp, #-20]

       str   r3, [fp, #-24]

.L2:

       ldr   r0, [fp, #-24]

       sub  sp, fp, #12

       ldmfd      sp, {fp, sp, pc}

       .size max, .-max

       .ident      "GCC: (GNU) 3.4.1"

以上代碼比較長,告訴大家一個密碼,其實原來說的main()函數其實也是一個普通的函數,只是由更進階的東東調用而已,你寫的普通函數不能調用而已。我們就先不管main()函數的事,重點從main()函數調用max()函數開始。用紅色標出來的是函數的調用開始。我們觀察一下在調用max()之前有兩句代碼,這是把調用max()時的參數a,b傳到了r0和r1中,在max()函數中再從r0和r1中來取,這就實現了主函數main()和max()函數傳參的機制。當然,這裡的傳參是利用的寄存器傳參,哎,這裡怎麼不是用棧傳參那,這和ARM編程時的程序呼叫規範有關,在ARM編程時有一個叫ATPCS的調用規範規定,我們編程時要按這個規範來編寫。關於這方面的知識可以參考ATPCS.pdf(英文官方資料)和ATPCS概述(中文版簡要介紹)。其中簡單的一個規則就是少於4個參數的都用r0~r3這四個寄存器傳參,當參數多於4個時,將剩餘的字資料傳送到資料棧中,入棧的順序與參數順序相反,即最後一個字資料先入棧。這裡參數只有兩個,因此只用了寄存器傳參。下面分析跳轉到max後組合語言做到工作。

 

下面看看arm的組合語言的棧幀的使用。

max:

       @ args = 0, pretend = 0, frame = 12

       @ frame_needed = 1, uses_anonymous_args = 0

mov ip, sp //先把開始建立棧幀的sp的值儲存

       stmfd      sp!, {fp, ip, lr, pc} //接著儲存相關資訊

       sub  fp, ip, #4//重新設定fp使其指向當前棧幀

       下面就是申請空間和有關計算了

       sub  sp, sp, #12//申請了三個整形變數,其中兩個局部變數a,b,另一個是用於存放傳回值。

       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   r3, [fp, #-24]

       b     .L2

.L3:

       ldr   r3, [fp, #-20]

       str   r3, [fp, #-24]

.L2:

       ldr   r0, [fp, #-24] //把傳回值放到R0中,在ATPCS中規定了函數的傳回值放到R0中。

       sub  sp, fp, #12// 清除你分配局部變數和傳回值的儲存空間

       ldmfd      sp, {fp, sp, pc}//恢複上一棧幀的相關資訊,並實現了函數返回。

       .size max, .-max

       .ident      "GCC: (GNU) 3.4.1"

上面說的有點亂,請你根據我前面說的用棧實現函數程序呼叫規範和具體案例自己好好分析一下。至於用棧傳參的情況你可以把參數的數量多於4個,然後編譯看看是怎麼實現的哦。

你現在是不是覺著棧這個東東很有魅力,用棧實現的程序呼叫控制是多麼的完美。呵呵。

看完了這個知識點,裡麵包括了c語言是怎麼實現的函數調用以及參數的傳遞的具體實現。

那你對學習c語言時遇到的一些概念,是不是更自然的接受了。比如局部變數,變數的生命期,變數的範圍等概念。你現在知道什麼是局部變數了吧,這種變數空間是隨著程式的執行動態分配以及消亡的。而全域變數是編譯、連結時預留的特定的記憶體地區,是不會消亡的,如果程式在作業系統之上運行,除非它的真箇程式結束。這方面的知識將在編譯器章節中詳細介紹。

其實,在電腦上有兩種分配空間的方法,一:把一段空間預留出來,或者初始化或者不初始化。這段空間就算是分配出來了,可以被程式使用。二:在程式運行期間用指令在棧的空間上分配。

課外話:我們學習新事物時,總會有一些疑問,這是好現象。你有疑問就說明你想弄明白這究竟是怎麼回事。你真正弄明白了才可能會使用這種新事物去解決類似的問題。如果你對新事物沒有任何疑問,而是一味的信任和記憶,那麼我說你不可能用它來解決任何問題,因為那隻是你一廂情願,那個新事物根本就不認識你,也不想搭理你。嘻嘻。從學習剛才的知識可以看出,如果一個概念你在他所講的層次上去思考,你怎麼也理解不了時,別灰心,別放棄這中疑問和好奇心。你只要有這顆心,你以後一定能對你的疑問做出一個完美的解答,當然一定不會是從原來的角度來理解的。就像C語言,如果從C語言的角度給你闡述概念,你就怎麼也理解不了,而從彙編和CPU的角度講就比較自然了。那麼,我們就知道了學習是一個逐漸的深入過程,但前提是你有好奇心想把這個東東弄明白,而且還從不想放棄。

做一個比喻;學習就像在地上挖井,你不可能一次就很容易的挖出甘泉,而可能會需要很多次努力,最終也不一定得到甘泉,但起碼這口井會越來越接近甘泉。我們不要放棄下面有甘泉的信心就好。還有,我們人本來就有享受學習過程的天性,你想想你自己學走路的情形,你可能想不起來了,你可以到網上搜一下小孩學走路的視頻,學習走路是很辛苦的,因為那要摔倒很多次,但他們在學習的過程中都是很開心的。只是我們長大了,就連這個最基本的樂趣也丟失了。原因有很多,我們長大了,考慮的事情多了,對外界的評價太過於重視,其實我們真的應該享受學習的過程,而不是最後的成績。在學習的過程中享受那種科學的嚴謹和美、巧妙的方法。哎,說多了~

好了,c語言函數的執行和棧的有關情況就說到這。

 

 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.