简体   繁体   English

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

[英]Difference between exit() and return in main() function in C

I've looked through the links What is the difference between exit and return? 我查看了链接退出和退货有什么区别? and return statement vs exit() in main() to find the answer, but in vain. 在main()中返回语句vs exit()来查找答案,但是徒劳无功。

Problem with the first link is that the answer assumes return from any function. 第一个链接的问题是答案假定从任何函数return I want to know the exact difference between the two when in main() function. 我想知道在main()函数中两者之间的确切差异。 Even if there's a little difference I'd like to know what it is. 即使有一点点差异,我也想知道它是什么。 Which is preferred and why? 哪个是首选,为什么? Is there any performance gain in using return over exit() (or exit() over return ) with all sorts of compiler optimizations turned off? 在关闭各种编译器优化的情况下,使用return over exit()(或exit() return )是否有任何性能提升?

Problem with the second link is I'm not interested in knowing what happens in C++. 第二个链接的问题是我不想知道C ++中发生了什么。 I want the answer specifically pertaining to C. 我想要特别关于C的答案。

EDIT: After recommendation by a person, I actually tried to compare the assembly output of the following programs: 编辑:经过一个人的推荐,我实际上试图比较以下程序的汇编输出:

Note: Using gcc -S <myprogram>.c 注意:使用gcc -S <myprogram>.c

Program mainf.c: 程序mainf.c:

int main(void){
 return 0;
}

Assembly output: 装配输出:

    .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

Program mainf1.c: 程序mainf1.c:

#include <stdlib.h>

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

Assembly output: 装配输出:

    .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

Noting that I'm not well versed with assembly, I can see some differences between the 2 programs with the exit() version being shorter than return version. 注意到我不熟悉汇编,我可以看到两个程序之间的差异, exit()版本比return版本短。 What's the difference? 有什么不同?

Disclaimer: This answer does not quote the C Standards. 免责声明:这个答案没有引用C标准。

TL;DR TL; DR

Both the methods jump into GLibC code , and to know exactly what that code is doing or which one is faster or more efficient, you'll need to read them. 这两种方法都会跳转GLibC代码中 ,并且为了确切地知道代码正在做什么或哪个代码更快或更高效,您需要阅读它们。 If you want to know more about the GLibC, you should check the sources for the GCC and GLibC. 如果您想了解有关GLibC的更多信息,您应该检查GCC和GLibC的来源。 There are links in the end for those. 最后有链接。


Syscalls, wrappers and GLibC Syscalls,包装和GLibC

First: there's a difference between exit(3) and _exit(2) . 第一: exit(3)_exit(2)之间有区别。 The first is a GLibC wrapper around the second, which is a system call . 第一个是围绕第二个的GLibC包装器,这是一个系统调用 The one we use in our program, and requires the inclusion of stdlib.h is exit(3) - the GLibC wrapper, not the system call. 我们在程序中使用的那个,需要包含stdlib.hexit(3) - GLibC包装器, 而不是系统调用。

Now, programs are not just your simple instructions. 现在,节目只是简单的指令。 They contain heavy loads of GLibC's own instructions . 它们包含大量GLibC 自己的指令 These GLibC functions serve several purposes related to loading and providing the library functionality you use. 这些GLibC函数有多种与加载和提供您使用的库功能相关的用途。 For that to work GLibC must be "inside" your program. 为了工作,GLibC必须在你的程序“内部”。

So, how is GLibC inside your program? 那么,GLibC如何在你的程序中? Well, it puts itself there through your compiler (it sets some static code and some hooks into the dynamic library) - most likely you're using gcc . 好吧,它通过你的编译器(它设置一些静态代码和一些钩子到动态库)放在那里 - 很可能你正在使用gcc


The 'return 0;' '返回0;' method 方法

I suppose you know what stack frames are, so I won't explain what they are. 我想你知道堆栈帧是什么,所以我不会解释它们是什么。 The cool thing to notice is that main() itself has it's own stack frame. 值得注意的是, main()本身拥有自己的堆栈框架。 And that stack frame returns somewhere and it must return... But, to where ? 那个堆栈框架返回某个地方,它必须返回...但是,到哪里

Lets compile the following: 让我们编译以下内容:

int main(void)
{
        return 0;
}

And compile and debug it with: 并编译和调试它:

$ 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
...

Now, stepi will make for the fun part. 现在, stepi将为有趣的部分做出贡献。 This will jump one instruction at a time, so it's perfect to follow function calls. 这将一次跳转一条指令 ,因此完全跟随函数调用。 After you press run stepi for the first time, just hold your finger on ENTER until you get tired. 在您第一次按下stepi后,只需按住ENTER键直到您感到疲倦。

What you must observe is the sequence in which functions are called with this method. 您必须注意的是使用此方法调用函数的顺序。 You see, ret is a "jumping" instruction ( edit: after David Hoelzer comment, I see that calling ret a simple jump is an over-generalization): after we pop rbp , ret itself will pop the return pointer from the stack and jump to it. 你看, ret是一个“跳跃”指令( 编辑:David Hoelzer评论之后,我看到调用ret一个简单的跳转是一种过度泛化):在我们弹出rbpret本身将从堆栈中弹出返回指针并跳转它。 So, if GLibC built that stack frame, retq is making our return 0; 所以,如果GLibC构建了那个堆栈帧, retq会使我们return 0; C statement jump right into GLibC's own code! C语句跳转到GLibC自己的代码! How clever! 多聪明啊!

The order of function calls I got started roughly like this: 我开始的函数调用顺序大致如下:

__libc_start_main
exit
__run_exit_handlers
_dl_fini
rtld_lock_default_lock_recursive
_dl_fini
_dl_sort_fini

The 'exit(0);' '退出(0);' method 方法

Compiling this: 编译:

#include <stdlib.h>
int main(void)
{
        exit(0);
}

And compiling and debugging... 并编译和调试......

$ 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
...

And the function sequence I got was: 我得到的功能序列是:

exit@plt
??
_dl_runtime_resolve
_dl_fixup
_dl_lookup_symbol_x
do_lookup_x
check_match
_dl_name_match
strcmp

List object's Symbols 列出对象的符号

There's a cool tool for printing the symbols defined within a binary. 有一个很酷的工具可以打印二进制文件中定义的符号。 It's nm . 这是nm I suggest you take a look into it as it will give you an idea of how much "crap" it's added in a simple program like the ones above. 我建议你看看它,因为它会让你知道它在一个像上面这样的简单程序中添加了多少“垃圾”。

To use it in the simplest form: 以最简单的形式使用它:

$ nm main
$ nm exit

That will print a list of symbols in the file. 这将打印文件中的符号列表。 Note that this list does not include references these functions will make. 请注意,此列表包括这些函数将提供的引用。 So if a given function in this list calls another function, the other probably won't be in the list. 因此,如果此列表中的给定函数调用另一个函数,则另一个函数可能不在列表中。


Conclusion 结论

It depends heavily on the way the GLibC choses to handle a simple stack frame return from main and how it implements the exit wrapper. 它在很大程度上取决于GLibC选择如何处理从main返回的简单堆栈帧以及它如何实现exit包装器的方式。 In the end, the _exit(2) system call will get called and you'll exit your process. 最后, _exit(2)系统调用将被调用,您将退出您的进程。

Finally , to really answer your question: both the methods jump into GLibC code, and to know exactly what that code is doing you'll need to read it. 最后 ,要真正回答你的问题:这两种方法都会跳转到GLibC代码中,并确切地知道代码正在做什么,你需要阅读它。 If you want to know more about the GLibC, you should check the sources for the GCC and GLibC. 如果您想了解有关GLibC的更多信息,您应该检查GCC和GLibC的来源。


References 参考

  • GLibC Source Repository : Look in stdlib/exit.c and stdlib/exit.h for the implementations. GLibC源代码库 :在stdlib/exit.cstdlib/exit.h查找实现。
  • Linux Kernel Exit Definition : look in kernel/exit.c for the _exit(2) system call implementation, and include/syscalls.h for the preprocessor magic behind it. Linux内核退出定义 :在kernel/exit.c查找_exit(2)系统调用实现,并在include/syscalls.h查找其后面的预处理器魔法。
  • GCC Sources : I do not know the gcc (compiler, not suite) sources, and would appreciate if anyone could point out where the runtime sequence is defined. GCC来源 :我不知道gcc (编译器,而不是套件)源,如果有人能指出运行时序列的定义,我将不胜感激。

Functionally, from the main() function there is really no difference in C. For example, even if you defined a function handler with the atexit() library call, both return() and exit() from main will call that function pointer. 从功能上来说,在main()函数中,C实际上没有区别。例如,即使您使用atexit()库调用定义了函数处理程序,main的return()exit()也将调用该函数指针。

The exit() call, however, has the flexibility that you can use it to cause a program to exit with a return code from any point within the code. 但是, exit()调用具有灵活性,您可以使用它来使程序从代码中的任何点返回代码退出。

There are the technical differences. 存在技术差异。 If you compile the following to assembly: 如果将以下内容编译为汇编:

int main()
{
  return 1;
}

the final portion of that code will be: 该代码的最后部分将是:

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

On the other hand, the following code compiled to assembly: 另一方面,以下代码编译为程序集:

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

will be identical in all respects except that it ends as follows: 在所有方面都是相同的,除了它结束如下:

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

Aside from the 1 being put into EDI rather than EAX as is required on the platform where I compiled this code as the calling convention to the _exit call, you'll note two differences. 除了在我编译此代码作为_exit调用的调用约定的平台上需要放入EDI而不是EAX ,您将注意到两个不同之处。 First, a stack alignment operation takes place to prepare for the function call. 首先,进行堆栈对齐操作以准备函数调用。 Second, rather than terminating with a retq , we are now calling into the system library, which will handle the final return code and return. 其次,我们现在调用系统库而不是以retq结束,它将处理最终的返回代码并返回。

There is practically no difference between calling exit or executing return from main as long as main returns a type that is compatible with int . 只要main返回与int兼容的类型,调用exit或从main执行return几乎没有区别。

From the C11 Standard: 从C11标准:

5.1.2.2.3 Program termination 5.1.2.2.3程序终止

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; 1如果main函数的返回类型是与int兼容的类型,则从初始调用到main函数的返回等同于调用exit函数,其中main函数返回的值作为其参数; 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函数的}返回值0.如果返回类型与int不兼容,则返回到主机环境的终止状态未指定。

exit is a system call while return is an instruction of the language. exit是系统调用,而return是语言的指令。

exit terminates current process, return returns from a function call. exit终止当前进程, return函数调用返回。

In the main() function, they both accomplish the same thing: main()函数中,它们都完成了同样的事情:

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

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

While in a function: 在功能中:

void f() {
    // code
    return; // return to where it was called from.
}

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

One major difference between using return and calling exit() in the main() program is that if you call exit() , the local variables in the main() still exist and are valid, whereas if you return , they are not. main()程序中使用return和调用exit()之间的一个主要区别是,如果调用exit()main()的局部变量仍然存在且有效,而如果return ,则不是。

This matters if you've done anything such as: 如果你做了以下任何事情,这很重要:

#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);
}

because now the function called (via atexit() ) from the startup code that called main() doesn't have a valid buffer for standard output. 因为现在调用main()的启动代码调用的函数(通过atexit() )没有标准输出的有效缓冲区。 Whether it crashes or merely gets thoroughly confused or prints garbage or appears to work is open to debate. 无论是崩溃还是仅仅彻底混淆或打印垃圾或似乎工作都是有争议的。

I called the program hysteresis . 我把程序叫做hysteresis When run with no arguments, it used exit() and worked correctly/sanely (the local space variable in function_using_stdout() was not sharing space with the I/O buffer for stdout ): 当没有参数运行时,它使用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
$

When called with at least one argument, things went haywire (the local space variable in function_using_stdout() was probably sharing space with the I/O buffer for stdout — unless that was being used by the code that executes the functions registered with atexit() ): 当使用至少一个参数调用时,事情变得混乱( function_using_stdout() _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
$

Most of the time, this sort of thing isn't a problem. 大多数时候,这种事情不是问题。 However, when it matters, it really does matter. 然而,当它重要时,它确实很重要。 And, note, it isn't visible as a problem until the program is exiting — which can make it tricky to debug. 并且,请注意,在程序退出之前,它不会显示为问题 - 这可能会使调试变得棘手。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM