exit()和C中main()函数的返回之间的区别


Difference between exit() and return in main() function in C

我已经浏览了链接,退出和返回之间有什么区别?和在main()中返回语句vs exit()。找到答案,但徒劳无功。

第一个链接的问题是,答案假设来自任何函数的return。我想知道在main()函数中这两者的确切区别。即使有点不同,我还是想知道它是什么。哪一个是首选,为什么?在关闭所有类型的编译器优化的情况下,使用returnover exit()(或exit()over return)是否有任何性能提高?

第二个链接的问题是我对C++中发生的事情不感兴趣。我想要一个与C有关的答案。

编辑:经过一个人的推荐,我实际上尝试比较以下程序的汇编输出:

注:使用gcc -S .c

程序Mainf.c:

1
2
3
int main(void){
 return 0;
}

程序集输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    .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

主程序1.c:

1
2
3
4
5
#include <stdlib.h>

int main(void){
 exit(0);
}

程序集输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    .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版本短。有什么区别?


免责声明:此答案不引用C标准。

DR

这两种方法都会跳到glibc代码中,为了准确地知道代码在做什么,或者哪个方法更快或更高效,您需要阅读它们。如果您想了解更多关于glibc的信息,应该检查gcc和glibc的来源。最后还有一些链接。

系统调用、包装和glibc

第一:出口(3)和出口(2)有区别。第一个是围绕第二个的glibc包装,这是一个系统调用。我们在程序中使用的,并且需要包含stdlib.h的是exit(3)—glibc包装器,而不是系统调用。

现在,程序不仅仅是简单的指令。它们包含了大量glibc自己的指令。这些glibc函数用于与加载和提供所使用的库功能相关的多个目的。要想让它起作用,glibc必须"在"你的程序中。

那么,glibc是如何进入你的程序的?好吧,它通过编译器把自己放在那里(它设置了一些静态代码和一些动态库的钩子)-很可能您使用的是gcc。

"返回0;"方法

我想你知道什么是堆栈帧,所以我不解释它们是什么。值得注意的是,main()本身有自己的堆栈帧。堆栈帧返回到某个地方,它必须返回…但是,去哪里?

让我们编译以下内容:

1
2
3
4
int main(void)
{
        return 0;
}

并编译和调试它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ 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将成为有趣的部分。这将一次跳转一条指令,因此最好遵循函数调用。当你第一次按run stepi键后,用手指按住enter键直到累为止。

您必须注意的是使用此方法调用函数的顺序。你看,ret是一个"跳转"指令(编辑:在david hoelzer评论之后,我看到调用ret是一个简单的跳转是一个过度概括):在弹出rbp之后,ret本身将从堆栈中弹出返回指针并跳转到它。所以,如果glibc构建了堆栈框架,那么retq将使我们的return 0;c语句直接跳转到glibc自己的代码中!多聪明啊!

函数调用的顺序大致如下:

1
2
3
4
5
6
7
__libc_start_main
exit
__run_exit_handlers
_dl_fini
rtld_lock_default_lock_recursive
_dl_fini
_dl_sort_fini

"exit(0);"方法

编译如下:

1
2
3
4
5
#include <stdlib.h>
int main(void)
{
        exit(0);
}

编译和调试…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ 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
...

我得到的功能序列是:

1
2
3
4
5
6
7
8
9
exit@plt
??
_dl_runtime_resolve
_dl_fixup
_dl_lookup_symbol_x
do_lookup_x
check_match
_dl_name_match
strcmp

列出对象的符号

有一个很酷的工具可以打印二进制文件中定义的符号。它是NM。我建议你仔细研究一下,因为它会让你知道在一个简单的程序中添加了多少"垃圾"。

要以最简单的形式使用它:

1
2
$ nm main
$ nm exit

它将打印文件中的符号列表。请注意,此列表不包含这些函数将生成的引用。因此,如果列表中的给定函数调用另一个函数,则另一个函数可能不在列表中。

