Linux中國

探秘「棧」之旅

早些時候,我們探索了 「內存中的程序之秘」,我們欣賞了在一台電腦中是如何運行我們的程序的。今天,我們去探索棧的調用,它在大多數編程語言和虛擬機中都默默地存在。在此過程中,我們將接觸到一些平時很難見到的東西,像 閉包 closure 、遞歸、以及緩衝溢出等等。但是,我們首先要作的事情是,描繪出棧是如何運作的。

棧非常重要,因為它追蹤著一個程序中運行的函數,而函數又是一個軟體的重要組成部分。事實上,程序的內部操作都是非常簡單的。它大部分是由函數向棧中推入數據或者從棧中彈出數據的相互調用組成的,而在堆上為數據分配內存才能在跨函數的調用中保持數據。不論是低級的 C 軟體還是像 JavaScript 和 C# 這樣的基於虛擬機的語言,它們都是這樣的。而對這些行為的深刻理解,對排錯、性能調優以及大概了解究竟發生了什麼是非常重要的。

當一個函數被調用時,將會創建一個 棧幀 stack frame 去支持函數的運行。這個棧幀包含函數的局部變數和調用者傳遞給它的參數。這個棧幀也包含了允許被調用的函數(callee)安全返回給其調用者的內部事務信息。棧幀的精確內容和結構因處理器架構和函數調用規則而不同。在本文中我們以 Intel x86 架構和使用 C 風格的函數調用(cdecl)的棧為例。下圖是一個處於棧頂部的一個單個棧幀:

在圖上的場景中,有三個 CPU 寄存器進入棧。 棧指針 stack pointer esp(LCTT 譯註:擴展棧指針寄存器) 指向到棧的頂部。棧的頂部總是被最後一個推入到棧且還沒有彈出的東西所佔據,就像現實世界中堆在一起的一疊盤子或者 100 美元大鈔一樣。

保存在 esp 中的地址始終在變化著,因為棧中的東西不停被推入和彈出,而它總是指向棧中的最後一個推入的東西。許多 CPU 指令的一個副作用就是自動更新 esp,離開寄存器而使用棧是行不通的。

在 Intel 的架構中,絕大多數情況下,棧的增長是向著低位內存地址的方向。因此,這個「頂部」 在包含數據的棧中是處於低位的內存地址(在這種情況下,包含的數據是 local_buffer)。注意,關於從 esplocal_buffer 的箭頭不是隨意連接的。這個箭頭代表著事務:它專門指向到由 local_buffer 所擁有的第一個位元組,因為,那是一個保存在 esp 中的精確地址。

第二個寄存器跟蹤的棧是 ebp(LCTT 譯註:擴展基址指針寄存器),它包含一個 基指針 base pointer 或者稱為 幀指針 frame pointer 。它指向到一個當前運行的函數的棧幀內的固定位置,並且它為參數和局部變數的訪問提供一個穩定的參考點(基址)。僅當開始或者結束調用一個函數時,ebp 的內容才會發生變化。因此,我們可以很容易地處理在棧中的從 ebp 開始偏移後的每個東西。如圖所示。

不像 espebp 大多數情況下是在程序代碼中通過花費很少的 CPU 來進行維護的。有時候,完成拋棄 ebp 有一些性能優勢,可以通過 編譯標誌 來做到這一點。Linux 內核就是一個這樣做的示例。

最後,eax(LCTT 譯註:擴展的 32 位通用數據寄存器)寄存器慣例被用來轉換大多數 C 數據類型返回值給調用者。

現在,我們來看一下在我們的棧幀中的數據。下圖清晰地按位元組展示了位元組的內容,就像你在一個調試器中所看到的內容一樣,內存是從左到右、從頂部至底部增長的,如下圖所示:

局部變數 local_buffer 是一個位元組數組,包含一個由 null 終止的 ASCII 字元串,這是 C 程序中的一個基本元素。這個字元串可以讀取自任意地方,例如,從鍵盤輸入或者來自一個文件,它只有 7 個位元組的長度。因為,local_buffer 只能保存 8 位元組,所以還剩下 1 個未使用的位元組。這個位元組的內容是未知的,因為棧不斷地推入和彈出,除了你寫入的之外,你根本不會知道內存中保存了什麼。這是因為 C 編譯器並不為棧幀初始化內存,所以它的內容是未知的並且是隨機的 —— 除非是你自己寫入。這使得一些人對此很困惑。

再往上走,local1 是一個 4 位元組的整數,並且你可以看到每個位元組的內容。它似乎是一個很大的數字,在8 後面跟著的都是零,在這裡可能會誤導你。

