標籤:
main()函數,想必大家都不陌生了,從剛開始寫程式的時候,大家便開始寫main(),我們都知道main是程式的入口。那main作為一個函數,又是誰調用的它,它是怎麼被調用的,返回給誰,返回的又是什嗎?這次我們來探討一下這個問題。
1. main()函數的形式
先來說說main函數的定義,較早開始寫C程式的肯定都用過這樣的定義void main(){},其實翻翻C/C++標準,從來沒有定義過void main()。
在C標準中main的定義只有兩種:
int main(void)
int main(int argc, char *argv[])
在C++標準中main的定義也只有兩種:
int main( )
int main(int argc, char *argv[])
換句話說:當你的程式不需要命令列參數的時候用int main(), 當需要命令列參數的時候請使用int main(int argc, char *argv[])
不過標準歸標準,在不同的平台上,不同的編譯器中對main()的定義方式總有自己的實現,比如早期編譯器對void main()的支援(現在gcc也支援,不過會給出一個warning)。特別的,因為曆史的原因,在Unix-like平台上,大多還支援
int main(int argc, char *argv[], char *envp[])
其使用方式我們稍後再談。
2. main()函數的返回
int main(...) 意味著需要return一個int值,如果不寫,有的編譯器會自動幫你添加一個return 0;,而有的則會返回一個隨機值。為了避免不必要的問題,建議寫的時候還是加上一個return 0;,浪費不了你多少時間,不是嗎?
所以一個完整的test.c檔案應該為:
int main(int argc, char *argv[])
{
return 0;
}
當然我們也可以嘗試著讓main返回一個long, double甚至是struct,更改main函數中的形參定義。這在有些編譯器上是能編譯通過的,不過可能會有一些警告(如GCC)。但是啟動並執行時候如果編譯器能做轉換的還好,如返回long,float. 如果不能的話(如返回struct,或者main(int argc, char *argv0,char *argv1,char *argv2))會造成segmentation fault。
3. main()的調用和返回
在瞭解了main()函數的定義和返回形式後,我們再來看看main函數是怎麼被調用的,它又"return"給了誰。在"gcc的編譯過程"一中,我們回顧了程式從源碼到可執行程式的過程,在"應用程式在linux上是如何被執行的"一文中,我們回顧了可執行檔怎麼被作業系統載入的,今天我們繼續這個過程。
上文提到不管是在load_elf_binary()中或者使用了動態連結程式庫,最後都執行到了應用程式的入口。不過這個入口不是main.而是_start()。
執行
gcc -o test test.c
readelf -a test
可以看到test檔案的Entry point address是0x80482e0,在往後看,這個地址是.text的地址(程式碼片段的開始),也是_start()的地址。在_start()中又會調用__libc_start_main(),主要做一些程式的初始化工作,感興趣的同學可以讀讀glibc中的源碼,注釋很清楚。然後主角登場了,在__libc_start_main()中最後會調用
int result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);//這是Unix-like下main函數的調用方式,這下大家明白main函數中形參的由來了吧。
result中放著main函數的傳回值,然後帶著這個值退出。
exit (result);
注意:雖然main函數是一個特殊的函數,是程式啟動並執行入口,但它畢竟也是一個函數,是可以被調用的。如:
int main()
{
if(...)
return 0;
main();
return 0;
}
不過要小心調用方式,和允出準則,避免無窮遞迴。
4. shell中執行程式
通過前幾次和上面的分析,我們終於基本弄清了應用程式的執行過程,再回顧一遍: 在某個互動式shell中敲入./test, 此shell fork()/clone()出一個子進程,這個子進程執行
execve("./test",char * const argv[], char * const envp[])
execve載入./test,並把參數argv[],envp[]一步一步傳遞下去。載入了./test之後,從./test的入口開始執行,即ELF檔案中的_start(),_start()調用__libc_start_main(),最後到了main。
int main(int argc, char *argv[], char *envp[])
看著這個main的定義和execve相似吧,沒錯main中的參數都是execve一步步傳遞下來的。argc是命令列參數個數,argv[]儲存著各個參數的指標(注意argv[0]通常是程式名,argv[1]開始才是命令列參數。這是由shell設定的),envp[]儲存著環境變數表。然而在標準C中只定義了int main(int argc, char *argv[]),所以unix-like平台也提供了全域變數environ指向環境變數表。
extern char **environ;
當然也可以用getenv和putenv來訪問特定的環境變數。
對了,父shell還在wait()./test的結束呢,不錯,test中main函數return的值,在被__libc_start_main() exit之後,終於被父shell抓住了,可以用$?訪問。
如$> ./test
$> echo $?
可以得到test返回的值。這樣,你就知道main()函數中return的意義,以及如何在shell中使用了吧。儘管可以return任何值,也建議用return 0來表示程式正常結束。這樣別人用shell指令碼調用你寫的程式的時候,就可以$?等於0來判斷你的程式是否正常執行了。
最後小結一下:
1. 避免使用void main(),盡量使用int main() 或者 int main(int argc, char *argv[])。
2. 在main的結尾記得 return int;, 最好用return 0;表示程式的正常結束。
3. main函數和普通函數一樣也是能被調用的。
4. main return的值最終會返回給其調用者,如shell中執行的程式,可以在shell中用$?得到其傳回值。
5. 在unix-like環境中,可以使用int main(int argc, char *argv[], char *envp[]), extern char **environ; , getenv()等方式來得到環境變數。
Linux上程式執行的入口--Main