對堆棧的認識

一.堆棧在地址空間中的位置

任何一個程序通常都包含代碼段和數據段,這些代碼和數據本身都是靜態的。程序想要運行,首先要由操作系統爲其創建進程,並在進程的虛擬地址空間爲其代碼段和數據段建立映射。光有代碼段和數據段是不夠的,進程在運行過程中還要有其動態環境,其中最重要的就是堆棧。如圖所示爲Linux下進程的地址空間分佈:

    wKioL1eIP6fCRxtVAAATKbUNbKs843.png

    首先,execv(2)會爲進程代碼段和數據段建立映射,真正將代碼段和數據段的內容讀入內存是由系統的缺頁異常處理程序按需完成的。另外,execv(2)還會將bss段清零,這就是爲什麼未賦初值的全局變量以及static變量其初值爲零的原因。進程用戶空間的最高位置用來存放程序運行時的命令行參數和環境變量的,在這段地址空間的下方和bss段的上方還留有一個很大的空間,而作爲進程動態運行環境的堆棧和堆就在其中,其中堆棧向下伸展,堆向上伸展。

二.堆棧幀的結構

    堆棧中實際上存放的是與每個函數對應的堆棧幀,當函數調用發生時,新的堆棧幀被壓入堆棧;當函數返回時,相應的堆棧幀從堆棧中彈出。堆棧幀結構如圖所示:

    wKioL1eIQuXA8sBkAAAY86SiHyw049.png

    堆棧幀的頂部爲函數的實參,下面是函數的返回地址以及前一個堆棧幀的指針,最下面是分配給函數的局部變量使用的空間。一個堆棧幀通常有兩個指針,一個爲堆棧幀指針,另一個爲棧頂指針。前者所指向的位置是固定的,而後者所指向的位置在函數運行過程中可變。因此,在函數訪問實參或者是局部變量時都是以堆棧幀指針爲基址,再加上偏移量。由圖可知,實參的偏移量爲正,局部變量的偏移量爲負。

三.函數棧幀的分析

    如下代碼:

    int function(int a,int b,int c)

    {

        char buffer[14];

        int sum;

        sum=a+b+c;

     return sum;

    }

    int main()

    {

        int i;

        i=function(1,2,3);

    }

函數function的堆棧幀如圖所示:

    wKioL1eIR0ChHDqmAAAYhE_wiuY880.png

(1)函數function堆棧幀的構建

    其中,function是在main函數中被調用的,三個實參的值分別爲1,2,3。由於C語言中參數傳遞遵循反向壓棧順序,,因此三個參數從右至左依次被壓入堆棧。接下來除了將控制轉移到function之外,還要將下一條指令addl的地址,也就是function函數的返回地址壓入堆棧。接着進入function函數,首先將main函數的堆棧幀指針ebp保存着堆棧中,並在下一次將當前的棧頂指針esp保存着堆棧幀指針ebp中,最後爲function函數的局部變量buffer[14]和sum 在堆棧中分配空間。

(2)函數function將a,b,c的值賦給sum的過程

    在函數中訪問實參和局部變量時都是以堆棧幀指針爲基址,再加上偏移量。在這裏堆棧幀指針就是ebp,爲了清楚起見,圖中標出了堆棧幀中所以成分相對於堆棧幀指針ebp的偏移。

(3)函數hufunction執行完之後與其對應的堆棧幀銷燬過程

    首先,leave指令將堆棧幀指針ebp拷貝到esp中(即esp=ebp,這樣esp指向ebp的地址,所以局部變量sum,buffer的空間就被釋放了),於是在堆棧中爲局部變量buffer[14]和sum分配的空間就被釋放了;

leave指令還有一個功能,就是從堆棧幀彈出一個機器字並將其存放到ebp中(即接着上邊的操作,從堆棧中pop一個字其實就是把previous ebp的值取出來放到ebp寄存器中),這樣ebp就被恢復爲main函數的堆棧幀指針了。後邊的ret指令再次從堆棧幀彈出一個機器字並將其存放在指令指針eip中(即將address of addl pop出來賦給寄存器eip),這樣控制就返回了main函數中的addl指令處,addl指令將棧頂指針esp加上12,這樣調入函數function之前壓入堆棧的三個實參所佔用的堆棧空間也就被釋放了。

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