本文最后更新于:2020年6月29日 晚上

* 当执行”Hello World“程序时,是从main函数开始执行的吗?答案当然是No,No,No! *

一个程序的运行步骤如下:

  • 操作系统在在创建进程后,把控制权交给了程序的入口,这个入口一般是运行库中的某个入口函数

  • 入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造等

  • 入口函数完成初始化后,调用main函数,正式开始执行程序主体部分

  • main函数执行完毕后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程

接下来具体分析的是基于glibc 2.6.1中静态链接的、用于可执行文件的情况

  1. 第一步:glibc的程序入口为_start(这个入口是有ld链接器默认的链接脚本指定的,可以通过相关参数设定入口)

    • _start由汇编实现且和平台相关

      //将汇编改写为伪代码
      void start()
      {
      	%ebp = 0;//使ebp为0,证明其是最外层函数
      	int argc = pop from stack;//从栈中获取argc,隐含envp
      	char **argv = top from stack;//从栈中获取argv
      	//调用_libc_start_main()函数
      	_libc_start_main(main, argc, argv, _libc_csu_init, _libc_csu_fini, edx, top of stack);
      }
    • 栈分布情况
      环境变量和参数数组

  2. 第二步:调用_libc_start_main()函数,下面对_libc_start_main()函数进行源码分析

    • 源码分析
      //glibc-2.6.1\csu\Libc-start.c
      //为使结构条理更加清晰,删减了部分代码
      /*
      _libc_start_main()参数说明
      main
      argc
      ubp_av:包括argv和envp
      函数指针init:main调用前的初始化工作
      函数指针fini:main结束后的收尾工作
      函数指针rtld_fini:动态加载有关的收尾工作(runtime loader)
      stack_end:指明栈地址,即最高的栈地址
      */
      STATIC int
      LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
      		 int argc, 
      		 char *__unbounded *__unbounded ubp_av,
      		 __typeof (main) init,
      		 void (*fini) (void),
      		 void (*rtld_fini) (void), 
      		 void *__unbounded stack_end)
      {
      	#if __BOUNDED_POINTERS__
      	  char **argv;
      	#else
      	# define argv ubp_av
      	#endif
      	  /* Result of the 'main' function.  */
      	  int result;
      	
      	......
      	
      	#ifndef SHARED
      	  char *__unbounded *__unbounded ubp_ev = &ubp_av[argc + 1];
      	  INIT_ARGV_and_ENVIRON;//将宏展开得到 __environ = ubp_ev,即让__environ指针指向envp
      	  /* Store the lowest stack address.  This is done in ld.so if this is
      	     the code for the DSO.  */
      	  __libc_stack_end = stack_end;
      	
      	......
      	
      	# ifdef DL_SYSDEP_OSCHECK
      	  if (!__libc_multiple_libcs)
      	    {
      	      /* This needs to run to initiliaze _dl_osversion before TLS
      		 setup might check it.  */
      	      DL_SYSDEP_OSCHECK (__libc_fatal);//检查操作系统版本
      	    }
      	# endif
      	
      	......
      	
      	__pthread_initialize_minimal ();
      	//__cxa_atexit()为glibc内部函数,等同于atexit
      	//rtld_fini在main函数结束后调用
      	__cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);
      	__libc_init_first (argc, argv, __environ);
      	//fini在main函数结束后调用
      	__cxa_atexit ((void (*) (void *)) fini, NULL, NULL);
      	(*init) (argc, argv, __environ MAIN_AUXVEC_PARAM);
      	
      	......
      	
      	result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);//开始执行main函数,result为退出码
      	
      	......
      	
      	exit (result);//程序开始退出
      }
    • 栈分布情况
      环境变量和参数数组(2)
  3. 第三步:调用exit()函数,下面对exit()函数进行源码分析

    //glibc-2.6.1\stdlib\Exit.c
    void exit (int status)
    {
    	//__exit_funcs存储由cxa_atexit和atexit注册的函数链表
    	//遍历该链表并逐个调用注册函数
    	while (__exit_funcs != NULL)
    	{
    	
    		struct exit_function_list *old;
    		
    		......
    
    		old = __exit_funcs;
    		__exit_funcs = __exit_funcs->next;//依次指向节点
    		if (__exit_funcs != NULL)
    			free (old);
        }
    
    	......
    
        _exit (status);
    }
  4. 第四步:调用_exit()函数,下面对_exit()函数进行源码分析

    // glibc-2.6.1\sysdeps\mach\hurd\Dl-sysdep.c
    //_exit函数调用后,进程就会直接结束
    //程序正常结束的两种情况
    //1.通过main函数正常返回
    //2.程序代码中使用exit
    void weak_function attribute_hidden _exit (int status)
    {
    	__proc_mark_exit (_dl_hurd_data->portarray[INIT_PORT_PROC], W_EXITCODE (status, 0), 0);
    	while (__task_terminate (__mach_task_self ()))
    		__mach_task_self_ = (__mach_task_self) ();
    }

经过对源代码的分析,可以肯定”Hello World“程序的确不是从main函数开始执行的!

综上所述,函数调用过程如下:
start -> libc_start_main -> exit -> _exit

* 终于写完了。。。经历了源码找不到、源码看不懂之后,最后还是搞定了。。(●ˇ∀ˇ●) *