mian函數返回值解析

轉載:http://driftcloudy.iteye.com/blog/1063275

本章是該系列最後一篇,打算看一下 exit 函數中究竟做了些什麼。

 

main函數的返回值

在第(5)篇裏完成了_cinit() 的分析之後,mainCRTStartup中接下來代碼是:

C代碼  收藏代碼
  1. __initenv = _environ;  
  2. mainret = main(__argc, __argv, _environ);  
  3. exit(mainret);  

很顯然, 其實main函數是可以接受第三個參數的,_environ是一個環境變量的指針,只不過一般情況下寫程序的時候用不到。從代碼中可以看出,調用完main函數後,其返回值mainret會被傳遞給exit 用作參數。

 

這裏首先要解決一個問題,如果main函數的返回值類型是void呢?

 

其實準確說寫成void main是不對的T T...根據C99的規定,main的返回類型必須是int,並且如果 main 函數的最後沒有寫 return 語句,編譯器要自動加入 return 0 ,表示程序正常退出。例如:

C代碼  收藏代碼
  1. #include <stdio.h>  
  2. void main()  
  3. {  
  4.     printf("%d",100);  
  5. }  

利用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 類似的函數,這裏一起順便看下~

C代碼  收藏代碼
  1. void __cdecl exit ( int status )  
  2. {  
  3.         doexit(status, 0, 0); /* full term, kill process */  
  4. }  
  5.   
  6. void __cdecl _exit ( int status)  
  7. {  
  8.         doexit(status, 1, 0); /* quick term, kill process */  
  9. }  
  10.   
  11. void __cdecl _cexit ( void )  
  12. {  
  13.         doexit(0, 0, 1);    /* full term, return to caller */  
  14. }  
  15.   
  16. void __cdecl _c_exit ( void )  
  17. {  
  18.         doexit(0, 1, 1);    /* quick term, return to caller */  
  19. }  

在crt0dat.c中定義了上面四個乍一看名字讓人很糾結的函數。根據代碼中的註釋,它們的大概作用爲:

  • exit 函數先進行清理工作(比如析構處理、關閉所有標準IO流),然後利用main 函數返回的status 來終結當前進程
  • _exit 函數用於快速終結進程,它並不進行那些“高層次”的清理
  • _cexit 同exit 函數一樣執行清理,它並不終結進程
  • _c_exit 同_exit 一樣執行清理,它並不終結進程

用通俗的話說,exit 是 _exit 的安全增強版,_cexit是_c_exit 的安全增強版。不過從它們的實現上看,本質上都是 doexit 函數在起作用。在doexit 的內部負責進行各種清理,然後再終結進程或者返還控制權給程序。

 

來看一下doexit 的大概實現,這裏忽略了一些條件編譯:

C代碼  收藏代碼
  1. // 是否需要終結進程,0表示終結當前進程,1表示返回控制權給程序  
  2. char _exitflag = 0;  
  3.   
  4. /* 
  5.  * 兩個標誌 
  6.  * 一旦進入了doexit ,_C_Termination_Done會被設置爲true 
  7.  * 在doexit 完成了所有清理工作後(進入內核之前),_C_Exit_Done 會被設置爲true 
  8.  */  
  9. int _C_Termination_Done = FALSE;  
  10. int _C_Exit_Done = FALSE;  
  11.   
  12. static void __cdecl doexit ( int code, int quick, int retcaller )  
  13. {  
  14.         if (_C_Exit_Done == TRUE)                          /*如果doexit()被遞歸的調用*/  
  15.                 TerminateProcess(GetCurrentProcess(),code);/*直接TerminateProcess終結當前進程*/  
  16.         _C_Termination_Done = TRUE;  
  17.   
  18.         /* 在執行其他清理的時候可能會用到retcaller,因此先將它賦值給全局變量_exitflag */  
  19.         _exitflag = (char) retcaller;  /* 0 = term, !0 = callable exit */  
  20.   
  21.         if (!quick) {  
  22.             /* 
  23.              * 如果該程序曾經利用_onexit 或者 atexit  註冊過函數,那麼在退出前需要執行這些函數。 
  24.              * 執行的順序與被註冊的順序相反,即採用LIFO的模式。 
  25.              * 利用atexit 來註冊函數的時候,內存中會生成一張函數指針列表, 
  26.              * __onexitbegin 和__onexitend 分別指向列表的頭部和尾部。 
  27.              * 
  28.              * 注意: 
  29.              * 是先從__onexitend指針開始,逐漸向前遍歷,直到__onexitbegin, 
  30.              * 這樣就能確保LIFO的調用順序。 
  31.              */  
  32.   
  33.             if (__onexitbegin) {  
  34.                 _PVFV * pfend = __onexitend;  
  35.   
  36.                 while ( --pfend >= __onexitbegin )  
  37.                 /* 
  38.                  * if current table entry is non-NULL, 
  39.                  * call thru it. 
  40.                  */  
  41.                 if ( *pfend != NULL )  
  42.                     (**pfend)();  
  43.             }  
  44.   
  45.             /* 
  46.              * 會進行endstdio之類的操作,進行清理 
  47.              */  
  48.             _initterm(__xp_a, __xp_z);  
  49.         }  
  50.   
  51.         /* 
  52.          * 調用C terminators,貌似實際上沒調用什麼函數 
  53.          */  
  54.         _initterm(__xt_a, __xt_z);  
  55.   
  56.         /* 如果定義了retcaller,那麼需要將控制權返回 */  
  57.         if (retcaller) {  
  58.             return;  
  59.         }  
  60.   
  61.         _C_Exit_Done = TRUE;  
  62.   
  63.         /* 結束進程 */  
  64.         ExitProcess(code);  
  65. }  
 

從上述實現可以看出,如果是對於正常的退出,doexit 進行4個步驟操作:

1. 執行 _onexit 或者 atexit 中已經註冊了的函數

2. _initterm(__xp_a, __xp_z)

3. _initterm(__xt_a, __xt_z)

4. ExitProcess(code)

 

析構

如果對象是定義在一個函數的內部,相當於局部變量,那麼在函數調用結束之前,會自動析構該對象。

如果是一個全局對象,那麼析構其實運行在上面4個步驟中的第1步,即調用_onexit、atexit 註冊過的函數時發生。

可以用一段簡單的示例代碼來說明這些問題:

Cpp代碼  收藏代碼
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3.   
  4. typedef struct foo1 {  
  5.     foo1() { printf("1"); }  
  6.     ~foo1() { printf("2"); }  
  7.     static void bar() { printf("3"); }  
  8. } Foo1;  
  9.   
  10. typedef struct foo2 {  
  11.     foo2() { printf("4"); }  
  12.     ~foo2() { printf("5"); }  
  13. } Foo2;  
  14.   
  15. Foo1 f1;  
  16.   
  17. void main()  
  18. {  
  19.     Foo2 f2;  
  20.     atexit(&Foo1::bar);  
  21. }  

這是一段C++代碼,因爲C中的struct是不被允許定義方法的。最終的輸出結果是:

運行結果
14532
 

這段示例代碼中定義了兩個變量,全局變量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 大概發生的事情順序如下所示:

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章