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.
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 :
Grâce à ces opérations, le programme se termine correctement.
Alors, pourquoi le retour de main() termine-t-il également correctement le programme ?
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 :
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.
__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 :
Grâce à ce mécanisme :
Dans les deux cas, la sortie est finalement appelée, garantissant ainsi la fin correcte du programme.
Les programmes C ont mis en place le mécanisme suivant :
Grâce à ce mécanisme :
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!