![679c22fdd9a6f630b926d0a0dc8df79c.png](https://i-blog.csdnimg.cn/blog_migrate/45e06ecf8570ded7165ce967a442466d.png)
前言
前面讲到了函数的调用,那么现在来说说函数的返回。返回操作就是调用的一个反操作,如果你还不了解调用过程,请移步:
醉卧沙场:简单函数的调用原理zhuanlan.zhihu.com![0f65316fbefcd66fdc1e5df51513cc50.png](https://i-blog.csdnimg.cn/blog_migrate/40b11b2628b0951c93ab4f10d3d28f4d.png)
前情回顾
还以上文中的程序为例:
int
以及其汇编代码:
func.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <func>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 89 75 f8 mov %esi,-0x8(%rbp)
e: 89 55 f4 mov %edx,-0xc(%rbp)
11: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
18: c7 45 f8 c8 00 00 00 movl $0xc8,-0x8(%rbp)
1f: c7 45 f4 2c 01 00 00 movl $0x12c,-0xc(%rbp)
26: 8b 45 fc mov -0x4(%rbp),%eax
29: 89 c6 mov %eax,%esi
2b: bf 00 00 00 00 mov $0x0,%edi
30: b8 00 00 00 00 mov $0x0,%eax
35: e8 00 00 00 00 callq 3a <func+0x3a>
3a: 8b 45 fc mov -0x4(%rbp),%eax
3d: c9 leaveq
3e: c3 retq
000000000000003f <main>:
3f: 55 push %rbp
40: 48 89 e5 mov %rsp,%rbp
43: 48 83 ec 10 sub $0x10,%rsp
47: 89 7d fc mov %edi,-0x4(%rbp)
4a: 48 89 75 f0 mov %rsi,-0x10(%rbp)
4e: ba 03 00 00 00 mov $0x3,%edx
53: be 02 00 00 00 mov $0x2,%esi
58: bf 01 00 00 00 mov $0x1,%edi
5d: e8 00 00 00 00 callq 62 <main+0x23>
62: b8 00 00 00 00 mov $0x0,%eax
67: c9 leaveq
68: c3 retq
上文我们说到了从main函数调用func()函数开始,到正式进入func()函数后,func()函数的栈空间大概是这个样子的:
=== func函数的栈底方向 ===
参数 3 <-- 20(%rbp)
参数 2 <-- 16(%rbp)
参数 1 <-- 12(%rbp)
返回地址 <-- 8(%rbp)
旧%rbp <-- (%rbp)
局部变量1 <-- -4(%rbp)
局部变量2 <-- -8(%rbp)
局部变量3 <-- -12(%rbp)
未使用空间 <-- -16(%rbp) 和 (%rsp)
=== func函数的栈顶方向 ===
函数返回
从11到3e,我们看看func()后面做了什么:
11: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
18: c7 45 f8 c8 00 00 00 movl $0xc8,-0x8(%rbp)
1f: c7 45 f4 2c 01 00 00 movl $0x12c,-0xc(%rbp)
26: 8b 45 fc mov -0x4(%rbp),%eax
29: 89 c6 mov %eax,%esi
2b: bf 00 00 00 00 mov $0x0,%edi
30: b8 00 00 00 00 mov $0x0,%eax
35: e8 00 00 00 00 callq 3a <func+0x3a>
3a: 8b 45 fc mov -0x4(%rbp),%eax
3d: c9 leaveq
3e: c3 retq
11到1f很明显对应:
a = 100;
b = 200;
c = 300;
后面26到35是调用printf的过程,我们这里不分析。
接着3a到3e是重点。3a行将-0x4(%rbp)的内容存放到eax寄存器中。为什么?因为AX寄存器是约定的默认用来保存函数返回值的寄存器。而-0x4(%rbp)我们上文已经说过了,就是临时变量a的位置。我们在func()函数的最后return a,所以这里将a的值保存到eax寄存器中作为返回值。
那func()返回的话应该返回到哪呢?这里当然是返回到调用它的main函数。main函数也是函数,也是有其栈空间的,注意main函数的起始地方也是有栈操作的:
000000000000003f <main>:
3f: 55 push %rbp
40: 48 89 e5 mov %rsp,%rbp
43: 48 83 ec 10 sub $0x10,%rsp
这几行和func()函数的前三行基本一样,main函数在调用func()函数之前栈的可能的情况是这样的:
main的参数(可能有)
main的返回地址
main之前的旧%rbp <-- (%rbp)
main的局部空间1
main的局部空间2
main的局部空间N <-- -16(%rbp) 和 (%rsp)
我们将这个状态的栈设为main状态,然后调用func()后就变成:
main的参数
main的返回地址
main之前的旧%rbp
main的局部空间1
main的局部空间2
main的局部空间N
func的参数 3 <-- 20(%rbp)
func的参数 2 <-- 16(%rbp)
func的参数 1 <-- 12(%rbp)
func返回main时候用的地址 <-- 8(%rbp)
main的%rbp <-- (%rbp)
func的局部变量a <-- -4(%rbp)
func的局部变量b <-- -8(%rbp)
func的局部变量c <-- -12(%rbp)
func的栈顶 <-- -16(%rbp) 和 (%rsp)
我们设这个状态的栈为func状态,主要的变动就是BP和SP寄存器的变动,当然还有IP寄存器。当func函数执行完之后就该返回了,那么就要从func状态回到main状态,怎么做呢?就是3d和3e两行:
3d: c9 leaveq
3e: c3 retq
这两行是func的返回操作,leaveq相当于:
movq %rbp, %rsp
popq %rbp
"movq %rbp, %rsp"让%rsp一下就指回了func的%rbp的位置,也就是现在变成这样:
main的参数
main的返回地址
main之前的旧%rbp
main的局部空间1
main的局部空间2
main的局部空间N
func的参数 3 <-- 20(%rbp)
func的参数 2 <-- 16(%rbp)
func的参数 1 <-- 12(%rbp)
func返回main时候用的地址 <-- 8(%rbp)
main的%rbp <-- (%rbp)和(%rsp)
也就是说func的局部变量空间被直接遗弃了。由于SP是栈指针,它指向的位置就是栈顶,超过它的位置都认为是无意义及未使用的位置。唉,多可怜,func的局部空间就被SP这轻轻一动废弃了。
然后"popq %rbp"这一句相当于动了%rbp和%rsp两个寄存器,注意上面的状态如果再出栈就是让“main的%rbp”值出栈,把这个值赋给%rbp,也就是让BP指回其在main函数栈时应该在的位置。然后%rsp继续向回移动。也就是现在的状态变为:
main的参数
main的返回地址
main之前的旧%rbp
main的局部空间1
main的局部空间2
main的局部空间N
func的参数 3 <-- 20(%rbp)
func的参数 2 <-- 16(%rbp)
func的参数 1 <-- 12(%rbp)
func返回main时候用的地址 <-- 8(%rbp)
这个状态就和main状态差不多了,就差rip没回来了。下面的retq就是让rip回来,让IP指向函数返回后的下一条指令。retq相当于:
popq %rip
从上面的状态可以看到,执行这条命令时%rsp指向的就是保存的返回地址,所以这个出栈动作就是将这个返回地址直接给%rip,然后%rsp再向回移动一下,现在状态变为:
main的参数
main的返回地址
main之前的旧%rbp
main的局部空间1
main的局部空间2
main的局部空间N
func的参数 3 <-- 20(%rbp)
func的参数 2 <-- 16(%rbp)
func的参数 1 <-- 12(%rbp)
到此,如果func()的参数是通过寄存器传递的,那就没有参数3, 2, 1的事了。但是我们假设了参数3, 2, 1是通过栈传递的,那这里就还需要对参数进行一下回收。所谓杀人偿命欠债还钱,谁干的谁来负责收拾,这也是编程的基本原则之一,如果一个函数分配了一段空间,那么尽量让这个函数自己释放这段空间,以免造成申请空间和释放空间的错乱。func的参数应该是调用者,也就是main函数放进栈中的,那么再返回栈后应由main函数来回收。回收只要执行和申请时的反操作就行了,如果申请时用的是sub SP,那么现在就用add SP,如果申请时用的时push,那么现在就用pop。总之将%rsp移动回main的栈顶,变为这样(由于我们的例子程序是使用寄存器传递的参数,所以就没有这个处理直接可以到下面这个状态了):
main的参数(可能有)
main的返回地址
main之前的旧%rbp <-- (%rbp)
main的局部空间1
main的局部空间2
main的局部空间N <-- -16(%rbp) 和 (%rsp)
到此也就完成了func()函数返回的全部动作,栈空间又变回了调用前的main状态。
总结
所以说栈空间的东西是很不牢靠的,SP轻轻动几下就废弃了很多东西,你的局部变量啊,参数啊什么的一下子就没意义了,下次再调用别的就给覆盖了。所以认准每一个变量的生存周期很重要。
到此一个基本的普通函数调用和返回过程就说完了。上面说到了函数返回处理的过程大致如下:
- 如果有返回值,保存到AX寄存器
- 舍弃函数自己的局部变量空间,移动SP
- 原BP地址出栈,重置BP指针,移动SP
- 返回地址出栈,重置IP指针,移动SP
- 如果有参数,舍弃参数空间,移动SP
更多内容请参阅:
醉卧沙场:README - 专栏文章总索引