日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

C語言超詳細解析函數棧幀_C 語言

作者:李逢溪 ? 更新時間: 2022-06-01 編程語言

一、前面

本章將以匯編視角看函數棧幀的內存是如何使用與回收的,為了降低匯編語言的理解成本,以圖示的方式講解每一步匯編指令所帶來的效果,來逐步展示函數棧幀的形成與銷毀的整個過程。

展示環境:win10 && vs2019

二、預備知識

這些預備知識理解與否對本篇文章并無很大關系,之所以預備這些知識是為了讓讀者能夠更加相信函數棧幀的形成與銷毀過程就是如此。

棧區:內存四區之一,內存為了使用和管理,被劃分為四部分,其中棧區就是內存被劃分的區域之一,棧的使用習慣是,先使用高地址部分,在使用底地址部分。

函數棧幀:即在調用函數時,為函數開辟的一塊內存空間,由于該內存空間在棧區,因此該空間被稱作函數棧幀,簡稱棧幀。

棧頂:故名思意,就是棧的頂部,更確切的說是指向存放在棧區數據的頂部。

棧底:棧的底部。

寄存器:寄存器cpu內部用來存放數據的一些小型存儲區域,用來暫時存放參與運算的數據和運算結果。簡單來說就是獨立于內存,用來存儲少量數據的器件。

ebp:棧底指針寄存器

esp:棧底指針寄存器

其它寄存器:ebx、esi、edi、ecx、eax

入棧(壓棧):先將棧頂指針向上移動四字節的大小空間,再將寄存器的數據放入那四字節空間。這里的向上移動是指向低地址處移動。

入棧指令:push?a。

圖解:以push a為例。

?出棧:將棧頂指針向下移動四字節,這里的向下是往低地址處移動四個字節的空間。并將這四個字節的數據放入某個寄存器中。

出棧指令:pop?a。

圖解:以pop? a為例。

簡單匯編操作指令

mov a b:將b賦值給a,c語言表示就是a=b。

sub a b:將a-b的結果賦值給a,c語言表述就是a=a-b。

add a b :將a+b的結果賦值給a,c語言表述就是a=a+b。

由于理解成本的原因,遇到的其它匯編指令本文會直接指出它的作用效果。

三、棧幀創建與銷毀

以Add函數調用為例

#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;
}

該代碼對應的匯編指令如下:

?需要說明的是,main函數也被別的函數調用的,調用關系是:__mainCRTStartup調用main函數,mainCRTStartup函數調用__mainCRTStartup。

再調用main函數之前,棧區是這樣的。

?指令分說:

int main()
{
00F71E40  push        ebp  
00F71E41  mov         ebp,esp  
00F71E43  sub         esp,0E4h  

以上圖為參照。

第一條指令:將寄存器ebp的值壓棧

第二條指令:將寄存器esp的值賦值給ebp

第三條指令:將esp-0E4h賦值給寄存器esp,形象的表述是esp向低地址方向移動4個字節,上端為低地址,下端為高地址,即向上移動4字節空間。

棧區視圖變為:

?這三條指令,簡單來說就是為main函數在棧區開辟了一塊空間(這塊空間大小系統會幫我們自動開辟好。)

指令分說:

00F71E49 push ebx
00F71E4A push esi
00F71E4B push edi

將三個寄存器的值壓入棧中

棧區視圖變為:

指令分說:

00F71E4C lea edi,[ebp-24h]
00F71E4F mov ecx,9
00F71E54 mov eax,0CCCCCCCCh
00F71E59 rep stos dword ptr es:[edi]

這四條指令我們就解讀了,效果就是將main函數的棧幀空間以16進制值cccccccc填充。

棧區視圖變為:

指令分說:

00F71E5B mov ecx,0F7C003h
00F71E60 call 00F7130C

這兩條指令是編譯器檢查用的,初學不必花費更多時間了解更細節的部分。

vs2013沒有這一檢查部分,vs2019檢查很嚴格。

指令分說:

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放入那塊空間。

第二條匯編指令:將14h放入[ ebp-14h ]這塊空間中,即把b放入那塊空間中。

第三條匯編指令:將0放入[?ebp-20h ]這塊空間中。即把z放入那塊空間中。

棧區圖示:

?簡單來說:就是將局部變量放入對應的函數棧幀中。

指令分說:

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字節的數據放入eax中。即把b=20的數據放入eax中。

第二條指令:將eax的數據壓棧。

第三條指令:將【ebp-8】這塊空間4字節的數據放入ecx中。即把a=10的數據放入ecx中。

第四條指令:將ecx的數據壓棧。

棧區視圖:

?這里的20和10,就是我們傳過去的實參,之后Add函數調用的x和y就是指這兩塊空間。

那么我們可以知道:函數傳參是從右向左傳的。這里就是先傳的b再傳的a。

指令分說:

00F71E82 call 00F710B4

調用的函數:
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是調用指令,調用Add函數。

經過上次的指令,這里我就直接介紹效果了。

00F71740 push ebp
00F71741 mov ebp,esp
00F71743 sub esp,0CCh

這三條指令,為Add函數在棧區開辟對應的空間大小。

棧區圖示:

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]

