深入理解內存 —— 函數棧幀的創建與銷毀

前言

????????一位優秀的程序員,必須對內存的分布有深刻的理解,在初學編程的時候,往往有諸如以下很多問題困擾著初學者,而通過今天的分享,我們就可以通過自己的觀察,將這些問題統統解決掉

  • 局部變量是怎么創建的?
  • 為什么局部變量的值是隨機值?
  • 函數是怎么傳參的?傳參的順序是怎么樣的?
  • 形參和實參是什么關系?
  • 函數調用是怎么調用的?
  • 函數調用后是怎么返回的?

目錄

棧與棧幀的概念? ? ? ??

棧幀是如何在電腦上運作的

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 的情況

?同理的,對 bc 都做初始化

?自此我們就完成了對數據的全部初始化,接下來就 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  

????????以上就是本次分享的全部內容了,希望對屏幕前的您有所幫助,如有內容上的錯誤,歡迎指出,也歡迎積極討論,內容制作不易,給個三連支持一下吧

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/43311.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/43311.shtml
英文地址,請注明出處:http://en.pswp.cn/news/43311.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

keepalived集群

keepalived概述 keepalived軟件就是通過vrrp協議來實現高可用功能。 VRRP通信原理 VRRP就是虛擬路由冗余協議&#xff0c;它的出現就是為了解決靜態路由的單點故障。 VRRP是通過一種競選一種協議機制來將路由交個某臺VRRP路由器。 VRRP 用IP多播的方式&#xff08;多播地…

微信小程序中pdf的上傳、下載及excel導出

微信小程序中pdf的上傳、下載及excel導出 pdf上傳上傳1&#xff1a;上傳2&#xff1a; pdf下載導出excel pdf上傳 上傳兩種方法&#xff1a; 上傳1&#xff1a; 1.用vant weapp組件&#xff1a; //pdf上傳--vant weapp組件 <view class"content"><van-u…

推薦算法知識

有志者&#xff0c;事竟成 1. 聯盟的含義 2. Attention 3. Transformer 4. Learning to rank的三種方式 1. point-wise 2. pair-wise 3. list-wise 5. 推薦系統中的校準&#xff08;如保序回歸等&#xff09; 6. 推薦系統中的偏差與處理&#xff08;如位置偏差等&#xff09…

C#中的泛型約束可以用在以下幾個地方?

1.泛型類型參數&#xff1a; 在定義泛型類型或泛型方法時&#xff0c;可以使用泛型約束來限制泛型類型參數的類型。這可以確保類型參數滿足特定的條件&#xff0c;從而在編譯時捕獲錯誤并提供更安全和可靠的代碼。 public class MyClass<T> where T : IComparable<T&…

【React學習】React中的setState方法

1. setState概述 setState 是React框架中&#xff0c;用于更新組件狀態的方法。 setState 方法由React組件繼承自 React.Component 類的一部分。通過調用 setState&#xff0c;可以告訴 React要更新組件的狀態&#xff0c;并觸發組件的重新渲染。 this.setState(newState, ca…

C語言中常見的一些語法概念和功能

常用代碼&#xff1a; 程序入口&#xff1a;int main() 函數用于定義程序的入口點。 輸出&#xff1a;使用 printf() 函數可以在控制臺打印輸出。 輸入&#xff1a;使用 scanf() 函數可以接收用戶的輸入。 條件判斷&#xff1a;使用 if-else 語句可以根據條件執行不同的代碼…

【力扣每日一題】2023.8.15 字符中的查找與替換

目錄 題目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代碼&#xff1a; 題目&#xff1a; 示例&#xff1a; 分析&#xff1a; 題目很長&#xff0c;簡而言之就是檢查字符串中對應索引的位置是否有特定的字符串&#xff0c;如果有&#xff0c;那么替換&#xff0c;返…

Ceph如何操作底層對象數據

1.基本原理介紹 1.1 ceph中的對象(object) 在Ceph存儲中&#xff0c;一切數據最終都會以對象(Object)的形式存儲在硬盤&#xff08;OSD&#xff09;上&#xff0c;每個的Object默認大小為4M。 通過rados命令&#xff0c;可以查看一個存儲池中的所有object信息&#xff0c;例如…

Optional的基礎運用

