心中有棧


博客書寫不易,您的點贊收藏是我前進的動力,千萬別忘記點贊、 收**藏 ^ _ ^ !

棧基本術語

棧 (Stack )
在漢語中類似地稱爲棧或堆疊,是一種線性存儲結構。是一種限定只能在棧頂執行操作(存儲、查找、插入、刪除)的數據結構,具有LIFO(後進先出)特性。棧在內存結構中和數據結構所說是兩個不同的含義和領域。

棧頂與棧底
允許元素插入與刪除的一端稱爲棧頂,另一端稱爲棧底。

進棧
向棧插入元素的操作,叫做進棧,也稱壓棧、入棧

出棧
棧頂元素的刪除操作,也叫做出棧。

棧的數據結構

用自定義棧來簡單描述棧的數據結構,其數據可以用順序存儲和鏈式存儲兩種。
1)順序存儲結構即用數組實現棧

/**
 * @author :羅發新
 * time  :2020/6/27 0027  14:58
 * email :[email protected]
 * desc  :
 */
class ArrayStack {

    private int[] stack;

    /**
     * 默認分配空間
     */
    private final int DEFAULT_SIZE = 10;

    /**
     * 當前元素的數量
     */
    private int currentCount;

    /**
     * 默認大小構造
     */
    public ArrayStack() {
        stack = new int[DEFAULT_SIZE];
        currentCount = 0;
    }

    /**
     * 指定大小構造
     *
     * @param size 創建的數組大小
     */
    public ArrayStack(int size) {
        stack = new int[size];
    }

    /**
     * 返回當前棧元素數量
     *
     * @return 當前數組Count
     */
    public int getSize() {
        return currentCount;
    }

    /**
     * @return 獲取棧頂元素
     */
    public int getTop() {
        return stack[getSize()];
    }

    /**
     * 入棧
     *
     * @param element 元素
     * @return 返回是否
     */
    public boolean push(int element) {
        if (currentCount == stack.length) {
            return false;
        } else {
            stack[getSize()] = element;
            currentCount++;
            return true;
        }
    }

    /**
     * 出棧
     *
     * @return 退出
     */
    public int pop() {
        int result;
        if (currentCount == 0) {
            throw new RuntimeException();
        } else {
            result = stack[getSize() - 1];
            currentCount--;
            return result;
        }
    }
}

2)鏈式存儲結構即用鏈表實現棧(單鏈表)

class ListNode {
    /**
     * 節點數據值
     */
    int value;
    /**
     * 指向下一節點的next指針
     */
    ListNode next;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public ListNode getNext() {
        return next;
    }

    public void setNext(ListNode next) {
        this.next = next;
    }

    public ListNode(int value, ListNode next) {
        this.value = value;
        this.next = next;
    }
}


class LinkedStack {
    /**
     * 指向棧頂元素的top指針
     */
    private ListNode top;

    /**
     * @return 獲取棧頂元素
     */
    public int getTop() {
        return top.value;
    }

    /**
     * 入棧
     *
     * @param value 插入的值
     */
    public void push(int value) {
        ListNode listNode = new ListNode(value, null);
        if (top == null) {
            top = listNode;
        } else {
            listNode.next = top;
            top = listNode;
        }
    }

    /**
     * 出棧
     *
     * @return 出棧的數據
     */
    public int pop() {
        int result;
        if (top == null) {
            throw new RuntimeException();
        } else {
            result = top.value;
            top = top.next;
            return result;
        }
    }
}

內存中的棧

棧存在原因

在內存中,棧是一種線性數據結構。函數的參數,函數的局部變量,寄存器的值(用以恢復寄存器),函數的返回地址以及用於結構化異常處理的數據等數據有序的組織在一起形成一個棧幀,一個棧中有多個棧幀。

在編程中變量常有局部變量和全局變量之分。局部變量只在調用它所在的函數時纔會生效,一旦函數返回就失效了,所以很多局部變量的生存週期遠遠低於整個程序的運行週期。

