C語(yǔ)言超詳細(xì)解析函數(shù)棧幀
一、前面
本章將以匯編視角看函數(shù)棧幀的內(nèi)存是如何使用與回收的,為了降低匯編語(yǔ)言的理解成本,以圖示的方式講解每一步匯編指令所帶來(lái)的效果,來(lái)逐步展示函數(shù)棧幀的形成與銷毀的整個(gè)過(guò)程。
展示環(huán)境:win10 && vs2019
二、預(yù)備知識(shí)
這些預(yù)備知識(shí)理解與否對(duì)本篇文章并無(wú)很大關(guān)系,之所以預(yù)備這些知識(shí)是為了讓讀者能夠更加相信函數(shù)棧幀的形成與銷毀過(guò)程就是如此。
棧區(qū):內(nèi)存四區(qū)之一,內(nèi)存為了使用和管理,被劃分為四部分,其中棧區(qū)就是內(nèi)存被劃分的區(qū)域之一,棧的使用習(xí)慣是,先使用高地址部分,在使用底地址部分。
函數(shù)棧幀:即在調(diào)用函數(shù)時(shí),為函數(shù)開(kāi)辟的一塊內(nèi)存空間,由于該內(nèi)存空間在棧區(qū),因此該空間被稱作函數(shù)棧幀,簡(jiǎn)稱棧幀。
棧頂:故名思意,就是棧的頂部,更確切的說(shuō)是指向存放在棧區(qū)數(shù)據(jù)的頂部。
棧底:棧的底部。
寄存器:寄存器cpu內(nèi)部用來(lái)存放數(shù)據(jù)的一些小型存儲(chǔ)區(qū)域,用來(lái)暫時(shí)存放參與運(yùn)算的數(shù)據(jù)和運(yùn)算結(jié)果。簡(jiǎn)單來(lái)說(shuō)就是獨(dú)立于內(nèi)存,用來(lái)存儲(chǔ)少量數(shù)據(jù)的器件。
ebp:棧底指針寄存器
esp:棧底指針寄存器
其它寄存器:ebx、esi、edi、ecx、eax
入棧(壓棧):先將棧頂指針向上移動(dòng)四字節(jié)的大小空間,再將寄存器的數(shù)據(jù)放入那四字節(jié)空間。這里的向上移動(dòng)是指向低地址處移動(dòng)。
入棧指令:push a。
圖解:以push a為例。

出棧:將棧頂指針向下移動(dòng)四字節(jié),這里的向下是往低地址處移動(dòng)四個(gè)字節(jié)的空間。并將這四個(gè)字節(jié)的數(shù)據(jù)放入某個(gè)寄存器中。
出棧指令:pop a。
圖解:以pop a為例。

簡(jiǎn)單匯編操作指令
mov a b:將b賦值給a,c語(yǔ)言表示就是a=b。
sub a b:將a-b的結(jié)果賦值給a,c語(yǔ)言表述就是a=a-b。
add a b :將a+b的結(jié)果賦值給a,c語(yǔ)言表述就是a=a+b。
由于理解成本的原因,遇到的其它匯編指令本文會(huì)直接指出它的作用效果。
三、棧幀創(chuàng)建與銷毀
以Add函數(shù)調(diào)用為例
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int Add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int z = 0;
z = Add(a, b);
printf("%d\n", z);
return 0;
}該代碼對(duì)應(yīng)的匯編指令如下:

需要說(shuō)明的是,main函數(shù)也被別的函數(shù)調(diào)用的,調(diào)用關(guān)系是:__mainCRTStartup調(diào)用main函數(shù),mainCRTStartup函數(shù)調(diào)用__mainCRTStartup。
再調(diào)用main函數(shù)之前,棧區(qū)是這樣的。

指令分說(shuō):
int main()
{
00F71E40 push ebp
00F71E41 mov ebp,esp
00F71E43 sub esp,0E4h 以上圖為參照。
第一條指令:將寄存器ebp的值壓棧
第二條指令:將寄存器esp的值賦值給ebp
第三條指令:將esp-0E4h賦值給寄存器esp,形象的表述是esp向低地址方向移動(dòng)4個(gè)字節(jié),上端為低地址,下端為高地址,即向上移動(dòng)4字節(jié)空間。
棧區(qū)視圖變?yōu)椋?/p>

