第九章 运行库
入口函数
一个程序完整的运行过程应该如下。
- 操作系统创建进程后,将控制权交给程序的入口处,入口处是运行库中的入口函数;
- 入口函数对运行库与程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造等;
- 入口函数在完成初始化之后调用main函数,开始执行程序主体部分;
- main函数返回后,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等;
C&C++运行库
C语言运行库
一个C语言运行库大致包含如下功能。
- 启动与退出:包括入口函数与入口函数所依赖的函数;
- 标准函数:由C语言标准规定的C语言标准库所包含的函数实现;
- I/O:I/O功能的封装与实现;
- 堆:堆的封装与实现;
- 语言实现:语言中一些特殊功能的实现;
- 调试:实现调试功能的代码;
其中,以C语言标准库占主要部分。
C语言标准库
ANSI C的标准库由24个C头文件组成,包含数学函数、字符/字符串处理、I/O等基本方面。
- 标准输入输出(stdio.h);
- 文件操作(stdio.h);
- 字符操作(ctype.h);
- 字符串操作(string.h);
- 数学函数(math.h);
- 资源管理(stdlib.h);
- 格式转换(stdlib.h);
- 时间与日期(time.h);
- 断言(assert.h);
- 各种类型的常数(limits.h、float.h);
除此之外,C语言标准库还有实现了一些扩展功能。
- 变长参数(stdarg.h);
- 非局部跳转(setjmp.h);
比如标准库中的printf函数就是变长参数。
关于非局部跳转,如下是一个示例。
1 |
|
程序的输出结果是
1 | Hello, World! |
glibc与GCC
glibc简介
glibc的发布版本由两部分组成——头文件(/usr/include/stdio.h、/usr/include/stdlib.h等)与二进制文件(动态标准库/lib/x86_64-linux-gnu/libc.so.6、静态标准库/usr/lib/x86_64-linux-gnu/libc.a),除了这些标准库以外,glibc还有几个辅助程序运行的运行库/usr/lib/x86_64-linux-gnu/crt1.o、/usr/lib/x86_64-linux-gnu/crti.o、/usr/lib/x86_64-linux-gnu/crtn.o。
程序的入口函数_start就包含于crt1.o,而crti.o与crtn.o是分别用来辅助.init段与.finit段中指令的执行。crt1.o为了支持.init段与.finit段,它会向__libc_start_main
传递两个函数指针__libc_csu_init
与__libc_csu_fini
,这两个函数指针指向函数_init()
与_finit()
。事实上,最终的输出文件的.init段与.finit段分别包含的就是_init()
与_finit()
,其中crti.o与crtn.o中的代码作为这两个函数的开始与结尾部分。
.init段与.finit段正好在main函数执行前后被执行,正好可以用来实现C++全局构造与析构。.init段与.finit段中分别有对一个执行所有构造/析构函数的函数的调用。具体执行所有构造/析构函数的工作由编译器完成。于是就要介绍下面的GCC平台相关目标文件。
GCC平台相关目标文件
在静态链接生成ELF可执行文件时用到如下GCC平台相关目标文件(均在/usr/lib/gcc目录下)。
- x86_64-linux-gnu/7/crtbeginT.o;具体实现C++全局构造。
- x86_64-linux-gnu/7/crtend.o;具体实现C++析构;
- x86_64-linux-gnu/7/libgcc.a;抹除不同平台的差异性,支持GCC的跨平台特性;对应的动态链接版本为libgcc_s.so。
- x86_64-linux-gnu/7/libgcc_eh.a;支持C++的异常处理;
综上,.init段与.finit段只是提供了一个在main函数执行前后执行的机制,具体的C++全局构造与析构分别由crtbeginT.o与crtend.o实现。在”C++全局构造与析构”这一节介绍。
运行库与多线程
C++全局构造与析构
_GLOBAL__I_Hw
对每个编译单元(cpp文件),GCC编译器会遍历其中所有的全局对象,生成一个特殊函数_GLOBAL__I_Hw
(对所在编译单元的所有全局对象进行初始化),然后将此函数的地址放在目标文件的.ctors段。
这样,在链接器链接这些目标文件时,根据”相同段合并原则”,链接器会把所有目标文件的.ctors段合并为输出文件的.ctors段;同样地,crtbeginT.o的.ctors段放在输出文件的.ctors段的开头,它并没有指向哪个_GLOBAL__I_Hw
,链接器会将后面构造函数的个数存放到这个位置,链接器将.ctors段的起始地址定义成符号__CTOR_LIST__
;crtbeginT.o的.ctors段放在输出文件的.ctors段的结尾,这个段的值为0,链接器将.ctors段的结尾地址定义为符号__CTOR_END__
。
C++全局构造
如下是全局构造的调用链。
1 | _start->__libc_start_main->__libc_csu_init->_init->_do_global_ctors_aux |
在_do_global_ctors_aux
函数中具体实现了C++全局构造,关键代码如下。
1 | void __do_global_ctors_aux(void){ |
析构
全局对象的析构与构造的工作类似,如.init变成.finit、_do_global_ctors_aux
变成_do_global_dtor_aux
、__CTOR_LIST__
对应__DTOR_LIST__
;但是注意,析构函数的调用顺序与构造函数的调用顺序相反。