前言
我們都知道函式是在堆疊區開辟空間的,但你是否知道:
- 函式堆疊幀是什么?
- 函式是如何在堆疊區開辟空間的呢?
- 函式的實參是如何傳參的?傳參的順序如何?
- 為什么函式形參無法改變外部的變數?
- 為什么說實參是形參的一份臨時拷貝? 形參和實參的關系又是什么?
- 函式是怎么呼叫的?
- 函式呼叫是如何回傳值的?
- 區域變數是如何在堆疊開辟空間的?
- 為什么說區域變數的值是隨機的?
這篇文章都已給你解答這些問題,接下來跟著我一起走下去,讓我們一起進入函式是如何在堆疊區中玩耍的旅途,我們慢慢來回倒這個問題
🦻🦻🦻注意:測驗環境是在vs2013編譯器下的,其他編譯器和本編譯器可能會有略微資料或者界面等差異,但是大體邏輯上是沒有變的,
文章目錄
- 前言
- main函式也是被呼叫的
- 函式堆疊幀
- 函式的創建呼叫程序
- 函式開始銷毀的程序
- 回答開頭的問題
- 一個還沒開始銷毀函式空間的記憶體布局圖
簡單的小常識:
👀:eax,ebx,ecx,edx,這些暫存器都是用來存放資料的;可以存放地址,可以存放值,
👀:esp 是存放堆疊頂指標,ebp是存放堆疊底指標,
👀:堆疊區的使用特點是:從高地址向低地址使用,并且當壓堆疊時候,暫存器essp–;彈堆疊時候,esp++;
main函式也是被呼叫的
我們寫的程式都是從main函式入口的,程式的開始也是從main函式入口進去,但是main函式其實也是被別的函式呼叫起來的,我們來看看具體代碼,是誰呼叫了main函式;
操作步驟:
- 在vs2013按下 F10,開始逐步除錯,進入主函式,然后,按下圖點擊進入呼叫堆疊的選項:

然后觀察那個黃色的小箭頭,就是除錯的按鈕,一直按F10,直到黃色小箭頭到達右花括號 },這說明,main函式執行完畢,也就是說明,main函式被呼叫完畢,但是被誰呼叫的呢?

我們再繼續按F10,此時后,我們就會跳轉到這個頁面:這個頁面就顯示著是main函式被呼叫了,在呼叫堆疊我們可以看到,這個main函式是被一個名為:__tmainCRTStartup() ;的函式呼叫,

其實繼續下去,還可以繼續看到__tmainCRTStartup()函式也是被呼叫的,但是我么那就看下去了,這里我是想要說明什么問題呢?
在main函式被呼叫時候,我們是在堆疊空間開辟一段空間,并且用,esp指標和ebp指標,指向這段空間給main函式去使用;
函式堆疊幀
👀:什么是函式堆疊幀呢?
在上面我們說到,main函式被呼叫時候,會在堆疊開辟一段空間,并且這段空間是由兩個指標,esp和ebp來維護的,被維護的這段空間,我們就叫做main函式的堆疊幀;就是可以給main函式去自由使用的空間,
如下圖:

esp 和 ebp兩個指標,是哪個函式正在被呼叫,就用來維護哪一段空間
👀并且發現了嗎? 堆疊頂指標esp是在低地址,堆疊底指標ebp是在高地址,當我們壓堆疊時候,也是從高地址往低地址壓堆疊,
函式的創建呼叫程序
接下來我以一段簡單的加法函式來講解函式的創建程序:
# 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(10, 20);
return 0;
}
我們知道,當我們開始呼叫代碼時候,從main函式入口進去,一旦進入main函式,就為main函式開辟了一段空間,用堆疊頂和堆疊頂指標去維護這段main函式的空間,一旦當程式走到,int c = Add(10,20);時候,Add函式被呼叫起來,一旦進入到Add函式,就會在堆疊區就用堆疊頂著堆疊底指標去指向這一段空間,用來維護Add函式,
大概是這個程序:

維護的細節到底是怎么樣的?讓我們來看看吧,
首先先按F10開始除錯,然后,在一個空白的界面右擊滑鼠,點擊轉到反匯編:

然后就會彈出一個反匯編的界面,這個界面是程式對應的匯編代碼:
如下圖:

不要被匯編代碼嚇走了,你看到這里,我可以保證你看的懂,就很簡單的匯編指令,不過你現在包滑鼠放到反匯編界面,右擊滑鼠,把那個顯示符號名的選項去掉,這是為了看到偏移地址的位置,不看變數名,

去掉之后,帶你簡單熟悉以下匯編的界面:
大概布局就是這個模樣,至于指令是什么功能接下來一步一步的分析;