Optional的基礎運用 簡介代碼示例 簡介 代碼示例 package org.example;import org.junit.Test;import java.util.Optional;public class OptionalTest {Testpublic void advance() {String str "hello";str null;// of(T t):封裝數據t生成Optional對象&#xff0c…

【筆試題心得】關于正則的一些整理

本文部分內容摘抄整理自 正則表達式 – 教程 | 菜鳥教程 在筆試的過程中&#xff0c;也常常會對正則表達式進行考察&#xff0c;這里對正則表達式的常見用法&#xff0c;做一個學習和總結。 正則表達式的模式可以包括以下內容&#xff1a; 字面值字符&#xff1a;例如字母、數…

數據結構:堆的實現

1.堆的概念 如果有一個關鍵碼的集合 K { k1 &#xff0c;k2 &#xff0c;k3 &#xff0c;…&#xff0c;kn }&#xff0c;把它的所有元素按完全二叉樹的順序存儲方式存儲在一個一維數組中&#xff0c;并且 k(i) < k(i*21) 和 k(i) < k(i*22)&#xff0c; i 0 &#xff…

MongoDB增刪改查操作

數據庫操作&#xff1a; 在MongoDB中&#xff0c;文檔集合存在數據庫中。 要選擇使用的數據庫&#xff0c;請在mongo shell程序中發出 use <db> 語句 // 查看有哪些數據庫 show dbs;// 如果數據庫不存在&#xff0c;則創建并切換到該數據庫&#xff0c;存在則直接切換到…

分布式消息中間件

消息中間件是Java開發消息隊列的一種中間件產品。中間件類似windows編程開發中的插件。工具插件在軟件工具中是中間插件。插件也是應用程序。消息的分發過程包裝之后是chatlog 系統或者是手機短信。系統與系統之間的通信通過消息的發送和接收。堆積頻繁過多的系統通知消息需要進…

C++之模板進階

模板進階 非類型模板參數模板的特化概念函數模板特化類模板特化全特化偏特化 模板分離編譯什么是分離編譯模板的分離編譯解決方法 模板總結 非類型模板參數 模板參數分兩種&#xff1a;類型形參與非類型形參。 類型形參&#xff1a;出現在模板參數列表中&#xff0c;跟在class…

docker安裝consul

1、下載consul鏡像 docker pull consul2、啟動consul docker run -d --restartalways --name consul -p 8500:8500 consul agent -server -bootstrap-expect1 -ui -bind0.0.0.0 -client0.0.0.03、查看consul日志 docker logs consul4、檢驗是否安裝成功

drawio----輸出pdf為圖片大小無空白(圖片插入論文)

自己在寫論文插入圖片時為了讓論文圖片放大不模糊&#xff0c;啥方法都試了&#xff0c;最后摸索出來這個。 自己手動畫圖的時候導出pdf總會出現自己的圖片很小&#xff0c;pdf的白邊很大如下如所示&#xff0c;插入論文的時候后雖然放大不會模糊&#xff0c;但是白邊很大會顯…

【數據結構OJ題】用隊列實現棧

原題鏈接&#xff1a;https://leetcode.cn/problems/implement-stack-using-queues/ 目錄 1. 題目描述 2. 思路分析 3. 代碼實現 1. 題目描述 2. 思路分析 可以用兩個隊列去實現一個棧&#xff0c;每次始終保持一個隊列為空。 入棧相當于給非空隊列進行入隊操作。 出棧相…

異步電機IM-改進的電壓模型磁鏈觀測器學習

導讀&#xff1a;本期文章主要介紹異步電機的改進型電壓模型磁鏈觀測器。傳統純積分形式的積分器在低速區域存在初始值問題和直流偏置問題&#xff0c;所以在實際應用中必須對電壓模型進行改進。本期文章中的對電壓模型改進是借鑒一篇IEEE中的方法。 如果需要文章中對應的仿真…

Apache Dubbo 云原生可觀測性的探索與實踐

作者&#xff1a;宋小生 - 平安壹錢包中間件資深工程師 Dubbo3 可觀測能力速覽 Apache Dubbo3 在云原生可觀測性方面完成重磅升級&#xff0c;使用 Dubbo3 最新版本&#xff0c;你只需要引入 dubbo-spring-boot-observability-starter 依賴&#xff0c;微服務集群即原生具備以…

貪心算法實現找零問題

思路&#xff1a; 使用 貪心算法 的思想 題目&#xff1a; 檸檬水找零 在檸檬水攤上&#xff0c;每一杯檸檬水的售價為5美元。顧客排隊購買你的產品,一次購買一杯。 每位顧客只買一杯檸檬水,然后向你付5美元、10美元或20美元。必須給每個顧客正確找零 注意,一開始你手頭沒有任何…