文章目录
  1. 入口函数
  2. C&C++运行库
    1. C语言运行库
    2. C语言标准库
    3. glibc与GCC
  3. 运行库与多线程
  4. C++全局构造与析构
    1. _GLOBAL__I_Hw
    2. C++全局构造
    3. 析构

入口函数

一个程序完整的运行过程应该如下。

  • 操作系统创建进程后,将控制权交给程序的入口处,入口处是运行库中的入口函数;
  • 入口函数对运行库与程序运行环境进行初始化,包括堆、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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <setjmp.h>

jmp_buf b;

void f(){
longjmp(b,1);
}

int main()
{
if(setjmp(b)){
printf("World!\n");
}
else{
printf("Hello, ");
f();
}
}

程序的输出结果是

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
2
3
4
5
6
7
void __do_global_ctors_aux(void){
/* Call constructor functions */
unsigned long nptrs=(unsigned long)__CTOR_LIST__[0];
unsigned long i;
for(i=nptrs;i>=1;i--)
__CTOR_LIST__[i]();
}

析构

全局对象的析构与构造的工作类似,如.init变成.finit、_do_global_ctors_aux变成_do_global_dtor_aux__CTOR_LIST__对应__DTOR_LIST__;但是注意,析构函数的调用顺序与构造函数的调用顺序相反。