程序被执行的过程
预编译--编译--汇编--链接
gcc hello.c
./a.out
预编译
(1)展开#define宏定义
(2)处理条件预编译指令 #ifdef #if #elif #endif
(3)删除所有注释
编译
经过编译器生成.s
汇编
经过汇编器生成.o
链接
处理各个模块间相互引用的部分,主要包括地址和空间分配、符号决议、重定位
目标文件
目标文件、静态库、动态库、可执行文件在结构上都差不多,这里以目标文件代替。比如linux中的ELF文件
以段的形式组合,主要包括:
ELF header | |
.text | 代码段 |
.data | 初始化了的全局变量和局部静态变量 |
.bss | 未初始化的全局变量和局部静态变量 |
.rodata | const修饰的变量、字符串常量 |
.symtab | 符号表 |
.rel.text | 对.text的重定位表,比如调用printf |
...... |
header
段表长度、段表位置、段数量、ELF文件类型(.o/.so/可执行文件,系统通过这个字段判断类型,而不是后缀)等信息
符号表(.symtab)
符号名(函数/变量名) —— 符号值(距段首的偏移地址)——符号所在段
C++符号修饰和函数签名
不同编译器可能不同
int func(int) ===> _Z4funci
int C::func(int) ===> _ZN1C4funcEi
extern "C" 主要就用来控制符号修饰,因为C和C++的符号修饰规则不同,如果需要调用C语言库的函数,采用C++的符号修饰,就会导致找不到符号的错误
强符号和弱符号
强符号:目标文件A和目标文件B同时定义全局变量global,会报错multiple definition of 'global'
TODO: 补充
链接
为什么不把不同目标文件直接顺序拼接
因为可能会存在很多很小的段,每个段会进行分页,分页后很短的段会出现内部碎片
分为两步
- 空间与地址分配(空间: 多个.o变成.exe后,elf文件空间怎么分配;地址: 每个段的虚拟地址和长度 )
- 符号解析与重定位(符号解析、重定位、调整代码中的地址)
空间与地址分配
(1)链接器扫描所有输入的目标文件,计算合并后每个段的位置和长度(比如a的.text和b的.text合并到一起);
在链接之前,所有目标文件的虚拟地址(VMA)是0,链接时就会进行分配,链接后的文件会记录每个段的虚拟地址。
(2)收集所有的符号定义和符号引用,计算他们的虚拟地址,并放到全局的符号表。
每个符号在各自段的offset记录在原先的.o文件中的符号表里,在确定段在全局的虚拟地址后,便可以确定这些符号的虚拟地址了
符号解析与重定位
一个文件如果用到外部文件函数/变量时,编译成目标文件后,编译器将相应语句对应的地址暂时设置为0。同时,编译器会做两件事:
- 符号表:记录某个符号所在段、在所属段的偏移地址
- 重定位表:比如.rel.text .rel.data
重定位表的结构:
- offset: 距离段首的偏移值
- info:重定位入口的类型和符号在符号表中的下标
链接器处理每个重定位入口,查找全局符号表,找到相应符号进行重定位
如果找不到会报"undefined reference to xxx"
C++链接过程的一些问题
重复代码消除
C++模版、虚函数表在每一个用到的文件都会编译成同样的内容,直接链接会造成重复,链接器会进行重复代码消除。
全局构造与析构
全局对象的构造函数在main之前执行,在main之后析构
所以C++的ELF文件中,还额外定义了两种段
- .init: main之前会执行的代码
- .fini: main之后会执行的代码
API和ABI
API是源代码级别的接口;ABI是二进制级别的接口
比如C++的对象内存布局是ABI的一部分; POSIX是一个API标准
静态库链接
静态库是一组目标文件的集合,多目标文件压缩、打包
链接器链接过程中,会把需要的目标文件从库中解压出来,并链接成一个文件