任何一个C 程序,它的背后都有一套庞大的代码来进行支撑,以使得该程序能够正常运行。这套代码至少包括入口函数,及其所依赖的函数所构成的函数集合。另外包括各种标准库函数的实现。
这样的一个代码集合称之为运行时库(Runtime Library),C 语言的运行时库,称为 C 运行库(CRT)。
C 运行库大致包含的功能:
1. 启动与退出函数
2. 由C 语言标准规定的C 语言标准库所拥有的函数实现。
3. I/O 功能的封装与实现。
4. 堆的封装和实现
5. 语言中一些特殊功能的实现。
6. 实现调试功能的代码。在这些运行库的组成成分中,C语言标准库占据了主要地位并且大有来头。C语言标准库是C语言标准化的基础函数库,我们平时使用的printf、exit等都是标准库中的一部分。标准库定义了C语言中普遍存在的函数集合,我们可以放心地使用标准库中规定的函数而不用担心在将代码移植到别的平台时对应的平台上不提供这个函数。
ANSIC的标准库由24个C头文件组成。与许多其他语言(如Java)的标准库不同,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
glibc即GNU C Library,是GNU旗下的C标准库。最初由自由软件基金会FSF(FreeSoftwareFoundation)发起开发,目的是为GNU操作系统开发一个C标准库。GNU操作系统的最初计划的内核是Hurd,一个微内核的构架系统。Hurd因为种种原因开发进展缓慢,而Linux因为它的实用性而逐渐风靡,最后取代Hurd成了GNU操作系统的内核。于是glibc从最初开始支持Hurd到后来渐渐发展成同时支持Hurd和Linux,而且随着Linux的越来越流行,glibc也主要关注Linux下的开发,成为了Linux平台的C标准库。
20世纪90年代初,在glibc成为Linux下的C运行库之前,Linux的开发者们因为开发的需要,从Linux内核代码里面分离出了一部分代码,形成了早期Linux下的C运行库。这个C运行库又被称为Linuxlibc。这个版本的C运行库被维护了很多年,从版本2一直开发到版本5。如果你去看早期版本的Linux,会发现/lib目录下面有libc.so.5这样的文件,这个文件就是第五个版本的Linux libc。1996年FSF发布了glibc2.0,这个版本的glibc开始支持诸多特性,比如它完全支持POSIX标准、国际化、IPv6、64-位数据访问、多线程及改进了代码的可移植性。在此时Linuxlibc的开发者也认识到单独地维护一份Linux下专用的C运行库是没有必要的,于是Linux开始采用glibc作为默认的C运行库,并且将2.x版本的glibc看作是Linuxlibc的后继版本。于是我们可以看到,glibc在/lib目录下的.so文件为libc.so.6,即第六个libc版本,而且在各个Linux发行版中,glibc往往被称为libc6。glibc在Linux平台下占据了主导地位之后,它又被移植到了其他操作系统和其他硬件平台,诸如FreeBSD、NetBSD等,而且它支持数十种CPU及嵌入式平台。目前最新的glibc版本号是2.8(2008年4月)。
glibc的发布版本主要由两部分组成:
一部分是头文件,比如stdio.h、stdlib.h等,它们往往位于/usr/include
另外一部分则是库的二进制文件部分。二进制部分主要的就是C语言标准库,它有静态和动态两个版本。
动态库位于/lib/libc.so.6;而静态标准库位于/usr/lib/libc.a。事实上glibc除了C标准库之外,还有几个辅助程序运行的运行库,这几个文件可以称得上是真正的“运行库”。它们就是/usr/lib/crt1.o、/usr/lib/crti.o和/usr/lib/crtn.o。
所以我们总结出:
glibc 运行库 就是我们常说的 Linux 下的C 运行库, 这个库中包含了C 标准库以及其他的东西。C标准库有静态和动态两个/lib/libc.so.6 , /usr/lib/libc.a. 当然这2个C标准库并不仅仅包含ANSIC定义的东西也包含了像线程之类的扩展的一些东西。 所以说 glibc 运行库是 标准C库的扩展。
crt1.o里面包含的就是程序的入口函数_start,由它负责调用__libc_start_main初始化libc并且调用main函数进入真正的程序主体。实际上最初开始的时候它并不叫做crt1.o,而是叫做crt.o,包含了基本的启动、退出代码。由于当时有些链接器对链接时目标文件和库的顺序有依赖性,crt.o这个文件必须被放在链接器命令行中的所有输入文件中的第一个,为了强调这一点,crt.o被更名为crt0.o,表示它是链接时输入的第一个文件。
后来由于C++的出现和ELF文件的改进,出现了必须在main()函数之前执行的全局/静态对象构造和必须在main()函数之后执行的全局/静态对象析构。为了满足类似的需求,运行库在每个目标文件中引入两个与初始化相关的段“.init”和“.finit”。运行库会保证所有位于这两个段中的代码会先于/后于main()函数执行,所以用它们来实现全局构造和析构就是很自然的事情了。链接器在进行链接时,会把所有输入目标文件中的“.init”和“.finit”按照顺序收集起来,然后将它们合并成输出文件中的“.init”和“.finit”。但是这两个输出的段中所包含的指令还需要一些辅助的代码来帮助它们启动(比如计算GOT之类的),于是引入了两个目标文件分别用来帮助实现初始化函数的crti.o和crtn.o。
与此同时,为了支持新的库和可执行文件格式,crt0.o也进行了升级,变成了crt1.o。crt0.o和crt1.o之间的区别是crt0.o为原始的,不支持“.init”和“.finit”的启动代码,而crt1.o是改进过后,支持“.init”和“.finit”的版本。这一点我们从反汇编crt1.o可以看到,它向libc启动函数__libc_start_main()传递了两个函数指针“__libc_csu_init”和“__libc_csu_fini”,这两个函数负责调用_init()和_finit(),我们在后面“C++全局构造和析构”的章节中还会详细分析。
为了方便运行库调用,最终输出文件中的“.init”和“.finit”两个段实际上分别包含的是_init()和_finit()这两个函数,我们在关于运行库初始化的部分也会看到这两个函数,并且在C++全局构造和析构的章节中也会分析它们是如何实现全局构造和析构的。crti.o和crtn.o这两个目标文件中包含的代码实际上是_init()函数和_finit()函数的开始和结尾部分,当这两个文件和其他目标文件安装顺序链接起来以后,刚好形成两个完整的函数_init()和_finit()。于是在最终链接完成之后,输出的目标文件中的“.init”段只包含了一个函数_init(),这个函数的开始部分来自于crti.o的“.init”段,结束部分来自于crtn.o的“.init”段。为了保证最终输出文件中“.init”和“.finit”的正确性,我们必须保证在链接时,crti.o必须在用户目标文件和系统库之前,而crtn.o必须在用户目标文件和系统库之后。链接器的输入文件顺序一般是:
ld crt1.o crti.o [user_objects] [system_libraries]crtn.o
由于crt1.o(crt0.o)不包含“.init”段和“.finit”段,所以不会影响最终生成“.init”和“.finit”段时的顺序。
yexiang@ubuntu:<x86_64-linux-gnu>$ nm crt1.o
0000000000000000 D __data_start
0000000000000000 W data_start
0000000000000000 R _IO_stdin_used
U __libc_csu_fini
U __libc_csu_init
U __libc_start_main
U main
0000000000000000 T _start
yexiang@ubuntu:<x86_64-linux-gnu>$ nm crti.o
0000000000000000 T _fini
U _GLOBAL_OFFSET_TABLE_
w __gmon_start__
0000000000000000 T _init
yexiang@ubuntu:<x86_64-linux-gnu>$ objdump crtn.o -s
crtn.o: file format elf64-x86-64
Contents of section .init:
0000 4883c408 c3 H....
Contents of section .fini:
0000 4883c408 c3 H....
在默认情况下,ld链接器会将libc、crt1.o等这些CRT和启动文件与程序的模块链接起来,但是有些时候,我们可能不需要这些文件,或者希望使用自己的libc和crt1.o等启动文件,以替代系统默认的文件,这种情况在嵌入式系统或操作系统内核编译的时候很常见。GCC提高了两个参数“-nostartfile”和“-nostdlib”,分别用来取消默认的启动文件和C语言运行库。
其实C++全局对象的构造函数和析构函数并不是直接放在.init和.finit段里面的,而是把一个执行所有构造/析构的函数的调用放在里面,由这个函数进行真正的构造和析构。
除了全局对象构造和析构之外,.init和.finit还有其他的作用。由于它们的特殊性(在main之前/后执行),一些用户监控程序性能、调试等工具经常利用它们进行一些初始化和反初始化的工作。当然我们也可以使用“__attribute__((section(“.init”)))”将函数放到.init段里面,但是要注意的是普通函数放在“.init”是会破坏它们的结构的,因为函数的返回指令使得_init()函数会提前返回,必须使用汇编指令,不能让编译器产生“ret”指令。
已经了解 crt1.o、crti.o和crtn.o,剩下的还有几个crtbeginT.o、libgcc.a、libgcc_eh.a、crtend.o。严格来讲,这几个文件实际上不属于glibc,它们是GCC的一部分,它们都位于GCC的安装目录下:
l /usr/lib/gcc/x86_64-linux-gnu/5/crtbeginT.o
l /usr/lib/gcc/x86_64-linux-gnu/5/libgcc.a
l /usr/lib/gcc/x86_64-linux-gnu/5/libgcc_eh.a
l /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o
首先是crtbeginT.o及crtend.o,这两个文件是真正用于实现C++全局构造和析构的目标文件。那么为什么已经有了crti.o和crtn.o之后,还需要这两个文件呢?我们知道,C++这样的语言的实现是跟编译器密切相关的,而glibc只是一个C语言运行库,它对C++的实现并不了解。而GCC是C++的真正实现者,它对C++的全局构造和析构了如指掌。于是它提供了两个目标文件crtbeginT.o和crtend.o来配合glibc实现C++的全局构造和析构。事实上是crti.o和crtn.o中的“.init”和“.finit”提供一个在main()之前和之后运行代码的机制,而真正全局构造和析构则由crtbeginT.o和crtend.o来实现。
由于GCC支持诸多平台,能够正确处理不同平台之间的差异性也是GCC的任务之一。比如有些32位平台不支持64位的 longlong类型的运算,编译器不能够直接产生相应的CPU指令,而是需要一些辅助的例程来帮助实现计算。libgcc.a里面包含的就是这种类似的函数,这些函数主要包括整数运算、浮点数运算(不同的CPU对浮点数的运算方法很不相同)等,而libgcc_eh.a则包含了支持C++的异常处理(ExceptionHandling)的平台相关函数。另外GCC的安装目录下往往还有一个动态链接版本的libgcc.a,为libgcc_s.so。