چرا هر دو () return و exit در main() کار می کنند
Summarize this content to 400 words in Persian Lang
مقدمه
در برنامه نویسی C، دو راه برای خاتمه دادن یک برنامه از تابع main وجود دارد: استفاده از 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 تمرکز دارد.
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
برای درک این موضوع، باید یک واقعیت مهم را بدانیم: برنامه های 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 دو نقش اصلی دارد:
قاب پشته مورد نیاز برای اجرای برنامه را راه اندازی می کند
آرگومان های خط فرمان (argc، argv) را برای تابع اصلی تنظیم می کند
پس از تکمیل این مقداردهی اولیه، __libc_start_main فراخوانی می شود.
این تابع وظیفه فراخوانی تابع اصلی را بر عهده دارد.
اکنون، بیایید نحوه عملکرد __libc_start_main را با جزئیات بررسی کنیم.
چگونه __libc_start_main باعث می شود بازگشت کار کند
__libc_start_call_main که توسط __libc_start_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 چگونه اجرا می شود و مقدار بازگشتی آن چگونه مدیریت می شود:
تابع اصلی را اجرا می کند و مقدار بازگشتی آن را در نتیجه ذخیره می کند
از مقدار بازگشتی از main به عنوان آرگومان خروج استفاده می کند
از طریق این مکانیسم:
هنگام استفاده از return در main → مقدار بازگشتی به __libc_start_main ارسال میشود و سپس آن را برای خروج ارسال میکند.
هنگامی که exit() مستقیماً در main فراخوانی می شود → برنامه بلافاصله خاتمه می یابد
در هر صورت، خروج در نهایت فراخوانی می شود و از پایان مناسب برنامه اطمینان حاصل می شود.
نتیجه گیری
برنامه های C دارای مکانیسم زیر هستند:
برنامه از _start شروع می شود
_start برای اجرای main آماده می شود
main از طریق __libc_start_main اجرا می شود
مقدار بازگشتی main را دریافت می کند و از آن به عنوان آرگومان خروج استفاده می کند
از طریق این مکانیسم:
حتی هنگام استفاده از return در main، مقدار بازگشتی به طور خودکار برای خروج ارسال می شود
در نتیجه، هم () return و exit برنامه را به درستی خاتمه می دهند
توجه داشته باشید که این مکانیسم محدود به گنو/لینوکس نیست. پیاده سازی های مشابه در سیستم عامل های دیگر (مانند ویندوز و macOS) و کتابخانه های مختلف استاندارد C وجود دارد.
مقدمه
در برنامه نویسی C، دو راه برای خاتمه دادن یک برنامه از تابع main وجود دارد: استفاده از 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 تمرکز دارد.
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
برای درک این موضوع، باید یک واقعیت مهم را بدانیم: برنامه های 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 دو نقش اصلی دارد:
- قاب پشته مورد نیاز برای اجرای برنامه را راه اندازی می کند
- آرگومان های خط فرمان (argc، argv) را برای تابع اصلی تنظیم می کند
پس از تکمیل این مقداردهی اولیه، __libc_start_main فراخوانی می شود.
این تابع وظیفه فراخوانی تابع اصلی را بر عهده دارد.
اکنون، بیایید نحوه عملکرد __libc_start_main را با جزئیات بررسی کنیم.
چگونه __libc_start_main باعث می شود بازگشت کار کند
__libc_start_call_main که توسط __libc_start_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 چگونه اجرا می شود و مقدار بازگشتی آن چگونه مدیریت می شود:
- تابع اصلی را اجرا می کند و مقدار بازگشتی آن را در نتیجه ذخیره می کند
- از مقدار بازگشتی از main به عنوان آرگومان خروج استفاده می کند
از طریق این مکانیسم:
- هنگام استفاده از return در main → مقدار بازگشتی به __libc_start_main ارسال میشود و سپس آن را برای خروج ارسال میکند.
- هنگامی که exit() مستقیماً در main فراخوانی می شود → برنامه بلافاصله خاتمه می یابد
در هر صورت، خروج در نهایت فراخوانی می شود و از پایان مناسب برنامه اطمینان حاصل می شود.
نتیجه گیری
برنامه های C دارای مکانیسم زیر هستند:
- برنامه از _start شروع می شود
- _start برای اجرای main آماده می شود
- main از طریق __libc_start_main اجرا می شود
- مقدار بازگشتی main را دریافت می کند و از آن به عنوان آرگومان خروج استفاده می کند
از طریق این مکانیسم:
- حتی هنگام استفاده از return در main، مقدار بازگشتی به طور خودکار برای خروج ارسال می شود
- در نتیجه، هم () return و exit برنامه را به درستی خاتمه می دهند
توجه داشته باشید که این مکانیسم محدود به گنو/لینوکس نیست. پیاده سازی های مشابه در سیستم عامل های دیگر (مانند ویندوز و macOS) و کتابخانه های مختلف استاندارد C وجود دارد.