自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

函數(shù)調(diào)用時(shí)棧是如何變化的?

系統(tǒng) Linux
大家都知道函數(shù)調(diào)用是通過(guò)棧來(lái)實(shí)現(xiàn)的,而且知道在棧中存放著該函數(shù)的局部變量。但是對(duì)于棧的實(shí)現(xiàn)細(xì)節(jié)可能不一定清楚。本文將介紹一下在Linux平臺(tái)下函數(shù)棧是如何實(shí)現(xiàn)的。

 

大家都知道函數(shù)調(diào)用是通過(guò)棧來(lái)實(shí)現(xiàn)的,而且知道在棧中存放著該函數(shù)的局部變量。但是對(duì)于棧的實(shí)現(xiàn)細(xì)節(jié)可能不一定清楚。本文將介紹一下在Linux平臺(tái)下函數(shù)棧是如何實(shí)現(xiàn)的。

棧幀的結(jié)構(gòu)

函數(shù)在調(diào)用的時(shí)候都是在??臻g上開(kāi)辟一段空間以供函數(shù)使用,所以,我們先來(lái)了解一下通用棧幀的結(jié)構(gòu)。

如圖所示,棧是由高地址向地地址的方向生長(zhǎng)的,而且棧有其棧頂和棧底,入棧出棧的地方就叫做棧頂。

在x86系統(tǒng)的CPU中,rsp是棧指針寄存器,這個(gè)寄存器中存儲(chǔ)著棧頂?shù)牡刂贰bp中存儲(chǔ)著棧底的地址。函數(shù)??臻g主要是由這兩個(gè)寄存器來(lái)確定的。

當(dāng)程序運(yùn)行時(shí),棧指針rsp可以移動(dòng),棧指針和幀指針rbp一次只能存儲(chǔ)一個(gè)地址,所以,任何時(shí)候,這一對(duì)指針指向的是同一個(gè)函數(shù)的棧幀結(jié)構(gòu)。

而幀指針rbp是不移動(dòng)的,訪問(wèn)棧中的元素可以用-4(%rbp)或者8(%rbp)訪問(wèn)%rbp指針下面或者上面的元素。

在明白了這些之后,下面我們來(lái)看一個(gè)具體的例子: 

  1. #include <stdio.h>  
  2. int sum (int a,int b)  
  3.  
  4.  int c = a + b;  
  5.  return c;  
  6.  
  7. int main()  
  8.  
  9.  int x = 5,y = 10,z = 0 
  10.  z = sum(x,y);  
  11.  printf("%d\r\n",z);  
  12.  return 0;  

反匯編如下,下面我們就對(duì)照匯編代碼一步一步分析下函數(shù)調(diào)用過(guò)程中棧的變化。 

  1. 0000000000000000 <sum> 
  2.    0: 55                    push   %rbp   
  3.    1: 48 89 e5              mov    %rsp,%rbp  
  4.    4: 89 7d ec              mov    %edi,-0x14(%rbp) # 參數(shù)傳遞  
  5.    7: 89 75 e8              mov    %esi,-0x18(%rbp) # 參數(shù)傳遞  
  6.    a: 8b 55 ec              mov    -0x14(%rbp),%edx  
  7.    d: 8b 45 e8              mov    -0x18(%rbp),%eax  
  8.   10: 01 d0                 add    %edx,%eax   
  9.   12: 89 45 fc              mov    %eax,-0x4(%rbp) # 局部變量  
  10.   15: 8b 45 fc              mov    -0x4(%rbp),%eax # 存儲(chǔ)結(jié)果  
  11.   18: 5d                    pop    %rbp  
  12.   19: c3                    retq     
  13. 000000000000001a <main> 
  14.   1a: 55                    push   %rbp # 保存%rbp。rbp,棧底的地址  
  15.   1b: 48 89 e5              mov    %rsp,%rbp # 設(shè)置新的棧指針。rsp 棧指針,指向棧頂?shù)牡刂?nbsp; 
  16.   1e: 48 83 ec 10           sub    $0x10,%rsp # 分配 16字節(jié)??臻g。%rsp = %rsp-16  
  17.   22: c7 45 f4 05 00 00 00  movl   $0x5,-0xc(%rbp) # 賦值  
  18.   29: c7 45 f8 0a 00 00 00  movl   $0xa,-0x8(%rbp) # 賦值  
  19.   30: c7 45 fc 00 00 00 00  movl   $0x0,-0x4(%rbp) # 賦值  
  20.   37: 8b 55 f8              mov    -0x8(%rbp),%edx    
  21.   3a: 8b 45 f4              mov    -0xc(%rbp),%eax   
  22.   3d: 89 d6                 mov    %edx,%esi # 參數(shù)傳遞 ,從右向左  
  23.   3f: 89 c7                 mov    %eax,%edi # 參數(shù)傳遞  
  24.   41: e8 00 00 00 00        callq  46 <main+0x2c> # 調(diào)用sum 
  25.   46: 89 45 fc              mov    %eax,-0x4(%rbp)   
  26.   49: 8b 45 fc              mov    -0x4(%rbp),%eax # 存儲(chǔ)計(jì)算結(jié)果  
  27.   4c: 89 c6                 mov    %eax,%esi  
  28.   4e: 48 8d 3d 00 00 00 00  lea    0x0(%rip),%rdi        # 55 <main+0x3b>  
  29.   55: b8 00 00 00 00        mov    $0x0,%eax  
  30.   5a: e8 00 00 00 00        callq  5f <main+0x45>  
  31.   5f: b8 00 00 00 00        mov    $0x0,%eax   
  32.   64: c9                    leaveq   
  33.   65: c3                    retq    

函數(shù)調(diào)用前

在函數(shù)被調(diào)用之前,調(diào)用者會(huì)為調(diào)用函數(shù)做準(zhǔn)備。首先,函數(shù)棧上開(kāi)辟了16字節(jié)的空間,存儲(chǔ)定義的3個(gè)int型變量,建立了main函數(shù)的棧。

接著,會(huì)給三個(gè)變量進(jìn)行賦值。

以下4行代碼是進(jìn)行參數(shù)傳遞。我們可以看到是函數(shù)參數(shù)是倒序傳入的:先傳入第N個(gè)參數(shù),再傳入第N-1個(gè)參數(shù)(CDECL約定)。 

  1. mov    -0x8(%rbp),%edx    
  2. mov    -0xc(%rbp),%eax   
  3. mov    %edx,%esi # 參數(shù)傳遞 ,從右向左  
  4. mov    %eax,%edi # 參數(shù)傳遞 

最后,會(huì)執(zhí)行到call指令處,調(diào)用sum函數(shù)。 

  1. callq  46 <main+0x2c> # 調(diào)用sum 

CALL指令內(nèi)部其實(shí)還暗含了一個(gè)將返回地址(即CALL指令下一條指令的地址)壓棧的動(dòng)作(由硬件完成)。

具體來(lái)說(shuō),call指令執(zhí)行時(shí),先把下一條指令的地址入棧,再跳轉(zhuǎn)到對(duì)應(yīng)函數(shù)執(zhí)行的起始處。

函數(shù)調(diào)用時(shí)

進(jìn)入sum函數(shù)后,我們看到函數(shù)的前兩行: 

  1. push   %rbp   
  2. mov    %rsp,%rbp 

這兩條匯編指令的含義是:首先將rbp寄存器入棧,然后將棧頂指針rsp賦值給rbp。

“mov rbp rsp”這條指令表面上看是用rsp覆蓋rbp原來(lái)的值,其實(shí)不然。

因?yàn)榻orbp賦值之前,原rbp值已經(jīng)被壓棧(位于棧頂),而新的rbp又恰恰指向棧頂。此時(shí)rbp寄存器就已經(jīng)處于一個(gè)非常重要的地位。

該寄存器中存儲(chǔ)著棧中的一個(gè)地址(原rbp入棧后的棧頂),從該地址為基準(zhǔn),向上(棧底方向)能獲取返回地址、參數(shù)值,向下(棧頂方向)能獲取函數(shù)局部變量值,而該地址處又存儲(chǔ)著上一層函數(shù)調(diào)用時(shí)的rbp值。

一般而言,%rbp+4處為返回地址,%rbp+8處為第一個(gè)參數(shù)值(最后一個(gè)入棧的參數(shù)值,此處假設(shè)其占用4字節(jié)內(nèi)存),%rbp-4處為第一個(gè)局部變量,%rbp處為上一層rbp值。

由于rbp中的地址處總是“上一層函數(shù)調(diào)用時(shí)的rbp值”,而在每一層函數(shù)調(diào)用中,都能通過(guò)當(dāng)時(shí)的%rbp值“向上(棧底方向)”能獲取返回地址、參數(shù)值,“向下(棧頂方向)”能獲取函數(shù)局部變量值。

緊接著執(zhí)行的四條指令。 

  1. mov    %edi,-0x14(%rbp) # 參數(shù)傳遞  
  2. mov    %esi,-0x18(%rbp) # 參數(shù)傳遞  
  3. mov    -0x14(%rbp),%edx  
  4. mov    -0x18(%rbp),%eax  
  5. add    %edx,%eax  
  6. mov    %eax,-0x4(%rbp) 

上述指令通過(guò)rbp加偏移量的方式將main傳遞給sum的兩個(gè)參數(shù)保存在當(dāng)前棧幀的合適位置,然后又取出來(lái)放入寄存器,看著有點(diǎn)兒多此一舉,這是因?yàn)樵诰幾g時(shí)未給gcc指定優(yōu)化級(jí)別,而gcc編譯程序時(shí),默認(rèn)不做任何優(yōu)化,所以看起來(lái)比較啰嗦。

需要說(shuō)明的是,sum的兩個(gè)參數(shù)和返回值都是int,在內(nèi)存中只占4個(gè)字節(jié),而圖中每個(gè)棧內(nèi)存單元按8字節(jié)地址邊界進(jìn)行了對(duì)齊,所以才是下圖中這個(gè)樣子。

再來(lái)看緊接著的三條指令。 

  1. add    %edx,%eax   
  2. mov    %eax,-0x4(%rbp) # 局部變量  
  3. mov    -0x4(%rbp),%eax # 存儲(chǔ)結(jié)果 

上述第一條指令負(fù)責(zé)執(zhí)行加法運(yùn)算并將并將結(jié)果存入eax中,第二條指令將eax中的值存入局部變量c所在的內(nèi)存,第三條指令將局部變量c的值讀取到eax中,可以看到,局部變量c被編譯器安排到了%rbp -0x4這個(gè)地址對(duì)應(yīng)的內(nèi)存中。

接下來(lái)繼續(xù)執(zhí)行 

  1. pop %rbp  
  2. retq 

這兩條指令的功能相當(dāng)于下面的指令: 

  1. mov %rbp,%rsp  
  2. pop %rbp  
  3. pop %rip 

即在操作上面兩條指令的時(shí)候,首先把rsp賦值,它的值是存儲(chǔ)調(diào)用函數(shù)rbp的值的地址,所以可以通過(guò)出棧操作,來(lái)給rbp賦值,來(lái)找回調(diào)用函數(shù)的rbp。

通過(guò)棧的結(jié)構(gòu),可以知道,rbp上面就是調(diào)用函數(shù)調(diào)用被調(diào)用函數(shù)的下一條指令的執(zhí)行地址,所以需要賦值給rip,來(lái)找回調(diào)用函數(shù)里的指令執(zhí)行地址。

整個(gè)函數(shù)跳轉(zhuǎn)回main的時(shí)候,他的rsp,rbp都會(huì)變回原來(lái)的main函數(shù)的棧指針,C語(yǔ)言程序就是用這種方式來(lái)確保函數(shù)的調(diào)用之后,還能繼續(xù)執(zhí)行原來(lái)的程序。

函數(shù)調(diào)用后

函數(shù)最后返回的時(shí)候,繼續(xù)執(zhí)行下面這條指令: 

  1. mov    %eax,-0x4(%rbp)  # 把sum函數(shù)的返回值賦給變量z 

上述指令將eax中的結(jié)果放入rbp  -0x4所指的內(nèi)存中,這里也是main的局部變量z所在位置。

再往后的指令如下: 

  1. mov    %eax,-0x4(%rbp)   
  2. mov    -0x4(%rbp),%eax # 計(jì)算結(jié)果  
  3. mov    %eax,%esi  
  4. mov    %eax,%esi  
  5. lea    0x0(%rip),%rdi  
  6. mov    $0x0,%eax  
  7. callq  5f <main+0x45> 

上述指令首先為printf準(zhǔn)備參數(shù),然后調(diào)用printf,具體過(guò)程和調(diào)用sum的過(guò)程相似,讓CPU直接執(zhí)行到main倒數(shù)第二條leave指令處。 

  1. mov    $0x0,%eax  

指令作用是將main返回值0放到寄存器eax,等main返回后調(diào)用main可拿到這個(gè)值。

執(zhí)行l(wèi)eave指令相當(dāng)于執(zhí)行如下兩條指令: 

  1. mov %rbp, %rsp  
  2. pop %rbp 

leave指令首先將rbp的值復(fù)制給rsp,rsp就指向rbp所指的棧單元。之后leave指令將該棧單元的值pop給rbp,如此,rsp和rbp就恢復(fù)成剛進(jìn)入main時(shí)的狀態(tài)。 

 

責(zé)任編輯:龐桂玉 來(lái)源: C語(yǔ)言與C++編程
相關(guān)推薦

2023-12-01 14:57:22

TCP連接

2020-11-17 14:28:56

數(shù)據(jù)中心

2014-11-10 10:52:33

Go語(yǔ)言

2022-05-19 15:08:43

技術(shù)函數(shù)調(diào)用棧Linux

2010-01-28 13:35:41

調(diào)用C++函數(shù)

2010-07-28 15:29:18

Flex函數(shù)

2022-03-22 10:51:53

數(shù)據(jù)棧數(shù)據(jù)

2023-11-29 16:29:09

線程java

2021-04-01 11:28:44

C++ LinuxWindows

2018-11-05 14:53:14

Go函數(shù)代碼

2019-07-23 15:04:54

JavaScript調(diào)用棧事件循環(huán)

2019-08-28 14:21:39

C++C接口代碼

2020-07-31 18:33:56

C++編程語(yǔ)言

2010-07-07 10:25:00

SQL Server索

2023-11-09 23:31:02

C++函數(shù)調(diào)用

2010-09-08 15:24:28

TCP IP協(xié)議棧

2022-09-27 08:01:48

遞歸函數(shù)GScript

2023-06-15 14:10:00

kubeletCNI插件

2022-04-02 07:52:47

DubboRPC調(diào)用動(dòng)態(tài)代理

2024-09-05 17:45:33

Vue函數(shù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)