作者的碼云地址:https://gitee.com/dongtiao-xiewei
后續作者會更新力扣的每日一題系列,原代碼會全部上傳碼云,推薦關注哦~筆芯~
目錄
記憶體管理和函式堆疊幀
一些準備作業
main函式堆疊幀
push壓堆疊操作
什么是壓堆疊?出堆疊?
ebp和esp
main函式空間的開辟
變數的創建以及傳參
創建變數
傳參(形式引數的創建):
進入函式內部
Add函式堆疊幀開辟
引數傳遞與回傳
重新回到main函式
總結與提示
記憶體管理和函式堆疊幀
我們知道計算機分配記憶體時主要分為以下的幾個區域

我們也知道,函式是在堆疊區上開辟空間的,
每一次的函式呼叫(包括main函式,自定義函式等)都會在堆疊區上開辟足夠大的空間 ,用于本次函式中的內部操作(資料保護,區域變數控制),這塊空間我們稱為函式堆疊幀,
這篇文章可以為大家解答以下問題:
- 區域變數怎么創建?
- 為什么區域變數具有隨機值?
- 函式怎么傳參?
- 形參與實參的關系?
- 函式的呼叫怎么進行?
- 函式怎么回傳值?
由于文章較長,而且涉及反匯編,也就是匯編語言的知識,如果讀者想知道答案,可以直接看堆疊區圖和總結與提示板塊,
我們的研究方式:
- VS2019
- 撰寫一段簡單的程式
- 在除錯程序中查看反匯編程序
正文分界線
正文分界線
一些準備作業
首先在VS2019中敲出以下代碼
//test.c
int Add(int x,int y)
{
int z=0;
z=x+y;
return z;
}
int main()
{
int a=0;
int b=0;
int c=0;
c=Add(a,b);
printf("%d\n",c);
return 0;
}
按F10開始逐程序除錯,F11可以逐陳述句除錯(也就是進入函式內部)
然后點擊反匯編選項,將會觀察到以下代碼

這是這個代碼全部的反匯編代碼
int main()
{
000818B0 push ebp
000818B1 mov ebp,esp
000818B3 sub esp,0E4h
000818B9 push ebx
000818BA push esi
000818BB push edi
000818BC lea edi,[ebp-24h]
000818BF mov ecx,9
000818C4 mov eax,0CCCCCCCCh
000818C9 rep stos dword ptr es:[edi]
000818CB mov ecx,offset _2A160C42_test@c (08C003h)
000818D0 call @__CheckForDebuggerJustMyCode@4 (08131Bh)
int a = 0;
000818D5 mov dword ptr [a],0
int b = 0;
000818DC mov dword ptr [b],0
int c = 0;
000818E3 mov dword ptr [c],0
c = Add(a, b);
000818EA mov eax,dword ptr [b]
000818ED push eax
000818EE mov ecx,dword ptr [a]
000818F1 push ecx
000818F2 call _Add (0810B4h)
000818F7 add esp,8
000818FA mov dword ptr [c],eax
printf("%d\n", c);
000818FD mov eax,dword ptr [c]
00081900 push eax
00081901 push offset string "%d\n" (087B30h)
00081906 call _printf (0810D2h)
0008190B add esp,8
return 0;
0008190E xor eax,eax
}
00081910 pop edi
00081911 pop esi
00081912 pop ebx
00081913 add esp,0E4h
00081919 cmp ebp,esp
0008191B call __RTC_CheckEsp (081244h)
00081920 mov esp,ebp
00081922 pop ebp
00081923 ret
這里筆者只講與本文相關,也就是printf以前的匯編指令,
注意:由于編譯器的不同,測驗結果可能與本文不同,此文僅做參考~
main函式堆疊幀
push壓堆疊操作
什么是壓堆疊?出堆疊?
我們知道堆疊區的使用規則是:先使用高地址,再使用低地址,回收也是先回收最頂上的地址,也就是低地址,再回收高地址,也就是先進后出,

這樣我們先分析以下幾行反匯編代碼:
000818B0 push ebp
000818B1 mov ebp,esp
000818B3 sub esp,0E4h
哦對了,還沒為大家介紹ebp和esp是啥,馬上補上~
ebp和esp
首先甩出定義:是計算機中可以存放地址的暫存器,用于維護函式堆疊幀
那么可能大家又好奇了,啥是維護函式堆疊幀啊?
我們已經知道,ebp和esp暫存器是用于存放地址的,那么是存放哪里的地址呢?
后文大家可以找到答案,不過還是先把結論甩在這兒吧:
- esp用于存放堆疊底指標,指向被維護函式最高地址
- ebp用于存放堆疊頂指標,指向被維護函式最低地址
所以,位于esp和ebp之間的空間,也就是記憶體為此次函式分配的空間,也就是函式堆疊幀啦~

解釋完了,我們接下來執行這幾句命令
main函式空間的開辟
這是執行到mov指令時的esp和ebp的地址

可以知道,第一步push指令將預開辟的main函式的最高地址存放至ebp中,第二步將ebp存放的地址同時存放到esp中
接下來執行這幾條指令
000818B3 sub esp,0E4h
000818B9 push ebx
000818BA push esi
000818BB push edi
000818BC lea edi,[ebp-24h]
000818C9 rep stos dword ptr es:[edi]
第一行,我們將esp往上移動,也就是往低地址移動0E4h這么大的位元組
第二行,將ebx,esi,edi,分別執行壓堆疊操作
第四行,LEA(load effective address)讀取此時esp的地址,為后文做下鋪墊
最后一行,將此時ebp和esp之間的空間全部初始化為cc cc cc cc


