本文最后更新于:2020年6月29日 晚上
* 当执行”Hello World“程序时,是从main函数开始执行的吗?答案当然是No,No,No! *
一个程序的运行步骤如下:
操作系统在在创建进程后,把控制权交给了程序的入口,这个入口一般是运行库中的某个入口函数
入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造等
入口函数完成初始化后,调用main函数,正式开始执行程序主体部分
main函数执行完毕后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程
接下来具体分析的是基于glibc 2.6.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); }
栈分布情况
第二步:调用_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);//程序开始退出 }
- 栈分布情况
- 源码分析
第三步:调用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); }
第四步:调用_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
* 终于写完了。。。经历了源码找不到、源码看不懂之后,最后还是搞定了。。(●ˇ∀ˇ●) *
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!