如果爲每個局部變量分配不同的空間,讓它們像全局變量一樣先用確定的地址定位,那麼會存在一些問題:

  1. 函數調用完畢局部變量失效,這分配的地址就沒有用了,這將導致內存空間的利用率大大降低。

  2. 發生了遞歸調用,會存在某個函數尚未返回。通過這種多次調用,相同名稱的局部變量會有不同的值,這些值必須同時保存在內存之中,而且又不能互相影響。所以它們必須要儲存在不同的地址,這樣當一次遞歸完成後會有大量空間被浪費。

  3. 對於函數形參,與局部變量也非常相似,它們都不能通過像全局變量一樣用固定的地址加以定位。

解決如上問題的方案就上將局部變量、形參等數據存儲在一種特殊的結構中,那就是。這種存儲函數形參和局部變量的棧,也稱之爲運行棧。

內存中的堆棧

在JVM中,棧擁有其中一部分的內存空間,它以一種特殊的方式進行內存的訪問。在此說的堆和棧不是數據結構中的堆和棧,是堆區和棧區中的堆棧。

  • 程序的堆棧是由處理器直接支持的。堆棧在內存中是從高地址向低地址擴展(這和自定義的堆棧從低地址向高地址擴展不同)

  • 在32位系統中,堆棧每個數據單元的大小爲4字節。小於等於4字節的數據,比如字節、字、雙字和布爾型,在堆棧中都是佔4個字節的。大於4字節的數據在堆棧中佔4字節整數倍的空間。

  • ESP寄存器總是指向堆棧的棧頂,執行PUSH命令向堆棧壓入數據時,ESP減4,然後把數據拷貝到ESP指向的地址;執行POP命令時,首先把ESP指向的數據拷貝到內存地址/寄存器中,然後ESP加4。

  • EBP寄存器是用於訪問堆棧中的數據的,它指向堆棧中間的某個位置。函數的參數地址比EBP的值高,而函數的局部變量地址比EBP的值低,因此參數或局部變量總是通過EBP加減一定的偏移地址來訪問的,比如,要訪問函數的第一個參數爲EBP+8。

棧分配

以C語言程序中來談論棧
1)每次我們開機的時候,系統都會初始化好棧指針(SP)。初始方法也很簡單,在boot_load代碼裏我們可以看到:ldr sp, =4096
這樣的語句就是讓SP指針指向這樣的地址。
2)注意棧指針SP指向的地址是內存中的地址,而不是cpu片內地址。內存資源相對cpu資源來說充裕多了,所以SP可以有很大的增長空間,這也是C語言可以寫複雜程序的前提。

函數調用方向

我們知道棧在不同的系統中的增長方向是不一樣的,Windows中是向下增長,但是棧的結構決定了它一定是先進後出的模型。
所以和我們函數調用的過程是類似的,最先調用的函數總是最後返回,而最後調用的函數則是最先返回。
棧的出棧方式決定函數的返回過程,棧的增長空間支持函數嵌套的複雜程度。

運行棧

運行棧中的數據分爲一個一個棧幀,每一個棧幀對應一次函數調用。所以棧幀中包含這次函數調用中的形參值,局部變量值,一些控制信息,臨時數據(例如複雜表達式計算的中間值,某些函數的返回值)。

運行棧的原理:

  1. 每次發生函數調用時,都會有一個棧幀被壓入運行棧中,而函數調用返回後,相應的棧幀會被彈出。

  2. 一個函數在執行的過程中,能夠直接隨機訪問它所對應的棧幀中的數據,即處在運行棧最頂端的棧幀的數據(執行中的函數的棧幀,總是處在運行棧的最頂端)。

  3. 當一個函數調用其他函數時,要爲它調用的函數設置實參,具體方式是在調用前把實參壓入棧中,運行棧中的這一部分空間是主調函數和被調函數都可以直接訪問的,也就是參數的形式結合就是通過這一公共空間來完成的。

  4. 雖然每一個函數在被調用時的形參和局部變量的地址都是不確定的,但是它們的地址相對於棧頂的地址卻是確定的,這樣就可以通過棧頂的地址,來間接定位函數形參和局部變量。

