程序的运行离不开运行库,运行库在后台默默的做了很多事情,本篇主要介绍一下进程运行过程中运行库在后台默默做了哪些事情?
入口函数
程序被加载后,首先运行的不是main函数,在main函数之前有其他代码初始化main的运行环境,main结束后还需进行相应的环境清理工作,这两部分的工作通常是在运行库中的入口函数中完成的。典型的程序运行流程如下:
- 1、调用exec创建进程加载映射相应的段后,程序的控制权首先交给运行库(这里运行库被该进程共享)的入口函数;
- 2、入口函数对程序运行环境进行初始化,包括堆、IO、线程和全局变量的初始化工作
- 3、之后调用main函数执行程序主体
- 4、main函数返回后,返回到入口函数,进行清理工作包括全局变量析构、堆释放、关闭IO等操作
下面简单介绍以下静态链接时可执行文件的入口函数实现(glibc中实现)过程
glibc入口函数名称为_start,其对应的汇编代码如下所示
_start:
xor ebp, ebp //清空ebp
pop esi //保存argc,esi = argc
mov esp, ecx //保存argv, ecx = argv
push esp //参数7保存当前栈顶
push edx //参数6
push __libc_csu_fini//参数5
push __libc_csu_init//参数4
push ecx //参数3
push esi //参数2
push main//参数1
call _libc_start_main
hlt
这段汇编对应的c伪代码如下
void _start(){
ebp = 0;
int argc = pop from stack;
char ** argv = pop from stack;
__libc_start_main(main, argc, argv, __libc_csu_init, __libc_csu_fini, edx, top of stack)
}
其中__libc_start_main执行过程大致如下
__libc_start_main(main, argc, argv, __libc_csu_init, __libc_csu_fini, edx, top of stack){
1、获取环境变量
2、线程相关初始化
3、libc运行环境初始化
4、调用atexit注册__libc_csu_fini,rtld_fini(和动态库加载的收尾工作)
5、__libc_csu_init
6、code = main()
7、exit(code)
}
exit中的执行过程大致如下
void exit(code){
while(exit_fun != NULL){
call exit_fun
exit_fun = next
}
_exit(status)内部调用系统调用sys_exit
}
总结一下glibc的入口函数执行过程如下
_start --> __libc_start_main --> libc和环境init --> main --> 执行注册的fini --> _exit --> sys_exit
其中的libc和环境init执行的内容较多,下一节详细叙述。
libc和环境init
在上述入口函数流程中libc和环境init过程主要包括两部分内容
- 1、堆和IO初始化
堆初始化:完成相应的数据结构初始化
IO初始化:建立打开文件表;继承父进程的打开文件fd;初始化标准输入输出流,即将fd->file->node通道打通 - 2、c/c++运行库初始化
这里主要执行的工作的进行C++全局变量的构造和析构,在介绍这部分内容前先简单介绍一下c语言的运行库。
C运行库(CRT)
1、简介
一个运行库通常是与平台相关的,与操作系统结合的比较紧密,C运行库可以看成是C语言程序和操作系统之间的抽象层,封装系统调用。linux和window的C语言运行库分别为glibc(GUN C Library)和MSVCRT(Microsoft visual C Run-Time),通常包括以下内容
1、启动与退出实现,即入口函数
2、标准函数实现,c语言标准库
3、I/O封装和实现
4、语言中特殊功能实现
5、调试功能实现
2、发布文件
对于一个发布的运行库通常包含两部分:头文件和相应的静态或动态库,glibc库通常包含以下内容
1、头文件
2、/lib/libc.so.6和/usr/lib/libc.a
3、crt1.o即glibc的启动文件,实现入口函数,并支持.init和.finit启动代码
4、crti.o和crtn.o辅助实现.init和.finit(这两个主要是为了在main前后执行代码的机制)
除此之外在在编译c++程序的时候通常还包括以下几个文件(这几个文件不属于glibc,而是属于GCC)
1、crtBeginT.o和crtend.o配合glibc实现c++的全局构造和析构(实际上glibc中的crti.o和crtn.o只是实现了在main函数前后执行代码的机制,真正的全局构造和析构由crtBeginT.o和crtend.o实现)
2、libgcc.a处理平台直接运算实现的差异性
3、libgcc_eh.a支持c++的异常处理
3、多线程
线程相关实现不是c/c++标准库的内容,因此线程相关库和网络、图像一样属于标准库之外的系统库。但是由于线程在现代程序设计中有着至关重要的地位,因此主流的C运行库在设计时均会考虑多线程相关内容,主要包括两方面
1、实现线程创建、控制和结束等操作,glibc下提供了pthread库
2、保证运行库在多线程环境下正常运行,之所以有这方面的需求是因为起初的运行库设计时还没有多线程的概念,因此没有这方面的考虑,为了能够在多线程环境下运行,主流运行库都提供了一个多线程的版本,相比而言主要的改进包括:使用TLS、加锁、改进函数的调用方式等。
下面将详细介绍以下c++的全局构造和析构
C++全局构造和析构
在前面介绍运行库的启动文件crt1.o,提到其实现了在main前后执行代码的机制,主要过程是链接器将所以目标文件的.init和.fini段合并成init()和fini()函数,后续配合crt1.o在main前后被执行。那么具体什么时候被执行?被谁调用?如何实现全局构造和析构呢?本节将顺着入口函数探索来解答这几个问题。
在入口函数的流程中__libc_start_main内部调用了__libc_csu_init,其具体实现如下
void __libc_csu_init(argc, argv, envp){
...
_init();
size = __init_array_end - __init_array_start;
for(size_t i = 0; i < size; ++i)
(*__init_array_start[i])(argc,argv,envp);
}
其中调用了一个_init函数,这个_init函数就是各个.init段合并成的函数,其内部又调用了__do_global_ctors_aux,这个函数位于crtBeginT.o中,其实现如下
void __do_global_ctors_aux(){
int nptrs = __CTOR_LIST__[0];
for(int i = nptrs; i >= 1; --i)
__CTOR_LIST__[i]();
}
这里比较明显,依次调用了__CTOR_LIST__内部指针指向的函数,可以很容易猜出应该就是各个全局对象的构造函数。那么这个__CTOR_LIST__是如何生成的呢?其生成流程大致如下
1、编译器在编译一个目标文件时,会收集该目标文件的所有全局对象的构造函数并生成一个统一构造函数在内部调用,同时将该函数指针放在目标文件的.ctors段
static void 统一构造函数(){
构造1
构造2
...
atexit(析构函数);//后续说明,保证析构的时候逆序
}
2、链接器链接时,收集所有目标的文件的.ctors段生成最终的输出文件的.ctors段,也就是最终的__CTOR_LIST_\。
但是这里面还存在一些细节,即__CTOR_LIST__到底存了多少个目标文件的.ctors段的指针(即这个数量__CTOR_LIST__[0])呢?由前面链接时可知,在链接时还而外链接了crtBeginT.o和crtend.o这两个目标文件,链接器最终会将数量存放在crtBeginT.o的.ctors段中并将起始地址定义为符号__CTOR_LIST__,crtend.o的.ctors段就则相对简单存放了一个0,并定义了符号__CTOR_END__。整个过程可以用如下图表示

析构实现原理和上述过程类似,__libc_start_main中调用了__cxa_exit注册了__libc_csu_fini函数,在exit中将调用__libc_csu_fini其具体过程和上述基本一致。但是保证析构顺序和构造严格反序过程加重了链接器的工作,这种方式逐渐被放弃,进而采用在每个目标文件的统一构造函数后注册该目标文件全局变量的析构函数的方式(如上述伪代码所示)实现析构。这种方式就是直接采用了atexit注册机制,而不是链接器自己去拼接析构函数指针列表~
end~
网友评论