函数栈帧的创建和销毁

1. 前言

在我们前期学习C语言时,可能会有很多疑问 ? 比如:

  • 局部变量是怎么创建的?
  • 为什么未初始化的局部变量的值是随机值?
  • 函数是怎样传参的?传参的顺序是怎样的?
  • 形参和实参是什么关系?
  • 函数调用是怎样做的?
  • 函数调用后是怎样返回的?

本章将在汇编层面讨论解释相关问题,环境为 CentOS7.6, 使用编译器是 GCC, 使用调试器为 GDB

2. 前置知识

我们都知道CPU中的寄存器,本章主要涉及的是

寄存器名称用途
eax通用寄存器,保留临时数据,常用于返回值
ebx通用寄存器,保留临时数
ebp栈底寄存器
esp栈顶寄存器
eip指令寄存器,保存当前指令的下一条指令的地址

其中重点关注的是 espebp 这两个寄存器, 两个寄存器存放的都是地址, ebp存放栈底地址, esp存放栈顶地址,两个寄存器用来维护栈帧空间,这块空间是在虚拟内存中栈空间中的.

在这里插入图片描述

同时因为涉及汇编代码,我们也简单了解一下一点汇编的指令

汇编指令用途
mov数据转移指令
push数据入栈,同时esp栈顶寄存器也要发生变化
pop数据弹出至指定位置,同时esp栈顶寄存器也要发生变化
sub减法命令
add加法命令
call函数调用 1.压入返回地址;2.转入目标函数
jump通过修改eip,转入目标函数,进行调用
ret恢复返回地址,压入eip,类似pop eip命令

3. c语言函数调用过程

为了观察到程序运行时,栈空间是如何创建,espebp这两个寄存器是如何维护的等等问题,这里,我创建了一个用来得到两数相加和的程序.

#include <stdio.h>

int Add(int num1, int num2)
{
    return num1 + num2;
}

int main(void)
{
    int a = 2;
    int b = 3;
    int c = 0;

    c = Add(a, b);

    printf("%d\n", c);

    return 0;
}

通过 gcc 编译这个程序得到可执行程序test,我们使用 gdb 对该程序进行调试,首先disass main反汇编main函数, 得到 main 函数第一条指令的地址为0x0000000000400541.然后使用这个地址作为断点,从头开始运行程序.

[root@VM-4-13-centos test]# gcc -g test.c -o test
[root@VM-4-13-centos test]# gdb test
(gdb) disass main
Dump of assembler code for function main:
   0x0000000000400541 <+0>:	    push   %rbp
   0x0000000000400542 <+1>:	    mov    %rsp,%rbp
   0x0000000000400545 <+4>:	    sub    $0x10,%rsp
   0x0000000000400549 <+8>:	    movl   $0x2,-0x4(%rbp)
   0x0000000000400550 <+15>:	movl   $0x3,-0x8(%rbp)
   0x0000000000400557 <+22>:	movl   $0x0,-0xc(%rbp)
   0x000000000040055e <+29>:	mov    -0x8(%rbp),%edx
   0x0000000000400561 <+32>:	mov    -0x4(%rbp),%eax
   0x0000000000400564 <+35>:	mov    %edx,%esi
   0x0000000000400566 <+37>:	mov    %eax,%edi
   0x0000000000400568 <+39>:	callq  0x40052d <Add>
   0x000000000040056d <+44>:	mov    %eax,-0xc(%rbp)
   0x0000000000400570 <+47>:	mov    -0xc(%rbp),%eax
   0x0000000000400573 <+50>:	mov    %eax,%esi
   0x0000000000400575 <+52>:	mov    $0x400620,%edi
   0x000000000040057a <+57>:	mov    $0x0,%eax
   0x000000000040057f <+62>:	callq  0x400410 <printf@plt>
   0x0000000000400584 <+67>:	mov    $0x0,%eax
   0x0000000000400589 <+72>:	leaveq 
   0x000000000040058a <+73>:	retq   
End of assembler dump.
(gdb) b *0x0000000000400541
Breakpoint 1 at 0x400541: file test.c, line 9.
(gdb) r
Starting program: /root/test/test 


在进入main函数之前,我们观察rip, rbp, rsp的内容.

  • rip记录了下一条指令的地址即main函数的第一条指令的地址.
  • rbp的值是0x0,说明这个时候还没有创建栈帧
  • rsp的值是0x7fffffffe468,这是栈顶地址
Breakpoint 1, main () at test.c:9
(gdb) i r rsp rip rbp
rsp            0x7fffffffe468	0x7fffffffe468
rip            0x400541	0x400541 <main>
rbp            0x0	0x0

在这里插入图片描述


接着运行main函数的指令,首先前三句是函数序言,主要作用是保存rbp以及为当前函数分配栈空间

(gdb) disass
Dump of assembler code for function main:
=> 0x0000000000400541 <+0>:	push   %rbp             ;将rbp的内容压入栈中进行保存
   0x0000000000400542 <+1>:	mov    %rsp,%rbp        ;将rsp的内容传递给rbp(此时原来的栈顶为栈底)
   0x0000000000400545 <+4>:	sub    $0x10,%rsp       ;分配主函数需要的栈空间