接下來我把機器碼的選項勾走不要,因為和接下來講的內容關系不大;
在呼叫main函式之前,也就是我們即將進去main函式的時候,我們是不是說了,有一個函式會呼叫main函式呀,同時這個__tmainCRTStartup()函式,也會在記憶體開辟一段空間,用esp和ebp維護:
在記憶體的布局如下:

接下來我們看main函式的匯編代碼:
push ebp,這是什么什么意思呢?就是把ebp的值壓入堆疊中,壓到哪兒呢?壓到堆疊頂,同時,esp指標會向上移動一位,
同時注意:不要以為壓堆疊了,就把ebp的指向給改變了,只是把一個ebp的值壓堆疊而已;
如下圖:

我們監視看看還沒有執行push ebp時候 esp 和 ebp的值是多少,這個值,也就是維護__tmainCRTStartup()函式的堆疊指標,

一旦我們按F10也就是執行了push ebp操作,你看esp的值發生了變化,并且,這個值是變小的,這說明壓堆疊進去了,且小了4個位元組

如果你還是覺得不像壓堆疊了,那我們從記憶體角度看看,esp指標指向的位置,里面存放的值是否和ebp相等,如果相等這就說明了一個問題:我們執行push ebp操作確實是把 ebp壓堆疊進去了,

好,理解了上面的push ebp,接下來就是 mov ebp,esp這是什么意思呢?就是mov 指令把 esp的值移動到,存放到ebp暫存器里面,那我們進一步思考 把esp值給ebp,而且esp和ebp又是堆疊指標,這類操作不就是像 ebp = esp嗎,這說明什么問題?這就說明 ebp指標指向了esp指標的記憶體空間啊.

所以我們從監視看,按下F10,即一旦執行 mov ebp,esp操作,就會看到ebp的值和esp值相等,這里的值是地址,

好的,我們繼續,這時候接下來的指令是 sub esp,0E4h ,這又是怎么理解sub這個指令?就是用esp暫存器的值減去0E4h,之后賦值給esp ,等價這條陳述句:esp = esp - 0E4h; 這個0E4h就是表示十六進制的數字;
那這條指令的作用在記憶體發生了什么?esp是一個指標,當指標減去一個數時候,就會發生偏移到這個esp-0E4h地址,也就是說,在記憶體中,這個堆疊底指標esp指向了 esp-0E4h這個記憶體地址;由于地址是減小了,所以esp指向了更低的地址,在記憶體的大概圖如下:

我們回頭看一看,是不是突然發現ebp和esp不再維護__tmainCRTStartup()函式的空間地址了?
使得就是不在維護了,那維護了誰?還記得是我們剛剛是從什么函式入口的嗎?對,就是main函式入口的,這就是esp和ebp維護的新空間,即main函式的堆疊幀;
這就是傳說中的當呼叫函式時候,就會在堆疊空間開辟一段記憶體空間,
當我們按了F10,觀察監視esp的值,會變小,變小了多少呢?就是小了0E4h的值;

我們從記憶體角度看看,esp指向的地方和ebp指向的地方之間就是main函式堆疊幀空間,那么在記憶體中到底有多少呢?只要我們找到esp和ebp對應的指標地址,就可以知道它們之間的地址了,

你有沒有發現,哇哦,好長啊,居然可以為一個main函式開辟那么多空間,嗯,確實,我也覺得好長,
接下來就繼續指向壓堆疊了,看匯編指令push ebx push esi push edi,就是往堆疊頂壓堆疊,先不用管這是什么意思,,只需知道,在堆疊空間,ebx esi edi這個三個暫存器的值依次被壓入堆疊了,同時不要忘記堆疊頂指標esp也會跟著指到堆疊頂,

繼續按F10,按3下,把剛剛三條指令指向完,觀察esp堆疊頂指標的值,變化了12個位元組:

接下到了 lea edi,[ebp+FFFFF1Ch]這個操作,lea是加載有效地址的意思,把 ebp+FFFFFF1Ch的地址加載到edi暫存器里面,為什么說是加載地址呢?因為這里有個 中括號[ ],他的意思就是把中括號[ ]里面值當作地址;那ebp+FFFFFF1Ch到底是什么地址?我們打開反匯編的加載符號選項,可以看到,ebp+FFFFFF1Ch就是 ebp-0e4h的地址.

那接下來來到了這幾條指令 mov ecx,39h mov eax,0CCCCCCCCh rep stos dword ptr es:[edi] ,這里有三條指令,我們不需要理解具體是干什么的,我們只需要理解這三條指令合起來就是做一件事情:
把 ebp-0E4h的地址到ebp的地址 ,這個范圍地址空間里面的每4個位元組的值賦值為0CCCCCCCCh,為什么是4個位元組,因為在rep stos dword ptr es:[edi]這條指令中,有個dword表示double word 就是4個位元組的意思,一個 word就是兩個位元組;
在記憶體布局的模樣就是大概這個樣子:

