重大錯誤說明 : 棧頂的指針始終是指向最后一個入棧元素的位置的,不是最后一個入棧元素的位置上面!請讀者留意 (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.父函數調用子函數,子函數創建棧幀,子函數完成后子函數的棧幀銷毀
下一篇 :
棧論 : 遞歸與棧式訪問,如何用棧實現所有遞歸操作(幼兒園題目篇)
護眼綠:
沒人看的結語:
首先很感謝你看到這里,辛苦了。
文章中某些地方可能不正確或不准確,代碼也可能不夠高效可讀,希望讀者能夠幫忙指正,共同學習進步。