Linux系統--棧幀詳解

一、 什麼是棧幀?

    什麼是棧幀,相信很多從事C編程的童鞋還是沒有搞明白,首先引用百度百科的經典解釋:“棧幀也叫過程活動記錄,是編譯器用來實現過程/函數調用的一種數據結構”。

    實際上,可以簡單理解爲:棧幀就是存儲在用戶棧上的(當然內核棧同樣適用)每一次函數調用涉及的相關信息的記錄單元。也許這樣感覺更復雜了,好吧,讓我們從棧開始來理解什麼是棧幀...

二、 棧(用戶棧和內核棧)

    在大學學習《數據結構》的時候,瞭解到棧作爲一種特殊的數據結構而存在(和“隊列”相反的記錄結構和操作規則),是一種只能在一端進行插入和刪除操作的特殊線性表

    它按照後進先出的原則存儲數據,先進入的數據被壓入棧底,最後的數據在棧頂,需要讀數據的時候從棧頂開始彈出數據(最後一個數據被第一個讀出來)。

    棧有很多自己的特性,它具有記憶功能,對棧的插入與刪除操作中,不需要改變棧底指針;而且棧是從高地址向低地址延伸的。每個函數的每次調用,都有它自己獨立的一個棧幀,這個棧幀中維持着所需要的各種信息。因此棧作用就是用來保持棧幀的活動記錄(即函數調用)。下面有這樣一幅圖(源自Unix環境高級編程第七章):


    對於一個棧來說,寄存器ebp和esp分別指向指向系統棧最上面一個棧幀的底部和棧幀頂部(實際上也是棧的頂部)。上圖可以清晰的看到棧位置在用戶空間的最頂部(從0xc0000000開始向下增長),下於堆對接,實際上堆與棧之間有很大的未使用空間,這裏不做詳述。

