上文http://blog.csdn.net/zr_lang/article/details/39962359我们说到了函数调用,有调用就要有返回,下面我们说函数的返回。首先为了方便先把代码粘贴到这里:
1#include 2void tmp_print(int a, int b, int c){ 3 printf ("a=%d, b=%d, c=%d\n", a, b, c); 4} 5 6int main(int argc, char *argv[]){ 7 int a=0, b=1, c=33; 8 tmp_print(a, b, c); 9 return 0; 10}
这个程序编译成目标文件,然后反汇编看到的代码段为:
Disassembly of section .text: 0000000000000000: 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: 8b 4d f4 mov -0xc(%rbp),%ecx 14: 8b 55 f8 mov -0x8(%rbp),%edx 17: 8b 45 fc mov -0x4(%rbp),%eax 1a: 89 c6 mov %eax,%esi 1c: bf 00 00 00 00 mov $0x0,%edi 21: b8 00 00 00 00 mov $0x0,%eax 26: e8 00 00 00 00 callq 2b 2b: c9 leaveq 2c: c3 retq 000000000000002d: 2d: 55 push %rbp 2e: 48 89 e5 mov %rsp,%rbp 31: 48 83 ec 20 sub $0x20,%rsp 35: 89 7d ec mov %edi,-0x14(%rbp) 38: 48 89 75 e0 mov %rsi,-0x20(%rbp) 3c: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 43: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp) 4a: c7 45 f4 21 00 00 00 movl $0x21,-0xc(%rbp) 51: 8b 55 f4 mov -0xc(%rbp),%edx 54: 8b 4d f8 mov -0x8(%rbp),%ecx 57: 8b 45 fc mov -0x4(%rbp),%eax 5a: 89 ce mov %ecx,%esi 5c: 89 c7 mov %eax,%edi 5e: e8 00 00 00 00 callq 63 63: b8 00 00 00 00 mov $0x0,%eax 68: c9 leaveq 69: c3 retq
在函数tmp_print被调用后栈的情况是
返回地址 <-- 8(%rbp)
旧%rbp <-- (%rbp)
局部变量1 <-- -4(%rbp)局部变量2 <-- -8(%rbp)
局部变量3 <-- -12(%rbp)
未使用空间 <-- -16(%rbp) 和 (%rsp)
为了说明参数通过栈传递的情况,我们假设了参数a,b,c是通过push入栈的(这点和上面的代码不太一样,上面是通过寄存器传递的),那么栈的情况就是:
===栈底方向===
参数 c <-- 32(%rbp)
参数 b <-- 24(%rbp)
参数 a <-- 16(%rbp)
返回地址 <-- 8(%rbp)
旧%rbp <-- (%rbp)
局部变量1 <-- -4(%rbp)局部变量2 <-- -8(%rbp)
局部变量3 <-- -12(%rbp)
未使用空间 <-- -16(%rbp) 和 (%rsp)
===栈顶方向===
好了,下面我们继续,以上内容如有不懂请参考上一篇文章。上面描述的是tmp_print所使用的栈的情况,那么tmp_print函数返回后会回到哪呢?这里是返回到main函数,main函数也是函数,注意观察main函数的起始代码,人家也是有栈空间的:
2d: 55 push %rbp 2e: 48 89 e5 mov %rsp,%rbp 31: 48 83 ec 20 sub $0x20,%rsp这几行和tmp_print函数的前三行基本一样,main函数在调用tmp_print函数之前栈的情况是这样的:
main的参数(可能有)
main的返回地址
main之前的旧%rbp <-- (%rbp)
main的局部空间1
main的局部空间2
main的局部空间N <-- -32(%rbp) 和 (%rsp)
我们将这个状态设为main状态,然后调用tmp_print后就变成:
main的参数
main的返回地址
main之前的旧%rbp
main的局部空间1
main的局部空间2
main的局部空间N
tmp_print的参数 c <-- 32(%rbp)
tmp_print的参数 b <-- 24(%rbp)
tmp_print的参数 a <-- 16(%rbp)
tmp_print返回main的地址 <-- 8(%rbp)
main的%rbp <-- (%rbp)
tmp_print的局部变量1 <-- -4(%rbp)
tmp_print的局部变量2 <-- -8(%rbp)
tmp_print的局部变量3 <-- -12(%rbp)
tmp_print的局部空间 <-- -16(%rbp) 和 (%rsp)
我们设这个状态为tmp_print状态,主要的变动就是BP和SP寄存器的变动,当然还有IP寄存器。当tmp_print函数执行完之后就该返回了,那么就要从tmp_print状态回到main状态,怎么做呢?就是调用时候的反操作。
2b: c9 leaveq 2c: c3 retq这两行是tmp_print的返回操作,leaveq相当于:
movq %rbp, %rsp
popq %rbp
movq %rbp, %rsp让%rsp一下就指回了tmp_print的%rbp的位置,也就是现在变成这样:
main的参数main的返回地址
main之前的旧%rbp
main的局部空间1
main的局部空间2
main的局部空间N
tmp_print的参数 c <-- 32(%rbp)
tmp_print的参数 b <-- 24(%rbp)
tmp_print的参数 a <-- 16(%rbp)
tmp_print返回main的地址 <-- 8(%rbp)
main的%rbp <-- (%rbp)和(%rsp)
也就是说tmp_print的局部变量空间被放弃了,由于SP是栈指针,它指向的位置就是栈顶,超过它的位置都认为是栈中无意义及未使用的位置。唉,多可怜,tmp_print的局部空间就被SP这轻轻一动废弃了。然后
popq %rbp
这一句相当于动了%rbp和%rsp两个寄存器,注意上面的状态如果再出栈就是让“main的%rbp”值出栈,把这个值赋给%rbp,然后%rsp继续向回移动。也就是现在的状态变为:
main的参数
main的返回地址
main之前的旧%rbp <-- (%rbp)
main的局部空间1
main的局部空间2
main的局部空间N
tmp_print的参数 c
tmp_print的参数 b
tmp_print的参数 a
tmp_print返回main的地址 <-- (%rsp)
这个状态就和main状态差不多了,就差rsp没回来了。下面的retq就是让rsp回来,让IP指向函数返回后的下一条指令。retq相当于:
popq %rip
因为上面的状态可以看到,执行这条命令时%rsp指向的就是保存的返回地址,所以这个出栈动作就是将这个返回地址直接给%rip,然后%rsp再向回移动一下,现在状态变为:
main的参数
main的返回地址
main之前的旧%rbp <-- (%rbp)
main的局部空间1
main的局部空间2
main的局部空间N
tmp_print的参数 c
tmp_print的参数 b
tmp_print的参数 a <-- (%rsp)
我们前面说了,tmp_print的参数是main函数传递的,如果是通过寄存器传递的这里就已经让%rsp回到main函数的栈顶了,但是如果是通过栈传递的参数,那么返回后还得回收一下这些栈。有人可能问,现在已经返回main函数了,tmp_print怎么回收这些参数空间。所谓杀人偿命欠债还钱,谁干的谁来负责收拾,这也是编程的基本原则之一,如果一个函数分配了一段空间,那么尽量让这个函数自己释放这段空间,以免造成申请空间和释放空间的错乱。通过tmp_print的参数应该是调用者,也就是main函数放进栈中的,那么再返回栈后应由main函数来回收。回收只要执行和申请时的反操作就行了,如果申请时用的是sub SP,那么现在就用add SP,如果申请时用的时push,那么现在就用pop。总之将%rsp移动回main的栈顶,变为这样:
main的参数(可能有)
main的返回地址
main之前的旧%rbp <-- (%rbp)
main的局部空间1
main的局部空间2
main的局部空间N <-- -32(%rbp) 和 (%rsp)
到此也就完成了tmp_print函数返回的全部动作,栈空间又变回了调用前的状态。所以说栈空间的东西是很不牢靠的,SP轻轻动几下就废弃了很多东西,你的局部变量啊,参数啊什么的一下子就没意义了,下次再调用别的就给覆盖了。所以认准每一个变量的生存周期很重要。
最后还有一个要说的就是返回值,由于我没给tmp_print函数设置返回值,所以从它就看不出来了。但是可以从main函数看出一下端倪。main函数最后:
63: b8 00 00 00 00 mov $0x0,%eax68: c9 leaveq
69: c3 retq
除了熟悉的,上面说过的leaveq和retq以外还有mov $0x0,%eax。这个就是将返回值放到AX寄存器中,也就是说AX寄存器是用来保存函数返回值的。
到此一个基本的普通函数调用和返回过程就说完了。