下面開始指向,main函式里面代碼了,來到int a= 10;即來到匯編指令: mov dword ptr [ebp-8],0Ah,意識就是把 0Ah放到ebp-8的地址空間,看看我們的記憶體布局 ebp在堆疊底,那么很容易就知道ebp-8在哪里了:
注意記憶體布局圖:里賣弄 ebp-8的位置值修改成為了 0Ah

繼續接下來的指令,一模一樣也是 指向int b = 20; mov dword ptr [ebp-14h],14h int c= 0; mov dword ptr [ebp-20h],0都是把 值放入對應的位置,在記憶體布局如下:
找到 ebp-14h ebp-20h這兩個記憶體地址就行:
到這里,你思考一下:你有沒有發現無形之中,你好像看到了區域變數在記憶體是如何存放在堆疊空間的了,
好了執行完后,你會來到這個 c = Add(10,20); 這是什么?這是函式呼叫,我們知道函式呼叫要傳參,來看看是如何如何傳參的,你看我為啥先說傳參?因為匯編指令就是這么玩的,為什么我不說函式呼叫為函式開辟堆疊空間?因為匯編就是先傳參,
如何傳參?
首先,push 14h 和 push 0Ah,就是壓堆疊,同時esp指標往上移動,先傳了20,在傳10,你有沒有發現,傳參的順序是從括號()的右邊往左邊傳

接下來到了 call 004E10E1 這個操作,這個操作是干什么的?call指令就是開始真正的呼叫 Add函式了,并且這個call指令還會把下條指令的地址進行壓堆疊;

在記憶體中,我們只要觀察 esp指標指向的值是否為 call指令的下一條指令即可,

呼叫函式時候,我們按F11;此時就是執行了call 004E10E1;此時就進入了Add函式,你仔細觀察一下,前面的匯編指令
004E13C0 push ebp
004E13C1 mov ebp,esp
004E13C3 sub esp,0CCh
004E13C9 push ebx
004E13CA push esi
004E13CB push edi
這操作是不是和main函式呼叫時候,很像?對就是很像,那你說這幾段匯編代碼做了什么?哎,就是給我們的Add函式開辟堆疊空間,開辟我們Add的堆疊幀
下面那幾條就是在Add函式的空間內,存放隨機值,

在記憶體布長這個模樣

此時,到了 int z = 0;這個代碼對應匯編就是 mov dword ptr [ebp-8],0,也就是在 ebp-8的位置存放 0 值,在記憶體布局中:

接下來到 z = x +y;這段代碼對應匯編:
z = x + y;
004E13E5 mov eax,dword ptr [ebp+8]
004E13E8 add eax,dword ptr [ebp+0Ch]
004E13EB mov dword ptr [ebp-8],eax
先看: mov eax,dword ptr [ebp+8] :
把ebp+8的地址指向的值放入 eax暫存器中,你看, ebp+8在哪兒?
是不是存放的剛好時我們在main函式堆疊幀里面值 014h;也就是20
add eax,dword ptr [ebp+0Ch] :
這個也是 add指令把 ebp+och地址里面的值,也在main函式的堆疊幀中,也就是10,加到eax里面,并賦值給eax,那么 eax就變成了 30;
mov dword ptr [ebp-8],eax:
最后把eax里面的30 放入到 ebp-8的位置,那ebp-8的位置就是變數 z的位置啊;
到此這個z = x +y;就是執行完了;
在記憶體布局時這樣的

到這里,你思考一下:是否:在 Add函式使用的變數 不是在 自己Add函式里面的,而是在main函式的堆疊幀里面的壓進去的值,也就是說在呼叫Add函式的時候,我們先把實參壓堆疊,此時,這個實參就是形參的一份拷貝了,你往記憶體布局下面看,你就發現,實參還是在函式的堆疊幀里面,根本沒有和形參發生真正的使用,這就是說實參為什么說就是形參的一份拷貝了,
接下來就到return z;這個陳述句了, 那對應的匯編: mov eax,dword ptr [ebp-8]
把 ebp -8的值放入到eax中,這個時候,并沒有發生什么函式呼叫完了就回傳的操作,只是把值放入到eax暫存器中,但是函式呼叫完后,就回傳到主函式把return陳述句的值帶回去是有錯的嗎?并沒有,還有的是,函式回傳了,不是說,區域變數都銷毀了嘛,那怎么還可以把區域變數的值帶回去,匯編代碼告訴你,在函式回傳的時候,是先把return z中的區域變數先保存起來,到一個暫存器先,等函式執行結束后,才開始回傳,
函式開始銷毀的程序
這時候當Add函式執行完后,就開始銷毀空間了,看看接下來的彈堆疊的匯編代碼
pop edi
pop esi
pop ebx
一旦執行了這三句匯編,就會從堆疊頂開始彈堆疊,此時esp++;如下圖;

