C語(yǔ)言函數(shù)棧幀詳解
前言
在c語(yǔ)言中我們會(huì)將一些功能單獨(dú)寫(xiě)成一個(gè)函數(shù),以供主函數(shù)調(diào)用,在表面來(lái)看調(diào)用的過(guò)程就是寫(xiě)出一個(gè)函數(shù)后,只需要在調(diào)用時(shí)中通過(guò)函數(shù)名將實(shí)參傳給形參就實(shí)現(xiàn)了整個(gè)過(guò)程,但實(shí)際上調(diào)用的過(guò)程遠(yuǎn)比你想的復(fù)雜,這其中函數(shù)棧幀起著關(guān)鍵作用。通過(guò)本篇文章,我將告訴你函數(shù)在調(diào)用時(shí)計(jì)算機(jī)內(nèi)究竟發(fā)生了什么?
一.函數(shù)棧幀是什么?
C語(yǔ)言中,每個(gè)棧幀對(duì)應(yīng)著一個(gè)未運(yùn)行完的函數(shù)。棧幀中保存了該函數(shù)的返回地址和局部變量。(來(lái)自百度百科)。
通過(guò)這句話我們可以提煉出兩個(gè)關(guān)鍵信息:
1.每個(gè)未運(yùn)行完的函數(shù)都有一個(gè)對(duì)應(yīng)的棧幀
2.棧幀保存了函數(shù)的返回地址和局部變量
先對(duì)棧幀有一個(gè)簡(jiǎn)單的概念,知道其主要作用是什么就行。
二、棧幀準(zhǔn)備知識(shí)
由于函數(shù)棧幀不光涉及c語(yǔ)言代碼知識(shí),如果是小白一定要認(rèn)真看本節(jié),這對(duì)幫助你理解棧幀非常重要,也不要因?yàn)榘l(fā)現(xiàn)這些知識(shí)你之前完全沒(méi)聽(tīng)說(shuō)過(guò)就產(chǎn)生畏難心理,事實(shí)上,我們只需要掌握期其中一些非常關(guān)鍵的地方就足夠了。
1.內(nèi)存分區(qū)
內(nèi)存中主要分為棧區(qū),堆區(qū),靜態(tài)區(qū),以及其他部分。
棧區(qū):由高地址往低地址增長(zhǎng),主要用來(lái)存放局部變量,函數(shù)調(diào)用開(kāi)辟的空間,與堆共享一段空間。(本篇重點(diǎn))
堆區(qū):由地地址向高地址增長(zhǎng),動(dòng)態(tài)開(kāi)辟的空間就在這里(malloc,realloc,calloc,free),與棧共享一段空間。
靜態(tài)區(qū):主要存放全局變量和靜態(tài)變量。

2.什么是棧?
前面已經(jīng)知道棧中存放了函數(shù)調(diào)用開(kāi)辟的空間即棧幀,因此我們要明白什么是棧幀,必須先知道什么是棧。
棧是一種數(shù)據(jù)結(jié)構(gòu),是一種只能在一端進(jìn)行插入和刪除操作的特殊線性表。它按照先進(jìn)后出的原則存儲(chǔ)數(shù)據(jù),先進(jìn)入的數(shù)據(jù)被壓入棧底,最后的數(shù)據(jù)在棧頂,需要讀數(shù)據(jù)的時(shí)候從棧頂開(kāi)始彈出數(shù)據(jù)(最后放入的數(shù)據(jù)被最先讀出來(lái))。
簡(jiǎn)單來(lái)講你可以把棧理解為一個(gè)彈夾,而我們放的數(shù)據(jù)就像子彈,當(dāng)我們射子彈時(shí),總是會(huì)把后壓入的彈先射出去,因?yàn)楹髩喝氲膹椧欢ㄊ欠旁谧钌厦娴模葔喝氲膹椇笊涑鋈?,因?yàn)橄葔喝氲膹椩谧钕旅妗_@就是棧最大的特點(diǎn)"先入后出,后入先出",而往棧中放數(shù)據(jù)我們稱(chēng)作壓棧(push),拿出棧中的數(shù)據(jù)我們叫出棧(pop)。
壓棧(push):

出棧(pop):

3.esp,ebp,eax寄存器
| ebp | ebp是基址指針,保存調(diào)用者函數(shù)的地址,總是指向當(dāng)前棧幀棧底 |
| esp | esp是被調(diào)函數(shù)指針,總指向函數(shù)棧棧頂 |
| eax | 累加器,用來(lái)乘除法,與函數(shù)返回值(本篇主要關(guān)注第二個(gè)功能) |
簡(jiǎn)單來(lái)講就是esp和ebp是兩個(gè)指針,ebp指向當(dāng)前棧幀棧底,esp指向函數(shù)棧棧頂。

能看到,ebp并不是指向整個(gè)函數(shù)棧的棧底,而是指向當(dāng)前棧幀的棧底,而由于esp總是指向棧頂,且棧只允許一個(gè)方向的操作,因此esp指向其實(shí)也是當(dāng)前棧幀的棧頂,不過(guò)當(dāng)前棧幀的棧頂始終與棧頂相同,因此說(shuō)esp指向的是棧頂。
三、詳解棧幀創(chuàng)建與銷(xiāo)毀全過(guò)程
有了以上知識(shí)就能夠初步理解棧幀從創(chuàng)建到銷(xiāo)毀的全過(guò)程了,接下來(lái)我會(huì)一步一步講解
假設(shè)我們有當(dāng)前代碼:
#include<stdio.h>
int add(int a, int b)
{
int c = 0;
c = a + b;
return c;
}
int main()
{
int a = 1;
int b = 1;
int sum;
sum = add(a, b);
return 0;
}
調(diào)用函數(shù)之前:
此時(shí)我們準(zhǔn)備執(zhí)行函數(shù)調(diào)用"sum = add(a,b);"此時(shí)棧中如下:

將傳入函數(shù)的值放入棧中
由于函數(shù)調(diào)用涉及到傳參,因此我們?cè)谡{(diào)用函數(shù)之前,需要先將傳入的參數(shù)保存,以方便函數(shù)的調(diào)用,因此需要將add函數(shù)的a=1,b=2,push入棧保存

函數(shù)執(zhí)行:
1.保護(hù)當(dāng)前ebp
由于我們馬上要?jiǎng)?chuàng)建新的棧幀空間,因此ebp和esp都得將變動(dòng),為了能夠讓我們調(diào)用完add函數(shù)后還能讓ebp回到當(dāng)前位置我們需要對(duì)ebp的值進(jìn)行保護(hù),即將此時(shí)ebp的值壓入棧(至于為什么不需要保護(hù)esp,看到后面你就能明白)

2.創(chuàng)建所需調(diào)用函數(shù)的棧幀空間
令ebp指向當(dāng)前esp的位置并根據(jù)add函數(shù)的參數(shù)個(gè)數(shù),創(chuàng)建一個(gè)大小合適的空間。
① ebp指向當(dāng)前esp的位置

②創(chuàng)建空間

3.保存局部變量
將add函數(shù)中創(chuàng)建的變量"int c = 0"放入剛剛開(kāi)辟的棧幀空間中

4.參數(shù)運(yùn)算
根據(jù)形參與局部變量,進(jìn)行對(duì)應(yīng)的運(yùn)算,這里執(zhí)行"c = a +b", 得到 c = 2,放入剛才c對(duì)應(yīng)的位置。

到次函數(shù)執(zhí)行就完成了,接下來(lái)就要開(kāi)始實(shí)現(xiàn)函數(shù)返回
函數(shù)返回:
1.存儲(chǔ)返回值
現(xiàn)在我們已經(jīng)達(dá)成了目的"add(a,b)",要將之前創(chuàng)建的add的函數(shù)棧銷(xiāo)毀,以使得我們能夠回到main函數(shù)中正常執(zhí)行,而在銷(xiāo)毀add的函數(shù)棧幀前我們的main函數(shù)可還沒(méi)有拿到運(yùn)算結(jié)果,因此我們需要先將需要返回的值存儲(chǔ)起來(lái),存儲(chǔ)的位置就是前面提到的eax寄存器,這里"return c",我們將c的值放到eax寄存器中。

2.銷(xiāo)毀空間
拿到了運(yùn)算結(jié)果后,我們就沒(méi)有任何任何顧慮了,可以直接銷(xiāo)毀函數(shù)的棧楨空間了。

3.ebp回上一棧幀棧底
此時(shí)ebp拿到之間存儲(chǔ)的上一棧幀棧底的值,回到相應(yīng)的位置,于此同時(shí),存儲(chǔ)的ebp沒(méi)有用了,也將被銷(xiāo)毀。

