C/C++ 函式呼叫過程,壓棧出棧
在x86的計算機系統中,記憶體空間中的棧主要用於儲存函式的引數,返回值,返回地址,本地變數等。一切的函式呼叫都要將不同的資料、地址壓入或者彈出棧。因此,為了更好地理解函式的呼叫,我們需要先來看看棧是怎麼工作的。
棧是什麼?
簡單來說,棧是一種LIFO形式的資料結構,所有的資料都是後進先出。這種形式的資料結構正好滿足我們呼叫函式的方式:父函式呼叫子函式,父函式在前,子函式在後;返回時,子函式先返回,父函式後返回。棧支援兩種基本操作,push和pop。push將資料壓入棧中,pop將棧中的資料彈出並存儲到指定暫存器或者記憶體中。
這裡是一個push操作的例子。假設我們有一個棧,其中黃色部分是已經寫入資料的區域,綠色部分是還未寫入資料的區域。現在我們將0x50壓入棧中:
// 將0x50的壓入棧
push $0x50
圖一:壓棧操作
我們再來看看pop操作的例子:
// 將0x50彈出棧
pop
圖二:出棧操作
這裡有兩點需要注意的,第一,上面例子中棧的生長方向是從高地址到低地址的,棧是向下生長的,因此這裡也用這種形式的棧;第二,pop操作後,棧中的資料並沒有被清空,只是該資料我們無法直接訪問。有了這些棧的基本知識,我們現在可以來看看在x86-32bit系統下,C語言函式是如何呼叫的了。
棧幀是什麼?
棧幀,也就是stack frame,其本質就是一種棧,只是這種棧專門用於儲存函式呼叫過程中的各種資訊(引數,返回地址,本地變數等)。棧幀有棧頂和棧底之分,其中棧頂的地址最低,棧底的地址最高,SP(棧指標)就是一直指向棧頂的。在x86-32bit中,我們用%ebp
%esp
指向棧頂,也就是棧指標。下面是一個棧幀的示意圖:
圖三:棧幀示意圖
一般來說,我們將%ebp
到%esp
之間區域當做棧幀(也有人認為該從函式引數開始,不過這不影響分析)。並不是整個棧空間只有一個棧幀,每呼叫一個函式,就會生成一個新的棧幀。在函式呼叫過程中,我們將呼叫函式的函式稱為“呼叫者(caller)”,將被呼叫的函式稱為“被呼叫者(callee)”。在這個過程中,1)“呼叫者”需要知道在哪裡獲取“被呼叫者”返回的值;2)“被呼叫者”需要知道傳入的引數在哪裡,3)返回的地址在哪裡。同時,我們需要保證在“被呼叫者”返回後,%ebp
,%esp
等暫存器的值應該和呼叫前一致。因此,我們需要使用棧來儲存這些資料。
為什麼引數從右至左壓棧
1.C方式引數入棧順序(從右至左)的好處就是可以動態變化引數個數。通過棧堆分析可知,自左向右的入棧方式,最前面的引數被壓在棧底。這樣的話,除非知道引數個數,否則是無法通過棧指標的相對位移求得最左邊的引數。這樣就變成了左邊引數的個數不確定,正好和動態引數個數的方向相反。
2. 更符合習慣。
採用這種順序,是為了讓程式設計師在使用C/C++的“函式引數長度可變”這個特性時更方便。
什麼是“函式引數長度可變”?printf就是一個例子,它的引數的個數就是可變的,連結(1)中介紹瞭如何自己寫一個引數長度可變的函式。
看下面這句話 printf("%d %d %d",1,2,3),在採用從右向左的引數入棧順序時,引數出棧順序時"%d %d %d",1,2,3。
如果採用從左向右的入棧順序,則出棧順序變為3,2,1,"%d %d %d"。
參考文件: https://www.cnblogs.com/sddai/p/9762968.html
參考文件:https://blog.csdn.net/hnyzyty/article/details/46427219