對Add函數棧幀做初始化,將里面的數據置換為cccccccc。(用于初始化棧幀的具體數值取決于編譯器)

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

取[ebp+8]空間的數據放入eax中

取 [ebp+0Ch]? 與eax的數據相加后放入eax中。

將eax的值放入ptr [ebp-8]中。

圖示:

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

代碼分說:

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函數棧幀的大小

所以esp向下移動到dbp的位置。

?之后pop?ebp,由于棧頂指向的是main函數棧幀的棧底,因此出棧ebp指向main函數棧幀的棧底。

圖示:

調用Add返回之后,繼續執行以下指令。

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向下移動8個字節,即銷毀x和y這兩塊連續的形參。

第二條指令:將寄存器eax保存的Add函數的返回值交給z。

圖示:

?之后的指令就是回收main函數的棧幀了,回收過程都差不多,就不細細講解了。

四、總結

以下函數調用為例。

#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函數調用__mainCRTStartup、__mainCRTStartup調用main函數。

棧區上先為以上兩個函數分配函數棧幀。

調用main函數時,為main函數分配函數棧幀(該大小是自動開辟的)開辟好空間后,用cccccccc數值填充main函數棧幀。(具體用什么數值初始化函數棧幀取決與編譯器)。

執行到int?a = 10時,將局部變量a的值放入main函數棧幀的某塊空間中,int b =20、int?z=0也是如此,它們的空間都在main函數的函數棧幀中。

當執行到z=Add(a,b)時。

先傳參,傳參順序是從右向左,所有先將b壓入棧中,在將a壓入棧中。

這兩塊空間就是y和、x。注意y和x并不在Add函數棧幀中,而是在main函數棧幀和Add函數棧幀之間的一塊獨立的空間。

然后為Add函數開辟函數棧幀,并ccccccc數值填充Add函數棧幀。(具體用什么數值初始化函數棧幀取決與編譯器)。

當執行到z=x+y時,在Add函數棧幀中取一塊空間作為局部變量z使用,在取出y和x空間的值,放入z中。(z是在Add函數棧幀中的)。

當執行到return?z時,將z的值放入寄存器中。

之后再銷毀Add函數的棧幀、銷毀形參x和y、將寄存器的值交給z。

之后銷毀main函數也是如此。

這里的銷毀不是將Add函數的棧幀數據置為0或者其他數,它里面的數據并不是直接丟失的,而是直接告訴操作系統,這塊空間我不需要了,Add函數棧幀里的數據還是存在的,只不過當你調用新函數時,Add函數棧幀這塊空間會被新函數占用,并初始化為cccccccc這樣的數值,那么Add函數棧幀空間數據也就丟失了。

原文鏈接:https://blog.csdn.net/m0_62171658/article/details/123747765

欄目分類
最近更新