Maison  >  Article  >  développement back-end  >  Pourquoi return et exit() fonctionnent dans main()

Pourquoi return et exit() fonctionnent dans main()

DDD
DDDoriginal
2024-11-08 09:37:02969parcourir

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

Introduction

En programmation C, il existe deux manières de terminer un programme à partir de la fonction principale : en utilisant return et en utilisant exit().

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

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

Pourquoi les deux méthodes peuvent-elles terminer le programme correctement, même si elles semblent complètement différentes ?
Dans cet article, nous allons résoudre ce mystère en comprenant comment les programmes C démarrent et se terminent réellement.
Notez que cet article se concentre sur l'implémentation dans les environnements GNU/Linux, en utilisant spécifiquement la glibc.

Comment fonctionne exit()

Tout d'abord, examinons le fonctionnement de la fonction de sortie pour comprendre le mécanisme de terminaison du programme.
La fonction exit est une fonction de bibliothèque standard qui termine correctement un programme.
En interne, la fonction _exit, qui est appelée par exit, est implémentée dans la glibc comme suit :

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

#ifdef ABORT_INSTRUCTION
      ABORT_INSTRUCTION;
#endif
    }
}

En regardant cette implémentation, nous pouvons voir que la fonction _exit reçoit un statut de sortie comme argument et appelle exit_group (numéro d'appel système 231).

Cet appel système effectue les opérations suivantes :

  1. Envoie une notification de fin de programme au noyau
  2. Le noyau effectue des opérations de nettoyage :
    • Libère les ressources utilisées par le processus
    • Mise à jour la table des processus
    • Effectue des procédures de nettoyage supplémentaires

Grâce à ces opérations, le programme se termine correctement.

Alors, pourquoi le retour de main() termine-t-il également correctement le programme ?

Point d'entrée caché du programme C

Pour comprendre cela, nous devons connaître un fait important : les programmes C ne démarrent pas réellement à partir de main.

Vérifions les paramètres par défaut de l'éditeur de liens (ld) pour voir le point d'entrée réel :

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

Comme le montre cette sortie, le point d'entrée réel d'un programme C est la fonction _start. main est appelé après _start.
La fonction _start est implémentée dans la bibliothèque standard, et dans la glibc, elle ressemble à ceci :

_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

La fonction _start a deux rôles principaux :

  1. Initialise le cadre de pile requis pour l'exécution du programme
  2. Configure les arguments de ligne de commande (argc, argv) pour la fonction principale

Une fois ces initialisations terminées, __libc_start_main est appelé.
Cette fonction est chargée d'appeler la fonction principale.

Maintenant, examinons en détail le fonctionnement de __libc_start_main.

Comment __libc_start_main fait fonctionner le retour

__libc_start_call_main, qui est appelé par __libc_start_main, est implémenté comme suit :

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

Dans cette mise en œuvre, les éléments clés sur lesquels se concentrer sont les suivants :

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

Ici, le point important est la manière dont la fonction principale est exécutée et sa valeur de retour est gérée :

  1. Exécute la fonction principale et stocke sa valeur de retour dans le résultat
  2. Utilise la valeur de retour de main comme argument pour la sortie

Grâce à ce mécanisme :

  • Lors de l'utilisation de return dans main → La valeur de retour est transmise à __libc_start_main, qui la transmet ensuite pour quitter
  • Lorsque exit() est appelé directement dans main → Le programme se termine immédiatement

Dans les deux cas, la sortie est finalement appelée, garantissant ainsi la fin correcte du programme.

Conclusion

Les programmes C ont mis en place le mécanisme suivant :

  1. Le programme démarre à partir de _start
  2. _start prépare l'exécution de main
  3. main est exécuté via __libc_start_main
  4. Reçoit la valeur de retour de main et l'utilise comme argument pour la sortie

Grâce à ce mécanisme :

  • Même lors de l'utilisation de return dans main, la valeur de retour est automatiquement transmise à exit
  • En conséquence, return et exit() terminent le programme correctement

Notez que ce mécanisme n'est pas limité à GNU/Linux ; des implémentations similaires existent dans d'autres systèmes d'exploitation (comme Windows et macOS) et différentes bibliothèques standard C.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn