前言:
為了深入學(xué)習(xí)C語(yǔ)言,也為了方便理解,我學(xué)習(xí)了函數(shù)棧幀。函數(shù)棧幀的創(chuàng)建和銷毀能夠讓我更加深刻的了解編程邏輯和語(yǔ)法。我們學(xué)習(xí)語(yǔ)法和編程邏輯都是基于封裝好的知識(shí)上得。因此,我們有必要對(duì)函數(shù)棧幀的創(chuàng)建和銷毀進(jìn)行學(xué)習(xí)。本篇博客將用來(lái)介紹函數(shù)棧幀的創(chuàng)建和銷毀的過(guò)程,希望大家一起學(xué)習(xí)。如有不足之處,請(qǐng)大家多多指出,謝謝!
注意:
這里我使用的是vs2022和大家展示。不同編譯器上展示的結(jié)果會(huì)有差異,但大體邏輯一樣(也能起到參考的作用)。版本越高的編譯器越不好觀察,不容易觀看函數(shù)棧幀創(chuàng)建和銷毀的過(guò)程,封裝過(guò)程也會(huì)復(fù)雜一下。
一、認(rèn)識(shí)相關(guān)寄存器和匯編指令
1.寄存器(寄存器是集成在cpu上的)
eax:累加寄存器,相對(duì)于其他寄存器,在運(yùn)算方面比較常用
ebx:基地址寄存器,在內(nèi)存尋址時(shí)存放基地址。
ecx:計(jì)數(shù)寄存器,用于循環(huán)操作,如重復(fù)的字符存儲(chǔ)操作或者數(shù)字統(tǒng)計(jì)。
edx:作為EAX的溢出寄存器,總是被用來(lái)放整數(shù)除法產(chǎn)生的余數(shù)。
esi:源變址寄存器,主要用于存放存儲(chǔ)單元在段內(nèi)的偏移量。通常在內(nèi)存操作指令中作為“源地址指針”使用。
edi:目的變址寄存器,主要用于存放存儲(chǔ)單元在段內(nèi)的偏移量。
ebp:棧底指針
esp:棧頂指針
esp和ebp這兩個(gè)寄存器中存放的是地址,這兩個(gè)地址是用來(lái)維護(hù)函數(shù)棧幀得;esp和ebp用來(lái)維護(hù)函數(shù)棧幀時(shí),正在調(diào)用什么函數(shù),就會(huì)維護(hù)那個(gè)函數(shù)。
rbp,rsp(64位編譯,對(duì)于32位編譯是ebp,esp寄存器)這2個(gè)寄存器中存放的是地址,這2個(gè)地址是用來(lái)維護(hù)函數(shù)棧幀的。
2.匯編指令
push:
壓棧,給棧頂放一個(gè)元素。(數(shù)據(jù)入棧,同時(shí)esp棧頂寄存器也要發(fā)生改變)
pop:
出棧,給棧頂刪除一個(gè)元素。(數(shù)據(jù)彈出至指定位置,同時(shí)esp棧頂寄存器也要發(fā)生改變)
mov:數(shù)據(jù)轉(zhuǎn)移指令。(后面的指針指向前面)
sub:減法命令。(前面的值減后面的值)
add:加法命令。
call:函數(shù)調(diào)用,1. 壓入返回地址 2. 轉(zhuǎn)入目標(biāo)函數(shù)
jump:通過(guò)修改eip,轉(zhuǎn)入目標(biāo)函數(shù),進(jìn)行調(diào)用。
lea:加載,把后面的有效地址加載到前面。
補(bǔ)充:
棧區(qū)的使用是從高地址到低地址
棧區(qū)的使用遵循先進(jìn)后出,后進(jìn)先出
棧區(qū)的放置是從高地址往低地址放置:push 是壓棧
刪除是從低往高刪除:pop 是出棧
如圖:
二、函數(shù)棧幀創(chuàng)建和銷毀的過(guò)程
本次演示以vs2022為例
演示代碼:
#include <stdio.h>
int ADD(int x,int y)
{
int z = x + y;
return z;
}
int main()
{
int a = 3,b=6,c=0;
c = ADD(a,b);
printf("%d\n", c);
return 0;
}
準(zhǔn)備工作:
1)按F10進(jìn)入函數(shù)調(diào)用模式:
2)打開(kāi)調(diào)用堆棧,出現(xiàn)調(diào)用堆棧窗口:
3)在調(diào)用模式下右擊鼠標(biāo)后,單擊轉(zhuǎn)到反匯編,進(jìn)入反匯編界面:
1.main函數(shù)的調(diào)用
main函數(shù)也可以被其他函數(shù)調(diào)用:
1)為了閱讀方便,我們把“顯示符號(hào)名”取消勾選。
2)按F10,從調(diào)用堆棧,我們可以看到main函數(shù)被別的函數(shù)調(diào)用:
main()函數(shù)被invoke_main()函數(shù)調(diào)用;
invoke_main()函數(shù)被__scrt_common_main_seh() 函數(shù)調(diào)用;
__scrt_common_main_seh()函數(shù)被__scrt_common_main() 函數(shù)調(diào)用;
__scrt_common_main() 函數(shù)被mainCRTStartup(void * __formal) 函數(shù)調(diào)用。
注意:
編譯器版本越高,反匯編越不容易觀察,編譯器版本過(guò)高,會(huì)優(yōu)化。
2.函數(shù)棧幀的創(chuàng)建
1)匯編代碼如下:
int main()
{
00CD18B0 push ebp
00CD18B1 mov ebp,esp
00CD18B3 sub esp,0E4h
00CD18B9 push ebx
00CD18BA push esi
00CD18BB push edi
00CD18BC lea edi,[ebp-24h]
00CD18BF mov ecx,9
00CD18C4 mov eax,0CCCCCCCCh
00CD18C9 rep stos dword ptr es:[edi]
00CD18CB mov ecx,0CDC008h
00CD18D0 call 00CD131B
int a = 3, b = 6,c = 0;
00CD18D5 mov dword ptr [ebp-8],3
00CD18DC mov dword ptr [ebp-14h],6
00CD18E3 mov dword ptr [ebp-20h],0
c = ADD(a,b);
00CD18EA mov eax,dword ptr [ebp-14h]
00CD18ED push eax
00CD18EE mov ecx,dword ptr [ebp-8]
00CD18F1 push ecx
00CD18F2 call 00CD1217
00CD18F7 add esp,8
00CD18FA mov dword ptr [ebp-20h],eax
printf("%d\n", c);
00CD18FD mov eax,dword ptr [ebp-20h]
00CD1900 push eax
00CD1901 push 0CD7B30h
00CD1906 call 00CD10CD
00CD190B add esp,8
return 0;
00CD190E xor eax,eax
}
00CD1910 pop edi
00CD1911 pop esi
00CD1912 pop ebx
00CD1913 add esp,0E4h
00CD1919 cmp ebp,esp
00CD191B call 00CD1244
00CD1920 mov esp,ebp
00CD1922 pop ebp
00CD1923 ret
2)給main函數(shù)開(kāi)辟空間
00CD18B0 push ebp /*壓棧,棧頂放一個(gè)元素,把ebp寄存器中的值進(jìn)行壓棧,此時(shí)的ebp中存放的是
invoke_main函數(shù)棧幀的ebp,esp-4*/
00CD18B1 mov ebp,esp /*把esp的值存放到ebp中,相當(dāng)于產(chǎn)生了main函數(shù)的
ebp,這個(gè)值就是invoke_main函數(shù)棧幀的esp*/
00CD18B3 sub esp,0E4h /*sub會(huì)讓esp中的地址減去一個(gè)16進(jìn)制數(shù)字0xe4,產(chǎn)生新的
esp,此時(shí)的esp是main函數(shù)棧幀的esp,此時(shí)結(jié)合上一條指令的ebp和當(dāng)前的esp,ebp和esp之間維護(hù)了一
個(gè)塊??臻g,這塊??臻g就是為main函數(shù)開(kāi)辟的,就是main函數(shù)的棧幀空間,這一段空間中將存儲(chǔ)main函數(shù)
中的局部變量,臨時(shí)數(shù)據(jù)已經(jīng)調(diào)試信息等。*/
00CD18B9 push ebx //將寄存器ebx的值壓棧,esp-4
00CD18BA push esi //將寄存器esi的值壓棧,esp-4
00CD18BB push edi //將寄存器edi的值壓棧,esp-4
/*上面3條指令保存了3個(gè)寄存器的值在棧區(qū),這3個(gè)寄存器的在函數(shù)隨后執(zhí)行中可能會(huì)被修改,所以先保存寄
存器原來(lái)的值,以便在退出函數(shù)時(shí)恢復(fù)。*/
//下面的代碼是在初始化main函數(shù)的棧幀空間。
//1. 先把ebp-24h的地址,放在edi中
//2. 把9放在ecx中
//3. 把0xCCCCCCCC放在eax中
//4. 將從ebp-0x24h到ebp這一段的內(nèi)存的每個(gè)字節(jié)都初始化為CCCCCCCCh
00CD18BC lea edi,[ebp-24h] //把后面有效的地址加載到前面空間里
00CD18BF mov ecx,9
00CD18C4 mov eax,0CCCCCCCCh /*每一次四個(gè)字節(jié),總共出了*/
00CD18C9 rep stos dword ptr es:[edi] //word是一個(gè)字兩個(gè)字節(jié);dword是兩個(gè)字,四個(gè)字節(jié)。
00CD18CB mov ecx,0CDC008h //把0CDC008h放在ecx里
00CD18D0 call 00CD131B //執(zhí)行 call指令之前先會(huì)把call 指令的下一條指令的地址進(jìn)行壓棧操作
圖示:
3)核心代碼
int a = 3, b = 6,c = 0;//變量a,b,c的創(chuàng)建和初始化,這就是局部的變量的創(chuàng)建和初始化
00CD18D5 mov dword ptr [ebp-8],3
00CD18DC mov dword ptr [ebp-14h],6
00CD18E3 mov dword ptr [ebp-20h],0
c = ADD(a,b);
00CD18EA mov eax,dword ptr [ebp-14h]
00CD18ED push eax
00CD18EE mov ecx,dword ptr [ebp-8]
00CD18F1 push ecx
00CD18F2 call 00CD1217
00CD18F7 add esp,8
00CD18FA mov dword ptr [ebp-20h],eax
1).給變量a、b、c創(chuàng)建初始化
int a = 3, b = 6,c = 0;//變量a,b,c的創(chuàng)建和初始化,這就是局部的變量的創(chuàng)建和初始化
00CD18D5 mov dword ptr [ebp-8],3 //把3放到ebp-8地址里
00CD18DC mov dword ptr [ebp-14h],6 //把6放到ebp-14h里
00CD18E3 mov dword ptr [ebp-20h],0 //把0放到ebp-20h里
圖示:
2).調(diào)用Add函數(shù)
c = ADD(a,b);
00CD18EA mov eax,dword ptr [ebp-14h] //把ebp-14h里的值給eax
00CD18ED push eax //壓棧,壓一個(gè)元素,寄存器eax里壓入ebp-14h里面的值
00CD18EE mov ecx,dword ptr [ebp-8] //把ebp-8里的值給ecx
00CD18F1 push ecx //壓棧,壓一個(gè)元素,寄存器exc里壓入ebp-8里面的值
00CD18F2 call 00CD1217 /*這條指令是去調(diào)用ADD函數(shù),把地址00CD18F7存放到地址00CD18F2里(call指令的下一條指令的地址),按一下F11,進(jìn)入被調(diào)函數(shù)ADD里(地址00CD1217),調(diào)用結(jié)束后,來(lái)到了下一條指令的地址處*/
00CD18F7 add esp,8
00CD18FA mov dword ptr [ebp-20h],eax
圖示:
3).進(jìn)入ADD函數(shù)(在call指令處按F11,然后再按一次F11)
這里我重新進(jìn)入調(diào)試模式,所以地址的位置也就發(fā)生了變化,意思還是不變的。
int main()
{
00C518B0 push ebp
00C518B1 mov ebp,esp
00C518B3 sub esp,0E4h
00C518B9 push ebx
00C518BA push esi
00C518BB push edi
00C518BC lea edi,[ebp-24h]
00C518BF mov ecx,9
00C518C4 mov eax,0CCCCCCCCh
00C518C9 rep stos dword ptr es:[edi]
00C518CB mov ecx,0C5C008h
00C518D0 call 00C5131B
int a = 3, b = 6,c = 0;
00C518D5 mov dword ptr [ebp-8],3
00C518DC mov dword ptr [ebp-14h],6
00C518E3 mov dword ptr [ebp-20h],0
c = ADD(a,b);
00C518EA mov eax,dword ptr [ebp-14h]
00C518ED push eax
00C518EE mov ecx,dword ptr [ebp-8]
00C518F1 push ecx
00C518F2 call 00C51217
00C518F7 add esp,8
00C518FA mov dword ptr [ebp-20h],eax
在按一下F11,進(jìn)入ADD函數(shù)里
4).創(chuàng)建ADD函數(shù)棧幀
5).ADD函數(shù)的執(zhí)行過(guò)程
int z = x + y;
00C51795 mov eax,dword ptr [ebp+8] //把ebp+8里面的值給eax
00C51798 add eax,dword ptr [ebp+0Ch] //eax里面的值加上ebp+0Ch地址里的值
00C5179B mov dword ptr [ebp-8],eax //eax的值放到ebp-8地址里
return z;
00C5179E mov eax,dword ptr [ebp-8] //eax相當(dāng)于全局的寄存器,ebp-8的值放到寄存器里。
如圖:
6),函數(shù)棧幀創(chuàng)建的視圖:
3.函數(shù)棧幀的銷毀
1)ADD函數(shù)棧幀的銷毀
00C517A1 pop edi //在棧頂彈出一個(gè)值,存放到edi中,esp+4
00C517A2 pop esi //在棧頂彈出一個(gè)值,存放到esi中,esp+4
00C517A3 pop ebx //在棧頂彈出一個(gè)值,存放到ebx中,esp+4
00C517A4 add esp,0CCh /*將esp的地址加上0cch,相當(dāng)于回收了ADD函數(shù)的棧幀空間*/
00C517AA cmp ebp,esp //判斷有沒(méi)有溢出
00C517AC call 00C51244 //call指令里放的是下一個(gè)指令的地址
00C517B1 mov esp,ebp //ebp里面的值放到esp里
00C517B3 pop ebp //出棧,彈出一個(gè)元素,dsp+4
00C517B4 ret /*call指令可以實(shí)現(xiàn)調(diào)用一個(gè)子程序,在子程序里使用ret指令,結(jié)束子程序的執(zhí)行并返回主函數(shù),讓主函數(shù)繼續(xù)往下執(zhí)行*/
圖示:
2).ADD函數(shù)棧幀銷毀后,回到主函數(shù):
調(diào)用完ADD函數(shù),回到main函數(shù)的時(shí)候,繼續(xù)往下執(zhí)行,可以看到:
00C518F7 add esp,8 //esp直接+8,相當(dāng)于跳過(guò)了main函數(shù)中壓棧的
00C518FA mov dword ptr [ebp-20h],eax /*將eax中值,存檔到ebp-20h的地址處,其實(shí)就是存儲(chǔ)到main函數(shù)中c變量中,而此時(shí)eax中就是ADD函數(shù)中計(jì)算的x和y的和,可以看出來(lái),本次函數(shù)的返回值是由eax寄存器帶回來(lái)的。程序是在函數(shù)調(diào)用返回之后,在eax中去讀取返回值的。*/
printf("%d\n", c);
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-530912.html
注意:
總結(jié):
1為什么局部變量不初始化內(nèi)容是隨機(jī)的或者是"燙"?
因?yàn)樵趧?chuàng)建函數(shù)棧幀的時(shí)候,中間的地址的值都是不確定的,而如果訪問(wèn)一個(gè)未初始化的變量,指向這些不確定的值,就是隨機(jī)值。而初始化為0CCCCCCCCh時(shí),遇到0xCCCC(兩個(gè)連續(xù)排列的0xCC)的漢字編碼就是“燙”,所以0xCCCC被當(dāng)作文本就是“燙”。
2.函數(shù)調(diào)用時(shí)參數(shù)時(shí)如何傳遞的?傳參的順序是怎樣的?
從創(chuàng)建局部變量的函數(shù)(比如main函數(shù))棧幀中通過(guò)內(nèi)存訪問(wèn),儲(chǔ)存在eax和ecx中再入棧(相當(dāng)于臨時(shí)拷貝)。
3.函數(shù)的形參和實(shí)參分別是怎樣實(shí)例化的?
實(shí)參是在函數(shù)棧幀里通過(guò)ebp內(nèi)存訪問(wèn)儲(chǔ)存的值。形參是由ebp內(nèi)存訪問(wèn)將棧中儲(chǔ)存的臨時(shí)變量。
4.函數(shù)調(diào)用結(jié)束后怎么返回值?
ADD函數(shù)中通過(guò)將在寄存器(eax)中相加得到的9,在移入ADD函數(shù)棧幀中c的地址位置,再將這個(gè)地址位置的值傳給eax,在銷毀ADD函數(shù)棧幀后,將eax中的值傳給main函數(shù)棧幀中創(chuàng)建的c地址位置。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-530912.html
到了這里,關(guān)于C語(yǔ)言——詳解函數(shù)棧幀的創(chuàng)建和銷毀的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!