C中main()函数中exit()和return的区别
Posted
技术标签:
【中文标题】C中main()函数中exit()和return的区别【英文标题】:Difference between exit() and return in main() function in C 【发布时间】:2016-05-30 01:16:26 【问题描述】:我浏览了链接What is the difference between exit and return? 和 return statement vs exit() in main() 去寻找答案,却徒劳无功。
第一个链接的问题是答案假定return
来自任何函数。我想知道在 main() 函数中两者之间的确切区别。即使有一点不同,我也想知道它是什么。哪个是首选,为什么?在关闭各种编译器优化的情况下,使用 return
而不是 exit()(或 exit() 而不是 return
)是否有任何性能提升?
第二个链接的问题是我对知道 C++ 中发生的事情不感兴趣。我想要专门针对 C 的答案。
编辑: 在一个人的推荐下,我实际上尝试比较了以下程序的汇编输出:
注意:使用gcc -S <myprogram>.c
程序 mainf.c:
int main(void)
return 0;
汇编输出:
.file "mainf.c"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.9.2-10ubuntu13) 4.9.2"
.section .note.GNU-stack,"",@progbits
程序 mainf1.c:
#include <stdlib.h>
int main(void)
exit(0);
汇编输出:
.file "mainf1.c"
.text
.globl main
.type main, @function
main:
.LFB2:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $0, %edi
call exit
.cfi_endproc
.LFE2:
.size main, .-main
.ident "GCC: (Ubuntu 4.9.2-10ubuntu13) 4.9.2"
.section .note.GNU-stack,"",@progbits
注意到我并不精通汇编,我可以看到两个程序之间的一些差异,exit()
版本比return
版本短。有什么区别?
【问题讨论】:
This answer 第一个问题似乎合适,不是吗? 您是否阅读了第二个问题的其他答案?他们中的一些人专门谈论 C。 您是在寻找符合 C 标准的答案,还是对事物的指令级别(内核代码、二进制结构等)感兴趣? 事物的指令级别。 【参考方案1】:在main()
程序中使用return
和调用exit()
之间的一个主要区别是,如果你调用exit()
,main()
中的局部变量仍然存在并且有效,而如果你调用@987654326 @,他们不是。
如果您做过以下事情,这很重要:
#include <stdio.h>
#include <stdlib.h>
static void function_using_stdout(void)
char space[512];
char *base = space;
for (int j = 0; j < 10; j++)
base += sprintf(base, "Hysterical raisins #%d (continued) ", j+1);
printf("%d..%d: %.24s\n", j*24, j*24+23, space + j * 24);
printf("Catastrophic elegance\n");
int main(int argc, char **argv)
char buffer[64]; // Deliberately rather small
setvbuf(stdout, buffer, _IOFBF, sizeof(buffer));
atexit(function_using_stdout);
for (int i = 0; i < 3; i++)
function_using_stdout();
printf("All done - exiting now\n");
if (argc > 1)
return 1;
else
exit(2);
因为现在从调用main()
的启动代码调用的函数(通过atexit()
)没有标准输出的有效缓冲区。它是崩溃还是仅仅被彻底弄糊涂了、打印出垃圾还是看起来有效,还有待商榷。
我调用了程序hysteresis
。当不带参数运行时,它使用了exit()
并正常/正常工作(function_using_stdout()
中的本地space
变量没有与stdout
的I/O 缓冲区共享空间):
$ ./hysteresis
'hysteresis' is up to date.
0..23: Hysterical raisins #1 (c
24..47: ontinued) Hysterical rai
48..71: sins #2 (continued) Hyst
72..95: erical raisins #3 (conti
96..119: nued) Hysterical raisins
120..143: #4 (continued) Hysteric
144..167: al raisins #5 (continued
168..191: ) Hysterical raisins #6
192..215: (continued) Hysterical r
216..239: aisins #7 (continued) Hy
Catastrophic elegance
0..23: Hysterical raisins #1 (c
24..47: ontinued) Hysterical rai
48..71: sins #2 (continued) Hyst
72..95: erical raisins #3 (conti
96..119: nued) Hysterical raisins
120..143: #4 (continued) Hysteric
144..167: al raisins #5 (continued
168..191: ) Hysterical raisins #6
192..215: (continued) Hysterical r
216..239: aisins #7 (continued) Hy
Catastrophic elegance
0..23: Hysterical raisins #1 (c
24..47: ontinued) Hysterical rai
48..71: sins #2 (continued) Hyst
72..95: erical raisins #3 (conti
96..119: nued) Hysterical raisins
120..143: #4 (continued) Hysteric
144..167: al raisins #5 (continued
168..191: ) Hysterical raisins #6
192..215: (continued) Hysterical r
216..239: aisins #7 (continued) Hy
Catastrophic elegance
All done - exiting now
0..23: Hysterical raisins #1 (c
24..47: ontinued) Hysterical rai
48..71: sins #2 (continued) Hyst
72..95: erical raisins #3 (conti
96..119: nued) Hysterical raisins
120..143: #4 (continued) Hysteric
144..167: al raisins #5 (continued
168..191: ) Hysterical raisins #6
192..215: (continued) Hysterical r
216..239: aisins #7 (continued) Hy
Catastrophic elegance
$
当使用至少一个参数调用时,事情变得一团糟(function_using_stdout()
中的本地 space
变量可能与 stdout
的 I/O 缓冲区共享空间——除非执行的代码正在使用它atexit()
注册的函数):
$ ./hysteresis aleph
0..23: Hysterical raisins #1 (c
24..47: ontinued) Hysterical rai
48..71: sins #2 (continued) Hyst
72..95: erical raisins #3 (conti
96..119: nued) Hysterical raisins
120..143: #4 (continued) Hysteric
144..167: al raisins #5 (continued
168..191: ) Hysterical raisins #6
192..215: (continued) Hysterical r
216..239: aisins #7 (continued) Hy
Catastrophic elegance
0..23: Hysterical raisins #1 (c
24..47: ontinued) Hysterical rai
48..71: sins #2 (continued) Hyst
72..95: erical raisins #3 (conti
96..119: nued) Hysterical raisins
120..143: #4 (continued) Hysteric
144..167: al raisins #5 (continued
168..191: ) Hysterical raisins #6
192..215: (continued) Hysterical r
216..239: aisins #7 (continued) Hy
Catastrophic elegance
0..23: Hysterical raisins #1 (c
24..47: ontinued) Hysterical rai
48..71: sins #2 (continued) Hyst
72..95: erical raisins #3 (conti
96..119: nued) Hysterical raisins
120..143: #4 (continued) Hysteric
144..167: al raisins #5 (continued
168..191: ) Hysterical raisins #6
192..215: (continued) Hysterical r
216..239: aisins #7 (continued) Hy
Catastrophic elegance
Al) Hysterical raisins #2 (continued) l raisins #1 (c
24..47: ontinued) Hysterical rai
48..71: l rai
48..71: nued) Hyst
72..95: 71: nued) Hyst
72..95: 7
96..119: nued) Hysterical raisins
120..143: #4 (continued) Hysteric
144..167: al raisins #5 (continued
168..191: ) Hysterical raisins #6
192..215: (continued) Hysterical r
216..239: aisins #7 (continued) Hy
Catastrophic elegance
$
大多数时候,这类事情不是问题。但是,当它重要时,它确实很重要。而且,请注意,在程序退出之前,它不会作为问题出现 - 这可能会使调试变得棘手。
【讨论】:
【参考方案2】:免责声明:此答案未引用 C 标准。
TL;DR
这两种方法跳转到 GLibC 代码,并且要确切知道该代码在做什么或者哪个更快或更高效,您需要阅读它们.如果您想了解更多关于 GLibC 的信息,您应该查看 GCC 和 GLibC 的源代码。最后有链接。
系统调用、包装器和 GLibC
第一:exit(3) 和_exit(2) 之间存在区别。第一个是GLibC 包装器,第二个是system call。我们在程序中使用并需要包含stdlib.h
的是exit(3)
- GLibC 包装器,不是系统调用。
现在,程序不只是您的简单说明。它们包含大量 GLibC 自己的指令。这些 GLibC 函数有多种用途,与加载和提供您使用的库功能有关。为此,GLibC 必须在您的程序“内部”。
那么,您的程序中的 GLibC 是怎样的?好吧,它通过你的编译器把自己放在那里(它在动态库中设置了一些静态代码和一些钩子)——很可能你正在使用gcc。
'返回 0;'方法
我想你知道stack frames 是什么,所以我不会解释它们是什么。值得注意的是main()
本身有它自己的堆栈框架。并且该堆栈帧返回某处并且它必须返回......但是,到哪里?
让我们编译以下内容:
int main(void)
return 0;
然后编译和调试它:
$ gcc -o main main.c
$ gdb main
(gdb) disass main
Dump of assembler code for function main:
0x00000000004005e8 <+0>: push %rbp
0x00000000004005e9 <+1>: mov %rsp,%rbp
0x00000000004005ec <+4>: mov $0x0,%eax
0x00000000004005f1 <+9>: pop %rbp
0x00000000004005f2 <+10>: retq
End of assembler dump.
(gdb) break main
(gdb) run
Breakpoint 1, 0x00000000004005ec in main ()
(gdb) stepi
...
现在,stepi
将成为有趣的部分。这将一次跳转一条指令,因此非常适合跟随函数调用。在您第一次按下运行stepi
后,只需将手指按住 ENTER 直到您感到疲倦。
您必须注意的是使用此方法调用函数的顺序。你看,ret
是一个“跳转”指令(edit: 在David Hoelzer 评论之后,我看到调用ret
一个简单的跳转是一种过度概括):在我们弹出@987654341 之后@, ret
本身会从堆栈中弹出返回指针并跳转到它。因此,如果 GLibC 构建了该堆栈框架,retq
正在使我们的 return 0;
C 语句直接跳转到 GLibC 自己的代码中!多么聪明!
我上手的函数调用顺序大致是这样的:
__libc_start_main
exit
__run_exit_handlers
_dl_fini
rtld_lock_default_lock_recursive
_dl_fini
_dl_sort_fini
'exit(0);'方法
编译:
#include <stdlib.h>
int main(void)
exit(0);
还有编译调试……
$ gcc -o exit exit.c
$ gdb exit
(gdb) disass main
Dump of assembler code for function main:
0x0000000000400628 <+0>: push %rbp
0x0000000000400629 <+1>: mov %rsp,%rbp
0x000000000040062c <+4>: mov $0x0,%edi
0x0000000000400631 <+9>: callq 0x4004d0 <exit@plt>
End of assembler dump.
(gdb) break main
(gdb) run
Breakpoint 1, 0x000000000040062c in main ()
(gdb) stepi
...
而我得到的函数序列是:
exit@plt
??
_dl_runtime_resolve
_dl_fixup
_dl_lookup_symbol_x
do_lookup_x
check_match
_dl_name_match
strcmp
列出对象的符号
有一个很酷的工具可以打印二进制文件中定义的符号。这是nm。我建议你看看它,因为它会让你知道它在像上面这样的简单程序中添加了多少“废话”。
以最简单的形式使用它:
$ nm main
$ nm exit
这将打印文件中的符号列表。请注意,此列表不包含这些函数将产生的引用。因此,如果此列表中的给定函数调用另一个函数,则另一个可能不会在列表中。
结论
这在很大程度上取决于 GLibC 选择处理从 main
返回的简单堆栈帧的方式以及它如何实现 exit
包装器。最后,_exit(2)
系统调用将被调用,您将退出您的进程。
最后,要真正回答您的问题:这两种方法都会跳转到 GLibC 代码中,并且要确切知道该代码在做什么,您需要阅读它。如果您想了解更多关于 GLibC 的信息,您应该查看 GCC 和 GLibC 的来源。
参考文献
GLibC Source Repository:在stdlib/exit.c
和 stdlib/exit.h
中查看实现。
Linux Kernel Exit Definition:在kernel/exit.c
中查看_exit(2)
系统调用实现,在include/syscalls.h
中查看其背后的预处理器魔法。
GCC Sources:我不知道gcc
(编译器,不是套件)的源代码,如果有人能指出运行时序列的定义位置,我将不胜感激。
【讨论】:
虽然这个答案很冗长,但它抓住了基本的实现差异。请注意,“查看 C 运行时以查看 return 的作用”对于 gcc 以外的其他工具集也是如此。call
进入 glibc 和 ret
使用堆栈上的返回指针返回 glibc 对我来说不仅仅是一个细微的区别。我真的很犹豫打电话给ret
jump 进入 libc。
@DavidHoelzer 我没有说ret
是对libc 的跳转。我说在这些情况下它会跳入 libc。在rbp
弹出后,ret
会弹出返回地址并跳转。但是,我很好奇您如何定义那些ret
s。你能解释一下吗?
@DavidHoelzer 我想我明白你的意思了。我编辑了答案,你看一下现在是否更清楚?
好吧,我只是不会这样说:“这两种方法都跳转到 GLibC 代码中,并且要确切知道该代码在做什么或者哪个更快或更高效,您将需要阅读它们”【参考方案3】:
只要main
返回与int
兼容的类型,调用exit
或从main
执行return
几乎没有区别。
来自 C11 标准:
5.1.2.2.3 程序终止
1 如果
main
函数的返回类型是与int
兼容的类型,则从初始调用main
函数返回相当于调用exit
函数的返回值main
函数作为它的参数;到达终止main
函数的返回值0。如果返回类型与
int
不兼容,则返回宿主环境的终止状态未指定。
【讨论】:
生成的程序集其实有细微的差别。 @DavidHoelzer,我应该强调等效。【参考方案4】:从功能上讲,与 main()
函数相比,C 中确实没有区别。例如,即使您使用 atexit()
库调用定义了函数处理程序,来自 main 的 return()
和 exit()
都会调用它函数指针。
但是,exit()
调用具有灵活性,您可以使用它使程序从代码中的任何位置退出并返回代码。
存在技术差异。如果将以下内容编译为程序集:
int main()
return 1;
该代码的最后部分将是:
movl $1, %eax
movl $0, -4(%rbp)
popq %rbp
retq
另一方面,以下代码编译为汇编:
#include<stdlib.h>
int main()
exit(1);
在所有方面都是相同的,只是它的结尾如下:
subq $16, %rsp
movl $1, %edi
movl $0, -4(%rbp)
callq _exit
除了将 1 放入 EDI
而不是 EAX
在我编译此代码作为 _exit
调用的调用约定的平台上,您会注意到两个不同之处。首先,进行堆栈对齐操作以准备函数调用。其次,我们现在不是以retq
结束,而是调用系统库,它将处理最终的返回码并返回。
【讨论】:
【参考方案5】:exit
是系统调用,return
是语言指令。
exit
终止当前进程,return
从函数调用返回。
在main()
函数中,它们都完成了同样的事情:
int main()
// code
return 0;
int main()
// code
exit(0);
在函数中:
void f()
// code
return; // return to where it was called from.
void f()
// code
exit(0); // terminates program
【讨论】:
以上是关于C中main()函数中exit()和return的区别的主要内容,如果未能解决你的问题,请参考以下文章