Heim >Backend-Entwicklung >C++ >Warum sowohl return als auch exit() in main() funktionieren

Warum sowohl return als auch exit() in main() funktionieren

DDD
DDDOriginal
2024-11-08 09:37:021005Durchsuche

Why Both return and exit() Work in main()

Einführung

In der C-Programmierung gibt es zwei Möglichkeiten, ein Programm über die Hauptfunktion zu beenden: mit Return und mit Exit().

int main() {
    printf("Hello, World!");
    return 0;    // Method 1: Normal termination
}

int main() {
    printf("Hello, World!");
    exit(0);     // Method 2:Normal termination
}

Warum können beide Methoden das Programm korrekt beenden, obwohl sie völlig unterschiedlich aussehen?
In diesem Artikel werden wir dieses Rätsel lösen, indem wir verstehen, wie C-Programme tatsächlich starten und beenden.
Beachten Sie, dass sich dieser Artikel auf die Implementierung in GNU/Linux-Umgebungen konzentriert, insbesondere auf die Verwendung von glibc.

So funktioniert exit()

Lassen Sie uns zunächst untersuchen, wie die Exit-Funktion funktioniert, um den Programmbeendigungsmechanismus zu verstehen.
Die Exit-Funktion ist eine Standardbibliotheksfunktion, die ein Programm ordnungsgemäß beendet.
Intern ist die Funktion _exit, die von Exit aufgerufen wird, in Glibc wie folgt implementiert:

void
_exit (int status)
{
  while (1)
    {
      INLINE_SYSCALL (exit_group, 1, status);

#ifdef ABORT_INSTRUCTION
      ABORT_INSTRUCTION;
#endif
    }
}

Wenn wir uns diese Implementierung ansehen, können wir sehen, dass die _exit-Funktion einen Exit-Status als Argument erhält und exit_group (Systemaufrufnummer 231) aufruft.

Dieser Systemaufruf führt die folgenden Vorgänge aus:

  1. Sendet eine Programmbeendigungsbenachrichtigung an den Kernel
  2. Der Kernel führt Bereinigungsvorgänge durch:
    • Gibt vom Prozess verwendete Ressourcen frei
    • Aktualisiert die Prozesstabelle
    • Führt zusätzliche Bereinigungsverfahren durch

Durch diese Vorgänge wird das Programm ordnungsgemäß beendet.

Warum beendet die Rückkehr von main() das Programm auch ordnungsgemäß?

Der versteckte Einstiegspunkt des C-Programms

Um dies zu verstehen, müssen wir eine wichtige Tatsache wissen: C-Programme beginnen eigentlich nicht im Hauptprogramm.

Überprüfen wir die Standardeinstellungen des Linkers (ld), um den tatsächlichen Einstiegspunkt zu sehen:

$ ld --verbose | grep "ENTRY"
ENTRY(_start)

Wie diese Ausgabe zeigt, ist der eigentliche Einstiegspunkt eines C-Programms die _start-Funktion. main wird nach _start.
aufgerufen Die _start-Funktion ist in der Standardbibliothek implementiert und in glibc sieht sie so aus:

_start:
    # Initialize stack pointer
    xorl %ebp, %ebp
    popq %rsi        # Get argc
    movq %rsp, %rdx  # Get argv

    # Setup arguments for main
    pushq %rsi       # Push argc
    pushq %rdx       # Push argv

    # Call __libc_start_main
    call __libc_start_main

Die Funktion _start hat zwei Hauptaufgaben:

  1. Initialisiert den für die Programmausführung erforderlichen Stapelrahmen
  2. Setzt Befehlszeilenargumente (argc, argv) für die Hauptfunktion ein

Nachdem diese Initialisierungen abgeschlossen sind, wird __libc_start_main aufgerufen.
Diese Funktion ist für den Aufruf der Hauptfunktion verantwortlich.

Lassen Sie uns nun im Detail untersuchen, wie __libc_start_main funktioniert.