這三條指令,簡(jiǎn)單來(lái)說(shuō)就是為main函數(shù)在棧區(qū)開(kāi)辟了一塊空間(這塊空間大小系統(tǒng)會(huì)幫我們自動(dòng)開(kāi)辟好。)
指令分說(shuō):
00F71E49 push ebx
00F71E4A push esi
00F71E4B push edi
將三個(gè)寄存器的值壓入棧中
棧區(qū)視圖變?yōu)椋?/p>

指令分說(shuō):
00F71E4C lea edi,[ebp-24h]
00F71E4F mov ecx,9
00F71E54 mov eax,0CCCCCCCCh
00F71E59 rep stos dword ptr es:[edi]
這四條指令我們就解讀了,效果就是將main函數(shù)的棧幀空間以16進(jìn)制值cccccccc填充。
棧區(qū)視圖變?yōu)椋?/p>

指令分說(shuō):
00F71E5B mov ecx,0F7C003h
00F71E60 call 00F7130C
這兩條指令是編譯器檢查用的,初學(xué)不必花費(fèi)更多時(shí)間了解更細(xì)節(jié)的部分。
vs2013沒(méi)有這一檢查部分,vs2019檢查很嚴(yán)格。
指令分說(shuō):
int a = 10; 00F71E65 mov dword ptr [ebp-8],0Ah int b = 20; 00F71E6C mov dword ptr [ebp-14h],14h int z = 0; 00F71E73 mov dword ptr [ebp-20h],0
第一條匯編指令:將0Ah放入[ ebp-8 ]這塊空間中,即把a(bǔ)放入那塊空間。
第二條匯編指令:將14h放入[ ebp-14h ]這塊空間中,即把b放入那塊空間中。
第三條匯編指令:將0放入[ ebp-20h ]這塊空間中。即把z放入那塊空間中。
棧區(qū)圖示:

簡(jiǎn)單來(lái)說(shuō):就是將局部變量放入對(duì)應(yīng)的函數(shù)棧幀中。
指令分說(shuō):
z = Add(a, b);
00F71E7A mov eax,dword ptr [ebp-14h]
00F71E7D push eax
00F71E7E mov ecx,dword ptr [ebp-8]
00F71E81 push ecx
第一條指令:將【ebp-20】這塊空間4字節(jié)的數(shù)據(jù)放入eax中。即把b=20的數(shù)據(jù)放入eax中。
第二條指令:將eax的數(shù)據(jù)壓棧。
第三條指令:將【ebp-8】這塊空間4字節(jié)的數(shù)據(jù)放入ecx中。即把a(bǔ)=10的數(shù)據(jù)放入ecx中。
第四條指令:將ecx的數(shù)據(jù)壓棧。
棧區(qū)視圖:

這里的20和10,就是我們傳過(guò)去的實(shí)參,之后Add函數(shù)調(diào)用的x和y就是指這兩塊空間。
那么我們可以知道:函數(shù)傳參是從右向左傳的。這里就是先傳的b再傳的a。
指令分說(shuō):
00F71E82 call 00F710B4
調(diào)用的函數(shù):
int Add(int x, int y)
{
00F71740 push ebp
00F71741 mov ebp,esp
00F71743 sub esp,0CCh
00F71749 push ebx
00F7174A push esi
00F7174B push edi
00F7174C lea edi,[ebp-0Ch]
00F7174F mov ecx,3
00F71754 mov eax,0CCCCCCCCh
00F71759 rep stos dword ptr es:[edi]
00F7175B mov ecx,0F7C003h
00F71760 call 00F7130C
int z = x + y;
00F71765 mov eax,dword ptr [ebp+8]
00F71768 add eax,dword ptr [ebp+0Ch]
00F7176B mov dword ptr [ebp-8],eax
return z;
00F7176E mov eax,dword ptr [ebp-8]
}
00F71771 pop edi
00F71772 pop esi
00F71773 pop ebx
00F71774 add esp,0CCh
00F7177A cmp ebp,esp
00F7177C call 00F71235
00F71781 mov esp,ebp
00F71783 pop ebp
00F71784 ret
第一條匯編指令:call是調(diào)用指令,調(diào)用Add函數(shù)。
經(jīng)過(guò)上次的指令,這里我就直接介紹效果了。
00F71740 push ebp
00F71741 mov ebp,esp
00F71743 sub esp,0CCh
這三條指令,為Add函數(shù)在棧區(qū)開(kāi)辟對(duì)應(yīng)的空間大小。
棧區(qū)圖示:

