C 프로그래밍에서는 메인 함수에서 프로그램을 종료하는 방법이 두 가지 있는데, return을 사용하는 것과 exit()를 사용하는 것입니다.
int main() { printf("Hello, World!"); return 0; // Method 1: Normal termination } int main() { printf("Hello, World!"); exit(0); // Method 2:Normal termination }
두 가지 방법이 완전히 다르게 보이지만 프로그램을 올바르게 종료할 수 있는 이유는 무엇입니까?
이 기사에서는 C 프로그램이 실제로 어떻게 시작하고 종료하는지 이해하여 이 미스터리를 풀어보겠습니다.
이 기사는 특히 glibc를 사용한 GNU/Linux 환경에서의 구현에 중점을 두고 있습니다.
먼저 프로그램 종료 메커니즘을 이해하기 위해 종료 기능이 어떻게 작동하는지 살펴보겠습니다.
종료 함수는 프로그램을 적절하게 종료시키는 표준 라이브러리 함수입니다.
내부적으로는 종료에 의해 호출되는 _exit 함수가 glibc에서 다음과 같이 구현됩니다.
void _exit (int status) { while (1) { INLINE_SYSCALL (exit_group, 1, status); #ifdef ABORT_INSTRUCTION ABORT_INSTRUCTION; #endif } }
이 구현을 보면 _exit 함수가 종료 상태를 인수로 받고 exit_group(시스템 호출 번호 231)을 호출하는 것을 볼 수 있습니다.
이 시스템 호출은 다음 작업을 수행합니다.
이러한 작업을 통해 프로그램이 정상적으로 종료됩니다.
그러면 왜 main()에서 복귀해도 프로그램이 제대로 종료되나요?
이를 이해하려면 중요한 사실을 알아야 합니다. C 프로그램은 실제로 메인에서 시작되지 않습니다.
실제 진입점을 보려면 링커(ld)의 기본 설정을 확인해 보겠습니다.
$ ld --verbose | grep "ENTRY" ENTRY(_start)
이 출력에서 볼 수 있듯이 C 프로그램의 실제 진입점은 _start 함수입니다. main은 _start 다음에 호출됩니다.
_start 함수는 표준 라이브러리에서 구현되며 glibc에서는 다음과 같습니다.
_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
_start 함수에는 두 가지 주요 역할이 있습니다.
이러한 초기화가 완료되면 __libc_start_main이 호출됩니다.
이 함수는 메인 함수 호출을 담당합니다.
이제 __libc_start_main이 어떻게 작동하는지 자세히 살펴보겠습니다.
__libc_start_main에 의해 호출되는 __libc_start_call_main은 다음과 같이 구현됩니다.
_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); }
이번 구현에서 중점적으로 살펴볼 부분은 다음과 같습니다.
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM); exit(result);
여기서 중요한 점은 main 함수가 어떻게 실행되고 반환 값이 처리되는지입니다.
이 메커니즘을 통해:
두 경우 모두 궁극적으로 종료가 호출되어 프로그램이 올바르게 종료되도록 합니다.
C 프로그램에는 다음과 같은 메커니즘이 있습니다.
이 메커니즘을 통해:
이 메커니즘은 GNU/Linux에만 국한되지 않습니다. 다른 운영 체제(예: Windows 및 macOS)와 다른 C 표준 라이브러리에도 유사한 구현이 존재합니다.
위 내용은 return과 exit()가 모두 main()에서 작동하는 이유의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!