三、棧幀

    棧幀表示程序的函數調用記錄,而棧幀又是記錄在棧上面,很明顯棧上保持了N個棧幀的實體,(實際上我們這裏說的棧幀是軟件上的概念,據說有硬件概念,不是很瞭解),那就可以說棧幀將棧分割成了N個記錄塊,但是這些記錄塊大小不是固定的,因爲棧幀不僅保存諸如:函數入參、出參、返回地址和上一個棧幀的棧底指針等信息,還保存了函數內部的自動變量(甚至可以是動態分配內存,alloca函數就可以實現,但在某些系統中不行),因此,不是所有的棧幀的大小都相同。

    下面通過一個簡單的實例,來分析棧幀的記錄活動(這個說明實例參考:http://blog.csdn.net/yxysdcl/article/details/5569351):

void func(int m, int n)
{
    int a, b;
    a = m;
    b = n;
}

int main()
{
    ...
    func(m, n);
L:  下一條語句
    ...

    return 0;
} 

    上面是一個簡單的可執行代碼,目的是爲了說明棧幀在棧中的存儲形式,因爲一個可執行程序在程序的開始嵌入了啓動例程代碼(這段彙編代碼由編譯器嵌入可執行程序的其實位置,這裏不深究該行爲),在執行時由啓動例程調用main函數,可以說main函數是第一個被調用的C代碼函數,暫且認爲是main函數是第一函數。

    這裏的main函數只是簡單調用了一個函數func,那麼在main調用func函數前,棧的情況是下面這個樣子的:

    此時棧中只有一個main函數的棧幀,從低地址esp(棧頂指針)到高地址ebp(棧幀棧底指針)的這塊區域,就是當前main函數的棧幀。當main中調用func時,寫成彙編大致是:

    push m

    push n; 兩個參數壓入棧

    call func; 調用func,將返回地址(實際上是當前PC值的下一個值)填入棧,並跳轉到func

    當成功跳轉到func函數中時,func函數的棧幀就已經形成了,但是形成新的棧幀之前,必須要重新記錄當前棧幀的棧底指針ebp,下面的保存和切換ebp的幾個動作是由系統自動完成的(就像Linux中的中斷一樣,在進入中斷處理函數前要做很多的準備工作:如保存當前執行環境,這樣才能在處理程序結束後,恢復打斷的進程的環境),可以說這幾個動作被系統自動加入:

    __func:

        push ebp; 函數調用之所以能夠返回,單靠保持返回地址是不夠的,這一步壓棧動作很重要,因爲我們要標記函數調用者棧幀的幀底,這樣才能找出保存了的返回地址,棧頂是不用保存的,因爲上一個棧幀的頂部講會是func的棧幀底部。(兩棧幀相鄰的)

        mov ebp, esp; 上一棧幀的頂部,就是這個棧幀的底部

        ;暫時先看現在的棧的情況

                 ;到這裏,此時新的棧幀開始了,由下圖中間的一根長長的橫線隔開兩個棧幀

                 sub esp, 8   ;  int a, b 這裏聲明瞭兩個int,所以esp減小8個字節來爲a,b分配空間

                 mov dword ptr [esp+4], [ebp+12];   a=m

                 mov dword ptr [esp], [ebp+8]; b=n         

     這樣,棧的情況變爲:

                    ret 8     ;  返回,然後8是什麼意思呢,就是自動變量佔用的字節數,當返回後,esp-8,釋放參數m,n的空間

     由此可見,通過ebp,能夠很容易定位到上面的參數。當從func函數返回時,首先esp移動到棧幀底部(即釋放自動變量),然後把上一個函數的棧幀底部指針彈出到ebp,再彈出返回地址到cs:ip上,esp繼續移動劃過參數,這樣,ebp,esp就回到了調用函數前的狀態,即現在恢復了原來的main的棧幀。

    OK,到這裏應該說明白了棧幀在棧幀的分佈和形成過程,那麼棧幀在我們編程過程中給我們什麼啓示呢?

(1)棧幀上的動態內存分配

    前面已經說明過一點:在大部分系統中,棧幀上可以進行動態內存的分配。malloc、calloc和realloc函數都是在堆上動態分配一塊內存,在使用過後一定要記得釋放動態分配的內存,否則就會產生內存泄露,最終降低系統的性能。

    但是如果要在棧幀上動態分配內存的話,那麼在函數返回時會自動釋放這些內存,而不必擔心忘記釋放動態分配的內存。我們知道在linux內核中,每個進程的棧只有1-2個頁的大小,即4K-8K大小,需要很珍惜的使用這部分空間;不過實用戶棧的空間很大,可以隨着需要動態的擴充,而不必擔心棧不夠用,因此我們還是可以放心的使用alloca動態分配函數在用戶棧幀上分配內存。

(2)函數調用深度

    在很多系統中都對函數調用的深度做了限制,函數調用深度是指函數嵌套的程度。函數嵌套的程度決定了在棧上同一時刻所擁有的棧幀的最大數量,函數調用的嵌套程度對用戶進程來說不是什麼問題,但是在內核中棧的大小固定且不能重新分配,因此調用的深度在內核中就存在很大的意義,這裏我們不做詳述。

(3)函數調用的參數

    棧幀部分已經描述了函數參數的保存位置,即保存在調用者棧幀的尾部固定長度偏移位置,程序運行時就根據函數的定義和該位置取參數進行相應的運算。

    注意:這裏函數調用的參數顯然存儲在函數調用者的棧幀中,而不是被調用函數的棧幀中。

(4)棧的回溯

    學習編程和Linux內核的童鞋一定經常聽到“棧的回溯”,它是指系統自主打印進程調用棧的行爲。從上面描述棧幀的情況可以看出,系統在將棧打印出來的順序應當是調用的反順序,它是從esp(低地址)一點一點向高地址回溯,這正是棧幀形成的反過程。因此,我們經常從下到上看函數的調用,不過有些日誌系統將導出的回溯信息重新排序,可以從上到下來查看函數調用順序。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章