• Linux上程序执行的入口--Main


    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()等方式来得到环境变量。

  • 相关阅读:
    边框渐变
    循环遍历对象的方法
    H5 判断手机设备类型及不同类型调起分享
    Java工具延时队列
    银行风控模型
    解除网页复制需登陆限制 鸟
    weditor安装出错一直装不上
    【刷题】【dp】蓝桥·回路计数
    【笔记】第六章 利用数组处理批量数据
    【笔记】【C语言】格式控制符 / 格式输出函数printf()详解
  • 原文地址:https://www.cnblogs.com/Ph-one/p/4594543.html
Copyright © 2020-2023  润新知