Wie __libc_start_main dafür sorgt, dass die Rückgabe funktioniert

__libc_start_call_main, das von __libc_start_main aufgerufen wird, wird wie folgt implementiert:

_Noreturn static void
__libc_start_call_main (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
                        int argc, char **argv
#ifdef LIBC_START_MAIN_AUXVEC_ARG
                            , ElfW(auxv_t) *auxvec
#endif
                        )
{
  int result;

  /* Memory for the cancellation buffer.  */
  struct pthread_unwind_buf unwind_buf;

  int not_first_call;
  DIAG_PUSH_NEEDS_COMMENT;
#if __GNUC_PREREQ (7, 0)
  /* This call results in a -Wstringop-overflow warning because struct
     pthread_unwind_buf is smaller than jmp_buf.  setjmp and longjmp
     do not use anything beyond the common prefix (they never access
     the saved signal mask), so that is a false positive.  */
  DIAG_IGNORE_NEEDS_COMMENT (11, "-Wstringop-overflow=");
#endif
  not_first_call = setjmp ((struct __jmp_buf_tag *) unwind_buf.cancel_jmp_buf);
  DIAG_POP_NEEDS_COMMENT;
  if (__glibc_likely (! not_first_call))
    {
      struct pthread *self = THREAD_SELF;

      /* Store old info.  */
      unwind_buf.priv.data.prev = THREAD_GETMEM (self, cleanup_jmp_buf);
      unwind_buf.priv.data.cleanup = THREAD_GETMEM (self, cleanup);

      /* Store the new cleanup handler info.  */
      THREAD_SETMEM (self, cleanup_jmp_buf, &unwind_buf);

      /* Run the program.  */
      result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
    }
  else
    {
      /* Remove the thread-local data.  */
      __nptl_deallocate_tsd ();

      /* One less thread.  Decrement the counter.  If it is zero we
         terminate the entire process.  */
      result = 0;
      if (atomic_fetch_add_relaxed (&__nptl_nthreads, -1) != 1)
        /* Not much left to do but to exit the thread, not the process.  */
    while (1)
      INTERNAL_SYSCALL_CALL (exit, 0);
    }

  exit (result);
}

Bei dieser Implementierung sind die wichtigsten Teile, auf die man sich konzentrieren sollte, wie folgt:

result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
exit(result);

Hier kommt es darauf an, wie die Hauptfunktion ausgeführt wird und wie ihr Rückgabewert behandelt wird:

  1. Führt die Hauptfunktion aus und speichert ihren Rückgabewert im Ergebnis
  2. Verwendet den Rückgabewert von main als Argument für Exit

Durch diesen Mechanismus:

  • Bei Verwendung von Return in Main → Der Rückgabewert wird an __libc_start_main übergeben, die ihn dann an Exit übergibt
  • Wenn exit() direkt in main aufgerufen wird → Das Programm wird sofort beendet

In beiden Fällen wird letztendlich „exit“ aufgerufen, um eine ordnungsgemäße Beendigung des Programms sicherzustellen.

Abschluss

C-Programme verfügen über den folgenden Mechanismus:

  1. Das Programm beginnt mit _start
  2. _start bereitet sich auf die Ausführung von main vor
  3. main wird über __libc_start_main
  4. ausgeführt
  5. Empfangt den Rückgabewert von main und verwendet ihn als Argument für Exit

Durch diesen Mechanismus:

  • Auch wenn return in main verwendet wird, wird der Rückgabewert automatisch an Exit übergeben
  • Daher beenden sowohl return als auch exit() das Programm ordnungsgemäß

Beachten Sie, dass dieser Mechanismus nicht auf GNU/Linux beschränkt ist; Ähnliche Implementierungen gibt es in anderen Betriebssystemen (wie Windows und macOS) und verschiedenen C-Standardbibliotheken.

Das obige ist der detaillierte Inhalt vonWarum sowohl return als auch exit() in main() funktionieren. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn