return和exit
标签(空格分隔): APUE
在每个函数结束之后,习惯上都会return一下。而在main函数里面,既可以用return,也可以用exit来结束进程。现在来看看return到底做了什么。
先写一个简单的C程序:
#include<stdio.h>
int add(int ,int);
int main(){
int a = 1;
int b = 2;
int c = add(a, b);
return 1;
}
int add(int a, int b){
int c =a + b;
return c;
}
然后反汇编,看看汇编的东西:
080483ed <main>:
80483ed: 55 push %ebp
80483ee: 89 e5 mov %esp,%ebp
80483f0: 83 e4 f0 and $0xfffffff0,%esp
80483f3: 83 ec 20 sub $0x20,%esp
80483f6: c7 44 24 14 01 00 00 movl $0x1,0x14(%esp)
80483fd: 00
80483fe: c7 44 24 18 02 00 00 movl $0x2,0x18(%esp)
8048405: 00
8048406: 8b 44 24 18 mov 0x18(%esp),%eax
804840a: 89 44 24 04 mov %eax,0x4(%esp)
804840e: 8b 44 24 14 mov 0x14(%esp),%eax
8048412: 89 04 24 mov %eax,(%esp)
8048415: e8 0b 00 00 00 call 8048425 <add>
804841a: 89 44 24 1c mov %eax,0x1c(%esp)
804841e: b8 00 00 00 00 mov $0x0,%eax
8048423: c9 leave
8048424: c3 ret
08048425 <add>:
8048425: 55 push %ebp
8048426: 89 e5 mov %esp,%ebp
8048428: 83 ec 10 sub $0x10,%esp
804842b: 8b 45 0c mov 0xc(%ebp),%eax
804842e: 8b 55 08 mov 0x8(%ebp),%edx
8048431: 01 d0 add %edx,%eax
8048433: 89 45 fc mov %eax,-0x4(%ebp)
8048436: 8b 45 fc mov -0x4(%ebp),%eax
8048439: c9 leave
804843a: c3 ret
其他的不用管,只要看两个程序的结尾,可以看到,两个程序最后面的内容。后面如果是有返回值的return,则相当于如下三条指令:
804841e: b8 00 00 00 00 mov $0x0,%eax
8048423: c9 leave
8048424: c3 ret
第一条指令是给eax寄存器附上返回值,后面的两条指令,相当于:
mov %ebp,%esp
pop %ebp
pop %rip
稍微有一点寄存器基础的,就知道,这三条指令是函数栈的回收。即return就是函数栈的回收指令。
下面再来看下exit。
和上面一样的程序,只是在main里面用exit(0)来退出程序。反汇编得到的汇编如下:
0804841d <main>:
804841d: 55 push %ebp
804841e: 89 e5 mov %esp,%ebp
8048420: 83 e4 f0 and $0xfffffff0,%esp
8048423: 83 ec 20 sub $0x20,%esp
8048426: c7 44 24 14 01 00 00 movl $0x1,0x14(%esp)
804842d: 00
804842e: c7 44 24 18 02 00 00 movl $0x2,0x18(%esp)
8048435: 00
8048436: 8b 44 24 18 mov 0x18(%esp),%eax
804843a: 89 44 24 04 mov %eax,0x4(%esp)
804843e: 8b 44 24 14 mov 0x14(%esp),%eax
8048442: 89 04 24 mov %eax,(%esp)
8048445: e8 10 00 00 00 call 804845a <add>
804844a: 89 44 24 1c mov %eax,0x1c(%esp)
804844e: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048455: e8 a6 fe ff ff call 8048300 <exit@plt>
可以看到和上面不一样的是:
804844e: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048455: e8 a6 fe ff ff call 8048300 <exit@plt>
他把esp所指向的地址里面的内容改为了0,然后调用exit。这里和上面的return不一样的是,他没有摧毁栈,然后返回上一级函数。
而exit()的作用是调用_exit()或者_Exit()系统调用,来直接结束进程。
再来说下atexit()函数。
int atexit(void (*func) (void));
通俗来说,atexit函数是一个终止处理程序,他登记那些想在退出main函数之后,再执行的函数,这些函数会在exit()函数中被调用。这里要注意两点:1.登记的函数只能是无输入无输出的函数。2.最后在exit函数中,调用终止处理程序的顺序和登记的时候是相反的。
关于第二点函数执行顺序的原因说一下自己的猜想:感觉应该是在atexit函数执行的时候,只是把函数指针压入exit的函数栈里面,所以根据栈的特性,最后的执行顺序和压入顺序是反的。
看一下例子:
#include<stdio.h>
#include<stdlib.h>
void print1(){
printf("exit1\n");
}
void print2()
{
printf("exit2\n");
}
int main(){
atexit(print1);
atexit(print1);
atexit(print2);
return 0;
}
结果是:
可以看到他的执行顺序是反过来的。而且注意,在C程序里面,最后的返回是用的return而不是exit。而终止处理程序实在exit函数里面掉用的。所以简单来说,在main函数里面return到了main函数的上层函数,在上层函数里面依旧调用了exit函数。
总结return和exit:
return只是一个销毁函数栈的指令,在销毁函数栈之后,会调到调用函数中去。而exit是三个作用:执行终止处理程序(注意执行顺序),清理标准I/O,调用_exit或者_Exit系统调用,结束进程。
而在非main函数中,一般使用return,因为exit直接就销毁进程了。
在main函数中,return会把main的栈给销毁掉,在回到上层调用函数,在上层调用函数里面会执行exit()函数来销毁进程。exit就不会销毁main的栈,而是直接就销毁进程。