1.3 函数调用反汇编解析以及调用惯例案例分析
首先來段代碼來瞧瞧:
#include <stdio.h>int add(int x,int y){int z;z=x+y;return z; }int main(){int r=add(3,4);printf("%d\n",r);return 0; }一個(gè)簡單的函數(shù)調(diào)用,我們把main函數(shù)里的r=add(3,4)反匯編:
可以看到,(這里采用c默認(rèn)的函數(shù)調(diào)用慣例,)首先進(jìn)行參數(shù)壓棧,看清楚了,是把參數(shù)從右往左壓棧,然后call這個(gè)函數(shù)。跟蹤,call跟進(jìn)去后,發(fā)現(xiàn)call指令執(zhí)行后,ESP寄存器減4,也就是說,有往棧里壓了個(gè)參數(shù)--函數(shù)返回地址。看內(nèi)存變化:
壓進(jìn)去的是0x00401081,從第一圖可以看到,這就是call指令后的下一句,也就是函數(shù)的返回地址,函數(shù)調(diào)用完后得知道往哪里返回啊。
其實(shí),call指令等價(jià)于兩步操作:push 返回地址 jmp 函數(shù)入口地址
我們繼續(xù)跟進(jìn),看被調(diào)函數(shù)的反匯編代碼:
首先,再提醒一下,前面知道了,已經(jīng)壓棧了三次,前兩次是壓棧形參,第三次是壓棧返回地址。看這里的前兩句,又把EBP壓棧了,然后把ESP賦值給了EBP。有沒有覺得奇怪呢?通常情況下,EBP都存儲(chǔ)基址,這里也不例外,由于后面可能出現(xiàn)多次壓棧出棧操作,ESP是變動(dòng)的,需要一個(gè)基址寄存器來加減偏移量去棧上的值,畢竟剛才也看到了,棧里可是有不少重要的東東哦,通過基址加減偏移量就可以訪問了。于是,EBP就暫時(shí)擔(dān)待了這個(gè)重任。后面,ESP做了個(gè)減法,為函數(shù)內(nèi)部局部變量等留下一定的棧空間,又壓棧了幾個(gè)寄存器,以備使用他們而不至于毀壞原有數(shù)據(jù)(后面再出棧就恢復(fù)了)。看核心代碼,z=x+y后面,把ebp+8地址的值賦給eax,思考一下,ebp+8是哪塊內(nèi)存?回憶下前面棧里都?jí)毫耸裁催M(jìn)去?ebp+0存的是ebp原有值,ebp+4存的是返回地址,ebp+8存的是最后一個(gè)被壓的參數(shù),ebp+0c存的是......所以這里就是作加法,然后賦值給了ebp-4.這又是什么呢?這是z的地址。在監(jiān)視窗口里可以看到z的地址就是如此。so,這里可以得出一個(gè)結(jié)論:ebp+x存的是返回地址、形參等;ebp-x存的是局部變量。
現(xiàn)在看return z后,又把z的值賦給了寄存器EAX,現(xiàn)在可以明白,函數(shù)返回值是借用寄存器來實(shí)現(xiàn)的。寄存器是個(gè)好東西,但是有一個(gè)缺點(diǎn),就是數(shù)量少。要是返回的數(shù)據(jù)比較多,比如結(jié)構(gòu)體,怎么辦?這個(gè)后面說。接著看圖,出棧,與前面一個(gè)個(gè)對應(yīng),注意順序蛤。然后,ret,這什么東東?經(jīng)跟蹤發(fā)現(xiàn),ret執(zhí)行后,ESP+4,也就是說,有數(shù)據(jù)出棧,細(xì)細(xì)看下,是返回地址。ret它給EIP提供了返回地址,即等價(jià)于POP EIP。完了嗎?沒有完。別忘了,棧上還有參數(shù)呢。先壓的是形參,現(xiàn)在還沒出呢。看圖:
ret后,即執(zhí)行到call后面的一句。這里ESP+8,這是維持棧平衡,之前形參占據(jù)的空間就釋放了,也稱之為調(diào)用方清棧。然后后面把寄存器存儲(chǔ)的返回值賦給r,也就是EBP-4,別忘了,r是main函數(shù)的局部變量呢。
這里順便提一個(gè)匯編知識(shí)。看那個(gè)call語句,他的16進(jìn)制編碼與返回值代碼有何端倪?FFFFFF84<>00401005。這是因?yàn)閏all語句不是直接傳絕對地址,而是傳偏移量,計(jì)算下0040107c與ffffff84結(jié)合能不能得到00401005呢?得出是00401000。結(jié)論:偏移量=跳轉(zhuǎn)到的地址-call指令后一條指令的起始地址。
?
下面,我把返回值改為一個(gè)結(jié)構(gòu)體:
#include <stdio.h>struct node{int x,y,z; };struct node f(struct node t){return t; }int main(){struct node r;r.x=1;r.y=2;r.z=5;f(r);return 0; }進(jìn)行反匯編,先看下main函數(shù)里的情況:
看call語句之前都做了什么?壓棧。這里把ESP賦給EAX,然后傳值給EAX+0/4/8等價(jià)于壓棧的push操作。注意一個(gè)地方,就是push edx。他又壓棧了個(gè)參數(shù),這是什么?后面就知道了。看函數(shù)里的反匯編代碼:
直接return t。看return后一句,把EBP+8上的值賦給eax,EBP+0是ebp原有值,EBP+4是函數(shù)返回地址,EBP+8是main里面call之前那個(gè)push EDX操作,so,這里把那個(gè)edx賦給了eax。而后面,把ebp+0C/10/14上的內(nèi)容都賦給了EAX+0/4/8。最后又把EBP+8也就是edx那個(gè)地址賦給了寄存器。別忘了,這里是儲(chǔ)存函數(shù)返回值的。前面說到,寄存器不夠用,那怎么辦?這里edx傳進(jìn)來一個(gè)地址,然后返回值都依次存進(jìn)了這個(gè)地址,這代表什么?緩沖地帶。既然寄存器使不動(dòng)了,那就靠內(nèi)存,這里拿這個(gè)緩沖地帶來中轉(zhuǎn)數(shù)據(jù),解決了返回值過多的問題。那么,調(diào)用結(jié)束后,main函數(shù)一定會(huì)把這個(gè)緩沖地帶的內(nèi)容取出來賦給某個(gè)變量的。我們來看看:
記好了,剛才緩沖地帶的地址放進(jìn)了EAX里面,這樣它就可以用來做基址了。故而把EAX+0/4/8賦給一塊內(nèi)存,這里可以看到,賦給了一個(gè)匿名局部變量,剛好與前面那個(gè)r的地址緊挨著呢。
故而總結(jié)下:c默認(rèn)的調(diào)用慣例,函數(shù)返回值可以用寄存器或內(nèi)存來存儲(chǔ),選擇方式依賴于寄存器是否有能力完成任務(wù)。
?
關(guān)于函數(shù)的調(diào)用慣例,可以參考我的這篇博文:http://www.cnblogs.com/jiu0821/p/4219545.html
下面講解一下一個(gè)有趣的案例,先看源碼:
#include <stdio.h>typedef int (*func)(int,int);func pfunc;int _stdcall add(int a,int b){return a+b; }void test(){pfunc=(func)add;pfunc(1,2);add(1,2); }int main(){test();return 0; }這里用的函數(shù)指針,有不熟悉的可以看我的這篇博文:http://www.cnblogs.com/jiu0821/p/4159487.html
簡單說下源碼,就是定義一個(gè)函數(shù)指針pfunc,把a(bǔ)dd強(qiáng)轉(zhuǎn)賦給pfunc,分別執(zhí)行pfunc(1,2)和add(1,2),看有沒有什么不一樣的地方。add被_stdcall約束。運(yùn)行一下會(huì)發(fā)現(xiàn),pfunc(1,2)運(yùn)行出現(xiàn)異常。什么原因呢?反匯編一下就知道了。
對比一下,發(fā)現(xiàn)pfunc多了一個(gè)出棧操作,而add沒有。那個(gè)cmp和call是異常處理,不用管,add也有,下面沒有截出來而已。還記得函數(shù)調(diào)用之后出棧操作是為了什么來著?維持棧平衡。這里我基本可以估摸出是棧出了問題。跟進(jìn)去看看:
pfunc與add指向同一塊內(nèi)存,因?yàn)楹瘮?shù)入口地址一樣嘛。直接看最后的清棧部分--ret 8。那個(gè)8是什么意思呢?ret 8等價(jià)于兩條指令:pop eip; add ?esp,8.
發(fā)現(xiàn)問題了嗎?pfunc執(zhí)行的時(shí)候,函數(shù)里出棧8字節(jié),外面main那里又出棧8字節(jié),畫蛇添足的結(jié)果就是自取滅亡。這里的端倪在于調(diào)用慣例不同。c缺省調(diào)用慣例是_cdecl,調(diào)用方清棧,而這里的_stdcall是被調(diào)用方清棧。pfunc使用的是_cdecl,卻執(zhí)行_stdcall約束的函數(shù),這可能不錯(cuò)嗎?源碼里的強(qiáng)轉(zhuǎn)如果去掉,編譯器會(huì)報(bào)錯(cuò)的,而強(qiáng)轉(zhuǎn)就是欺騙編譯器。有時(shí)候,欺騙別人,往往到最后,把自己也騙了。
?
讓我想起來之前在博問里一個(gè)博友遇到的問題:http://q.cnblogs.com/q/71965/
const_cast實(shí)行強(qiáng)轉(zhuǎn),成功地騙過了編譯器,但是程序員自己寫出了未定義行為。我還是直接把我的回復(fù)截過來好了。
const對象是不允許修改的,而const_cast的存在是為了有些特殊情況需要表面去除const屬性,比如函數(shù)傳參,把const對象傳進(jìn)非const屬性參數(shù)里,表面修改屬性實(shí)則不改變其內(nèi)容。而你這里表面上是修改了,*x=3,這種修改const對象屬于c++標(biāo)準(zhǔn)里未定義行為,針對這樣的,是由編譯器來自行處理的。你可以看到他們地址都相同,但卻值不一樣,這是編譯器的處理效果。我們需要做的是,避免編程里出現(xiàn)這種未定義行為。c/c++賦給了我們強(qiáng)大的權(quán)力,我們不要去胡作非為......
總結(jié)一下就是,強(qiáng)轉(zhuǎn)是很方便,但我們使用的時(shí)候,千萬要注意,使用得當(dāng)。
轉(zhuǎn)載于:https://www.cnblogs.com/jiu0821/p/4504917.html
總結(jié)
以上是生活随笔為你收集整理的1.3 函数调用反汇编解析以及调用惯例案例分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: fenby C语言
- 下一篇: 实现一个 DFA 正则表达式引擎 - 4