棧論 : 遞歸與棧式訪問,如何用棧實現所有遞歸操作(函數調用底層篇)


重大錯誤說明 : 棧頂的指針始終是指向最后一個入棧元素的位置的,不是最后一個入棧元素的位置上面!請讀者留意 (PS : 后來又看了一下,好像也不是什么大問題...)

上一篇 :

棧論 : 遞歸與棧式訪問,如何用棧實現所有遞歸操作(基礎知識篇)

 

2.函數調用底層篇(了解遞歸調用的硬件實現)

一開始,main函數沒有調用add之前他的棧幀如下圖,當然,下面只是簡略介紹,實際上內存布局比下面更復雜(省略了寄存器等)。

 

 

 

 當要調用add函數的時候main 將 自己的變量拷貝后壓入棧中,我們稱之為“形參”

 

 

上圖中變量c 和變量d的拷貝就是所謂的”形參“

 接下來將main函數的ebp地址壓入棧中保存,以便add函數調用完之后恢復main在內存中的棧幀

 

 

 

 

接着 就是重要的環節,add函數的棧幀創建,add函數的棧幀創建在add函數自己的操作里。

 

沒想到吧?add函數的棧幀是add函數自己創建的。一般的思維都是父對象為子對象創建空間,再讓子對象自己發揮,可能這是比較袒護孩子的行為吧,你看函數調用卻是讓自己的孩子去開創天地,值得學習。(當然 這是win10下匯編的得出的結果,可能不同系統不一樣)

 

add函數本身操作 :

 

1.將esp 的值賦給ebp,這里的ebp就是add函數自己棧幀的棧底了。

 

2.讓esp = esp - X ; X是一個位移量,表示esp要上移,esp上移的這個位移量差不多是add函數棧幀的大小。(還有一些寄存器之類的會占用空間,忽略不計)

 

如圖:

 

 

 

 

這時候的棧應該是這樣的

 

 

 

 

 

 

 接下來,涉及到最重要環節!棧幀之間的通信

add函數的內部操作是 兩個數相加,這兩個數是形參,難道在add函數的棧幀中要訪問在main函數棧幀中的形參嗎?沒錯,就是直接訪問。

我們來看看a + b 的匯編過程

 

 

 

 

對匯編不了解的同學可以先把 eax理解成一個變量,這個變量不在內存中(當然也就不在我們的棧區中)。mov是放進去的意思,理解把逗號右邊的值放到(賦給)左邊變量上(eax)去。 add是把逗號左右兩邊的數加起來,放到左邊去。

 

 

我們發現,a + b 無非是把 ebp + 8, ebp + 12(十六進制數0Ch的十進制數)讀取到的值加起來並且放到eax變量里而已。

而從 ebp + 8 和 ebp + 12 讀取到的正好是main函數棧幀中的形參

 

 

 

 

 

 

棧幀通信總結1.

子函數直接調用父函數棧幀內的形成,訪問父函數

 

這是子向父索求信息,那么父向子索取信息呢?聰明的你可能已經猜到了,返回值!

 

子函數返回過程:

子函數完成之后,子函數的棧幀會被廢棄掉

上面大圈里的小圈,兩句匯編就是把棧頂和棧底移動回原來的main棧幀處。

 

 

 

 

 

在我們剛剛看到的a+b之后,子函數已經沒什么大動作了,也就是說我們操作完之后的數是放在eax里的。

父函數就是通過訪問子函數結束后遺留在eax中的數來和子函數通信,也就是說,eax里的是子函數的返回值!

 

從匯編也可以看到main在調用完add函數之后,為e賦值的時候直接訪問了eax;

 

 

 

 

add    esp,8

 

這句還是要好好說一說的,子函數返回之后esp還在形參的上面,既然子函數完成了,形參也沒必要存在,於是需要把他們廢棄掉,廢棄的方法是把他們移除esp和ebp之間,也就是讓esp下降就好了。

 

 

 

 

 

 

 

 

棧幀通信總結2.

父函數直接訪子函數在EAX中遺留的返回值

 

綜上,我們得出以下幾點結論。

1.子函數直接調用父函數棧幀內的形成,訪問父函數

2.父函數直接訪子函數在EAX中遺留的返回值

3.父函數調用子函數,子函數創建棧幀,子函數完成后子函數的棧幀銷毀

 

下一篇 :

棧論 : 遞歸與棧式訪問,如何用棧實現所有遞歸操作(幼兒園題目篇)

 

護眼綠:

 

沒人看的結語:

 

首先很感謝你看到這里,辛苦了。

 

文章中某些地方可能不正確或不准確,代碼也可能不夠高效可讀,希望讀者能夠幫忙指正,共同學習進步。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM