從反匯編角度窺探平時(shí)開發(fā)調(diào)用的函數(shù)或者方法的本質(zhì)产镐。平時(shí)我們編寫的高級(jí)語(yǔ)言最終通過(guò)編譯器鹅龄、鏈接生成機(jī)CPU執(zhí)行的機(jī)器指令驴娃。 不同的CPU對(duì)應(yīng)著不同著機(jī)器指令花竞,并且每一條機(jī)器指令對(duì)應(yīng)著一條匯編署浩。
先看一個(gè)最簡(jiǎn)單的C語(yǔ)言函數(shù)揉燃,這里主要通過(guò)C++來(lái)反編譯分析匯編指令。
可以通過(guò)反匯編看到調(diào)用func函數(shù)的匯編指令筋栋,當(dāng)前環(huán)境是8086匯編炊汤。
通過(guò)最終的匯編指令可以看出,在執(zhí)行調(diào)用一個(gè)函數(shù):本質(zhì)就是通過(guò)call
指令調(diào)用函數(shù)在代碼段的地址進(jìn)行直接調(diào)用弊攘。
注意:在上面的匯編指令可以看到當(dāng)函數(shù)執(zhí)行完畢抢腐,執(zhí)行ret
匯編指令退出函數(shù)。其實(shí)一個(gè)完整的函數(shù)調(diào)用必定包含call
和ret
指令襟交。
那么只有了解了call
和ret
才能徹底從最根本了解函數(shù)的調(diào)用過(guò)程迈倍。
call 標(biāo)號(hào)
1.將下一條指令的偏移地址入棧
2.轉(zhuǎn)到標(biāo)號(hào)出執(zhí)行指令
ret
將棧頂?shù)闹党鰲#x值給IP
下面通過(guò)匯編代碼調(diào)用 printf
函數(shù)標(biāo)號(hào)打印 HelloWorld 執(zhí)行驗(yàn)證上面的結(jié)論捣域。
在即將執(zhí)行執(zhí)行 printf
函數(shù)之前棧頂指針SP指向內(nèi)存單元的數(shù)據(jù)啼染。
上面說(shuō)到執(zhí)行函數(shù)前會(huì)將下一條指令的偏移地址入棧醋界,上圖可以看出的下一條CPU執(zhí)行的指令偏移地址IP為:000D。開始執(zhí)行提完,看下棧頂指針SP的指向和指向內(nèi)存單元的數(shù)據(jù)
函數(shù) printf
執(zhí)行完畢后,執(zhí)行 ret
指令丘侠,棧頂偏移地址出棧賦值給 IP
中徒欣,棧頂指針向上移動(dòng)兩個(gè)字節(jié)。
不管什么開發(fā)語(yǔ)言最終都會(huì)轉(zhuǎn)成二進(jìn)制匯編指令蜗字,對(duì)應(yīng)著相應(yīng)的匯編指令打肝,本質(zhì)都是一致的。這里是通過(guò)C++反匯編窺探函數(shù)調(diào)用本質(zhì)挪捕。
上述介紹只是最簡(jiǎn)單函數(shù)調(diào)用粗梭,一說(shuō)到函數(shù)首先就會(huì)想到函數(shù)的三要素,函數(shù)的返回值级零、函數(shù)的參數(shù)断医、局部變量**。
返回值
如果調(diào)用函數(shù)想拿到函數(shù)返回值奏纪,就得有容器來(lái)存放返回值鉴嗤,我們可以想到用棧、數(shù)據(jù)區(qū)序调、寄存器來(lái)保存醉锅。
首先棧段不可以的,如下圖发绢,函數(shù)內(nèi)部push返回值硬耍,棧頂存儲(chǔ)的是CPU函數(shù)執(zhí)行完畢后的IP的偏移地址。
可以考慮將返回值放入數(shù)據(jù)段边酒,這個(gè)需要與調(diào)用者約好協(xié)議经柴,比如約定好將返回值放在ds:[0]
這樣側(cè)面證明了數(shù)據(jù)段里的數(shù)據(jù)是全局,全局區(qū)的數(shù)據(jù)是作用域是全局的甚纲。上面的實(shí)例代碼好比下面的C++代碼口锭。
在實(shí)際中,大多數(shù)平臺(tái)介杆,windows鹃操、linux、Android等通常的做法是將方法返回值放在寄存器ax春哨。其實(shí)這樣的效率比上面返回值放在全局區(qū)效率高荆隘,CPU從寄存器中讀取數(shù)據(jù)要快,放在全局區(qū)需要從內(nèi)存先讀取到寄存器赴背。
下面在X86環(huán)境下寫一段代碼看下匯編指令
參數(shù)
同樣我們先考慮將參數(shù)放入數(shù)據(jù)段來(lái)實(shí)現(xiàn)一個(gè)求和的函數(shù)椰拒。
放在數(shù)據(jù)段是可以的晶渠,在我們概念中形參的作用于是數(shù)據(jù)函數(shù)內(nèi)部,函數(shù)執(zhí)行完畢形參所占用的內(nèi)存空間會(huì)被回收燃观。這樣就很明顯了褒脯,通常,形參是放在棧中的缆毁。
注意:在函數(shù)調(diào)用完畢后番川,一定要保證棧平衡,否者會(huì)導(dǎo)致棧的空間會(huì)被用完脊框,通常保持棧平衡有兩種方式:內(nèi)平棧和外平棧颁督。
上面的案例是使用了外平棧方式,也就是在函數(shù)調(diào)用完畢后浇雹,對(duì)棧頂指針進(jìn)行回復(fù)到函數(shù)調(diào)用前的位置沉御。
對(duì)于函數(shù)的封裝性跟人覺的棧內(nèi)平衡的方式會(huì)好一些,讓函數(shù)調(diào)用者不用關(guān)心內(nèi)部細(xì)節(jié)昭灵。函數(shù)的形參本質(zhì)了解后吠裆,接下來(lái)窺探最后一個(gè)函數(shù)的局部變量本質(zhì),這個(gè)相對(duì)復(fù)雜一些烂完。
局部變量
函數(shù)的內(nèi)部需要定義局部變量硫痰,C語(yǔ)言特別簡(jiǎn)單,那么在匯編中怎么分配內(nèi)存空間給局部變量呢窜护,局部變量的作用域只是當(dāng)前函數(shù)效斑,函數(shù)執(zhí)行完畢后局部所棧中的空間被回收,因此局部變量空間分配還是通過(guò)棧來(lái)實(shí)現(xiàn)柱徙。
上面開始沒有問(wèn)題缓屠,唯一缺陷是在函數(shù)內(nèi)部調(diào)用函數(shù)時(shí),由于我們沒有對(duì)bp進(jìn)行恢復(fù)护侮,一旦對(duì)函數(shù)內(nèi)部在調(diào)用函數(shù)就會(huì)存存在問(wèn)題敌完, 因此需要對(duì)bp進(jìn)行記錄和恢復(fù)。
函數(shù)的調(diào)用流程總結(jié)
1 push參數(shù)羊初,參數(shù)入棧
2 將函數(shù)的返回地址(下一條指令的地址)入棧
3 保護(hù)sp滨溉,將sp賦值給bp
4 分配一定的空間給函數(shù)的局部變量使用(讓sp減去該空間大小)长赞,為了安全晦攒,用CC填充(int 3h)
5 保護(hù)寄存器, 因?yàn)樵诤瘮?shù)執(zhí)行過(guò)程中會(huì)修改寄存器的值,所以在修改之前保存一下之前的值得哆,后面再還原
6 具體的業(yè)務(wù)代碼
7 恢復(fù)寄存器的值脯颜,跟第5步相反
8 將bp賦值給sp,恢復(fù)bp
9 返回(ret)