深度剖析c語言main函式---main函式的執行順序
在之前的文章中,介紹了main函式的返回值 和 main函式的傳參,本文主要介紹一下main函式的執行順序。可能有的人會說,這還用說,main函式肯定是程式執行的第一個函式。那麼,事實果然如此嗎?相信在看了本文之後,會有不一樣的認識。
為什麼說main()是程式的入口
linux系統下程式的入口是”_start”,這個函式是linux系統庫(Glibc)的一部分,當我們的程式和Glibc連結在一起形成最終的可執行檔案的之後,這個函式就是程式執行初始化的入口函式。
通過一個測試程式來說明:
#include <stdio.h>
int main()
{
printf ("Hello world\n");
return 0;
}
編譯:
gcc testmain.c -nostdlib # -nostdlib (不連結標準庫)
程式執行會引發錯誤:/usr/bin/ld: warning: cannot find entry symbol _start; 未找到這個符號
所以說:1. 編譯器預設是找 __start 符號,而不是 main
2. __start 這個符號是程式的起始
3. main 是被標準庫呼叫的一個符號
那麼,這個_start和main函式有什麼關係呢?下面我們來進行進一步探究。
_start函式的實現
該入口是由ld連結器預設的連結指令碼指定的,當然使用者也可以通過引數進行設定。_start由彙編程式碼實現。大致用如下偽程式碼表示:
void _start()
{
%ebp = 0;
int argc = pop from stack
char ** argv = top of stack;
__libc_start_main(main, argc, argv, __libc_csu_init, __linc_csu_fini,
edx, top of stack);
}
對應的彙編程式碼如下:
_start:
xor ebp, ebp //清空ebp
pop esi //儲存argc,esi = argc
mov esp, ecx //儲存argv, ecx = argv
push esp //引數7儲存當前棧頂
push edx //引數6
push __libc_csu_fini//引數5
push __libc_csu_init//引數4
push ecx //引數3
push esi //引數2
push main//引數1
call _libc_start_main
hlt
可以看出,在呼叫_start之前,裝載器就會將使用者的引數和環境變數壓入棧中。
main函式執行之前的工作
從_start的實現可以看出,main函式執行之前還要做一系列的工作。主要就是初始化系統相關資源:
Some of the stuff that has to happen before main():
set up initial stack pointer
initialize static and global data
zero out uninitialized data
run global constructors
Some of this comes with the runtime library's crt0.o file or its __start() function. Some of it you need to do yourself.
Crt0 is a synonym for the C runtime library.
1.設定棧指標
2.初始化static靜態和global全域性變數,即data段的內容
3.將未初始化部分的賦初值:數值型short,int,long等為0,bool為FALSE,指標為NULL,等等,即.bss段的內容
4.執行全域性構造器,類似c++中全域性建構函式
5.將main函式的引數,argc,argv等傳遞給main函式,然後才真正執行main函式
main之前執行的程式碼
下面,我們就來說說在mian函式執行之前到底會執行哪些程式碼:
(1)全域性物件的建構函式會在main 函式之前執行。
(2)一些全域性變數、物件和靜態變數、物件的空間分配和賦初值就是在執行main函式之前,而main函式執行完後,還要去執行一些諸如釋放空間、釋放資源使用權等操作
(3)程序啟動後,要執行一些初始化程式碼(如設定環境變數等),然後跳轉到main執行。全域性物件的構造也在main之前。
(4)通過關鍵字attribute,讓一個函式在主函式之前執行,進行一些資料初始化、模組載入驗證等。
示例程式碼
①、通過關鍵字attribute
#include <stdio.h>
__attribute__((constructor)) void before_main_to_run()
{
printf("Hi~,i am called before the main function!\n");
printf("%s\n",__FUNCTION__);
}
__attribute__((destructor)) void after_main_to_run()
{
printf("%s\n",__FUNCTION__);
printf("Hi~,i am called after the main function!\n");
}
int main( int argc, char ** argv )
{
printf("i am main function, and i can get my name(%s) by this way.\n",__FUNCTION__);
return 0;
}
②、全域性變數的初始化
#include <iostream>
using namespace std;
inline int startup_1()
{
cout<<"startup_1 run"<<endl;
return 0;
}
int static no_use_variable_startup_1 = startup_1();
int main(int argc, const char * argv[])
{
cout<<"this is main"<<endl;
return 0;
}
至此,我們就聊完了main函式執行之前的事情,那麼,你是否還以為main函式也是程式執行的最後一個函式呢?結果當然不是,在main函式執行之後還有其他函式可以執行,main函式執行完畢之後,返回到入口函式,入口函式進行清理工作,包括全域性變數析構、堆銷燬、關閉I/O等,然後進行系統呼叫結束程序。
main函式之後執行的函式
1、全域性物件的解構函式會在main函式之後執行;
2、用atexit註冊的函式也會在main之後執行。
atexit函式
原形:
int atexit(void (*func)(void));
atexit 函式可以“註冊”一個函式,使這個函式將在main函式正常終止時被呼叫,當程式異常終止時,通過它註冊的函式並不會被呼叫。編譯器必須至少允許程式設計師註冊32個函式。如果註冊成功,atexit 返回0,否則返回非零值,沒有辦法取消一個函式的註冊。在 exit 所執行的任何標準清理操作之前,被註冊的函式按照與註冊順序相反的順序被依次呼叫。每個被呼叫的函式不接受任何引數,並且返回型別是 void。被註冊的函式不應該試圖引用任何儲存類別為 auto 或 register 的物件(例如通過指標),除非是它自己所定義的。多次註冊同一個函式將導致這個函式被多次呼叫。
函式呼叫的最後的操作就是出棧過程。main()同樣也是一個函式,在結束時,按出棧的順序呼叫使用atexit函式註冊的,所以說,函式atexit是註冊的函式和函式入棧出棧一樣,是先進後出的,先註冊的後執行。
通過atexit可以註冊回撥清理函式。可以在這些函式中加入一些清理工作,比如記憶體釋放、關閉開啟的檔案、關閉socket描述符、釋放鎖等等。
示例程式碼
#include<stdio.h>
#include<stdlib.h>
void fn0( void ), fn1( void ), fn2( void ), fn3( void ), fn4( void );
int main( void )
{
//注意使用atexit註冊的函式的執行順序:先註冊的後執行
atexit( fn0 );
atexit( fn1 );
atexit( fn2 );
atexit( fn3 );
atexit( fn4 );
printf( "This is executed first.\n" );
printf("main will quit now!\n");
return 0;
}
void fn0()
{
printf( "first register ,last call\n" );
}
void fn1(
{
printf( "next.\n" );
}
void fn2()
{
printf( "executed " );
}
void fn3()
{
printf( "is " );
}
void fn4()
{
printf( "This " );
}