天天看點

函數棧幀的建立和銷毀以及與C、C++聯系

重新補了一定的C基礎和C++基礎後,來結合csapp把函數棧幀簡單複習一下

1、什麼是函數棧幀

如果接觸過指針,知道指針的大小在32位下是四個位元組,在64位下是八個位元組。那麼指針的大小,就定義為一幀的大小。這就是棧幀。

什麼是函數棧幀呢?

首先在代碼的資料和函數存儲,可以大緻分為幾個區。

  • 棧區:存放局部變量,形式參數
  • 堆區:動态記憶體配置設定,如malloc,calloc,realloc,free
  • 靜态區:存放全局變量,靜态變量

當然在連結那一塊更加細分,不過這裡大緻分成這樣就可以。

那函數棧幀實際上就是每一次的函數調用都要在記憶體中開辟空間,開辟的那一塊就叫函數棧幀。

那函數棧幀裡面存什麼,存放一些變量資料。

函數棧幀的建立和銷毀以及與C、C++聯系

2、代碼的反彙編

我們可以在GCC反彙編或者VS反彙編下來觀察。兩者的彙編代碼文法略有不同。不過可以互通。這裡分别展示VS和GCC底下的反彙編。GCC反彙編想顯示代碼名字編譯的時候±g

#include<stdio.h>
using namespace std;
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);
	return 0;
}
           

這一串代碼背後到底發生了什麼?如何看到背後的問題,VS有反彙編。

注:反彙編要在調試開始後,在調試–視窗部分–反彙編。

  • VS
int main()
{
00641770  push        ebp  
00641771  mov         ebp,esp  
00641773  sub         esp,0E4h  
00641779  push        ebx  
0064177A  push        esi  
0064177B  push        edi  
0064177C  lea         edi,[ebp-0E4h]  
00641782  mov         ecx,39h  
00641787  mov         eax,0CCCCCCCCh  
0064178C  rep stos    dword ptr es:[edi]  
0064178E  mov         ecx,offset [email protected] (064C003h)  
00641793  call        @[email protected] (064120Dh)  
	int a = 10;
00641798  mov         dword ptr [a],0Ah  
	int b = 20;
0064179F  mov         dword ptr [b],14h  
	int c = 0;
006417A6  mov         dword ptr [c],0  
	c = Add(a, b);
006417AD  mov         eax,dword ptr [b]  
006417B0  push        eax  
006417B1  mov         ecx,dword ptr [a]  
006417B4  push        ecx  
006417B5  call        Add (0641186h)  
006417BA  add         esp,8  
006417BD  mov         dword ptr [c],eax  
	return 0;
006417C0  xor         eax,eax  
}
           
  • GCC
    • 00000000000005fa <Add>:
       5fa:	55                   	push   %rbp
       5fb:	48 89 e5             	mov    %rsp,%rbp
       5fe:	89 7d ec             	mov    %edi,-0x14(%rbp)
       601:	89 75 e8             	mov    %esi,-0x18(%rbp)
       604:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)
       60b:	8b 55 ec             	mov    -0x14(%rbp),%edx
       60e:	8b 45 e8             	mov    -0x18(%rbp),%eax
       611:	01 d0                	add    %edx,%eax
       613:	89 45 fc             	mov    %eax,-0x4(%rbp)
       616:	8b 45 fc             	mov    -0x4(%rbp),%eax
       619:	5d                   	pop    %rbp
       61a:	c3                   	retq   
      
      000000000000061b <main>:
       61b:	55                   	push   %rbp
       61c:	48 89 e5             	mov    %rsp,%rbp
       61f:	48 83 ec 10          	sub    $0x10,%rsp
       623:	c7 45 f4 0a 00 00 00 	movl   $0xa,-0xc(%rbp)
       62a:	c7 45 f8 14 00 00 00 	movl   $0x14,-0x8(%rbp)
       631:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)
       638:	8b 55 f8             	mov    -0x8(%rbp),%edx
       63b:	8b 45 f4             	mov    -0xc(%rbp),%eax
       63e:	89 d6                	mov    %edx,%esi
       640:	89 c7                	mov    %eax,%edi
       642:	e8 b3 ff ff ff       	callq  5fa <Add>
       647:	89 45 fc             	mov    %eax,-0x4(%rbp)
       64a:	b8 00 00 00 00       	mov    $0x0,%eax
       64f:	c9                   	leaveq 
       650:	c3                   	retq   
       651:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
       658:	00 00 00 
       65b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)
      
                 
    相比之下我覺得GCC的反彙編更好看一點,但是兩者本質是一樣的。這裡我選擇用GCC的來分析。

在這裡幫助了解的幾個彙編指令就是push和call以及ret。以及需要提及的幾個寄存器

  • 寄存器
    • eax
    • ebx
    • ecx
    • edx
    • ebp
    • esp
    • 其中ebp和esp這2個寄存器中存放的是位址。這2個位址是用來維護函數棧幀的。每一個函數調用都要建立一個空間。 ebp是基指針,esp是棧指針。另外%rip是程式下一條指令的位址。
  • push xxx指令
    • Push指令的作用就是把目前的值放到棧裡面去,然後将棧頂拓展.對應的就是将esp-1. 【這裡描述的位址是下低上高】
    • 函數棧幀的建立和銷毀以及與C、C++聯系
  • call指令
    • call指令調用的過程
    • 其中%rip 是存放程式計數器的值。 %rsp存放棧指針。
    • 當調用了call之後,将%rsp往下擴充,并在新的棧幀中放入call後的指令的位址,用以傳回的時候回到這個地方。同時更改rip的值為call的值。

      -

      函數棧幀的建立和銷毀以及與C、C++聯系
    • 函數棧幀的建立和銷毀以及與C、C++聯系
  • ret指令
    • 用于傳回,其對應于return。
    • 作用是彈出棧頂的值指派給%rip并将%rsp回收空間

3、反彙編代碼的分析與棧幀

函數棧幀的建立和銷毀以及與C、C++聯系
#include<stdio.h>
using namespace std;
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);
	return 0;
}
           
00000000000005fa <Add>:
 5fa:	55                   	push   %rbp
 5fb:	48 89 e5             	mov    %rsp,%rbp
 5fe:	89 7d ec             	mov    %edi,-0x14(%rbp)
 601:	89 75 e8             	mov    %esi,-0x18(%rbp)
 604:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)
 60b:	8b 55 ec             	mov    -0x14(%rbp),%edx
 60e:	8b 45 e8             	mov    -0x18(%rbp),%eax
 611:	01 d0                	add    %edx,%eax
 613:	89 45 fc             	mov    %eax,-0x4(%rbp)
 616:	8b 45 fc             	mov    -0x4(%rbp),%eax
 619:	5d                   	pop    %rbp
 61a:	c3                   	retq   

000000000000061b <main>:
 61b:	55                   	push   %rbp
 61c:	48 89 e5             	mov    %rsp,%rbp
 61f:	48 83 ec 10          	sub    $0x10,%rsp
 623:	c7 45 f4 0a 00 00 00 	movl   $0xa,-0xc(%rbp)
 62a:	c7 45 f8 14 00 00 00 	movl   $0x14,-0x8(%rbp)
 631:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)
 638:	8b 55 f8             	mov    -0x8(%rbp),%edx
 63b:	8b 45 f4             	mov    -0xc(%rbp),%eax
 63e:	89 d6                	mov    %edx,%esi
 640:	89 c7                	mov    %eax,%edi
 642:	e8 b3 ff ff ff       	callq  5fa <Add>
 647:	89 45 fc             	mov    %eax,-0x4(%rbp)
 64a:	b8 00 00 00 00       	mov    $0x0,%eax
 64f:	c9                   	leaveq 
 650:	c3                   	retq   
 651:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
 658:	00 00 00 
 65b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

           
函數棧幀的建立和銷毀以及與C、C++聯系
  1. Main函數過程調用Add前分析
    1. 61b:	55                   	push   %rbp
       61c:	48 89 e5             	mov    %rsp,%rbp
       61f:	48 83 ec 10          	sub    $0x10,%rsp
       623:	c7 45 f4 0a 00 00 00 	movl   $0xa,-0xc(%rbp)
       62a:	c7 45 f8 14 00 00 00 	movl   $0x14,-0x8(%rbp)
       631:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)
       638:	8b 55 f8             	mov    -0x8(%rbp),%edx
       63b:	8b 45 f4             	mov    -0xc(%rbp),%eax
       63e:	89 d6                	mov    %edx,%esi
       640:	89 c7                	mov    %eax,%edi
                 
    2. 對于這部分的彙編代碼和圖的簡要解釋
      1. 在(61b)之前的是調用main函數之前的棧幀,main函數也是函數,它是被其他函數調用的。(再下去就要涉及os的一些知識了)
      2. push %rbp
        1. 儲存此時的%rbp值,目的是為了之後能傳回上一個調用main函數的%rbp值
      3. 兩個打⭐的%rbp是同一個值。
      4. mov %rsp,%rbp
        1. 是将%rbp的值賦給%rsp,此時%rsp和%rbp指向一塊位置。目的是為了新函數的管理
      5. sub $0x10,%rsp
        1. %rsp往下,實際上就是開辟main函數的棧幀空間
        2. %rsp和%rsp一起此時對應一個區域,能管理這塊棧幀
      6. movl $0xa,-0xc(%rbp)
        1. 給a指派10
      7. movl $0x14,-0x8(%rbp)
        1. 給b指派20
      8. movl $0x0,-0x4(%rbp)
        1. 給c指派0
      9. mov -0x8(%rbp),%edx
        1. %edx儲存b的值,準備傳參
      10. mov -0xc(%rbp),%eax
        1. %eax儲存a的值,準備傳參
      11. mov %edx,%esi
        1. 實際上這裡就是Add函數參數中的對形參的一個指派(c++中就是拷貝)
      12. mov %eax,%edi
        1. 實際上這裡就是Add函數參數中的對形參的一個指派(c++中就是拷貝)
      13. 可以發現,C語言中給被調函數參數指派的過程實際上是調用函數準備的。
  2. Add過程分析
    1. 00000000000005fa <Add>:
       5fa:	55                   	push   %rbp
       5fb:	48 89 e5             	mov    %rsp,%rbp
       5fe:	89 7d ec             	mov    %edi,-0x14(%rbp)
       601:	89 75 e8             	mov    %esi,-0x18(%rbp)
       604:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)
       60b:	8b 55 ec             	mov    -0x14(%rbp),%edx
       60e:	8b 45 e8             	mov    -0x18(%rbp),%eax
       611:	01 d0                	add    %edx,%eax
       613:	89 45 fc             	mov    %eax,-0x4(%rbp)
       616:	8b 45 fc             	mov    -0x4(%rbp),%eax
       619:	5d                   	pop    %rbp
       61a:	c3                   	retq   
                 
    2. 12行的過程和main函數開始的12行的過程是一樣的。
      1. 同樣是為了基指針和棧指針的管理
    3. 3~6行則是函數内部的一個初始化
      1. 可以看到,此時的Add函數内部的參數存放在棧空間中。是以能了解為什麼說函數内的局部變量存放在棧空間中。
    4. 7~8行将參數的值複制到寄存器中進行運算,結果儲存在%eax中。【同時注意一般函數的調用參數和傳回的寄存器都是有約定俗稱的協定,如11行一般以%eax作為傳回值的寄存器】
    5. 9行完成運算,此時運算後的值存在%eax中
    6. 10行,将函數内的局部變量c的值指派為計算得到的答案
    7. 11行,此時将局部變量c的值(棧空間的值指派給%eax),準備傳回
      1. 可能有些奇怪為什麼看似多此一舉?這是對應C/C++中,值傳回的時候,是将值拷貝給一個臨時變量,再傳回的。
    8. 12行的pop %rbp。
      1. 将%rbp獲得此時棧頂指針指向的空間的内容。也就是回到main函數的時候%rbp的值
      2. %rsp回收空間。
    9. 13行的retrun。
      1. 程式計數器器(%rip)跳轉回Call Add指令的下一條指令的位址。
      2. %rsp回收空間
      3. 此時回到Main函數
    10.
    函數棧幀的建立和銷毀以及與C、C++聯系
  3. 回到Main函數後的過程
    1. 647:	89 45 fc             	mov    %eax,-0x4(%rbp)
       64a:	b8 00 00 00 00       	mov    $0x0,%eax
       64f:	c9                   	leaveq 
       650:	c3                   	retq   
       651:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
       658:	00 00 00 
       65b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)
                 
    2. 1行–将獲得傳回值指派給Main裡的c
    3. 2行–清空%eax
    4. 3行–leava
      1. ATT彙編的leava等價于
        1. movl %ebp, %esp

          popl %ebp 【32位下】

        2. 若為64bits 将使用rbp和rsp 寄存器
      2. 執行的是和Add函數pop的時候一樣的操作
    5. 4行retq
      1. 和Add函數的ret一樣,Main也是函數,也是要傳回到上一層

至此就完成了該程式的基本運作。

然而每個編譯器的具體處理情況略有不同。

再來看眼VS下的Add()

函數棧幀的建立和銷毀以及與C、C++聯系

VS下的Add就在004516F3的位置又進行了開辟Add函數空間的操作。

而且存在對空間進行初始化rep stos。其中這也是為什麼VS用未初始化的空間一般調試出來的是燙燙燙燙燙燙。因為vs一般用該位元組碼進行空間的初始化。

4、函數棧幀的建立和銷毀與C以及C++的聯系

比如

  • 函數傳參的形參拷貝
  • 函數值傳回時的臨時變量
  • 對被調用函數臨時變量傳址傳回或者傳引用傳回的error
    • 該空間位址完全可以被其他指令覆寫,導緻錯誤輸出。

繼續閱讀