棧幀(Stack Frame)

函數調用經常是嵌套的,在同時刻,棧中會有多個函數的信息()。每個未完成運行的函數佔用一個獨立的連續區域,稱作棧幀(Stack Frame)。

棧幀特性

棧幀有如下特點:

  1. 一個堆棧幀對應一次函數的調用。在函數開始時,對應的堆棧幀已經完整地建立了,在函數退出時,整個函數幀將被銷燬。所有的局部變量在函數幀建立時就已經分配好空間了,而不是隨着函數的執行而不斷創建和銷燬的

  2. 棧幀存放着函數參數,局部變量和恢復前一棧幀所需要的數據等。

  3. 棧幀是堆棧的邏輯片段,當調用函數時邏輯棧幀被壓入堆棧,當函數返回時邏輯棧幀被從堆棧中彈出。

  4. 編譯器利用棧幀,使得函數參數和函數中局部變量的分配與釋放對程序員透明。

  5. 編譯器將控制權移交函數本身之前,插入特定代碼將函數參數壓入棧幀中,並分配足夠的內存空間用於存放函數中的局部變量。

  6. 使用棧幀的一個好處是使得遞歸變爲可能,因爲對函數的每次遞歸調用,都會分配給該函數一個新的棧幀,這樣就巧妙地隔離當前調用與上次調用。

  7. 棧幀的邊界由EBP和ESP界定,通過EBP分析確定分配在函數棧幀上的局部變量空間準確值

  8. 棧幀是運行時概念,若程序不運行,就不存在棧和棧幀。

  9. 棧幀的創建和清理都是由函數調用者Caller和被調用的函數Callee完成的。

棧幀的構建、傳值和銷燬

棧幀的構建
以如下代碼爲例:

public static void main(String[] args) {

        int result = add(3, 4);
        int b=result*10;

    }

    private static int add(int a, int b) {
        int c = A*2;
        int d = b*3;
        return add2(c,d) + c + d;
    }

    private static int add2(int c, int d) {
        return d + c ;
    }
   
  • 在調用add()函數之前,這時main以及之前的函數對應的棧幀已經存在棧中了。
    在這裏插入圖片描述

  • 當add函數被調用時,首先將a=3,b=4壓入堆棧。一般來說,參數是由右往左入棧的,所以入棧順序是4、3。
    在這裏插入圖片描述

  • 然後將函數add的指令地址 0x00*****壓入棧中
    在這裏插入圖片描述

  • 當add()方法的地址被壓入棧後,此時需要將EBP寄存器中的值壓入棧中。此時EBP中的值還是main函數的,用它可以訪問main函數的局部變量和參數,壓入EBP值是爲了後面退出add()時取出回覆。再將EBP的值存入棧後,此時會重新給EBP賦新的值,其新值指向前一個EBP值內存地址。
    在這裏插入圖片描述

  • 接下來add()函數爲局部變量分配地址,它不是一個個入棧而是通過一定規則ESP=ESP-0x00F3(該值僅舉例,需要根據一定規則確定),這樣分配一塊地址空間存放局部變量,其局部變量地址可能不連續,有空隙。

  • 然後將通用寄存器中的值入棧,那麼一個完整的棧幀就建立起來了。
    在這裏插入圖片描述
    +同理將add2()同樣加入棧中,得到最終完整的棧如圖。
    在這裏插入圖片描述
    棧幀的傳值
    棧幀中如何傳遞返回值,函數調用者和被調用函數都有關於返回值存放的“約定”,如下:

  1. 首先,如果返回值等於4字節,函數將把返回值賦予EAX寄存器,通過EAX寄存器返回。例如返回值是字節、字、雙字、布爾型、指針等類型,都通過EAX寄存器返回。

  2. 如果返回值等於8字節,函數將把返回值賦予EAX和EDX寄存器,通過EAX和EDX寄存器返回,EDX存儲高位4字節,EAX存儲低位4字節。例如返回值類型爲__int64或者8字節的結構體通過EAX和EDX返回。

  3. 如果返回值爲double或float型,函數將把返回值賦予浮點寄存器,通過浮點寄存器返回。
    在這裏插入圖片描述

