C語言超詳細講解函數(shù)棧幀的創(chuàng)建和銷毀
1、本節(jié)目標
C語言絕命七連問,你能回答出幾個?
- 局部變量是如何創(chuàng)建的?
- 為什么局部變量不初始化其內(nèi)容是隨機的?
- 有些時候屏幕上輸出的"燙燙燙"是怎么來的?
- 函數(shù)調(diào)用時參數(shù)時如何傳遞的?傳參的順序是怎樣的?
- 函數(shù)的形參和實參的關(guān)系是什么?
- 函數(shù)的返回值是如何帶回的?
- 函數(shù)是怎樣在棧區(qū)上開辟和釋放空間的?
想要對上面的這六個問題做出準確深入的回答,我們需要學(xué)習(xí)函數(shù)棧幀的創(chuàng)建和銷毀相關(guān)知識,在正式進入函數(shù)棧幀之前,我們還需要了解一些相關(guān)的寄存器和匯編指令。
2、相關(guān)寄存器
- eax:通用寄存器,保留臨時數(shù)據(jù),常用于返回值。
- ebx:通用寄存器,保留臨時數(shù)據(jù)。
- ebp:棧底寄存器,用來記錄棧底的地址。
- esp:棧頂寄存器,用來記錄棧頂?shù)牡刂贰?/li>
- eip:指令寄存器,保存當前指令的下一條指令的地址。
3、相關(guān)匯編指令
- mov:數(shù)據(jù)轉(zhuǎn)移指令。
- sub:減法命令。
- add:加法命令。
- push:數(shù)據(jù)入棧,同時esp棧頂寄存器也要發(fā)生改變。
- pop:數(shù)據(jù)彈出至指定位置,同時esp棧頂寄存器也要發(fā)生改變。
- call:函數(shù)調(diào)用,1. 壓入返回地址 2. 轉(zhuǎn)入目標函數(shù)。
- jump:通過修改eip,轉(zhuǎn)入目標函數(shù),進行調(diào)用。
- lea:傳遞地址指令,用于加載有效地址。
- ret:恢復(fù)返回地址,壓入eip,類似pop eip命令。
4、什么是函數(shù)棧幀
函數(shù)棧幀(stack frame)就是函數(shù)調(diào)用過程中在程序的調(diào)用棧(call stack)所開辟的空間,這些空間是用來存放:
- 函數(shù)參數(shù)和函數(shù)返回值。
- 臨時變量(包括函數(shù)的非靜態(tài)的局部變量以及編譯器自動生產(chǎn)的其他臨時變量)。
- 保存上下文信息(包括在函數(shù)調(diào)用前后需要保持不變的寄存器)。
同時,每一次函數(shù)調(diào)用,編譯器都會為該函數(shù)分配一塊空間,而這塊空間就被稱為這個函數(shù)的函數(shù)棧幀;并且,這塊空間是由兩個寄存器來維護的:esp寄存器(記錄棧頂?shù)牡刂罚┖蚭bp寄存器(記錄棧底的地址)。
5、什么是調(diào)用堆棧
函數(shù)調(diào)用堆棧是反饋函數(shù)調(diào)用邏輯的。我們以main函數(shù)的調(diào)用為例:
我們可以看到,mainCRTStartup調(diào)用__scrt_common_main,__scrt_common_main調(diào)用__scrt_common_main_seh,__scrt_common_main_seh調(diào)用_SCRT_STARTUP_MAIN,_SCRT_STARTUP_MAINmain,main調(diào)用Add。
6、函數(shù)棧幀的創(chuàng)建和銷毀
我們以一段程序為例講解函數(shù)棧幀:(注意: 函數(shù)棧幀的創(chuàng)建和銷毀過程,在不同的編譯器上實現(xiàn)的方法和細節(jié)會有所差異,一般來說,越新的編譯器對函數(shù)棧幀的封裝就越嚴密,本次演示以VS2019為例。)
演示代碼
#include<stdio.h> int Add(int x, int y) { int z = 0; z = x + y; return z; } int main() { int a = 3; int b = 5; int ret = 0; ret = Add(a, b); printf("%d", ret); return 0; }
(1)、main函數(shù)棧幀的創(chuàng)建與初始化
(2)、main函數(shù)的核心代碼
(3)、Add函數(shù)的調(diào)用過程
F11進入Add函數(shù)內(nèi)部,觀察Add函數(shù)的反匯編代碼
代碼執(zhí)行到Add函數(shù)的時候,就要開始創(chuàng)建Add函數(shù)的棧幀空間了。
在Add函數(shù)中創(chuàng)建棧幀的方法和在main函數(shù)中是相似的,在棧幀空間的大小上略有差異而已。
1. 將main函數(shù)的 ebp 壓棧。
2. 計算新的 ebp 和 esp。
3. 將 ebx , esi , edi 寄存器的值保存。
4. 計算求和,在計算求和的時候,我們是通過 ebp 中的地址進行偏移訪問 到了函數(shù)調(diào)用前壓棧進去的參數(shù),這就是形參訪問。
5. 將求出的和放在 eax 寄存器中準備帶回。
(4)、Add函數(shù)棧幀的銷毀
當函數(shù)調(diào)用要結(jié)束返回的時候,前面創(chuàng)建的函數(shù)棧幀也開始銷毀,具體銷毀過程如下:
(5)、調(diào)用完成
調(diào)用完Add函數(shù),回到main函數(shù)的時候,繼續(xù)往下執(zhí)行,可以看到:
00BE185D add esp,8 esp直接+8,相當于跳過了main函數(shù)中壓棧的a’和b’。
00BE1860 mov dword ptr [ebp-20h],eax 將eax中值,存檔到ebp-0x20的地址處,其實就是存儲到main函數(shù)中ret變量中,而此時eax中就是Add函數(shù)中計算的x和y的和,可以看出來,本次函數(shù)的返回值是由eax寄存器帶回來的。程序是在函數(shù)調(diào)用返回之后,在eax中去讀取返回值的。
7、對開篇問題的解答
當我們完整的了解了函數(shù)棧幀創(chuàng)建和銷毀的過程后,我們就可以回答開篇提到的問題了:
1.局部變量是如何創(chuàng)建的?
局部變量的創(chuàng)建是在局部變量所在的函數(shù)的棧幀創(chuàng)建完成并初始化后,然后在該棧幀內(nèi)為局部變量分配空間的。
2.為什么局部變量不初始化其內(nèi)容是隨機的?
因為編譯器在創(chuàng)建函數(shù)棧幀后會在棧幀空間里面放入一個值,而這個值是隨機的。
3.有些時候屏幕上輸出的"燙燙燙"是怎么來的?
因為main函數(shù)調(diào)用時,在棧區(qū)開辟的空間的其中每一個字節(jié)都被初始化為0xCC,而如果我們定義的是一個未初始化的數(shù)組,且這個數(shù)組恰好在這塊空間上創(chuàng)建,因為0xCCCC(兩個連續(xù)排列的0xCC)的漢字編碼是“燙”,所以屏幕上輸出的就是燙燙燙。
4.函數(shù)調(diào)用時參數(shù)時如何傳遞的?傳參的順序是怎樣的?
我們在調(diào)用函數(shù)之前,就會在棧頂上從右向左依次壓入需要傳遞的參數(shù),在創(chuàng)建好被調(diào)函數(shù)的函數(shù)棧幀后通過指針的偏移量來使用傳遞過去的參數(shù),而不是在被調(diào)函數(shù)的函數(shù)棧幀內(nèi)創(chuàng)建形參。
5.函數(shù)的形參和實參的關(guān)系是什么?
形參是實參的一份臨時拷貝,二者的存儲位置不同,形參的改變不會影響實參。
6.函數(shù)的返回值是如何帶回的?
函數(shù)的返回值通過eax寄存器帶回。
7.函數(shù)是怎樣在棧區(qū)上開辟和釋放空間的?
函數(shù)通過改變esp和edp的指向來創(chuàng)建和銷毀空間,空間銷毀并不會清除該空間中的數(shù)據(jù),下一次使用該空間時新數(shù)據(jù)直接覆蓋原數(shù)據(jù)即可。
到此這篇關(guān)于C語言超詳細講解函數(shù)棧幀的創(chuàng)建和銷毀的文章就介紹到這了,更多相關(guān)C語言函數(shù)棧幀內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
VC++文件監(jiān)控之ReadDirectoryChangesW
文章主要介紹文件監(jiān)控的另一種實現(xiàn)方式,利用ReadDirectoryChangesW來實現(xiàn)文件的監(jiān)控,希望對大家有幫助2019-04-04有關(guān)C++繼承與友元、繼承與類型轉(zhuǎn)換詳解
下面小編就為大家?guī)硪黄嘘P(guān)C++繼承與友元、繼承與類型轉(zhuǎn)換詳解。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01