函式堆疊幀
- 🎂前言
- 🌹堆疊幀的概念
- 💖準備作業
- 😀main函式堆疊幀的創建及初始化
- 😁main函式的被呼叫
- 😂main函式堆疊幀的開辟
- 🤣main函式堆疊幀的初始化
- 👩臨時變數的創建,
- 👨Add函式堆疊幀的創建
- 🧑Add函式堆疊幀的創建
- 👧Add函式堆疊幀的初始化
- 🎈Add函式實作加法運算
- 🧨Add函式回傳值實作
- 🎆Add函式堆疊幀的銷毀
- 🎇回傳到main函式指令
- 🍕🍕總結
🛸🛸文章開始之前,我想對各位提幾個問題,看看你們能答出幾個,看完本文之后,你們又能回答出幾個?
- 區域變數是怎么創建的?
- 為什么區域變數的值是隨機值?
- 函式是怎么傳參的?傳參的順序是怎樣的?
- 形參和實參是什么關系?
- 函式呼叫是怎么做的?
- 函式呼叫結束后是怎么回傳的?
🎂前言
研究的函式: 一個加法函式,
原因:加法函式是比較簡單的函式,實作邏輯比較單一,可以更為清楚的觀察到函式堆疊幀的創建和銷毀,而不是花費更多精力去研究復雜的函式,
#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(a, b);
printf("%d\n", c);
return 0;
}
使用的編譯器: VS2013,
原因:版本過高過新的編譯器在對堆疊幀分配上進行的封裝處理較為完善,我們在學習時不易于看清楚里面的具體步驟,較低版本的編譯器在學習時較為友好,

研究的方法: 圖解,
原因:本文將以畫圖、截圖配上文字解釋加以說明,可以更加直觀的理解函式堆疊幀的分配情況,

🌹堆疊幀的概念
堆疊幀是指為一個函式呼叫單獨分配的那部分堆疊空間, 比如,當運行中的程式呼叫另一個函式時,就要進入一個新的堆疊幀,原來函式的堆疊幀稱為呼叫者的幀,新的堆疊幀稱為當前幀, 被呼叫的函式運行結束后當前幀全部收縮,回到呼叫者的幀,
💖準備作業
- 將代碼編輯在編譯器中,

- 開始除錯并按下滑鼠右鍵,(按下F10)

- 轉到反匯編,

😀main函式堆疊幀的創建及初始化
😁main函式的被呼叫
首先我們需要明確,main()函式也就是我們平時說的主函式,他其實也是需要被其他函式呼叫的,
- 我們先在除錯狀態下打開呼叫堆疊視窗,

顯示如下:

- 接下來我們一直按F10進行除錯,直到主函式return 0被回傳,即可出現以下界面,

往上翻即可找到呼叫main()函式的函式,

也就是說main()函式其實是被一個叫__tmainCRTStartup的函式所呼叫的,
😂main函式堆疊幀的開辟
我們知道,函式和區域變數的開辟是在堆疊上完成的,并且堆疊的使用習慣是先使用高地址,后使用低地址,
假設堆疊空間如下:

我們知道main函式也是被其他函式呼叫的,所以在堆疊上其實還有編譯器為__tmainCRTStartup函式開辟的空間,這一點心中要明確,
接下來我們看反匯編里的匯編指令:

這一部分匯編指令其實就是對main函式的堆疊幀進行開辟,
這里介紹一下大家對指令里陌生的東西:
暫存器:ebp,esp,ebx,esi,edi,ecx,eax等等,
其中我們需要著重記住幾個暫存器的功能,
維護函式堆疊幀的暫存器:
- esp - 存放指向堆疊頂的地址的暫存器,
- ebp - 存放指向堆疊底的地址的暫存器,
初始化函式值的暫存器:
- edi - 用于存放開始進行初始化的地址,
- ecx - 用于存放初始化元素的數量,
- eax - 用于存放將要初始化為什么東西的內容,
下面先給出在執行main函式之前堆疊的情況:

