轉載:http://driftcloudy.iteye.com/blog/1063275
本章是該系列最後一篇,打算看一下 exit 函數中究竟做了些什麼。
main函數的返回值
在第(5)篇裏完成了_cinit() 的分析之後,mainCRTStartup中接下來代碼是:
- __initenv = _environ;
- mainret = main(__argc, __argv, _environ);
- exit(mainret);
很顯然, 其實main函數是可以接受第三個參數的,_environ是一個環境變量的指針,只不過一般情況下寫程序的時候用不到。從代碼中可以看出,調用完main函數後,其返回值mainret會被傳遞給exit 用作參數。
這裏首先要解決一個問題,如果main函數的返回值類型是void呢?
其實準確說寫成void main是不對的T T...根據C99的規定,main的返回類型必須是int,並且如果 main 函數的最後沒有寫 return 語句,編譯器要自動加入 return 0 ,表示程序正常退出。例如:
- #include <stdio.h>
- void main()
- {
- printf("%d",100);
- }
利用VS2010進行build,OD進入main函數:
注意倒數第二行,這裏將EAX清0。其實 main 函數也是一個標準的__cdecl 函數,其return的值會存放在EAX中,因此這裏等於會返回一個0 。可見VS2010 這點上還是滿足C99 標準的,即使程序員寫的是 void main,它依然悄悄的在最後添上 return 0。
來看看VC 6,如果用VC 6來build同樣一段代碼,則main函數爲:
很顯然,這裏並沒有將EAX的值清0再retn,但是接下來依然會從EAX 中拿值賦給mainret 。換句話說,用VC6 編譯的時候,main函數並不會有默認的返回值,真正傳進exit函數的還是main調用完後的EAX值,不過鬼知道這個時候EAX 是什麼。這裏可以看出 VC6並沒有遵循C99的規範,貌似VC6是98年出來的,想想也算情有可原了...
exit _exit _cexit _c_exit
由於有一系列和 exit 類似的函數,這裏一起順便看下~
- void __cdecl exit ( int status )
- {
- doexit(status, 0, 0); /* full term, kill process */
- }
- void __cdecl _exit ( int status)
- {
- doexit(status, 1, 0); /* quick term, kill process */
- }
- void __cdecl _cexit ( void )
- {
- doexit(0, 0, 1); /* full term, return to caller */
- }
- void __cdecl _c_exit ( void )
- {
- doexit(0, 1, 1); /* quick term, return to caller */
- }
在crt0dat.c中定義了上面四個乍一看名字讓人很糾結的函數。根據代碼中的註釋,它們的大概作用爲:
- exit 函數先進行清理工作(比如析構處理、關閉所有標準IO流),然後利用main 函數返回的status 來終結當前進程
- _exit 函數用於快速終結進程,它並不進行那些“高層次”的清理
- _cexit 同exit 函數一樣執行清理,它並不終結進程
- _c_exit 同_exit 一樣執行清理,它並不終結進程
用通俗的話說,exit 是 _exit 的安全增強版,_cexit是_c_exit 的安全增強版。不過從它們的實現上看,本質上都是 doexit 函數在起作用。在doexit 的內部負責進行各種清理,然後再終結進程或者返還控制權給程序。
來看一下doexit 的大概實現,這裏忽略了一些條件編譯:
- // 是否需要終結進程,0表示終結當前進程,1表示返回控制權給程序
- char _exitflag = 0;
- /*
- * 兩個標誌
- * 一旦進入了doexit ,_C_Termination_Done會被設置爲true
- * 在doexit 完成了所有清理工作後(進入內核之前),_C_Exit_Done 會被設置爲true
- */
- int _C_Termination_Done = FALSE;
- int _C_Exit_Done = FALSE;
- static void __cdecl doexit ( int code, int quick, int retcaller )
- {
- if (_C_Exit_Done == TRUE) /*如果doexit()被遞歸的調用*/
- TerminateProcess(GetCurrentProcess(),code);/*直接TerminateProcess終結當前進程*/
- _C_Termination_Done = TRUE;
- /* 在執行其他清理的時候可能會用到retcaller,因此先將它賦值給全局變量_exitflag */
- _exitflag = (char) retcaller; /* 0 = term, !0 = callable exit */
- if (!quick) {
- /*
- * 如果該程序曾經利用_onexit 或者 atexit 註冊過函數,那麼在退出前需要執行這些函數。
- * 執行的順序與被註冊的順序相反,即採用LIFO的模式。
- * 利用atexit 來註冊函數的時候,內存中會生成一張函數指針列表,
- * __onexitbegin 和__onexitend 分別指向列表的頭部和尾部。
- *
- * 注意:
- * 是先從__onexitend指針開始,逐漸向前遍歷,直到__onexitbegin,
- * 這樣就能確保LIFO的調用順序。
- */
- if (__onexitbegin) {
- _PVFV * pfend = __onexitend;
- while ( --pfend >= __onexitbegin )
- /*
- * if current table entry is non-NULL,
- * call thru it.
- */
- if ( *pfend != NULL )
- (**pfend)();
- }
- /*
- * 會進行endstdio之類的操作,進行清理
- */
- _initterm(__xp_a, __xp_z);
- }
- /*
- * 調用C terminators,貌似實際上沒調用什麼函數
- */
- _initterm(__xt_a, __xt_z);
- /* 如果定義了retcaller,那麼需要將控制權返回 */
- if (retcaller) {
- return;
- }
- _C_Exit_Done = TRUE;
- /* 結束進程 */
- ExitProcess(code);
- }
從上述實現可以看出,如果是對於正常的退出,doexit 進行4個步驟操作:
1. 執行 _onexit 或者 atexit 中已經註冊了的函數
2. _initterm(__xp_a, __xp_z)
3. _initterm(__xt_a, __xt_z)
4. ExitProcess(code)
析構
如果對象是定義在一個函數的內部,相當於局部變量,那麼在函數調用結束之前,會自動析構該對象。
如果是一個全局對象,那麼析構其實運行在上面4個步驟中的第1步,即調用_onexit、atexit 註冊過的函數時發生。
可以用一段簡單的示例代碼來說明這些問題:
- #include <stdio.h>
- #include <stdlib.h>
- typedef struct foo1 {
- foo1() { printf("1"); }
- ~foo1() { printf("2"); }
- static void bar() { printf("3"); }
- } Foo1;
- typedef struct foo2 {
- foo2() { printf("4"); }
- ~foo2() { printf("5"); }
- } Foo2;
- Foo1 f1;
- void main()
- {
- Foo2 f2;
- atexit(&Foo1::bar);
- }
這是一段C++代碼,因爲C中的struct是不被允許定義方法的。最終的輸出結果是:
這段示例代碼中定義了兩個變量,全局變量f1和局部變量f2,並且利用atexit註冊了一個函數bar 。
根據第(5)篇中的描述,f1 的初始化工作在_cinit 函數中調用_initterm( __xc_a, __xc_z )時完成,至於f2 的初始化,肯定是在運行至main函數中Foo2 f2 一句時纔開始進行。當main函數中的語句都執行完畢(此時尚未退出main函數),開始對f2 執行析構。析構完畢隨後就退出main 調用,進入exit----> doexit,開始上述的4個步驟。在第1步中會運行註冊的bar函數,然後調用f1 的析構函數,在第2步中調用endstdio 關閉IO,第3步沒做啥,第4步ExitProcess。
因此從 cinit ----> main ----> exit 大概發生的事情順序如下所示: