函数栈帧的创建和销毁以及与C、C++联系

本文深入探讨了C/C++中函数栈帧的概念,通过GCC和VS的反汇编例子展示了函数调用时栈帧的创建、使用和销毁过程。详细分析了`push`、`call`、`ret`指令的作用,以及`ebp`和`esp`寄存器在维护栈帧中的角色。同时,解释了参数传递、局部变量存储以及返回值处理的细节,揭示了C/C++中函数调用的底层机制。
摘要由CSDN通过智能技术生成

重新补了一定的C基础和C++基础后,来结合csapp把函数栈帧简单复习一下

1、什么是函数栈帧

如果接触过指针,知道指针的大小在32位下是四个字节,在64位下是八个字节。那么指针的大小,就定义为一帧的大小。这就是栈帧。

什么是函数栈帧呢?

首先在代码的数据和函数存储,可以大致分为几个区。

  • 栈区:存放局部变量,形式参数
  • 堆区:动态内存分配,如malloc,calloc,realloc,free
  • 静态区:存放全局变量,静态变量

当然在链接那一块更加细分,不过这里大致分成这样就可以。

那函数栈帧实际上就是每一次的函数调用都要在内存中开辟空间,开辟的那一块就叫函数栈帧。

那函数栈帧里面存什么,存放一些变量数据。

在这里插入图片描述

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 _DA109D2B_te@cpp (064C003h)  
00641793  call        @__CheckForDebuggerJustMyCode@4 (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. 【这里描述的地址是下低上高】
    • 在这里插入图片描述
  • call指令

    • call指令调用的过程

    • 其中%rip 是存放程序计数器的值。 %rsp存放栈指针。

    • 当调用了call之后,将%rsp往下扩展,并在新的栈帧中放入call后的指令的地址,用以返回的时候回到这个地方。同时更改rip的值为call的值。
      -在这里插入图片描述

    • 在这里插入图片描述

  • ret指令

    • 用于返回,其对应于return。
    • 作用是弹出栈顶的值赋值给%rip并将%rsp回收空间

3、反汇编代码的分析与栈帧
在这里插入图片描述

#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)

在这里插入图片描述

  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.在这里插入图片描述

  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()

在这里插入图片描述

VS下的Add就在004516F3的位置又进行了开辟Add函数空间的操作。

而且存在对空间进行初始化rep stos。其中这也是为什么VS用未初始化的空间一般调试出来的是烫烫烫烫烫烫。因为vs一般用该字节码进行空间的初始化。

4、函数栈帧的创建和销毁与C以及C++的联系

比如

  • 函数传参的形参拷贝
  • 函数值返回时的临时变量
  • 对被调用函数临时变量传址返回或者传引用返回的error
    • 该空间地址完全可以被其他指令覆盖,导致错误输出。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值