函数栈帧的创建和销毁

文章目录

  • 1. 前言
  • 2. 前置知识
  • 3. c语言函数调用过程

1. 前言

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

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

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

2. 前置知识

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

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

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

函数栈帧的创建和销毁_第1张图片

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

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

3. c语言函数调用过程

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

#include 

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

函数栈帧的创建和销毁_第2张图片


接着运行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

函数栈帧的创建和销毁_第3张图片


接着运行下面三个指令

   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

函数栈帧的创建和销毁_第4张图片


接着运行下面四条指令

   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

函数栈帧的创建和销毁_第5张图片


接着转入 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

函数栈帧的创建和销毁_第6张图片


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

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

函数栈帧的创建和销毁_第7张图片


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

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

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

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

函数栈帧的创建和销毁_第8张图片


接着执行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.回到最初的情况,程序结束

本章完.

你可能感兴趣的:(linux)