這里是編譯器為__tmainCRTStartup函式開辟的函式堆疊幀,可以看見,ebp暫存器指向堆疊底,esp暫存器指向堆疊頂,以此來維護__tmainCRTStartup函式的函式堆疊幀,
下面我們一一分析main函式的匯編指令操作:

push ebp
指將ebp暫存器中的值進行壓堆疊操作(push),
即在編譯器已為__tmainCRTStartup函式開辟的堆疊幀上面進行壓堆疊,

因為esp是指向堆疊頂的暫存器,所以每次壓堆疊之后esp暫存器所指的位置會上升,反之如果執行pop彈出的操作,esp暫存器所指的位置則會下降,

此時__tmainCRTStartup函式的函式堆疊幀也隨之增加,

下一個操作,

mov ebp,esp
這條操作的指令是將esp的值賦給ebp,
也就是說讓esp里存放所指向的地址賦給ebp,那么ebp所指向的位置將會發生更改,

此時ebp和esp指向了同一位置,
但是注意:
- 此時ebp和esp沒有在維護__tmainCRTStartup函式了,但不代表他的函式堆疊幀被銷毀了,因為堆疊空間的使用只能先銷毀低地址,再銷毀高地址,
- 將來main函式回傳之后,esp暫存器和ebp暫存器還是要回來維護__tmainCRTStartup函式的,這里第一條指令push的ebpc操作就是伏筆,在main函式回傳值后會執行pop這個ebp的操作,直接把ebp暫存器彈向之前存放的位置,也就是__tmainCRTStartup函式的堆疊底,
再下一個操作:

sub esp,0E4h
這里解釋一下,sub就是減法操作,這里0E4h表示十六進制的數字,h為識別符號,所以0E4h其實就是十進制的228,
合起來就是將esp里存放的地址減去0E4h的大小,
因為上面是低地址下面是高地址,所以減去0E4h應該是向上走,

現在ebp和esp所維護的這段空間就是為main函式開辟的函式堆疊幀,

至此,main函式的函式堆疊幀就開辟完成了,
🤣main函式堆疊幀的初始化
我們在寫代碼的時候一定出現過一個問題,就是使用未初始化的變數或內容進行列印,結果控制臺輸出的東西完全是我們意想不到的結果,例如:

為什么這里會出現隨機值呢?
下面就可以給出答案,

下面三條指令全部是push,
push ebx
push esi
push edi

三次push壓堆疊之后,esp的位置自動發生變化,main函式的函式堆疊幀也隨即增大,

接下來:
lea: load effective address(加載有效地址)
lea edi,[ebp - 0E4h]
前面介紹過幾個重要的暫存器:

所以這里的edi是用來存放開始進行初始化的地址的,也就是把【ebp - 0E4h】這個地址放進edi保存起來,

下面兩個操作都是針對初始化用的暫存器的:
mov ecx,39h
mov eax,0CCCCCCCCh
ecx是存放初始化內容的次數的,所以是把39h這個次數存放在暫存器ecx中,
而eax是存放要初始化為的內容的,所以將0CCCCCCCCh存放進eax暫存器中,
接下來的指令就是初始化的關鍵:
rep stos dword ptr es:[edi]
dword的意思是double word - 一個word是兩個位元組,所以一個dword是4個位元組,
整個指令的意思是從edi存放的位置開始往下每四個位元組算一次,重復ecx里存放的值這么多次,把這些內容全部改為eax里存放的值,
也就是從ebp - 0E4h位置開始往下39h個整型的位置全部初始化為0CCCCCCCCh
至此main函式堆疊幀里的內容已被全部初始化,

此時堆疊里的情況:

為了防止有的碼友不相信,這里我們計算一波,
十六進制39h轉換成十進制是57,

57次,一次4個位元組,也就是57乘以4等于228個位元組,
而十六進制0E4h轉化為十進制正號等于228,

所以至此,main函式堆疊幀里的所有內容全部被初始化為0CCCCCCCCh,
👩臨時變數的創建,

這里的指令看的不夠清晰,因為編譯器默認顯示了變數名,這不適合我們學習具體情況,所以我們應該把顯示變數名給勾選掉,


把勾選去掉即可,效果如下:

這里就可以把具體位置看的比較清晰,
move dword ptr [ebp-8],0Ah
十六進制的0Ah轉換成十進制也就是10,這條指令的意思就是將0Ah這個數放進[ebp-8]的位置,
也就是把ebp-8這個位置分配給變數a,將里面的值賦為10,


move dword ptr [ebp-14h],14h
同上,這里的十六進制數字14h轉換為十進制是20,將20放進[ebp-14h]的位置,


move dword ptr [ebp-20],0
同上,將0賦給[ebp-20h]的位置,也就是為c變數開辟空間并賦值,


看起來似乎到了函式呼叫了,但其實不然,呼叫函式之前,先在主調函式內創建實參的臨時拷貝,再進行呼叫函式,接下來一一分析,
move eax,dwor ptr [ebp-14h]
這句指令的意思是將[ebp-14h]位置存放的值賦給eax暫存器,
而我們可以看到:ebp - 14h的位置不就是我們剛剛創建的b變數嗎?
這個操作把實參b的值存放到了暫存器eax中,
下一指令:
push eax
將eax壓堆疊,這里我們記住eax中存放的值就是實參b的值,


move ecx,dword ptr [ebp-8]
push ecx
同上,將[ebp-8]位置的值放在ecx里,之后將ecx壓堆疊,
而ebp-8位置放的就是a變數,

執行到這里,其實就不難看出上面的操作其實是在給Add函式傳參,開辟兩個空間存放實參的臨時拷貝,
注意: 這里傳參的順序是先傳b后傳a,并且是在main函式的堆疊幀內部進行的,
👨Add函式堆疊幀的創建
在創建Add函式的函式堆疊幀之前,編譯器還做了一件事情:

call 00B910E1
乍一看這個指令非常奇怪,但我們將除錯進行下去,直到call指令的時候按F11進行逐陳述句除錯,
會跳轉到這個步驟,

這就是call指令的下一條指令,編譯器把呼叫函式之后的下一條指令存放在堆疊上,將來被呼叫函式回傳之后,便可根據這個地址直接執行呼叫函式后需要執行的指令,
在這一點上就可以體現編譯器對函式堆疊幀的呼叫的嚴謹,
既要考慮到如何呼叫函式分配空間,也要考慮到函式呼叫結束怎么回到本該執行的下一條指令,

之后便開始對Add函式堆疊幀的創建,
🧑Add函式堆疊幀的創建

注意第一個操作:
push ebp
這里把ebp中存放的值進行壓堆疊,也就是說這個位置存放的是原來ebp所指向的位置,

這里給出標記: ebp:main
表示的是這里的ebp存放的是main函式堆疊底的位置,將來在pop這個值得時候將會把ebp直接彈回main函式堆疊底的位置,繼而繼續維護main函式,

接下來的操作和開辟main函式堆疊幀十分相似:
mov ebp,esp
sub esp,0CCh
先將esp指向的位置賦給ebp,這樣ebp就會和esp指向同一個位置,作為即將開辟堆疊幀的堆疊底,
再給esp減去0CCh的值,也就是往上偏移0CCh個位元組長度,十六進制0CCh轉化為十進制為204,這也就是編譯器為Add函式分配的函式堆疊幀的大小,
注: 堆疊幀空間分配是編譯器自行決定的,無法人為估測,
此時ebp到esp之間的部分就是編譯器為Add函式分配的函式堆疊幀,

👧Add函式堆疊幀的初始化
和main函式一樣,在函式堆疊幀創建完畢之后,會通過三個暫存器對函式堆疊幀
初始化,這里再次把暫存器作用給大家展示:


首先進行三個push指令
push ebx
push esi
push edi


前面介紹過:
lea:load effective address(加載有效地址)
lea edi,[ebp+FFFFFF34h]
move ecx,33h
move eax,0CCCCCCCCh
指令的意思是:
- 將[ebp+FFFFFF34]地址加載到暫存器edi中,
- 將33h作為次數放進暫存器ecx中,
- 將0CCCCCCCCh作為要初始化為的內容存放在eax中,
而FFFFFF34的二進制序列是:11111111111111111111111100110100
顯然,這是一個負數,所有ebp+FFFFFF34其實他的地址是在減小,所以此時存放的位置其實是可以計算得到的,

