網站首頁 編程語言 正文
寫在前面
我們知道,每一次函數調用都需要在棧區上為其開辟一塊空間,這塊空間就叫做這個函數的棧幀。
而棧是從高地址向低地址延伸的。每個函數的每次調用,都有它自己獨立的一個棧幀,這個棧幀中維持著所需要的各種信息。寄存器ebp指向當前的棧幀的底部(高地址),寄存器esp指向當前的棧幀的頂部(低地址)。
這樣我們就了解了寄存器ebp和寄存器esp中存放的是地址,這兩個地址是用來維護函數棧幀的。比如:調用main函數, 我們為main函數分配棧幀空間, 那么棧幀維護如下:
下面我們通過一段代碼分析一下,函數棧幀創建和銷毀的過程:(棧幀這部分內容在不同的編譯器上實現存在差異, 但是思想大致都是一致的。本文是在vs2013編譯器下實現的。)
#include <stdio.h> int Add(int x, int y) { int z = 0; z = x + y; return z; } int main(void) { int a = 10; int b = 20; int ret = 0; ret = Add(a, b);//計算a+b printf("%d\n", ret); return 0; }
我們在調試過程打開調用堆棧
可以看出,main函數是在__tmainCRTStartup函數內部被調用的,而__tmainCRTStartup函數又是在mainCRTStartup函數內部調用的。
為了能更加清楚的看到棧幀創建和銷毀的過程,我們轉到上面代碼對應的反匯編代碼:
int main(void) { 009D3F40 push ebp //將edp壓入棧幀 009D3F41 mov ebp,esp //將esp的值賦給edp 009D3F43 sub esp,0E4h //esp-0E4h 009D3F49 push ebx 009D3F4A push esi 009D3F4B push edi 009D3F4C lea edi,[ebp+FFFFFF1Ch] 009D3F52 mov ecx,39h 009D3F57 mov eax,0CCCCCCCCh 009D3F5C rep stos dword ptr es:[edi] int a = 10; 009D3F5E mov dword ptr [ebp-8],0Ah int b = 20; 009D3F65 mov dword ptr [ebp-14h],14h int ret = 0; 009D3F6C mov dword ptr [ebp-20h],0 ret = Add(a, b);//計算a+b 009D3F73 mov eax,dword ptr [ebp-14h] 009D3F76 push eax 009D3F77 mov ecx,dword ptr [ebp-8] 009D3F7A push ecx 009D3F7B call 009D11F9 009D3F80 add esp,8 009D3F83 mov dword ptr [ebp-20h],eax printf("%d\n", ret); 009D3F86 mov esi,esp 009D3F88 mov eax,dword ptr [ebp-20h] 009D3F8B push eax 009D3F8C push 9D5860h 009D3F91 call dword ptr ds:[009D9118h] 009D3F97 add esp,8 009D3F9A cmp esi,esp 009D3F9C call 009D1140 return 0; 009D3FA1 xor eax,eax } 009D3FA3 pop edi 009D3FA4 pop esi 009D3FA5 pop ebx 009D3FA6 add esp,0E4h 009D3FAC cmp ebp,esp 009D3FAE call 009D1140 009D3FB3 mov esp,ebp 009D3FB5 pop ebp 009D3FB6 ret
main函數的調用 main函數棧幀的創建
經過剛才我們的理解,在準備調用main函數的時候,調用main函數的那個函數的棧幀已經開辟好了。
然后將ebp壓入棧幀,保存了指向棧底的ebp的地址,而此時esp指向新的棧頂位置;接著將esp的值賦給了ebp,產生了新的ebp;用esp減去一個16進制數0E4H(這里就是為main函數預開辟空間)。緊接著三個壓棧指令,分別將ebx,esi,edi,壓入棧幀。加載完有效地址以后,將為main函數預開辟空間全部初始化為0xCCCCCCCC。最后創建了三個局部變量a,b,ret并進行了初始化。
Add函數的調用
函數傳參
將b的值存入寄存器eax中,再將eax壓入棧中;將a的值存入寄存器ecx中,再將將ecx壓入棧中;這里看出參數是從右向左傳遞的。緊接著執行call指令,這里就是調用Add函數,同時將call指令的下一條指令的地址壓入棧中,然后執行call指令的時候按F11 , 就進入了Add函數內部。
Add函數棧幀的創建
首先將main()函數的ebp壓入棧,保存指向main()函數棧幀底部的ebp的地址,此時esp指向新的棧頂位置;將esp的值賦給ebp,產生新的ebp,即Add()函數棧幀的ebp;給esp減去一個16進制數0E4H,這里是為Add()函數預開辟空間;緊接著三個壓棧指令,分別將ebx,esi,edi,壓入棧幀。加載完有效地址以后,將為Add函數預開辟空間全部初始化0xCCCCCCCC。在緊接著創建了變量z,將形參的a和b相加的結果存儲到z中;最后將結果存儲到eax寄存器中,通過寄存器帶回了函數的返回值。
Add函數棧幀的銷毀
edi、esi、ebx依次出棧,esp 會向下移動;然后將ebp的值賦給esp,使esp指向ebp指向的地方;接著ebp 出棧,同時將出棧的內容給ebp,此時ebp又指向了main函數棧幀的底部,最后執行ret 指令,表示出棧一次,并跳轉到出棧的內容的地址處,也就是call指令的下一條指令處。
main函數棧幀的銷毀
main函數棧幀的銷毀和Add函數棧幀銷毀的過程的思想都是一樣的,這里就不做多贅述了。
總結
通過上面的例子,我們知道了局部變量是如何創建的,知道了為什么創建局部變量不初始化,會導致里面的內容是隨機值;對函數是如何傳參的,以及傳參順序是如何也有了較為深入的了解。
原文鏈接:https://blog.csdn.net/m0_50655389/article/details/119458313
相關推薦
- 2022-08-31 Linux環境下安裝python3_python
- 2022-09-25 edge或谷歌瀏覽器打開默認是百度或其他,怎么修改成自己想要的頁面
- 2022-12-01 Django+Ajax異步刷新/定時自動刷新實例詳解_python
- 2022-09-03 一起聊聊C++中的特殊成員函數_C 語言
- 2022-12-23 Android入門之Menu組件的使用教程詳解_Android
- 2022-10-05 Iptables防火墻四表五鏈概念及使用技巧詳解_安全相關
- 2022-09-02 docker搭建redis哨兵集群并且整合springboot的實現_docker
- 2022-09-03 golang架構設計開閉原則手寫實現_Golang
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支