C語(yǔ)言中函數(shù)棧幀的創(chuàng)建和銷毀的深層分析
一、本文目標(biāo)
1、局部變量是怎么創(chuàng)建的?
2、為什么局部變量的值是隨機(jī)值?
3、函數(shù)是怎么傳參的?傳參的順序是怎樣的?
4、形參和實(shí)參是什么關(guān)系?
5、函數(shù)調(diào)用是怎么做的?
6、函數(shù)調(diào)用結(jié)束后是怎么返回的?
當(dāng)我們深入理解函數(shù)棧幀創(chuàng)建和銷毀,答案自然就清楚了。正文開始:
二、基礎(chǔ)知識(shí)
1、寄存器
寄存器名稱 | 簡(jiǎn)介 |
eax | "累加器" 它是很多加法乘法指令的缺省寄存器。 |
ebx | "基地址"寄存器, 在內(nèi)存尋址時(shí)存放基地址。 |
ecx | 計(jì)數(shù)器,是重復(fù)(REP)前綴指令和LOOP指令的內(nèi)定計(jì)數(shù)器。 |
edx | 總是被用來(lái)放整數(shù)除法產(chǎn)生的余數(shù)。 |
esi | 源索引寄存器 |
edi | 目標(biāo)索引寄存器 |
ebp | (棧底指針)"基址指針",存放的是地址,用來(lái)維護(hù)函數(shù)棧幀 |
esp | (棧頂指針)專門用作堆棧指針,存放的是地址,用來(lái)維護(hù)函數(shù)棧幀 |
2、代碼案例
本文依賴的編譯器:VS2013
#include<stdio.h> int Add(int x, int y) { int z = 0; z = x + y; return z; } int main() { int a = 10; int b = 20; int c = 0; c = Add(a, b); printf("%d\n", c); return 0; }
3、總體棧幀概況
每一個(gè)函數(shù)調(diào)用都要在棧區(qū)為它開辟空間,像上述的代碼中,有肉眼可見的main函數(shù)和Add函數(shù),相應(yīng)的需要為它倆開辟空間,但其實(shí)main函數(shù)也是被調(diào)用的,當(dāng)我們針對(duì)上述代碼按下F10,按到return 0時(shí)再按一次,就會(huì)跳出以下界面
由圖得知,main函數(shù)是被__tmainCRTStartup函數(shù)調(diào)用的,而 __tmainCRTStartup又是被mainCRTStartup調(diào)用的。先看下總體函數(shù)棧幀開辟情況:
兩個(gè)重要知識(shí)點(diǎn):
- 壓棧(push):給棧頂放一個(gè)元素
- 出棧(pop) :從棧頂刪除一個(gè)元素
接下來(lái)會(huì)詳細(xì)講解下函數(shù)棧幀的開辟情況:
4、所需反匯編代碼總覽
int main()
{
00031410 push ebp
00031411 mov ebp,esp
00031413 sub esp,0E4h
00031419 push ebx
0003141A push esi
0003141B push edi
0003141C lea edi,[ebp+FFFFFF1Ch]
00031422 mov ecx,39h
00031427 mov eax,0CCCCCCCCh
0003142C rep stos dword ptr es:[edi]
int a = 10;
0003142E mov dword ptr [ebp-8],0Ah
int b = 20;
00031435 mov dword ptr [ebp-14h],14h
int c = 0;
0003143C mov dword ptr [ebp-20h],0
c=Add(a,b);
00031443 mov eax,dword ptr [ebp-14h]
00031446 push eax
00031447 mov ecx,dword ptr [ebp-8]
0003144A push ecx
0003144B call 00C210E1
00031440 add esp,8
00031443 mov dword ptr [ebp-20h],eax
printf("%d", c);
00241456 mov esi,esp
00241458 mov eax,dword ptr [ebp-20h]
0024145B push eax
0024145C push 245858h
00241461 call dword ptr ds:[00249114h]
00241467 add esp,8
0024146A cmp esi,esp
0024146C call 0024113B
return 0;
00241471 xor eax,eax
}
00031451 pop edi
00031452 pop esi
00031453 pop ebx
00031454 add esp,0E4h
0003145A cmp ebp,esp
0003145C call __RTC_CheckEsp (03113Bh)
00031461 mov esp,ebp
00031463 pop ebp
00031464 ret
int Add(int x, int y)
{
000313C0 push ebp
000313C1 mov ebp,esp
000313C3 sub esp,0CCh
000313C9 push ebx
000313CA push esi
000313CB push edi
000313CC lea edi,[ebp-0CCh]
000313D2 mov ecx,33h
000313D7 mov eax,0CCCCCCCCh
000313DC rep stos dword ptr es:[edi]
int z = 0;
000313DE mov dword ptr [ebp-8],0
z = x + y;
000313E5 mov eax,dword ptr [ebp+8]
000313E8 add eax,dword ptr [ebp+0ch]
000313EB mov dword ptr [ebp-8],eax
return z;
000313EE mov eax,dword ptr [ebp-8]
}
000313F1 pop edi
000313F2 pop esi
000313F3 pop ebx
000313F4 mov esp,ebp
000313F6 pop ebp
000313F7 ret
三、函數(shù)棧幀創(chuàng)建銷毀過程
1、_tmainCRTStartup函數(shù)(調(diào)用main函數(shù))棧幀的創(chuàng)建
根據(jù)上文,我們已經(jīng)知曉main函數(shù)是被_tmainCRTStartup函數(shù)所調(diào)用的,自然要為它開辟棧幀,這塊空間應(yīng)該由ebp和sep倆寄存器來(lái)維護(hù),前提是下面高地址,上面低地址。如圖:
此時(shí)進(jìn)入main函數(shù),首先要push進(jìn)行壓棧:
push ebp就是把ebp壓到棧頂上,此時(shí)sep相應(yīng)的移動(dòng)到新棧頂上,可以通過監(jiān)視來(lái)驗(yàn)證:
圖示如下:
接下來(lái)執(zhí)行mov操作:
此行代碼意思就是把sep賦給ebp,所以ebp指向的位置即為sep所指向的位置,但是源操作地址位置不變,可通過監(jiān)視來(lái)驗(yàn)證
接著執(zhí)行sub操作:
該操作就是給esp減去個(gè)0E4h ,此時(shí)esp的位置就要往上面去,通過監(jiān)視觀察:
此時(shí)此刻執(zhí)行完sub操作,其實(shí)就已經(jīng)進(jìn)入到下文的main函數(shù)棧幀的開辟,至此_tmainCRTStartup函數(shù)棧幀的開辟已完成。圖示見下文:
2、main函數(shù)棧幀的創(chuàng)建
接上文,圖示如下:
接下來(lái)進(jìn)行三次push操作: 把ebx、sei、edi順次壓棧壓進(jìn)去,相應(yīng)的esp也要往上走。
通過監(jiān)視看看:
圖示如下:
接下來(lái)執(zhí)行下列三個(gè)步驟
操作lea(load effecitve address)加載有效地址。就是相當(dāng)于把[ebp+FFFFFF1Ch]放到edi里頭,顯示符號(hào)名后[ebp+FFFFFF1Ch]就是[ebp-0E4h],前面已經(jīng)執(zhí)行過-0E4h,這里再執(zhí)行一次放到edi里頭去。接著mov把39h放到ecx里頭去,再mov此時(shí)eax放的就是0CCCCCCCCh
上述操作執(zhí)行后的目的就是從剛才的edi開始向下的39h次這么多個(gè)dword(1個(gè)word2字節(jié),2dword4個(gè)字節(jié))全部改為0CCCCCCCCh
通過監(jiān)視看下:
圖示如下:
至此,main棧幀的開辟已經(jīng)完成,接下來(lái)就要執(zhí)行正式有效代碼,見下文:
3、main函數(shù)內(nèi)執(zhí)行有效代碼(變量)
接下來(lái)執(zhí)行以下操作:
先mov把0Ah(10)放到ebp-8的位置上,同理把14h(20)放到ebp-14h上,把0放到ebp-20h上,如圖:
此時(shí)此刻a、b、c這三個(gè)變量均已創(chuàng)建完成,接下來(lái)進(jìn)行Add函數(shù)調(diào)用:先進(jìn)行傳參
首先,mov把ebp-14h(b=20)放到eax里頭。接下來(lái)再push, 壓棧把eax(20)放到棧頂,相應(yīng)esp也要移動(dòng),同理mov把ebp-8(a=10)放到ecx里頭,再push把ecx放到棧頂。如圖所示:
接著執(zhí)行call操作,調(diào)用Add函數(shù),按F10執(zhí)行到call時(shí),按下F11,此時(shí)就跳到Add函數(shù)內(nèi)部并且把call指令的下一條指令的地址壓到棧頂。這么做的目的是在接下來(lái)跳到Add函數(shù)里去回來(lái)時(shí)方便回到該地址,如圖:
按下F11,此時(shí)就正式進(jìn)入Add函數(shù)內(nèi)部 并為其開辟棧幀,詳情見下文:
4、Add函數(shù)棧幀的創(chuàng)建
int Add(int x, int y)
{
000313C0 push ebp
000313C1 mov ebp,esp
000313C3 sub esp,0CCh
000313C9 push ebx
000313CA push esi
000313CB push edi
000313CC lea edi,[ebp-0CCh]
000313D2 mov ecx,33h
000313D7 mov eax,0CCCCCCCCh
000313DC rep stos dword ptr es:[edi]
而前面這些操作跟先前main函數(shù)內(nèi)部操作一樣,其實(shí)就是在為Add函數(shù)準(zhǔn)備我們的棧幀
首先,push ebp把ebp壓棧到棧頂,再mov把esp賦給ebp,再sub,把esp-去0CCh,此步驟就是在為Add函數(shù)開辟空間,接著進(jìn)行三次push,同main函數(shù)那樣,同理,依舊是初始化成CCCCCCCC,詳細(xì)過程不再贅述,跟上文main函數(shù)一樣,如圖所示:
至此,Add棧幀的開辟已基本完成,接下來(lái)就要執(zhí)行正式有效代碼,見下文:
5、Add函數(shù)內(nèi)執(zhí)行有效代碼
接上文:
int z = 0;
000313DE mov dword ptr [ebp-8],0
z = x + y;
000313E5 mov eax,dword ptr [ebp+8]
000313E8 add eax,dword ptr [ebp+0ch]
000313EB mov dword ptr [ebp-8],eax
return z;
000313EE mov eax,dword ptr [ebp-8]
}
首先,把0放到ebp-8的位置上,接著mov把ebp+8的值放到eax里頭去,此時(shí)eax就是10。再add給eax加上ebp+0ch,就是把20加進(jìn)去,此時(shí)eax就是30,加完后再把eax(30)放到ebp-8里頭去,最終的結(jié)果(30)放到z里頭去。
此時(shí)Add函數(shù)內(nèi)部有效代碼執(zhí)行完畢,見圖:
接下來(lái)就要進(jìn)行返回了,也就是Add函數(shù)棧幀的銷毀,見下文:
6、Add函數(shù)棧幀的銷毀
return z;
000313EE mov eax,dword ptr [ebp-8]
}
000313F1 pop edi
000313F2 pop esi
000313F3 pop ebx
000313F4 mov esp,ebp
000313F6 pop ebp
000313F7 ret
上文已經(jīng)知道此時(shí)已經(jīng)把ebp-8的值(30)放到eax里頭去,接下來(lái)執(zhí)行三次pop,一次彈出,esp就會(huì)加加一次,如圖:
接著,把ebp賦給esp,再pop把ebp彈出,此時(shí)esp也要移動(dòng),此時(shí)esp和ebp又回到了先前維護(hù)main函數(shù)棧幀的樣子。如圖所示:
此時(shí)esp指向的就是call指令的下一條指令的地址,再按一次F10,此時(shí)反匯編就會(huì)這樣:
0003144B call 00C210E1
00031440 add esp,8
00031443 mov dword ptr [ebp-20h],eax
printf("%d", c);
00241456 mov esi,esp
00241458 mov eax,dword ptr [ebp-20h]
0024145B push eax
0024145C push 245858h
00241461 call dword ptr ds:[00249114h]
00241467 add esp,8
0024146A cmp esi,esp
0024146C call 0024113B
return 0;
00241471 xor eax,eax
}
此時(shí)我們就會(huì)明白先前存放call指令的下一條指令的地址就是為了方便回來(lái),先前ret執(zhí)行后esp的位置發(fā)生變化:
此時(shí)Add函數(shù)的棧幀算是真正銷毀,接下來(lái)進(jìn)行main函數(shù)棧幀的銷毀 。
7、main函數(shù)棧幀的銷毀
0003144B call 00C210E1
00031440 add esp,8
00031443 mov dword ptr [ebp-20h],eax
printf("%d", c);
00241456 mov esi,esp
00241458 mov eax,dword ptr [ebp-20h]
0024145B push eax
0024145C push 245858h
00241461 call dword ptr ds:[00249114h]
00241467 add esp,8
0024146A cmp esi,esp
0024146C call 0024113B
return 0;
00241471 xor eax,eax
}
通過反匯編代碼得知,此時(shí)指向add操作把esp加上8,此時(shí)就把x和y這兩個(gè)形參釋放回來(lái)了,指向如圖所示位置:
接下來(lái)mov把eax放到ebp-20h上,而eax就是我們出Add函數(shù)時(shí)計(jì)算的和,此時(shí)和就被我們帶回來(lái)了,接下來(lái)就是main函數(shù)棧幀的銷毀了,跟上文Add函數(shù)棧幀的銷毀沒有太大區(qū)別,這里不多做贅述。
而反匯編代碼如下:
00241471 xor eax,eax
}
00031451 pop edi
00031452 pop esi
00031453 pop ebx
00031454 add esp,0E4h
0003145A cmp ebp,esp
0003145C call __RTC_CheckEsp (03113Bh)
00031461 mov esp,ebp
00031463 pop ebp
00031464 ret
四、總結(jié)
至此,函數(shù)棧幀的創(chuàng)建和銷毀正式結(jié)束,而本文一開始的幾個(gè)問題(目標(biāo))也能清晰得知:
如下:
1、局部變量是怎么創(chuàng)建的?
首先,為函數(shù)分配好棧幀空間并初始化后,然后給局部變量在棧幀里頭分配一點(diǎn)空間。
2、為什么局部變量的值是隨機(jī)值?
因?yàn)殡S機(jī)值是我們?cè)陂_辟棧幀時(shí)就放進(jìn)去的,而我們初始化的時(shí)候,就是把隨機(jī)值給覆蓋了。
3、函數(shù)是怎么傳參的?傳參的順序是怎樣的?
當(dāng)我要調(diào)用函數(shù)之前,就已經(jīng)push、push把這兩個(gè)參數(shù)從右向左壓棧壓進(jìn)去,當(dāng)我們真正進(jìn)入形參函數(shù)的時(shí)候,在Add函數(shù)棧幀里頭通過指針的偏移量找到了形參。
4、形參和實(shí)參是什么關(guān)系?
形參確實(shí)是在壓棧時(shí)開辟的空間,形參和實(shí)參只是值上是相同的,空間上是獨(dú)立的,形參是實(shí)參的一份臨時(shí)拷貝,改變形參不會(huì)影響實(shí)參。
5、函數(shù)調(diào)用結(jié)束后是怎么返回的?
我們?cè)谡{(diào)用之前就已經(jīng)把call指令下一條指令的地址給壓進(jìn)去,當(dāng)函數(shù)調(diào)用完要返回的時(shí)候,就會(huì)跳轉(zhuǎn)到call指令下一條指令的地址,返回值是通過寄存器帶回來(lái)的。
到此這篇關(guān)于C語(yǔ)言中函數(shù)棧幀的創(chuàng)建和銷毀的深層分析的文章就介紹到這了,更多相關(guān)C語(yǔ)言 函數(shù)棧幀內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單計(jì)算器
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單計(jì)算器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05深入C++中構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù)、賦值操作符、析構(gòu)函數(shù)的調(diào)用過程總結(jié)
本篇文章是對(duì)C++中構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù)、賦值操作符、析構(gòu)函數(shù)的調(diào)用過程進(jìn)行了總結(jié)與分析,需要的朋友參考下2013-05-05C語(yǔ)言如何利用ASCII碼表統(tǒng)計(jì)字符串每個(gè)字符出現(xiàn)的次數(shù)
這篇文章主要介紹了C語(yǔ)言如何利用ASCII碼表統(tǒng)計(jì)字符串每個(gè)字符出現(xiàn)的次數(shù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01C語(yǔ)言修煉之路悟徹?cái)?shù)組真妙理?巧用下標(biāo)破萬(wàn)敵上篇
在C語(yǔ)言和C++等語(yǔ)言中,數(shù)組元素全為指針變量的數(shù)組稱為指針數(shù)組,指針數(shù)組中的元素都必須具有相同的存儲(chǔ)類型、指向相同數(shù)據(jù)類型的指針變量。指針數(shù)組比較適合用來(lái)指向若干個(gè)字符串,使字符串處理更加方便、靈活2022-02-02C語(yǔ)言關(guān)鍵字之a(chǎn)uto register詳解
這篇文章主要為大家介紹了C語(yǔ)言關(guān)鍵字之a(chǎn)uto register,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2022-01-01C/C++中時(shí)間庫(kù)函數(shù)的使用詳解
這篇文章主要為大家詳細(xì)介紹了C/C++中的時(shí)間相關(guān)知識(shí)總結(jié),例如時(shí)間庫(kù)函數(shù)的使用以及獲取本地時(shí)間的不同方法,文中的示例代碼講解詳細(xì),需要的可以參考一下2022-11-11C++小知識(shí):C/C++中不要按值傳遞數(shù)組
今天小編就為大家分享一篇關(guān)于C++小知識(shí):C/C++中不要按值傳遞數(shù)組,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-01-01詳解C語(yǔ)言的結(jié)構(gòu)體中成員變量偏移問題
這篇文章主要介紹了C語(yǔ)言的結(jié)構(gòu)體中成員變量偏移問題,以講解如何編寫宏來(lái)對(duì)成員變量進(jìn)行修改為主,需要的朋友可以參考下2016-04-04