11.1.2 入口函数如何实现(2)
图11-2实际上就是我们根据从_start源代码分析得到的栈布局,让__environ指针指向原来紧跟在argv数组之后的环境变量数组。
|
| (点击查看大图)图11-2 环境变量和参数数组(2) |
图11-2中实线箭头代表ubp_av,而虚线箭头代表__environ。另外这段代码还将栈底地址存储在一个全局变量里,以留作它用。
为什么要分两步赋值给__environ呢?这又是为了兼容bounded惹的祸。实际上,INIT_ARGV_and_ENVIRON根据bounded支持的情况有多个版本,以上仅仅是假定不支持bounded的版本。
接下来有另一个宏:
DL_SYSDEP_OSCHECK (__libc_fatal); |
这是用来检查操作系统的版本,宏的具体内容就不列出了。接下来的代码颇为繁杂,我们过滤掉大量信息之后,将一些关键的函数调用列出:
__pthread_initialize_minimal(); __cxa_atexit(rtld_fini, NULL, NULL); __libc_init_first (argc, argv, __environ); __cxa_atexit(fini, NULL, NULL); (*init)(argc, argv, __environ);
|
这一部分进行了一连串的函数调用,注意到__cxa_atexit函数是glibc的内部函数,等同于atexit,用于将参数指定的函数在main结束之后调用。所以以参数传入的fini和rtld_fini均是用于main结束之后调用的。在__libc_start_main的末尾,关键的是这两行代码:
result = main (argc, argv, __environ); exit (result); } |
在最后,main函数终于被调用,并退出。然后我们来看看exit的实现:
_start -> __libc_start_main -> exit: void exit (int status) { while (__exit_funcs != NULL) { ... __exit_funcs = __exit_funcs->next; } ... _exit (status); } |
其中__exit_funcs是存储由__cxa_atexit和atexit注册的函数的链表,而这里的这个while循环则遍历该链表并逐个调用这些注册的函数,由于其中琐碎代码过多,这里就不具体列出了。最后的_exit函数由汇编实现,且与平台相关,下面列出i386的实现:
_start -> __libc_start_main -> exit -> _exit: _exit: movl 4(%esp), %ebx movl $__NR_exit, %eax int $0x80 hlt |
可见_exit的作用仅仅是调用了exit这个系统调用。也就是说,_exit调用后,进程就会直接结束。程序正常结束有两种情况,一种是main函数的正常返回,一种是程序中用exit退出。在__libc_start_main里我们可以看到,即使main返回了,exit也会被调用。exit是进程正常退出的必经之路,因此把调用用atexit注册的函数的任务交给exit来完成可以说万无一失。
我们看到在_start和_exit的末尾都有一个hlt指令,这是作什么用的呢?在Linux里,进程必须使用exit系统调用结束。一旦exit被调用,程序的运行就会终止,因此实际上_exit末尾的hlt不会执行,从而__libc_start_main永远不会返回,以至_start末尾的hlt指令也不会执行。_exit里的hlt指令是为了检测exit系统调用是否成功。如果失败,程序就不会终止,hlt指令就可以发挥作用强行把程序给停下来。而_start里的hlt的用处也是如此,但是为了预防某种没有调用exit(这里指的不是exit系统调用)就回到_start的情况(例如有人误删了__libc_main_start末尾的exit)。
MSVC CRT入口函数
相信读者对glibc的入口函数已经有了一些了解。但可惜的是glibc的入口函数书写得不是非常直观。事实上,我们也没从glibc的入口函数了解到多少内容。为了从另一面看世界,我们再来看看Windows下的运行库的实现细节。下面是Microsoft Visual Studio 2003里crt0.c(位于VC安装目录的crt\src)的一部分。这里也删除了一些条件编译的代码,留下了比较重要的部分。MSVC的CRT默认的入口函数名为mainCRTStartup:
int mainCRTStartup(void) { ...
|
【责任编辑:
云霞 TEL:(010)68476606】