入口函数与程序初始化浅析

1.程序开始的真相

操作系统装载程序之后,首先运行的代码并不是main的第一行,而是某些别的代码。这些代码负责准备好main函数执行所需要的环境,并且负责调用main函数。

铁证1:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 int a = 3;
 5 
 6 int main(int argc, char* argv[])
 7 {
 8     int* p = (int*)malloc(sizeof(int));
 9     scanf("%d", p);
10     printf("%d", a + *p);
11     free(p);
12 }

铁证2:

 1 #include <string>
 2 using namespace std;
 3 string v;
 4 double foo()
 5 {
 6     return 1.0;
 7 }
 8 
 9 double g = foo();
10 int main(){}

铁证3:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 void foo(void)
 4 {
 5     printf("bye!\n");
 6 }
 7 int main()
 8 {
 9     atexit(&foo);
10     printf("endof main\n");
11 }

三个程序均可以正常/正确的输入输出。

一个典型的程序运行步骤大致如下:

  • 操作系统在创建进程后, 把控制权交到了程序的入口,这个入口往往是运行库中的某个入口函数。
  • 入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造等等。
  • 入口函数在完成初始化之后,调用main函数,正式开始执行程序主体部分。
  • main函数执行完毕之后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程。

2.GLIBC入口函数的实现

以最简单的静态glibc用于可执行文件为例。

glibc的程序入口为_start(由ld链接器默认的链接脚本所指定)。_start由汇编实现,实现如下:

 1 libc\sysdeps\i386\elf\Start.S:
 2 _start:
 3     xorl %ebp, %ebp
 4     popl %esi
 5     movl %esp, %ecx
 6 
 7     pushl %esp
 8     pushl %edx
 9     pushl $__libc_csu_fini
10     pushl $__libc_csu_init
11     pushl %ecx
12     pushl %esi
13     pushl main
14     call __libc_start_main
15 
16     hlt

改写后的伪代码如下:

1 void _start()
2 {
3     %ebp = 0;
4     int argc = pop from stack
5     char** argv = top of stack;
6     __libc_start_main( main, argc, argv, __libc_csu_init, __libc_csu_fini, edx, top of stack );
7 }

其中argv除了指向参数表外,还隐含紧接着环境变量表。这个环境变量表要在__libc_start_main里从argv内提取出来。

所谓“环境变量”,是存在于系统中的一些公用数据,任何程序都可以访问。通常来说,环境变量存储的都是一些系统的公共信息,例如系统搜索路径,当前OS版本等。在Linux下,直接在命令行中输入export即可查看。

实际执行代码的函数是__libc_start_main,代码很长,我们一段段看:

_start -> __libc_start_main

 1 int __libc_start_main(
 2     int (*main) (int, char**, char**),
 3     int argc,
 4     char* __unbounded* __unbounded ubp_av,
 5     __typeof (main) init,
 6     void (*fini) (void),
 7     void (*rtld_fini) (void),
 8     void* __unbounded stack_end)
 9 {
10 #if __BOUNDED_POINTERS__
11     char **argv;
12 #else
13 #define argv ubp_av
14 #endif
15     int result;

这是__libc_start_main的函数头部,可见和_start函数里的调用一致,一共有7个参数,其中main由第一个参数传入,紧接着是argc和argv(这里称为ubp_av,因为其中还包含了环境变量表)。除了main的函数指针外,外部还要传入3个函数指针,分别是:

  • init:main调用前的初始化工作。
  • fini:main结束后的收尾工作。
  • rtld_fini:和动态加载有关的收尾工作,rtld是RunTime LoaDer。

最后的stack_end表明了栈底的地址,即最高的栈地址。

1     char** ubp_ev = &ebp_av[argc + 1];
2     INIT_ARGV_and_ENVIRON;   // _environ = ubp_ev;
3     __libc_stack_end = stack_end;

第2行是一个宏定义,让__environ指针指向原来紧跟在argv数组之后的环境变量数组。

接下来有另一个宏:

  DL_SYSDEP_OSCHECK (__libc_fatal);

用来检查操作系统的版本,宏的具体内容就不列出了。

④接下来的代码颇为复杂,我们过滤掉大量信息之后,将一些关键的函数调用列出:

1     __pthread_initialize_minimal();
2     cxa_atexit(rtld_fini, NULL, NULL);
3     libc_init_first (argc, argv, __environ);
4     __cxa_atexit(fini, NULL, NULL);
5     (*init)(argc, argv, __environ);

这部分代码进行了一连串的系统调用,注意到,__cxa_atexit函数是glibc的内部函数,等同于atexit,用于将参数指定的函数在main结束之后调用。所以以参数传入的fini和rtld_fini均是用于main结束之后调用的。

在__libc_start_main的末尾,关键的是这两行代码:

1     result = main (argc, argv, __environ);
2     exit (result);
3 }

在最后,main函数终于被调用,并退出。

然后我们来看看exit的实现:

_start -> __libc_start_main -> exit

 1 void exit (int status)
 2 {
 3     whie (__exit_funcs != NULL)
 4     {
 5         ...
 6         __exit_funcs = __exit_funcs->next;
 7     }
 8     ...
 9     _exit (status);
10 }

其中,__exit_funcs是存储由_cxa_atexit和atexit注册的函数的链表,而这里的这个while循环则遍历该链表并逐个调用这些注册的函数,由于其中琐碎代码过多,这里就不具体列出了。

最后的_exit函数由汇编实现,且与平台有关:

_start -> __libc_start_main -> exit -> _exit

1 _exit:
2     movl 4(%esp), %ebp
3     movl $__NR_exit, %eax
4     int $0x80
5     hlt

可见,_exit的作用仅仅是调用了exit这个系统调用。也就是说,_exit调用后,进程就会直接结束。程序正常结束有两种情况,一种是main函数的正常返回,一种是程序中用exit退出。

在__libc_start_main里我们可以看到,即使main返回了,exit也会被调用。exit是进程正常退出的必经之路,因此把调用atexit注册的函数的任务交给exit来完成可以说万无一失。

总结:GLIBC入口函数的调用过程为 _start -> __libc_start_main -> main -> exit -> _exit

MSVC的入口函数的过程就不详细阐述了,总体流程如下:

  1. 初始化和OS版本有关的全局变量。
  2. 初始化堆。
  3. 初始化I/O
  4. 获取命令行参数和环境变量。
  5. 初始化C库的一些数据。
  6. 调用main并记录返回值。
  7. 检查错误并将main的返回值返回。
posted @ 2023-06-09 11:56  Hell0er  阅读(353)  评论(0编辑  收藏  举报