首先0x0000000000400541 <+0>: push %rbp:将rbp的内容压入栈中,同时rsp-8,此时rsp的值为0x7fffffffe460
接着0x0000000000400542 <+1>: mov %rsp,%rbp :将rsp此时的内容传递给rbp,开辟栈空间,此时栈底地址 rbp 为之前栈顶的地址0x7fffffffe460
最后0x0000000000400545 <+4>: sub $0x10,%rsp:主函数需要用到0x10的栈空间,将 rsp 减去这个值, 此时 栈顶地址 rsp 为 0x7fffffffe450


三条指令完成后,观察三个寄存器的状态

(gdb) i r rip rsp rbp
rip            0x400549	0x400549 <main+8>
rsp            0x7fffffffe450	0x7fffffffe450
rbp            0x7fffffffe460	0x7fffffffe460

在这里插入图片描述


接着运行下面三个指令

   0x0000000000400549 <+8>:	    movl   $0x2,-0x4(%rbp)
   0x0000000000400550 <+15>:	movl   $0x3,-0x8(%rbp)
   0x0000000000400557 <+22>:	movl   $0x0,-0xc(%rbp)

分别将 a, b, c 的值压入存放在之前开辟的栈空间内,观察内存,确实存放了进去

(gdb) x/ 0x7fffffffe45c
0x7fffffffe45c:	2
(gdb) x/ 0x7fffffffe458
0x7fffffffe458:	3
(gdb) x/ 0x7fffffffe454
0x7fffffffe454:	0

在这里插入图片描述


接着运行下面四条指令

   0x000000000040055e <+29>:	mov    -0x8(%rbp),%edx
   0x0000000000400561 <+32>:	mov    -0x4(%rbp),%eax
   0x0000000000400564 <+35>:	mov    %edx,%esi
   0x0000000000400566 <+37>:	mov    %eax,%edi

这是调用函数前的准备操作,将第一个参数的值存入esi,第二个参数的值存入edi.这就是为什么说形参是实参的一份临时拷贝.


参数准备好了之后,接着执行 call 指令调用 Add 函数

0x0000000000400568 <+39>:	callq  0x40052d <Add>

call 指令, 执行的时候会先将此时 rip 存放的地址 即下一条指令的地址压入栈中,以便返回函数继续执行下一条指令
在本程序, 即将0x40056d压入栈中

(gdb) x/x 0x7fffffffe448
0x7fffffffe448:	0x0040056d

在这里插入图片描述


接着转入 Add 函数

   0x000000000040052d <+0>:	    push   %rbp
   0x000000000040052e <+1>:	    mov    %rsp,%rbp
   0x0000000000400531 <+4>:	    mov    %edi,-0x4(%rbp)
   0x0000000000400534 <+7>: 	mov    %esi,-0x8(%rbp)
   0x0000000000400537 <+10>:	mov    -0x8(%rbp),%eax
   0x000000000040053a <+13>:	mov    -0x4(%rbp),%edx
   0x000000000040053d <+16>:	add    %edx,%eax
   0x000000000040053f <+18>:	pop    %rbp
   0x0000000000400540 <+19>:	retq  

一样先是函数序言,将rbp的值压入栈中,同时rsp-8,因为在函数中没有创建临时变量,此时rsprbp的值都为0x7fffffffe440

(gdb) x/x 0x7fffffffe440
0x7fffffffe440:	0xffffe460
(gdb) i r rsp rbp
rsp            0x7fffffffe440	0x7fffffffe440
rbp            0x7fffffffe440	0x7fffffffe440

在这里插入图片描述


接着将两个参数寄存器的值压入栈中

   0x0000000000400531 <+4>:	    mov    %edi,-0x4(%rbp)
   0x0000000000400534 <+7>: 	mov    %esi,-0x8(%rbp)

在这里插入图片描述


然后执行相加指令,并把结果放入返回值寄存器eax

   0x000000000040053d <+16>:	add    %edx,%eax

最后函数结束,归还栈空间

  1. 将当前rsp所指内存中的值0x7fffffffe460放入rbp寄存器,这样,rbp就恢复到了还未执行 Add 函数时的值,也就是 main 函数的栈帧起始地址
  2. 将rsp寄存器的值加8, 这样rsp就指向了包含 Add 函数下一条指令地址0x000000000040056d的内存
   0x000000000040053f <+18>:	pop    %rbp

在这里插入图片描述


接着执行retq指令,该指令会把 rsp 指向的栈空间的 0x000000000040056d去给 rip,同时rsp - 8 为 0x7fffffffe450
注意此时返回值寄存器 eax 的值为 5

(gdb) i r eax
eax            0x5	5

回到主函数,继续执行 printf 函数, 操作相似这里就不过多赘述.
最后主函数结束时,rbp 取到当前 rbp指向的内存空间的值, rsp重新加上 0x10,最后再加上0x8.回到最初的情况,程序结束

本章完.
d去给 rip,同时rsp - 8 为 0x7fffffffe450`
注意此时返回值寄存器 eax 的值为 5

(gdb) i r eax
eax            0x5	5

回到主函数,继续执行 printf 函数, 操作相似这里就不过多赘述.
最后主函数结束时,rbp 取到当前 rbp指向的内存空间的值, rsp重新加上 0x10,最后再加上0x8.回到最初的情况,程序结束

本章完.

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值