十六進制數33h的十進制形式為51,也就是要重復進行51次值覆寫,
覆寫值為0CCCCCCCCh,

rep stos dword ptr es:[edi]
最后的指令就是從edi暫存器放的位置開始往下33h次進行值覆寫,覆寫內容為0CCCCCCCCh,

🎈Add函式實作加法運算

mov dword ptr [edp-8],0
這是開辟臨時變數的步驟,
將0賦給edp-8的位置,也就是給變數z開辟了一塊空間,


mov eax,dword ptr [ebp+8]
將ebp+8位置的值放進暫存器eax中保存,
可以從圖上看到,ebp+8的位置時從main函式傳過來的實參臨時拷貝中的10,

此時
eax: 10
add eax,dword ptr [ebp+0Ch]
這里我們可以計算,0Ch轉換為十進制也就是12,所有ebp+12應該是從當前ebp位置往下數3個格子(因為一個格子是4個位元組),
找到ebp+0Ch的位置:

將這里面的值加到暫存器eax中去:
此時
eax: 30

mov dword ptr [ebp-8],eax
把暫存器eax里的值放進[ebp-8]的位置里,

此時就已經完成了計算功能,
🧨Add函式回傳值實作
函式功能實作之后,就要回傳函式值了,

mov eax,dword ptr [ebp-8]
指令:將[ebp-8]地址的值放進eax暫存器,也就是把剛才計算結果30存放進暫存器中,
🎆Add函式堆疊幀的銷毀
pop是出堆疊指令,把堆疊中的值彈出到指定的地方,
pop edi
pop esi
pop ebx
連續三個pop,將之前初始化Add函式是壓堆疊的三個元素彈出,

三個元素出堆疊后,Add函式的堆疊幀隨之減少,esp所指向的位置也隨機發生更改,

mov esp,ebp
和創建函式堆疊幀時的操作類似,但又不同,將ebp的值賦給esp,
即esp將會直接指向ebp指向的位置,

一旦執行完上面的操作指令,也就意味著esp,ebp兩個暫存器不再維護Add函式堆疊幀了,開辟的空間將全部返還給作業系統,


pop ebp
將堆疊頂的元素彈出到ebp位置,
注意看這里的堆疊頂所放元素:
前文中已經提到過,這里壓堆疊ebp的用途,就是為了在銷毀Add函式之后ebp可以找到main函式堆疊底位置,繼而繼續維護main函式的堆疊幀,

所以這條指令將使ebp指向原main函式堆疊底,

此時Add函式堆疊幀已全部銷毀,
🎇回傳到main函式指令
接下來esp就指向了00B910E1,
前文提到過,這是call呼叫指令的下一條指令,所以直接回傳到main函式的下一條指令,

現在在反匯編除錯按F10將直接從Add函式跳轉到main函式call指令的下一條指令,

add esp,8
把8加給esp暫存器,讓其向下移動兩個單元格(一個格子4個位元組)

此時堆疊頂兩個元素就不再被維護,main函式的函式堆疊幀也隨之減少,

mov dword ptr [ebp-20h],eax
將eax暫存器里的值賦給ebp-20h位置,eax是我們在Add函式里計算結束后存放的回傳值(30),ebp-20的位置時變數c的地址,

至此,Add函式的功能,堆疊幀開辟到結束就全部解釋完畢了,
🍕🍕總結
函式堆疊幀用到匯編語言的知識,用最底層的角度看待函式呼叫的關系,
文章開頭的幾個問題其實在閱讀到這的時候應該都能夠解決了,
請注意:搞清楚函式堆疊幀并不能讓你寫代碼更厲害,刷演算法更牛逼,函式堆疊幀僅僅是類似于修煉內功一樣的存在,理清楚底層的邏輯有助于我們思考一些比較復雜的問題,
例如遞回演算法,用函式堆疊幀的思想就很容易掌握,
最后,別忘了👍點贊👍+?收藏?+👀關注👀走一波~
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/305504.html
標籤:其他
上一篇:C++認知繼承