Intel 處理器是 小端 little endian 機器,這表示在內存中的數字也是首先從小的一端開始的。因此,在一個多位元組數字中,較小的部分在內存中處於最低端的地址。因為一般情況下是從左邊開始顯示的,這背離了我們通常的數字表示方式。我們討論的這種從小到大的機制,使我想起《格里佛遊記》:就像小人國的人們吃雞蛋是從小頭開始的一樣,Intel 處理器處理它們的數字也是從位元組的小端開始的。

因此,local1 事實上只保存了一個數字 8,和章魚的腿數量一樣。然而,param1 在第二個位元組的位置有一個值 2,因此,它的數學上的值是 2 * 256 = 512(我們與 256 相乘是因為,每個位置值的範圍都是從 0 到 255)。同時,param2 承載的數量是 1 * 256 * 256 = 65536

這個棧幀的內部數據是由兩個重要的部分組成:前一個棧幀的地址(保存的 ebp 值)和函數退出才會運行的指令的地址(返回地址)。它們一起確保了函數能夠正常返回,從而使程序可以繼續正常運行。

現在,我們來看一下棧幀是如何產生的,以及去建立一個它們如何共同工作的內部藍圖。首先,棧的增長是非常令人困惑的,因為它與你你預期的方式相反。例如,在棧上分配一個 8 位元組,就要從 esp 減去 8,去,而減法是與增長不同的奇怪方式。

我們來看一個簡單的 C 程序:

Simple Add Program - add.c

int add(int a, int b)
{
    int result = a + b;
    return result;
}

int main(int argc)
{
    int answer;
    answer = add(40, 2);
}

簡單的加法程序 - add.c

假設我們在 Linux 中不使用命令行參數去運行它。當你運行一個 C 程序時,實際運行的第一行代碼是在 C 運行時庫里,由它來調用我們的 main 函數。下圖展示了程序運行時每一步都發生了什麼。每個圖鏈接的 GDB 輸出展示了內存和寄存器的狀態。你也可以看到所使用的 GDB 命令,以及整個 GDB 輸出。如下:

第 2 步和第 3 步,以及下面的第 4 步,都只是函數的 序言 prologue ,幾乎所有的函數都是這樣的:ebp 的當前值被保存到了棧的頂部,然後,將 esp 的內容拷貝到 ebp,以建立一個新的棧幀。main 的序言和其它函數一樣,但是,不同之處在於,當程序啟動時 ebp 被清零。

如果你去檢查棧下方(右邊)的整形變數(argc),你將找到更多的數據,包括指向到程序名和命令行參數(傳統的 C 的 argv)、以及指向 Unix 環境變數以及它們真實的內容的指針。但是,在這裡這些並不是重點,因此,繼續向前調用 add()

mainesp 減去 12 之後得到它所需的棧空間,它為 ab 設置值。在內存中的值展示為十六進位,並且是小端格式,與你從調試器中看到的一樣。一旦設置了參數值,main 將調用 add,並且開始運行:

現在,有一點小激動!我們進入了另一個函數序言,但這次你可以明確看到棧幀是如何從 ebp 到棧建立一個鏈表。這就是調試器和高級語言中的 Exception 對象如何對它們的棧進行跟蹤的。當一個新幀產生時,你也可以看到更多這種典型的從 ebpesp 的捕獲。我們再次從 esp 中做減法得到更多的棧空間。

ebp 寄存器的值拷貝到內存時,這裡也有一個稍微有些怪異的位元組逆轉。在這裡發生的奇怪事情是,寄存器其實並沒有位元組順序:因為對於內存,沒有像寄存器那樣的「增長的地址」。因此,慣例上調試器以對人類來說最自然的格式展示了寄存器的值:數位從最重要的到最不重要。因此,這個在小端機器中的副本的結果,與內存中常用的從左到右的標記法正好相反。我想用圖去展示你將會看到的東西,因此有了下面的圖。

在比較難懂的部分,我們增加了注釋:

這是一個臨時寄存器,用於幫你做加法,因此沒有什麼警報或者驚喜。對於加法這樣的作業,棧的動作正好相反,我們留到下次再講。

對於任何讀到這裡的人都應該有一個小禮物,因此,我做了一個大的圖表展示了 組合到一起的所有步驟

一旦把它們全部布置好了,看上起似乎很乏味。這些小方框給我們提供了很多幫助。事實上,在計算機科學中,這些小方框是主要的展示工具。我希望這些圖片和寄存器的移動能夠提供一種更直觀的構想圖,將棧的增長和內存的內容整合到一起。從軟體的底層運作來看,我們的軟體與一個簡單的圖靈機器差不多。

這就是我們棧探秘的第一部分,再講一些內容之後,我們將看到構建在這個基礎上的高級編程的概念。下周見!

via:https://manybutfinite.com/post/journey-to-the-stack/

作者:Gustavo Duarte 譯者:qhwdw 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國