00F71749 push ebx
00F7174A push esi
00F7174B push edi
將ebx,esi,edi入棧。
圖示:

00F7174C lea edi,[ebp-0Ch]
00F7174F mov ecx,3
00F71754 mov eax,0CCCCCCCCh
00F71759 rep stos dword ptr es:[edi]
對(duì)Add函數(shù)棧幀做初始化,將里面的數(shù)據(jù)置換為cccccccc。(用于初始化棧幀的具體數(shù)值取決于編譯器)

00F7175B mov ecx,0F7C003h
00F71760 call 00F7130C
編譯器做的檢查,不必理會(huì)。
int z = x + y;
00F71765 mov eax,dword ptr [ebp+8]
00F71768 add eax,dword ptr [ebp+0Ch]
00F7176B mov dword ptr [ebp-8],eax
取[ebp+8]空間的數(shù)據(jù)放入eax中
取 [ebp+0Ch] 與eax的數(shù)據(jù)相加后放入eax中。
將eax的值放入ptr [ebp-8]中。
圖示:

return z;
00F7176E mov eax,dword ptr [ebp-8]
返回時(shí),通過(guò)寄存器的方式,將返回值交給寄存器。
00F71771 pop edi
00F71772 pop esi
00F71773 pop ebx
00F71774 add esp,0CCh
00F7177A cmp ebp,esp
00F7177C call 00F71235
00F71781 mov esp,ebp
00F71783 pop ebp
00F71784 ret
代碼分說(shuō):
00F71771 pop edi
00F71772 pop esi
00F71773 pop ebx
將edi、esi、ebx出棧
圖示:

00F71774 add esp,0CCh
00F7177A cmp ebp,esp
00F7177C call 00F71235
00F71781 mov esp,ebp
00F71783 pop ebp
0CCh是Add函數(shù)棧幀的大小
所以esp向下移動(dòng)到dbp的位置。
之后pop ebp,由于棧頂指向的是main函數(shù)棧幀的棧底,因此出棧ebp指向main函數(shù)棧幀的棧底。
圖示:

調(diào)用Add返回之后,繼續(xù)執(zhí)行以下指令。
00A717F7 add esp,8
00A717FA mov dword ptr [ebp-20h],eax
return 0;
00A717FD xor eax,eax
}
00A717FF pop edi
00A71800 pop esi
00A71801 pop ebx
00A71802 add esp,0E4h
00A71808 cmp ebp,esp
00A7180A call 00A71235
00A7180F mov esp,ebp
00A71811 pop ebp
00A71812 ret
第一條指令:將esp向下移動(dòng)8個(gè)字節(jié),即銷毀x和y這兩塊連續(xù)的形參。
第二條指令:將寄存器eax保存的Add函數(shù)的返回值交給z。
圖示:

