如果不思考,單純地做做試驗,此次實驗很簡單,但這幾句簡簡單單的代碼卻包括了底層的機制:如參數是如何傳遞的,堆棧是如何增長的,各個寄存器的作用又是怎樣的。
實驗截圖如下,不是很復雜。
刪除指導匯編器和鏈接器的命令(即是那些以.開頭的),得到真正對我們分析匯編代碼有用的這一部分。
匯編代碼:
g:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl $3, %eax
popl %ebp
ret
f:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl 8(%ebp), %eax
movl %eax, (%esp)
call g
leave
ret
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $8, (%esp)
call f
addl $1, %eax
leave
ret
C語言代碼:
int g(int x)
{
return x + 3;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(8) + 1;
}
這個程序中有3層調用,main函數調用f(),f()調用g(),
main函數運行。
程序是這樣運行的,每個函數都有屬于自己的堆棧,這一段堆棧叫做棧幀,在32位機上由ebp(幀指針)和esp(棧指針)來劃定范圍。
main函數執行的時候,先把自己ebp的值保存下來,然后,再將ebp賦值為esp,這樣便開始了一個新的棧幀,此時ebp和esp指向了同一處,而這個內存單元里面保存的是ebp的值,esp便開始了對棧幀大小的修改。
為什么要保存ebp的值呢,在函數運行的過程中,ebp一般是不會被修改的,當然,除了下面這兩句:
pushl %ebp
movl %esp, %ebp
這兩句匯編代碼巧妙地只用了一個ebp寄存器就成功地區分開兩個函數的棧幀,因為它保存著上一個函數的esp的值,ebp的位置又固定不變。
之前講過,為了區分開每個函數的棧幀,需要保存ebp和esp,因為在每個函數的執行過程中,想獲取當前棧幀的數據,而esp又是變化的,無法把esp當作可靠的參考,此時之前保存的ebp就派上了用場,因為ebp恒定指向此棧幀的最開始。
更為有意思的是以ebp作為參考這種機制在32位機中參數的傳遞有著重要的意義,接下來在g()函數執行的過程中,就可以看到這樣設計的妙處
接下來的
subl $4, %esp
movl $8, (%esp)
是為調用f()準備好參數,參數的壓棧順序是從右向左壓棧,分別是參數n,參數n-1,直到參數1。本次只有一個參數,故只保存了立即數8到棧中。
有一點是需要記住的,只有32位系統才有棧幀的概念,并且只能通過棧來傳遞參數,而64位機則是主要通過寄存器傳參,只有當寄存器不夠用才使用棧,這樣帶來的好處就是效率更高。
調用f()函數call f
肯定會做一件事情,即將返回地址addl $1, %eax
指令的地址壓棧。f()函數運行的時候,同樣的道理,保存當前的ebp值,設定ebp的指向。
subl $4, %esp
,為壓棧做準備。
movl 8(%ebp), %eax
movl %eax, (%esp)```
則是通過對ebp的帶偏移量的寄存器間接尋址(***基址+偏移量***)找到main函數保存的參數值。因為堆棧是向下增長,那么ebp+4,指向的是上個函數的ret地址,ebp+8則指向的是我們保存的參數8。`movl 8(%ebp), %eax`將8保存到自己的堆棧中去。然后調用g()函數。
***不過我覺得如果使用GCC匯編的時候,優化等級調高,那么,完全沒必要浪費這2條指令,因為本身f()函數什么都沒做,只是負責傳遞參數給g()而已,那么直接利用g()的ebp便可尋址到傳入的參數。***
*其實,可能直接把g()函數給優化掉了吧?*
g()函數中的形成棧幀的前兩句不再多解釋,`movl 8(%ebp), %eax`是把傳入的參數8存儲到eax中,接下來的`addl $3, %eax`則把3加到eax中,`popl %ebp`則是恢復上個函數的ebp,為ret做準備。
`ret`又做了什么呢?`ret`相當于`pop %eip`,恢復eip的值,即調用者的下一條語句。在這個代碼中,eip被賦值為f()函數中leave指令的地址,同時由eax返回結果11。
接下來到了f()中的`leave`,也就相當于
movl %ebp,%esp
popl %ebp
第一條把ebp的值賦值給esp,原因在于在當前的棧幀中ebp保存的是當前棧幀的esp值(還記得`movl %esp, %ebp`嗎?),esp指向的地方實際保存著ebp,`popl %ebp`則把保存的ebp彈出來(`pushl %ebp`),然后ret指令彈出eip,返回eax到main函數。
main函數中,把1加到eax上,重復同樣的動作,整個程序結束。
最后的結構大概是這樣:

至于說GCC堅持的x86編程指導防止,也就是說一個函數使用的所有棧空間必須是16字節的整數倍(包括保存的%ebp值的4個字節和返回值的4個字節),在這個代碼中并未體現出來,不知道是不是因為GCC比較新的緣故。