如果不思考宝鼓,單純地做做試驗刑棵,此次實驗很簡單,但這幾句簡簡單單的代碼卻包括了底層的機制:如參數(shù)是如何傳遞的蛉签,堆棧是如何增長的胡陪,各個寄存器的作用又是怎樣的。
實驗截圖如下碍舍,不是很復雜柠座。
刪除指導匯編器和鏈接器的命令(即是那些以.開頭的),得到真正對我們分析匯編代碼有用的這一部分。
匯編代碼:
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層調(diào)用,main函數(shù)調(diào)用f(),f()調(diào)用g(),
main函數(shù)運行授滓。
程序是這樣運行的,每個函數(shù)都有屬于自己的堆棧副编,這一段堆棧叫做棧幀,在32位機上由ebp(幀指針)和esp(棧指針)來劃定范圍。
main函數(shù)執(zhí)行的時候,先把自己ebp的值保存下來爆哑,然后,再將ebp賦值為esp了嚎,這樣便開始了一個新的棧幀泪漂,此時ebp和esp指向了同一處,而這個內(nèi)存單元里面保存的是ebp的值歪泳,esp便開始了對棧幀大小的修改。
為什么要保存ebp的值呢露筒,在函數(shù)運行的過程中呐伞,ebp一般是不會被修改的,當然慎式,除了下面這兩句:
pushl %ebp
movl %esp, %ebp
這兩句匯編代碼巧妙地只用了一個ebp寄存器就成功地區(qū)分開兩個函數(shù)的棧幀伶氢,因為它保存著上一個函數(shù)的esp的值,ebp的位置又固定不變瘪吏。
之前講過癣防,為了區(qū)分開每個函數(shù)的棧幀,需要保存ebp和esp掌眠,因為在每個函數(shù)的執(zhí)行過程中蕾盯,想獲取當前棧幀的數(shù)據(jù),而esp又是變化的蓝丙,無法把esp當作可靠的參考级遭,此時之前保存的ebp就派上了用場,因為ebp恒定指向此棧幀的最開始渺尘。
更為有意思的是以ebp作為參考這種機制在32位機中參數(shù)的傳遞有著重要的意義挫鸽,接下來在g()函數(shù)執(zhí)行的過程中,就可以看到這樣設計的妙處
接下來的
subl $4, %esp
movl $8, (%esp)
是為調(diào)用f()準備好參數(shù)鸥跟,參數(shù)的壓棧順序是從右向左壓棧丢郊,分別是參數(shù)n,參數(shù)n-1,直到參數(shù)1。本次只有一個參數(shù)枫匾,故只保存了立即數(shù)8到棧中迅诬。
有一點是需要記住的,只有32位系統(tǒng)才有棧幀的概念婿牍,并且只能通過棧來傳遞參數(shù)侈贷,而64位機則是主要通過寄存器傳參,只有當寄存器不夠用才使用棧等脂,這樣帶來的好處就是效率更高俏蛮。
調(diào)用f()函數(shù)call f
肯定會做一件事情,即將返回地址addl $1, %eax
指令的地址壓棧上遥。f()函數(shù)運行的時候搏屑,同樣的道理,保存當前的ebp值粉楚,設定ebp的指向辣恋。
subl $4, %esp
,為壓棧做準備模软。
movl 8(%ebp), %eax
movl %eax, (%esp)```
則是通過對ebp的帶偏移量的寄存器間接尋址(***基址+偏移量***)找到main函數(shù)保存的參數(shù)值伟骨。因為堆棧是向下增長,那么ebp+4,指向的是上個函數(shù)的ret地址燃异,ebp+8則指向的是我們保存的參數(shù)8携狭。`movl 8(%ebp), %eax`將8保存到自己的堆棧中去。然后調(diào)用g()函數(shù)回俐。
***不過我覺得如果使用GCC匯編的時候逛腿,優(yōu)化等級調(diào)高,那么仅颇,完全沒必要浪費這2條指令单默,因為本身f()函數(shù)什么都沒做,只是負責傳遞參數(shù)給g()而已忘瓦,那么直接利用g()的ebp便可尋址到傳入的參數(shù)搁廓。***
*其實,可能直接把g()函數(shù)給優(yōu)化掉了吧政冻?*
g()函數(shù)中的形成棧幀的前兩句不再多解釋枚抵,`movl 8(%ebp), %eax`是把傳入的參數(shù)8存儲到eax中,接下來的`addl $3, %eax`則把3加到eax中明场,`popl %ebp`則是恢復上個函數(shù)的ebp汽摹,為ret做準備。
`ret`又做了什么呢苦锨?`ret`相當于`pop %eip`逼泣,恢復eip的值趴泌,即調(diào)用者的下一條語句。在這個代碼中拉庶,eip被賦值為f()函數(shù)中l(wèi)eave指令的地址嗜憔,同時由eax返回結(jié)果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函數(shù)珊拼。
main函數(shù)中,把1加到eax上流炕,重復同樣的動作澎现,整個程序結(jié)束。
最后的結(jié)構(gòu)大概是這樣:
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/4811496-1f8e875869ae3bbb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
至于說GCC堅持的x86編程指導防止每辟,也就是說一個函數(shù)使用的所有椊1瑁空間必須是16字節(jié)的整數(shù)倍(包括保存的%ebp值的4個字節(jié)和返回值的4個字節(jié)),在這個代碼中并未體現(xiàn)出來影兽,不知道是不是因為GCC比較新的緣故揭斧。