前言
????????一位優秀的程序員,必須對內存的分布有深刻的理解,在初學編程的時候,往往有諸如以下很多問題困擾著初學者,而通過今天的分享,我們就可以通過自己的觀察,將這些問題統統解決掉
- 局部變量是怎么創建的?
- 為什么局部變量的值是隨機值?
- 函數是怎么傳參的?傳參的順序是怎么樣的?
- 形參和實參是什么關系?
- 函數調用是怎么調用的?
- 函數調用后是怎么返回的?
目錄
棧與棧幀的概念? ? ? ??
棧幀是如何在電腦上運作的
1.c語言代碼
2.反匯編代碼
主函數:
add函數:
函數棧幀的創建
1.創建?_tmainCRTStartup 的棧幀
2.創建 main 的棧幀
3.main函數數據的初始化?
4.add函數傳參
5.創建add函數的棧幀
?6.add函數數據的初始化
7. add函數的返回
函數棧幀的銷毀
1.add函數棧幀的銷毀
2.add函數值的返回
?3.main函數棧幀的銷毀
棧與棧幀的概念? ? ? ??
首先,什么是棧?
????????在數據結構中我們學過 “棧” 這種結構,在數據結構中, 棧是限定僅在表尾進行插入或刪除操作的線性表。棧是一種數據結構,它按照后進先出的原則存儲數據,先進入的數據被壓入棧底,最后的數據在棧頂,需要讀數據的時候從棧頂開始彈出數據。
????????在計算機系統中,棧也可以稱之為棧內存是一個具有動態內存區域,存儲函數內部(包括? main 函數)的局部變量和方法調用和函數參數值,是由系統自動分配的,一般速度較快;存儲地址是連續且存在有限棧容量,會出現溢出現象程序可以將數據壓入棧中,也可以將數據從棧頂彈出。壓棧操作使得棧增大,而彈出操作使棧減小。 棧用于維護函數調用的上下文,離開了棧函數調用就沒法實現。
那什么是棧幀呢?
????????每一次函數的調用,都會在調用棧(call stack)上維護一個獨立的棧幀(stack frame)。每個獨立的棧幀一般包括:
- 函數的返回地址和參數
- 臨時變量: 包括函數的非靜態局部變量以及編譯器自動生成的其他臨時變量
- 函數調用的上下文
????????棧是從高地址向低地址延伸,一個函數的棧幀用 ebp 和 esp 這兩個寄存器來劃定范圍.ebp 指向當前的棧幀的底部,esp 始終指向棧幀的頂部;
ebp 指向當前的棧幀的底部
ebp 寄存器又被稱為幀指針(Frame Pointer)
esp 始終指向棧幀的頂部
esp 寄存器又被稱為棧指針(Stack Pointer)
????????另外,經過筆者的測試,這也與編譯環境有關使用不同的編譯器,或者不同的環境下,我們能直觀看見的都是不一樣的,但是倆者都是寄存器,只是體現不同罷了
- ????????32位機器(esp,ebp)
- ????????64位機器(rsp,rbp)
以下是筆者在VS2022上進行的測試:
棧幀是如何在電腦上運作的
????????要想搞懂這個問題,我們就需要結合編譯器給我們提供的反匯編代碼,結合上我們寫的代碼進行分析
????????我們先實現一個將倆個數相加的函數功能,然后在放進 main 函數中,并且進行調用,完成后輸出結果,然后結束 main 函數。整個代碼邏輯非常簡單,具體實現如下:
1.c語言代碼
#define _CRT_SECURE_NO_WARNINGS 1
#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", c);return 0;
}
2.反匯編代碼
????????我們完成上述代碼后,按 F10 進行調試,然后鼠標右鍵單擊 “轉到反匯編”,然后我們就可以看到反匯編代碼了
主函數:
int main()
{
001818D0 push ebp
001818D1 mov ebp,esp
001818D3 sub esp,0E4h
001818D9 push ebx
001818DA push esi
001818DB push edi
001818DC lea edi,[ebp-24h]
001818DF mov ecx,9
001818E4 mov eax,0CCCCCCCCh
001818E9 rep stos dword ptr es:[edi]
001818EB mov ecx,18C008h
001818F0 call 0018132F int a = 10;
001818F5 mov dword ptr [ebp-8],0Ah int b = 20;
001818FC mov dword ptr [ebp-14h],14h int c = 0;
00181903 mov dword ptr [ebp-20h],0 c = add(a, b);
0018190A mov eax,dword ptr [ebp-14h]
0018190D push eax
0018190E mov ecx,dword ptr [ebp-8]
00181911 push ecx
00181912 call 00181023
00181917 add esp,8
0018191A mov dword ptr [ebp-20h],eax printf("%d", c);
0018191D mov eax,dword ptr [ebp-20h]
00181920 push eax
00181921 push 187B30h
00181926 call 001810D7
0018192B add esp,8 return 0;
0018192E xor eax,eax
}
00181930 pop edi
00181931 pop esi
00181932 pop ebx
00181933 add esp,0E4h
00181939 cmp ebp,esp
0018193B call 00181253
00181940 mov esp,ebp
00181942 pop ebp
00181943 ret
add函數:
int add(int x, int y)
{
00181870 push ebp
00181871 mov ebp,esp
00181873 sub esp,0CCh
00181879 push ebx
0018187A push esi
0018187B push edi
0018187C lea edi,[ebp-0Ch]
0018187F mov ecx,3
00181884 mov eax,0CCCCCCCCh
00181889 rep stos dword ptr es:[edi]
0018188B mov ecx,18C008h
00181890 call 0018132F int z = 0;
00181895 mov dword ptr [ebp-8],0 z = x + y;
0018189C mov eax,dword ptr [ebp+8]
0018189F add eax,dword ptr [ebp+0Ch]
001818A2 mov dword ptr [ebp-8],eax return z;
001818A5 mov eax,dword ptr [ebp-8]
}
001818A8 pop edi
001818A9 pop esi
001818AA pop ebx
001818AB add esp,0CCh
001818B1 cmp ebp,esp
001818B3 call 00181253
001818B8 mov esp,ebp
001818BA pop ebp
001818BB ret
函數棧幀的創建
????????我們知道,我要使用某一個函數,就要去調用他,一般常見的情況是在函數里面調用別的函數,就比如上面寫的那一段很簡單的代碼,我們在 main 函數里面調用了 add 函數來實現了將倆個數相加的操作,?main? 函數是我們人為寫的上去的,本身編譯器是不會自帶 main 函數的,當我們的代碼寫完了準備編譯的時候,編譯器得先掃描整個代碼,找到 main 函數,然后從 main 函數開始執行代碼,換言之 main 函數也是函數,也是需要被調用的。
? ? ? ? 那么編譯器用什么來拿到 main 函數,并且成功的調用他的呢?關于這一點,不同的編譯器的實現是不一樣的,比如在VS編譯器中是使用的 _tmainCRTStartup 這樣的內置函數來調用的。
1.創建?_tmainCRTStartup 的棧幀
編譯器拿到一段完整的程序后首先會在棧區開辟一塊空間,如下圖所示:
2.創建 main 的棧幀
從這里開始結合反匯編代碼進行觀察
首先將 edp 押棧
001818D0 push ebp
?然后改變 edp?的指向
001818D1 mov ebp,esp
然后移動 esp 移動 0e4h 個單位
001818D3 sub esp,0E4h
?到這里,其實就已經完成了對 main 函數棧區的創建,如圖所示:
3.main函數數據的初始化?
?然后我們再繼續結合反匯編代碼 進行觀察:
在這里連續押了3個元素入棧
001818D9 push ebx 001818DA push esi 001818DB push edi
如圖所示:?
?????????然后對剛才開辟的空間進行了初始化,并且全部賦值為 cccccccc ,這也解釋了為什么平常沒有初始化的數據的隨機值是 ccccccccc?
001818DC lea edi,[ebp-24h] 001818DF mov ecx,9 001818E4 mov eax,0CCCCCCCCh 001818E9 rep stos dword ptr es:[edi]
?在完成初始化后,初始化 a=10,在這里一個 word 是 2 個字節,一個 dword 是 4 個字節
int a = 10; 001818F5 mov dword ptr [ebp-8],0Ah
????????
????????我們可以成功的觀察到,在 edp-8 這個位置,已經存放了 a=10,其余位置的 cccccccc 還是保留不變,這也就解釋了平常隨機值的大小為 cccccccc 的情況
?同理的,對 b 和 c 都做初始化
?自此我們就完成了對數據的全部初始化,接下來就 add 函數了
4.add函數傳參
在這里我們可以注意,傳入的地址
- edp-14h? 就是之前初始化的 b=20
- edp-8? ??就是之前初始化的 a=10
????????也就是進行了函數傳參的操作,通過下面的代碼,我們更加可以理解函數的形參是實參的一份臨時拷貝
c = add(a, b); 0018190A mov eax,dword ptr [ebp-14h] 0018190D push eax 0018190E mov ecx,dword ptr [ebp-8] 00181911 push ecx
5.創建add函數的棧幀
這里的 call 就是調用的意思
00181912 call 00181023 00181917 add esp,8 0018191A mov dword ptr [ebp-20h],eax
??
?????????按 F11 進入函數觀察,我們會發現,這里的操作和上述 main 函數棧幀的操作幾乎一模一樣,也就是說,這里實際上是在創建 add 函數的棧幀
int add(int x, int y) { 00181870 push ebp 00181871 mov ebp,esp 00181873 sub esp,0CCh 00181879 push ebx 0018187A push esi 0018187B push edi 0018187C lea edi,[ebp-0Ch] 0018187F mov ecx,3 00181884 mov eax,0CCCCCCCCh 00181889 rep stos dword ptr es:[edi] 0018188B mov ecx,18C008h
?
?6.add函數數據的初始化
和上述 main 函數數據的初始化基本上是一樣的
int z = 0; 00181895 mov dword ptr [ebp-8],0 z = x + y; 0018189C mov eax,dword ptr [ebp+8] 0018189F add eax,dword ptr [ebp+0Ch] 001818A2 mov dword ptr [ebp-8],eax
這里就不再贅述,結果就是對 edp 附近的字節進行操作,最終達到成功賦值的目的
7. add函數的返回
????????我們知道,函數使用的空間是臨時的,在退出這個函數之后,他使用的這部分空間就被銷毀了,那空間都被銷毀了,該怎么樣把返回值返回呢?
這是返回值 z 的創建位置: edp-8
int z = 0; 00181895 mov dword ptr [ebp-8],0
這是返回時的語句
return z; 001818A5 mov eax,dword ptr [ebp-8]
????????我們觀察發現,編譯器是將 edp-8 的值放在了 eax 中,那 eax 是什么呢? eax 其實是寄存器,寄存器不會因為 add 函數的銷毀而銷毀,他會持續的存在,用來保存 z 的值
函數棧幀的銷毀
1.add函數棧幀的銷毀
????????pop 是彈出棧的意思,連續從棧頂彈出三個寄存器,之后繼續更改 esp 和 edp 指向的位置,最后,ret 會回到之前 call 指令留下的下一條指令的地址
001818A8 pop edi 001818A9 pop esi 001818AA pop ebx 001818AB add esp,0CCh 001818B1 cmp ebp,esp 001818B3 call 00181253 001818B8 mov esp,ebp 001818BA pop ebp 001818BB ret
如圖所示:
?
?????????此時的棧頂指針,棧底指針就可以做到重新維護 main 函數的棧幀空間,因為之前 call 指令留下的地址,我們就可以做到 “出去又可以回來” 這對于我們管理空間是非常高效穩定的+
2.add函數值的返回
????????這里實際上是更改棧頂指針的指向,通過這樣的操作,我們就可以達到釋放形參的目的,值得注意的是這段代碼的最后一行
c = add(a, b); 0018190A mov eax,dword ptr [ebp-14h] 0018190D push eax 0018190E mov ecx,dword ptr [ebp-8] 00181911 push ecx 00181912 call 00181023 00181917 add esp,8 0018191A mov dword ptr [ebp-20h],eax
????????我們會發現,這里的 ebp-20h 和 eax 分別對應了前面對于 c 的初始化和對于 z 的值的保存,也就是說,這里就是將之前 eax 寄存器里放的 z 的值賦給 c,從而達到了
c = add(a, b);
?的語句效果
int c = 0; 00181903 mov dword ptr [ebp-20h],0
return z; 001818A5 mov eax,dword ptr [ebp-8]
?3.main函數棧幀的銷毀
????????這里也是連續從棧頂彈出三個寄存器,之后繼續更改 esp 和 edp 指向的位置,最后 ret 退回上一級調用 main 函數的內置函數中,具體過程同上,這里就不再繼續贅述
00181930 pop edi
00181931 pop esi
00181932 pop ebx
00181933 add esp,0E4h
00181939 cmp ebp,esp
0018193B call 00181253
00181940 mov esp,ebp
00181942 pop ebp
00181943 ret
????????以上就是本次分享的全部內容了,希望對屏幕前的您有所幫助,如有內容上的錯誤,歡迎指出,也歡迎積極討論,內容制作不易,給個三連支持一下吧