歡迎光臨
每天分享高質量文章

探秘“棧”之旅 | Linux 中國

棧非常重要,因為它追蹤著一個程式中運行的函式,而函式又是一個軟體的重要組成部分。
— Gustavo Duarte


致謝
編譯自 | https://manybutfinite.com/post/journey-to-the-stack/ 
 作者 | Gustavo Duarte
 譯者 | qhwdw ? ? ? ? ? 共計翻譯:109 篇 貢獻時間:197 天

早些時候,我們探索了 “記憶體中的程式之秘”[1],我們欣賞了在一臺電腦中是如何運行我們的程式的。今天,我們去探索棧的呼叫,它在大多數編程語言和虛擬機中都默默地存在。在此過程中,我們將接觸到一些平時很難見到的東西,像閉包closure、遞迴、以及緩衝上限溢位等等。但是,我們首先要作的事情是,描繪出棧是如何運作的。

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

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

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

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

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

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

不像 esp, ebp 大多數情況下是在程式代碼中通過花費很少的 CPU 來進行維護的。有時候,完成拋棄 ebp 有一些性能優勢,可以通過 編譯標誌[2] 來做到這一點。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 程式:

  1. Simple Add Program - add.c

  2. int add(int a, int b)

  3. {

  4.    int result = a + b;

  5.    return result;

  6. }

  7. int main(int argc)

  8. {

  9.    int answer;

  10.    answer = add(40, 2);

  11. }

簡單的加法程式 - add.c

假設我們在 Linux 中不使用命令列引數去運行它。當你運行一個 C 程式時,實際運行的第一行代碼是在 C 運行時庫里,由它來呼叫我們的 main 函式。下圖展示了程式運行時每一步都發生了什麼。每個圖鏈接的 GDB 輸出展示了記憶體和暫存器的狀態。你也可以看到所使用的 GDB 命令[3],以及整個 GDB 輸出[4]。如下:

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

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

在 main 從 esp 減去 12 之後得到它所需的棧空間,它為 a 和 b 設置值。在記憶體中的值展示為十六進制,並且是小端格式,與你從除錯器中看到的一樣。一旦設置了引數值,main 將呼叫 add,並且開始運行:

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

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

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

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

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

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

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


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

作者:Gustavo Duarte[7] 譯者:qhwdw 校對:wxy

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

赞(0)

分享創造快樂

© 2021 知識星球   网站地图