glibc程序入口
程序从main开始吗?
由全局变量可知,main函数执行前就已经完成全局变量的初始化了,由此判断main并非程序的入口!
从全局对象及atexit可知,在main结束之后,仍然执行全局对象的析构及调用atexit注册的收尾函数,由此判断main结束不代表进程结束!
到底哪个函数才是程序的入口呢?
视平台不同,入口函数(又称入口点)的名称不同。在此,仅讨论linux系统的入口(glibc),windows的入口可参见《程序员的自我修养》P324。
glibc的启动过程在不同的情况下差别较大,比如静态glibc和动态glibc,glibc用于可执行文件和用于共享库,这样的差别可组合出4种情况,在此以最简单的可执行文件+静态glibc为例进行说明。
glibc的入口函数为_start(由ld链接器默认的链接脚本指定,可通过参数人为设定自己的入口)。_start由汇编实现,平台相关。书中以i386为例
这省略了不重要代码,可以看到_start最终调用了__libc_start_main。加粗部分为对该部分的完整调用过程:前7个push完成给__libc_start_main(有7个形参)传递参数的任务。
在看回最开始的三条指令前,必须清楚的知道:在调用_start前,装载器会把用户的参数和环境变量压入栈中。
明白了这点,再看回前三条指令:
- xor指令(异或)的作用是将ebp寄存器置0,目的是表明当前是程序的最外层函数。
- pop指令将argc值(即参数个数)传递给esi寄存器
- mov指令将arg0的地址存放到ecx寄存器
通过以上分析,可将_start简化成如下可读性代码:
接着分析__libc_start_main,接口如下:
与上述push的七个参数正好一致,并且参数由右至左入栈(涉及调用惯例问题)。其中:
- ubp_av为arg0的地址
- init为main调用前的初始化函数
- fini为main调用后的收尾工作
- rtld_fini处理与动态加载有关的收尾工作(rtld = runtime loader)
- stack_end为栈底地址,即栈空间的起始地址(栈空间由高地址往地址增长),__libc_start_main内部将会保存栈底地址以备他用。
鉴于__libc_start_main代码略长,就不po出来了,以下简单讲解它主要完成的工作:
- 保存环境变量栈地址(作为参数传递给init函数和main函数)
- 保存栈底地址(书里没讲解具体用途)
- 检查操作系统版本
- 通过glibc内部函数__cxa_atexit(功能类似atexit)注册收尾函数rtld_fini和fini
- 执行init函数
- 调用main函数
- 调用exit函数
exit的代码如下:
void exit (int status) {
while (__exit_funcs != NULL) {
// __exit_funcs是存储由__cxa_atexit和atexit注册的函数链表
// 执行注册函数
__exit_funcs = __exit_funcs->next;
}
...
_exit(status);
}
最后的_exit由汇编实现,平台相关。以i386为例:
可见,_exit仅仅调用了exit这个系统调用(__NR_exit与exit系统调用 一样吗?如果一样的话,岂不是陷入endless loop?)。_exit调用后,进程就结束了。
由此可见,进程正常结束有两种情况:
- main正常返回即未调用exit系统调用,在__libc_start_main执行完main后会主动调用exit
- 若main里主动调用exit,则不执行进程直接结束,不返回到__libc_start_main
亦即,无论如何,exit必然会被执行到,它是进程结束的必经之路。
hlt作用
正常情况下,调用exit后进程就结束了,即hlt不会被执行。而当exit执行失败时,进程无法正常结束,此时调用hlt即可让进程结束,释放资源。
_exit里的hlt和_start里的hlt作用相似!
代码分析路径
__start -> __libc_start_main -> exit -> _exit()
__libc_start_main主要代码
int __libc_start_main (
int (*main)(int, char**, char **),
int argc,
char* __unbounded *__unbounded ubp_av,
__typeof (main)init,
void (*fini)(void),
void (*rtld_fini)(void),
void* __unbounded stack_end) {
#if __UNBOUNDED_POINTERS__
char** argv;
#else
#define argv ubp_av
#endif
int result;
...
char** ubp_ev = &ubp_av[argc + 1];
__environ = ubp_ev;
__libc_stack_end = stack_end;
...
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);
...
result = main(argc, argv, __environ);
exit(result);
}