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.cstdlib/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会弹出返回地址并跳转。但是,我很好奇您如何定义那些rets。你能解释一下吗? @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的区别的主要内容,如果未能解决你的问题,请参考以下文章

c语言exit和return有啥区别

c语言exit和return的区别

exit 和return的区别

c中的exit()函数的作用,是退出程序还是跳出函数?

C语言中exit();怎么用?

C语言中exit(0)与exit(1)有啥区别??