caller會在壓入最左邊的參數後,再壓入一個指針,我們姑且叫它ReturnValuePointer,ReturnValuePointer指向caller局部變量區的一塊未命名的地址,這塊地址將用來存儲callee的返回值。其步驟爲:

  1. 函數返回時,callee把返回值拷貝到ReturnValuePointer指向的地址中,然後把ReturnValuePointer的地址賦予EAX寄存器。

  2. 函數返回後,caller通過EAX寄存器找到ReturnValuePointer地址值,然後通過ReturnValuePointer找到返回值,最後,caller把返回值拷貝到負責接收的局部變量上。

你或許會有這樣的疑問,函數返回後,對應的堆棧幀已經被銷燬,而ReturnValuePointer是在該堆棧幀中,不也應該被銷燬了嗎?
對的,堆棧幀是被銷燬了,但是保存返回值的地址被賦給了通用寄存器EAX,返回值在內存中還是存在的,當EAX將地址值拷貝給局部變量時就可以了。

棧幀的銷燬
當函數將返回值賦予某些寄存器或者拷貝到堆棧的某個地方後,函數開始清理堆棧幀,準備退出。堆棧幀的清理順序和堆棧建立的順序剛好相反,最後入棧的先出棧。

  1. 如果有對象存儲在棧幀中,對象的析構函數會被函數調用。

  2. 從堆棧中彈出先前的通用寄存器的值,將值再次賦給通用寄存器。

  3. ESP加上某個值,回收局部變量的地址空間(加上的值和堆棧幀建立時分配給局部變量的地址大小相同)。

  4. 從堆棧中彈出先前的EBP寄存器的值,將值賦給EBP寄存器,重新指向前一個棧幀棧底

  5. 從堆棧中彈出函數的返回地址,準備跳轉到函數的返回地址處繼續執行。

  6. ESP加上某個值,回收所有的參數地址。

前面1-5條都是由callee完成的。而第6條,是由caller或者callee完成是由函數使用的調用約定(calling convention )來決定的。calling convention在這裏不重點講解,請自己查閱資料。

EBP和ESP

EBP
棧幀基地址指針,EBP指向當前棧幀底部(高地址),在當前棧幀內位置固定。
1) EBP用於訪問參數和局部變量,如上圖發現參數地址高於EBP值,局部變量地址低於EBP值。
2)每個參數和局部變量相對EBP的地址偏移值是固定的,對於參數和局部變量是通過EBP值加特定偏移量來訪問的,如上圖add棧幀中,EBP+8爲第一個參數值,EBP-4爲第一個局部變量地址
3) 每個EBP地址總是指向前一個棧幀的EBP指針地址

ESP
堆棧指針,指向當前棧幀頂部(低地址),當程序執行時ESP會隨着數據的入棧和出棧而移動。

phsh 和 pop 指令的格式
push寄存器:將一個寄存器中的數據入棧。先改變SP,後向SS:SP傳送
pop寄存器:用一個寄存器接受出棧的數據。先讀取SS:SP數據,後改變SP

博客書寫不易,您的點贊收藏是我前進的動力,千萬別忘記點贊、 收**藏 ^ _ ^ !

相關鏈接:

  1. 心中有堆:https://blog.csdn.net/luo_boke/article/details/106928990
  2. 心中有樹——基礎:https://blog.csdn.net/luo_boke/article/details/106980011
  3. 常見排序算法解析:https://blog.csdn.net/luo_boke/article/details/106762372
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章