返回局部变量或临时变量的地址_简单函数的返回原理

679c22fdd9a6f630b926d0a0dc8df79c.png

前言

前面讲到了函数的调用,那么现在来说说函数的返回。返回操作就是调用的一个反操作,如果你还不了解调用过程,请移步:

醉卧沙场:简单函数的调用原理​zhuanlan.zhihu.com
0f65316fbefcd66fdc1e5df51513cc50.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轻轻动几下就废弃了很多东西,你的局部变量啊,参数啊什么的一下子就没意义了,下次再调用别的就给覆盖了。所以认准每一个变量的生存周期很重要。

到此一个基本的普通函数调用和返回过程就说完了。上面说到了函数返回处理的过程大致如下:

  1. 如果有返回值,保存到AX寄存器
  2. 舍弃函数自己的局部变量空间,移动SP
  3. 原BP地址出栈,重置BP指针,移动SP
  4. 返回地址出栈,重置IP指针,移动SP
  5. 如果有参数,舍弃参数空间,移动SP

更多内容请参阅:

醉卧沙场:README - 专栏文章总索引

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值