在之前的文章中,介绍了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 " );
}