结论

这在很大程度上取决于glibc选择如何处理从main返回的简单堆栈帧,以及它如何实现exit包装器。最后,将调用_exit(2)系统调用,您将退出进程。

最后,要真正回答您的问题:这两种方法都会跳到glibc代码中,为了确切地知道代码在做什么,您需要阅读它。如果您想了解更多关于glibc的信息,应该检查gcc和glibc的来源。

工具书类

  • glibc源代码库:查看stdlib/exit.cstdlib/exit.h中的实现。
  • Linux内核出口定义:查看kernel/exit.c中的_exit(2)系统调用实现,以及include/syscalls.h中的预处理器魔力。
  • gcc来源:我不知道EDOCX1(编译器,不是套件)来源,如果有人能指出运行时序列的定义,我会很感激。


只要main返回一个与int兼容的类型,从main调用exit或执行return之间几乎没有区别。

根据C11标准:

5.1.2.2.3 Program termination

1 If the return type of the main function is a type compatible with int, a return from the initial call to the main function is equivalent to calling the exit function with the value returned by the main function as its argument; reaching the } that terminates the main function returns a value of 0. If the return type is not compatible with int, the termination status returned to the host environment is unspecified.


从功能上讲,从main()函数中,c实际上没有区别。例如,即使使用atexit()库调用定义了函数处理程序,来自main的return()exit()都将调用该函数指针。

但是,exit()调用具有灵活性,您可以使用它使程序从代码中的任何点退出返回代码。

存在技术差异。如果将以下内容编译为assembly:

1
2
3
4
int main()
{
  return 1;
}

该代码的最后部分将是:

1
2
3
4
movl $1, %eax
movl $0, -4(%rbp)
popq %rbp
retq

另一方面,以下代码编译为程序集:

1
2
3
4
5
#include<stdlib.h>
int main()
{
  exit(1);
}

各方面均相同,但结尾如下:

1
2
3
4
subq $16, %rsp
movl $1, %edi
movl $0, -4(%rbp)
callq _exit

除了1被放入EDI而不是EAX中(这是在我将此代码编译为_exit调用的调用约定的平台上需要的),您将注意到两个不同之处。首先,进行堆栈对齐操作以准备函数调用。第二,我们现在调用系统库,它将处理最终返回代码并返回,而不是以retq终止。


exit是系统调用,return是语言指令。

exit终止当前进程,return从函数调用返回。

main()函数中,它们都完成了相同的事情:

1
2
3
4
5
6
7
8
9
int main() {
    // code
    return 0;
}

int main() {
    // code
    exit(0);
}

在函数中:

1
2
3
4
5
6
7
8
9
void f() {
    // code
    return; // return to where it was called from.
}

void f() {
    // code
    exit(0); // terminates program
}

main()程序中使用return和调用exit()的一个主要区别是,如果调用exit()程序,main()中的局部变量仍然存在并且是有效的,而如果使用return程序,它们就不是有效的。

如果你做过以下事情,这很重要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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
"
, j*24, j*24+23, space + j * 24);
    }
    printf("Catastrophic elegance
"
);
}

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
"
);
    if (argc > 1)
        return 1;
    else
        exit(2);
}

因为现在从名为main()的启动代码调用的函数(通过atexit()没有标准输出的有效缓冲区。它是崩溃,还是只是彻底混乱,还是打印垃圾,还是看起来起作用,这一点值得商榷。

我打电话给hysteresis。在没有参数的情况下运行时,它使用exit()并正确/正常工作(function_using_stdout()中的本地space变量没有与stdout的I/O缓冲区共享空间):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
$ ./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()中注册的函数的代码正在使用该变量):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
$ ./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
$

大多数时候,这类事情不是问题。然而,当它重要的时候,它真的很重要。而且,请注意,在程序退出之前,它不会作为一个问题出现——这会使调试变得很困难。