4.銷(xiāo)毀形參
形參也不再有用,因此也隨即銷(xiāo)毀。(這里也讓我們明白:由于形參在調(diào)用完函數(shù)后就會(huì)銷(xiāo)毀,且與實(shí)參根本不是同一地址,因此形參的改變無(wú)法影響實(shí)參。)

5.main函數(shù)拿到返回值
在講解main函數(shù)怎么拿到返回值前,我想先問(wèn)一個(gè)問(wèn)題:
上圖中所謂的前一棧幀指的是什么?
大家都知道,我們編寫(xiě)的c程序都是從一個(gè)main函數(shù)開(kāi)始的,實(shí)際上,代碼并不是直接從main函數(shù)開(kāi)始運(yùn)行的,main函數(shù)的本質(zhì)也是一個(gè)被其他代碼調(diào)用的函數(shù),至于被誰(shuí)調(diào)用,這里就不展開(kāi)講解了,這里提出這個(gè)問(wèn)題是想要大家知道:
main函數(shù)是一個(gè)函數(shù),它有自己的棧幀。
因此所謂的前一棧幀實(shí)際上就是調(diào)用add函數(shù)的main函數(shù)的棧幀。
因此我們要讓main函數(shù)拿到返回值,只需要把eax寄存器中的值放入main棧幀中sum對(duì)應(yīng)的位置就行。(這里也能讓我們明白:由于我們只有一個(gè)eax寄存器,因此c語(yǔ)言的函數(shù)只能有一個(gè)返回值。)

至此棧幀的創(chuàng)建與銷(xiāo)毀結(jié)束,函數(shù)調(diào)用完成。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來(lái)幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
C++實(shí)現(xiàn)編碼轉(zhuǎn)換的示例代碼
這篇文章主要介紹了C++實(shí)現(xiàn)編碼轉(zhuǎn)換的示例代碼,幫助大家快捷的實(shí)現(xiàn)編碼轉(zhuǎn)換,感興趣的朋友可以了解下2020-08-08
C語(yǔ)言的數(shù)字游戲算法效率問(wèn)題探討實(shí)例
這篇文章主要介紹了C語(yǔ)言的數(shù)字游戲算法效率問(wèn)題探討實(shí)例,需要的朋友可以參考下2014-04-04
詳解state狀態(tài)模式及在C++設(shè)計(jì)模式編程中的使用實(shí)例
這篇文章主要介紹了state狀態(tài)模式及在C++設(shè)計(jì)模式編程中的使用實(shí)例,在設(shè)計(jì)模式中策略用來(lái)處理算法變化,而狀態(tài)則是透明地處理狀態(tài)變化,需要的朋友可以參考下2016-03-03
C++?OpenCV紅綠燈檢測(cè)Demo實(shí)現(xiàn)詳解
OpenCV(Open Source Computer Vision Library)是開(kāi)源的計(jì)算機(jī)視覺(jué)和機(jī)器學(xué)習(xí)庫(kù),提供了C++、 C、 Python、 Java接口,并支持Windows、 Linux、 Android、 Mac OS平臺(tái),下面這篇文章主要給大家介紹了關(guān)于C++?OpenCV紅綠燈檢測(cè)Demo實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2022-11-11
簡(jiǎn)述C語(yǔ)言中system()函數(shù)與vfork()函數(shù)的使用方法
這篇文章主要介紹了簡(jiǎn)述C語(yǔ)言中system()函數(shù)與vfork()函數(shù)的使用方法,是C語(yǔ)言入門(mén)學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-08-08
優(yōu)先隊(duì)列(priority_queue)的C語(yǔ)言實(shí)現(xiàn)代碼
本文簡(jiǎn)要介紹一種基于數(shù)組二叉堆實(shí)現(xiàn)的優(yōu)先隊(duì)列,定義的數(shù)據(jù)結(jié)構(gòu)和實(shí)現(xiàn)的函數(shù)接口說(shuō)明如下2013-10-10
Qt實(shí)現(xiàn)數(shù)據(jù)導(dǎo)出到xls的示例代碼
導(dǎo)入導(dǎo)出數(shù)據(jù)到csv由于語(yǔ)法簡(jiǎn)單,適用場(chǎng)景有限,于是本文將為大家介紹Qt如何實(shí)現(xiàn)導(dǎo)出數(shù)據(jù)到xls,感興趣的小伙伴可以跟隨小編一起試一試2022-01-01

