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++11?std::transform函數(shù)使用小結(jié)
std::transform是C++標(biāo)準(zhǔn)庫(kù)中的一個(gè)算法,它用于對(duì)輸入范圍內(nèi)的元素進(jìn)行操作,并將結(jié)果存儲(chǔ)在輸出范圍內(nèi),本文就介紹了std::transform函數(shù)的具體使用,感興趣的可以了解一下2023-09-09
C語(yǔ)言中將日期和時(shí)間以字符串格式輸出的方法
這篇文章主要介紹了C語(yǔ)言中將日期和時(shí)間以字符串格式輸出的方法,分別是ctime()函數(shù)和asctime()函數(shù),注意參數(shù)區(qū)別,需要的朋友可以參考下2015-08-08
基于Qt開發(fā)獲取CTP量化交易接口測(cè)試數(shù)據(jù)工具
這篇文章主要為大家詳細(xì)介紹了如何使用Qt軟件開發(fā)K線股P相關(guān)軟件,先開發(fā)一個(gè)通過CTP量化交易的sdk獲取相關(guān)推送數(shù)據(jù)的工具,需要的可以參考下2024-04-04
C++&&Opencv實(shí)現(xiàn)控制臺(tái)字符動(dòng)畫的方法
這篇文章主要介紹了C++&&Opencv實(shí)現(xiàn)控制臺(tái)字符動(dòng)畫的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07
淺析C++中dynamic_cast和static_cast實(shí)例語(yǔ)法詳解
這篇文章主要介紹了淺析C++中dynamic_cast和static_cast實(shí)例演示,包括static_cast語(yǔ)法知識(shí)和static_cast的作用講解,namic_cast 語(yǔ)法詳解,需要的朋友可以參考下2021-07-07
C語(yǔ)言實(shí)現(xiàn)支持動(dòng)態(tài)拓展和銷毀的線程池
這篇文章主要為大家介紹了C語(yǔ)言實(shí)現(xiàn)支持動(dòng)態(tài)拓展和銷毀的線程池,感興趣的小伙伴們可以參考一下2016-01-01
淺談C語(yǔ)言函數(shù)調(diào)用參數(shù)壓棧的相關(guān)問題
下面小編就為大家?guī)?lái)一篇淺談C語(yǔ)言函數(shù)調(diào)用參數(shù)壓棧的相關(guān)問題。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2016-09-09
C/C++: Inline function, calloc 對(duì)比 malloc
以下是對(duì)c/c++中的malloc函數(shù)與calloc函數(shù)的區(qū)別以及它們之間的聯(lián)系進(jìn)行了介紹,需要的朋友可以過來(lái)參考下2016-07-07