之后的指令就是回收main函數(shù)的棧幀了,回收過(guò)程都差不多,就不細(xì)細(xì)講解了。
四、總結(jié)
以下函數(shù)調(diào)用為例。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int Add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int z = 0;
z = Add(a, b);
return 0;
}初始:mainCRTStartup函數(shù)調(diào)用__mainCRTStartup、__mainCRTStartup調(diào)用main函數(shù)。
棧區(qū)上先為以上兩個(gè)函數(shù)分配函數(shù)棧幀。
調(diào)用main函數(shù)時(shí),為main函數(shù)分配函數(shù)棧幀(該大小是自動(dòng)開(kāi)辟的)開(kāi)辟好空間后,用cccccccc數(shù)值填充main函數(shù)棧幀。(具體用什么數(shù)值初始化函數(shù)棧幀取決與編譯器)。
執(zhí)行到int a = 10時(shí),將局部變量a的值放入main函數(shù)棧幀的某塊空間中,int b =20、int z=0也是如此,它們的空間都在main函數(shù)的函數(shù)棧幀中。
當(dāng)執(zhí)行到z=Add(a,b)時(shí)。
先傳參,傳參順序是從右向左,所有先將b壓入棧中,在將a壓入棧中。
這兩塊空間就是y和、x。注意y和x并不在Add函數(shù)棧幀中,而是在main函數(shù)棧幀和Add函數(shù)棧幀之間的一塊獨(dú)立的空間。
然后為Add函數(shù)開(kāi)辟函數(shù)棧幀,并ccccccc數(shù)值填充Add函數(shù)棧幀。(具體用什么數(shù)值初始化函數(shù)棧幀取決與編譯器)。
當(dāng)執(zhí)行到z=x+y時(shí),在Add函數(shù)棧幀中取一塊空間作為局部變量z使用,在取出y和x空間的值,放入z中。(z是在Add函數(shù)棧幀中的)。
當(dāng)執(zhí)行到return z時(shí),將z的值放入寄存器中。
之后再銷毀Add函數(shù)的棧幀、銷毀形參x和y、將寄存器的值交給z。
之后銷毀main函數(shù)也是如此。
這里的銷毀不是將Add函數(shù)的棧幀數(shù)據(jù)置為0或者其他數(shù),它里面的數(shù)據(jù)并不是直接丟失的,而是直接告訴操作系統(tǒng),這塊空間我不需要了,Add函數(shù)棧幀里的數(shù)據(jù)還是存在的,只不過(guò)當(dāng)你調(diào)用新函數(shù)時(shí),Add函數(shù)棧幀這塊空間會(huì)被新函數(shù)占用,并初始化為cccccccc這樣的數(shù)值,那么Add函數(shù)棧幀空間數(shù)據(jù)也就丟失了。
到此這篇關(guān)于C語(yǔ)言超詳細(xì)解析函數(shù)棧幀的文章就介紹到這了,更多相關(guān)C語(yǔ)言 函數(shù)棧幀內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于函數(shù)調(diào)用方式__stdcall和__cdecl詳解
下面小編就為大家?guī)?lái)一篇關(guān)于函數(shù)調(diào)用方式__stdcall和__cdecl詳解。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-09-09
C/C++通過(guò)HTTP實(shí)現(xiàn)文件上傳與下載的示例詳解
WinInet是 Microsoft Windows 操作系統(tǒng)中的一個(gè) API 集,用于提供對(duì) Internet 相關(guān)功能的支持,它包括了一系列的函數(shù),使得 Windows 應(yīng)用程序能夠進(jìn)行網(wǎng)絡(luò)通信、處理 HTTP 請(qǐng)求、FTP 操作等,本文給大家介紹了C/C++通過(guò)HTTP實(shí)現(xiàn)文件上傳與下載,需要的朋友可以參考下2023-12-12
C++分析如何用虛析構(gòu)與純虛析構(gòu)處理內(nèi)存泄漏
虛析構(gòu)和純虛析構(gòu)共性:可以解決父類指針釋放子類對(duì)象,都需要有具體的函數(shù)實(shí)現(xiàn);虛析構(gòu)和純虛析構(gòu)區(qū)別:如果是純虛析構(gòu),該類屬于抽象類,無(wú)法實(shí)例化對(duì)象2022-08-08
C++模擬實(shí)現(xiàn)vector示例代碼圖文講解
這篇文章主要介紹了C++容器Vector的模擬實(shí)現(xiàn),Vector是一個(gè)能夠存放任意類型的動(dòng)態(tài)數(shù)組,有點(diǎn)類似數(shù)組,是一個(gè)連續(xù)地址空間,下文更多詳細(xì)內(nèi)容的介紹,需要的小伙伴可以參考一下2023-02-02
C語(yǔ)言 fseek(f,0,SEEK_SET)函數(shù)案例詳解
這篇文章主要介紹了C語(yǔ)言 fseek(f,0,SEEK_SET)函數(shù)案例詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08
C語(yǔ)言?動(dòng)態(tài)內(nèi)存管理全面解析
動(dòng)態(tài)內(nèi)存是相對(duì)靜態(tài)內(nèi)存而言的。所謂動(dòng)態(tài)和靜態(tài)就是指內(nèi)存的分配方式。動(dòng)態(tài)內(nèi)存是指在堆上分配的內(nèi)存,而靜態(tài)內(nèi)存是指在棧上分配的內(nèi)存,本文帶你深入探究C語(yǔ)言中動(dòng)態(tài)內(nèi)存的管理2022-02-02

