C プログラミングでは、main 関数からプログラムを終了するには、return を使用する方法と exit() を使用する方法の 2 つがあります。
int main() { printf("Hello, World!"); return 0; // Method 1: Normal termination } int main() { printf("Hello, World!"); exit(0); // Method 2:Normal termination }
まったく異なるように見えるにもかかわらず、両方の方法でプログラムを正しく終了できるのはなぜですか?
この記事では、C プログラムが実際にどのように開始および終了するかを理解することで、この謎を解き明かしていきます。
この記事は、GNU/Linux 環境、特に glibc を使用した実装に焦点を当てていることに注意してください。
まず、プログラム終了メカニズムを理解するために、exit 関数がどのように機能するかを調べてみましょう。
exit 関数は、プログラムを適切に終了するための標準ライブラリ関数です。
内部的には、exit によって呼び出される _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 プログラムは実際には main から開始されないという重要な事実を知る必要があります。
実際のエントリ ポイントを確認するために、リンカー (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 関数には 2 つの主な役割があります:
これらの初期化が完了すると、__libc_start_main が呼び出されます。
この関数は 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 関数がどのように実行され、その戻り値がどのように処理されるかです。
このメカニズムを通じて:
どちらの場合も、最終的に exit が呼び出され、プログラムが適切に終了することが保証されます。
C プログラムには次のメカニズムが組み込まれています:
このメカニズムを通じて:
このメカニズムは GNU/Linux に限定されないことに注意してください。同様の実装は、他のオペレーティング システム (Windows や macOS など) やさまざまな C 標準ライブラリにも存在します。
以上がreturn と exit() の両方が main() で機能する理由の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。