原文:manybutfinite.com/post/

翻譯:RobotCode俱樂部

現在讓我們來看看函數棧楨的誕生,以便在腦海中清晰地勾勒出它們是如何一起工作的。棧楨的增長一開始令人困惑,因為它發生在與你預期相反的方向。例如,要在棧上分配8個位元組,就要從esp中減去8,而減法是一種奇怪的增長方式。

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);
}

假設我們在沒有命令行參數的Linux中運行以上程序。當你運行一個C程序時,第一個實際執行的代碼是在C runtime庫中,然後它調用我們的main函數。下面的圖一步一步地展示了程序運行時的情況。每個圖鏈接到顯示內存和寄存器狀態的GDB輸出。你還可以看到所使用的GDB命令和整個GDB輸出。我們開始吧:

步驟2、3和下面的4是函數序言,這對於幾乎所有函數都是通用的:將ebp的當前值保存到棧的頂部,然後將esp複製到ebp,建立一個新的棧楨。main的序言與其他任何序言一樣,但有一個特性,即在程序啟動時ebp被歸零。

如果您要檢查argc下面的堆棧(向右),您會發現更多的數據,包括指向程序名和命令行參數(傳統的C argv)的指針,以及指向Unix環境變數及其實際內容的指針。但這在這裡並不重要,不是我們要講的重點,繼續:

在 main從esp中減去12以獲得所需的堆棧空間之後,它為a和b設置值。內存中的值以十六進位和little-endian格式顯示,就像在調試器中看到的那樣。一旦設置好參數值,main調用add並開始運行:

現在有一些興奮:我們得到另一個序言,但這一次你可以清楚地看到棧幀是如何形成一個鏈表的,從ebp開始,然後沿著堆棧向下。這就是高級語言中的調試器和異常對象獲取堆棧跟蹤的方式。你還可以看到,當一個新的幀誕生時,ebp更典型地趕上了esp。同樣,我們從esp中減去以得到更多的堆棧空間。

在將ebp寄存器值複製到內存中時,位元組還會發生稍微奇怪的反轉。這裡所發生的是,寄存器並沒有真正意義上的自定義性:在寄存器中沒有像內存那樣的「增長地址」。因此,按照慣例,調試器以最自然的格式向人類顯示寄存器值:從最有效數字到最低有效數字。因此,在little-endian機器中複製的結果以通常的內存從左到右的表示法顯示相反。

困難的部分已經過去,我們補充說:

至此,add完成了它的工作,add函數執行完,此時堆棧操作將反向執行,我們在下一篇文章中再講這部分反向操作的內容。

任何讀過這篇文章的人都應該得到一個紀念品,所以我做了一個大圖,以書獃子的驕傲展示了所有步驟的組合。如下:

一旦將幾個步驟全部「擺好」,它看起來就很溫順了。事實上,「小盒子」是計算機科學的主要工具。我希望這些圖片和寄存器動作能夠提供一種直觀的、將堆棧增長和內存內容整合在一起的直觀感受。近距離觀察,我們的軟體看起來離一台簡單的圖靈機不遠了。

這就是我們堆棧之旅的第一部分。下面將看到在此基礎上構建的更高級的編程概念。下周見。

--未完待續

由於本人水平有限,翻譯必然有很多不妥的地方,歡迎指正。

同時,歡迎關注下方微信公眾號,一起交流學習:)

推薦閱讀:
相关文章