之后來到這句匯編:
mov esp,ebp
這個意思就是esp指向了ebp,此時就發生了一個重大的事情,一旦esp指向了ebp,這就說明,這段空間的堆疊幀沒了,也就是說Add函式被銷毀了,你發現了嗎?在函式被銷毀的時候,我們的return z 還是沒有回傳啊,不用擔心,因為z已經被保存到暫存器eax里面了,及時函式的空間沒有了,eax里面的值還是存在的;那什么時候回傳,別急,往下看;

繼續看 pop ebp:這個匯編指令就很厲害了,當我們 pop ebp時候,
ebp就不指向剛剛的空間了,而是回到了main函式原來是ebp所指向的空間,我們要知道 ebp指向的那段空間里面存放的就是 main函式的地址,并且,esp也會++;
所以記憶體布局是這個樣子

之后來到 ret 這條指令,這條指令的意思是 回傳到堆疊頂元素存放值地址指令那里,同時彈出堆疊頂元素:
此時esp++的位置

那我們看現在堆疊頂就是 call指令得下一條指令,還記得是什么嘛?就是執行完函式得時候時候,即Add(int x,int y);j就會回到主調函式 c = Add(10,20);此時,我們并沒有執行完這條陳述句
所以你就可以思考:為什么剛剛我要把 call指令下一條指令壓堆疊呢?就是為了還能回到主調函式函式;
之后就來到了 add esp 8;這個意思就是esp+8 賦值給esp,所以 esp指向了下面8個位元組的位置,這也說明 形參 x, y 空間銷毀;
esp的指向如下圖

接下來到 :mov dword ptr [ebp-20h],eax ,這個就強了,到這里才是把 eax的值 放到 ebp-20h的地方去,ebp_20h就是 變數c的值,此時,這算終于把Add函式回傳的 z給帶回來了,

啊,這里終于解釋完了,add函式的創建和銷毀了,那么main函式的銷毀機制也是一樣的我就不過多解釋了,你們可以試一試,
回答開頭的問題
接下來我們終于看完了,來回答一下開頭的問題吧:測一測自己,
- 函式堆疊幀是什么?
函式堆疊幀就是esp和ebp維護的空間,這段空間可以共供給函式使用,一旦esp和ebp發生變化,指向另一段新的空間時候,這個就發生了函式呼叫,此時 esp 和ebp又會重新維護那一段空間,總的來說:函式堆疊幀就是函式能夠利用的空間范圍,并且由esp和ebp指標維護;
- 函式是如何在堆疊區開辟空間的呢?
函式在堆疊區開辟空間,一旦進入函式體,首先做的事情是給函式設定堆疊指標,用來維護堆疊空間,同時會給堆疊空間賦值 0CCCCCCCCh,即給維護的空間賦值為隨機值;并且我們知道堆疊的使用是從高地址向低地址使用的;
- 函式的實參是如何傳參的?傳參的順序如何?
函式傳參是先碰到呼叫函式,先給形實參壓堆疊,壓堆疊的時候,還是在主調函式體里面,壓堆疊的順序是從實參串列的右邊到左邊依次入堆疊;
當進入到函式體內部時候,形參是使用主調函式體里面的實參資料;這就是實參傳參給形參,這也說明了,實參是形參的一份臨時拷貝,
- 為什么函式形參無法改變外部的變數?
因為我們在呼叫函式的時候,是給實參壓堆疊一份資料進去堆疊頂的,形參使用的資料就是在呼叫函式中實參的資料,所以說,形參改變不了外面函式的變數
- 為什么說實參是形參的一份臨時拷貝? 形參和實參的關系又是什么?
這個參考上面的兩個問題;
- 函式是怎么呼叫的?
函式的呼叫,首先,會給實參壓堆疊,其次會保存呼叫函式體執行結束后的下一條指令的地址,然后給函式設定堆疊指標,用來維護函式的空間,同時,為函式的賦值為隨機值,這個時候,執行函式體的代碼,如果函式執行有回傳值,先把回傳值放到一個暫存器臨時保存,然后函式體執行結束,銷毀了函式的堆疊空間,回到主調函式的呼叫函式中,此時銷毀形參,同時把回傳值帶回來,
- 函式呼叫是如何回傳值的?
參考上面的回答
- 區域變數是如何在堆疊開辟空間的?
區域變數是以壓堆疊的形式創建的,并且,創建時從高地址向低地址使用空間,在創建區域變數的時候,函式堆疊空間的地址已經時隨機值了,此時當你創建區域變數時候,得到的也是隨值
- 為什么說區域變數的值是隨機的?
參考上面的回答;
一個還沒開始銷毀函式空間的記憶體布局圖

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/292838.html
標籤:其他
