【问题标题】:Difference between exit() and return in main() function in CC中main()函数中exit()和return的区别
【发布时间】:2021-10-22 01:24:11
【问题描述】:

我浏览了链接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 标准的答案,还是对事物的指令级别(内核代码、二进制结构等)感兴趣?
  • 事物的指令级别。

标签: 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
      }
      

      【讨论】:

        猜你喜欢
        • 2014-05-01
        • 2010-10-02
        • 2016-12-08
        • 2012-03-14
        • 2010-10-12
        • 2010-09-29
        • 1970-01-01
        • 2011-09-23
        相关资源
        最近更新 更多