這里是main函式空間開辟的關鍵步驟的堆疊區圖

這里也可以順便解釋為什么變數沒有初始化是隨機值的原因~
不同編譯器初始化的值不一樣,但VS2019是cc cc cc cc,對應漢字也就是大家熟知的燙燙燙,,,
至此,main函式空間開辟完畢~
變數的創建以及傳參
創建變數
由以下幾段反匯編代碼實作
int a = 0;
000818D5 mov dword ptr [a],0
int b = 0;
000818DC mov dword ptr [b],0
int c = 0;
000818E3 mov dword ptr [c],0
以下是創建變數時的堆疊區圖

傳參(形式引數的創建):
先甩結論:形式引數創建在main函式的頂部,并不在函式內部創建
以下幾行代碼可以解釋函式傳參
000818EA mov eax,dword ptr [b]
000818ED push eax
000818EE mov ecx,dword ptr [a]
000818F1 push ecx
由于區域變數有先進后出,所以為了確保a能夠先被呼叫,a將比b后創建,這樣確保了a會被b先使用,也就是引數是從右向左傳遞的
這幾行代碼反應成漢語就是:
將b的值存進eax中并進行壓堆疊操作,再對a進行相同操作


當然,進行了這步操作,為了能維護函式,還需要將ecx最頂端的地址存放在esp中
然后接下來一條指令
000818F2 call _Add (0810B4h)
將add的地址存放在頂部,這次一個伏筆,后文會提到~

進入函式內部
這是函式內部的反匯編代碼
int Add(int x, int y)
{
00081770 push ebp
00081771 mov ebp,esp
00081773 sub esp,0CCh
00081779 push ebx
0008177A push esi
0008177B push edi
0008177C lea edi,[ebp-0Ch]
0008177F mov ecx,3
00081784 mov eax,0CCCCCCCCh
00081789 rep stos dword ptr es:[edi]
0008178B mov ecx,offset _2A160C42_test@c (08C003h)
00081790 call @__CheckForDebuggerJustMyCode@4 (08131Bh)
int z = 0;
00081795 mov dword ptr [z],0
z = x + y;
0008179C mov eax,dword ptr [x]
0008179F add eax,dword ptr [y]
000817A2 mov dword ptr [z],eax
return z;
000817A5 mov eax,dword ptr [z]
}
000817A8 pop edi
000817A9 pop esi
000817AA pop ebx
000817AB add esp,0CCh
000817B1 cmp ebp,esp
000817B3 call __RTC_CheckEsp (081244h)
000817B8 mov esp,ebp
000817BA pop ebp
000817BB ret
Add函式堆疊幀開辟
這段代碼與main函式同理,為Add開辟函式堆疊幀
00081770 push ebp
00081771 mov ebp,esp
00081773 sub esp,0CCh
00081779 push ebx
0008177A push esi
0008177B push edi
0008177C lea edi,[ebp-0Ch]
0008177F mov ecx,3
00081784 mov eax,0CCCCCCCCh
00081789 rep stos dword ptr es:[edi]
0008178B mov ecx,offset _2A160C42_test@c (08C003h)
00081790 call @__CheckForDebuggerJustMyCode@4 (08131Bh)

引數傳遞與回傳
int z = 0;
00081795 mov dword ptr [z],0
z = x + y;
0008179C mov eax,dword ptr [x]
0008179F add eax,dword ptr [y]
000817A2 mov dword ptr [z],eax
return z;
000817A5 mov eax,dword ptr [z]
由這幾段的代碼得出結論,計算可以直接在eax,ecx,也就是臨時變數上進行
最后將得出的結果放入eax中

000817A8 pop edi
000817A9 pop esi
000817AA pop ebx
000817AB add esp,0CCh
000817B1 cmp ebp,esp
000817B3 call __RTC_CheckEsp (081244h)
000817B8 mov esp,ebp
000817BA pop ebp
000817BB ret
1-3行代碼涉及將最頂上三個元素彈出
而其中mov esp,ebp(將ebp的地址存盤到esp地址中)這條指令,成功實作了將Add函式堆疊幀回收
pop ebp 指令將ebp彈入原main最下部,可以繼續維護原main函式
而最后的ret指令,可以實作回到main函式中call指令發出的地方,也就是存放地址的地方,可以實作整個程式能夠繼續按照流程繼續下去,

重新回到main函式
000818F7 add esp,8
000818FA mov dword ptr [c],eax
第一步,將原來為臨時變數x,y開辟的空間回收
第二步,將eax中存放的z的值傳給c
至此,成功實作z的回傳

總結與提示
- 區域變數在main函式堆疊幀中創建,資料型別決定了記憶體修改權限大小
- 不同的編譯器初始化放進的值不一樣,從而導致了不初始化將會出現隨機值的情況
- 函式傳參是在main最頂上執行壓堆疊操作,從右到左傳參,并沒有創建在函式內部
- 形參是實參的一份臨時拷貝
- 函式執行必須先創建函式堆疊幀,再進行函式操作
- 函式若要回傳值,需要先將回傳值暫時存在暫存器中,方便函式空間被回收后回傳
本期內容到此結束啦!由于作者水平有限,文章中如有不足與疏漏之處在所難免,希望各位大佬提出你們寶貴的意見!筆芯~
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/292392.html
標籤:其他
下一篇:函式堆疊幀的創建和銷毀
