1. 静态链接
"""
1. 在链接中, 目标文件之间相互拼合实际上是目标文件之间对地址的引用, 即对函数和变量的地址的引用. 在链接中,
我们将函数和变量统称为符号, 函数名或变量名就是符号名.
2. 链接过程中很关键的一部分就是符号的管理, 每一个目标文件都会有一个相应的符号表, 这个表里面记录了目标文件中
所用到的所有符号. 每个定义的符号有一个对应的值, 叫做符号值. 对于变量和函数来说, 符号值就是它们的地址.
"""
2. ELF 符号表结构
/* Symbol table entry. */
typedef struct {
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
// nm SimpleSection.o // 查看SimpleSection.o的符号结果
3. 特殊符号
"""
- __executable_start: 该符号为程序的起始地址, 不是入口地址, 是程序的最开始的地址.
- __etext__: 该符号为代码段结束地址, 即代码段最末尾的地址.
- _edata: 该符号为数据段结束地址, 即数据段最末尾的地址.
- _end: 该符号为程序结束地址. 以上地址都为程序被装载时的虚拟地址.
"""
4. 编译器默认函数和初始化的全局变量为强符号, 未初始化的全局变量为弱符号.
"""
- 规则1: 不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号); 如果有多个强符号定义, 则链接器报符号重复定义错误.
- 规则2: 如果一个符号在某个目标文件中是强符号, 在其他文件中是弱符号, 那么选择强符号.
- 规则3: 如果一个符号在所有目标文件中都是弱符号, 那么选择其中占用空间最大的一个.
"""
extern int ext; // 外部引用
int weak; // 弱符号
int strong = 1 // 强符号
__attribute__((weak)) weak2 = 2 // 弱符号
int main() // 强符号
{
return 0;
}
5. 符号决议
"""
- 对外部目标文件的符号引用在目标文件被最终链接成可执行文件时, 它们须要被正确决议, 如果没有找到该符号的定义,
链接器就会报符号未定义错误(强引用).
- 在处理弱引用时, 如果该符号有定义, 则链接器将该符号的引用决议; 如果该符号未被定义, 则链接器对于该引用不报错.
对于未定义的弱引用, 链接器不认为它是一个错误. 一般对于未定义的弱引用, 链接器默认其为0, 或者是一个特殊的值,
以便于程序代码能够识别.
"""
__attribute__ ((weakref)) void foo();
int main()
{
if (foo()) foo(); // 避免非法地址访问
}
6. 静态链接
"""
1) 空间与地址分配: 扫描所有的输入目标文件, 获得它们的各个段的长度, 属性和位置, 并且将输入目标文件中的符号表中所有的符号
定义和符号引用收集起来, 统一放到一个全局符号表. 这一步中, 链接器将能够获得所有输入目标文件的段长度, 并且将它们合并,
计算输出文件中各个段合并后的长度与位置, 并建立映射关系.
2) 符号解析与重定位: 使用上面第一步中收集到的所有信息, 读取输入文件中段的数据, 重定位信息, 并且进行符号解析与重定位,
调整代码中的地址等.事实上第二步是链接过程的核心, 特别是重定位过程.
3) -> odjdump -r a.o 查看目标文件的重定位表
"""
7. 符号解析
"""
- 重定位过程也伴随着符号的解析过程, 每个目标文件都可能定义一些符号, 也可能引用到定义在其他目标文件的符号. 重定位的
过程中, 每个重定位的入口都是对一个符号的引用, 那么当链接器需要对某个符号的引用进行重定位时, 它就要确定这个符号的
目标地址. 这时候链接器就会去查找由所有输入.
- 目标文件的符号表组成的全局符号表, 找到相应的符号后进行重定位.
- readelf -s a.o 查看 a.o 的符号表
"""
8. COMMON
"""
- GCC的"fno-common"允许我们把所有未初始化的全局变量不以COMMON块的形式处理, 或者使用 __attribute__ 扩展:
int global __attribute__ ((nocommon))
- 一旦一个未初始化的全局变量不是以 COMMON 块的形式存在, 那么它就相当于一个强符号, 如果其他目标文件中还有同一个变量的
强符号定义, 链接就会发生符号重定义错误.
"""
9. C++相关问题
"""
1) 重复代码消除: C++编译器在很多时候会产生重复的代码, 比如模板, 外部内联函数和虚函数表都有可能在不同的编译单元里
生成相应的代码.
2) 全局构造与析构: C++的全局对象的构造函数在 main 之前被执行, C++全局对象的析构函数在 main 之后被执行.
- .init: 该段里面保存的是可执行指令, 它构成了进程的初始化代码. 因此, 当一个程序开始运行时,
在 main 函数被调用之前, Glibc 的初始化部分安排执行这个段的中的代码.
- .fini: 该段保存着进程终止代码指令. 因此, 当一个程序的 main 函数正常退出时, Glibc 会安排执行这个段的代码.
"""
10. Application Binary Interface
"""
符号修饰标准, 变量内存布局, 函数调用方式等这些跟可执行代码二进制兼容性相关的内容称为ABI.
- 对于C语言的目标代码来说, 以下几个方面会决定目标文件之间是否二进制兼容:
1): 内置类型的大小和存储器中的放置方式
2): 组合类型的存储方式和内存分布
3): 外部符号与用户定义的符号之间的命名方式和解析方式
4): 函数调用方式, 比如参数入栈顺序, 返回值如何保持
5): 堆栈的分布方式
6): 寄存器使用约定, 函数调用时那些寄存器可以修改, 那些需要保存, 等等.
- 对于C++来说, 做到二进制兼容比C来得更为不易:
1): 继承类体系的内存分布, 如基类, 虚基类在继承类中的位置
2): 指向成员函数的指针的内存分布, 如何通过指向成员函数的指针来调用成员函数, 如何传递 this 指针
3): 如何调用虚函数, vtable的内容和分布形式, vtable指针在object中的位置
4): template 如何实例化
5): 外部符号的修饰
6): 全局对象的构造和析构
7): 异常的产生和捕获机制
8): 标准库的细节问题, RTTI如何实现
9): 内联函数访问细节
"""
11. 文件格式
"""
1): PE/COFF文件与ELF文件非常相似, 它们都是基于段的结构的二进制文件格式. Windows下最常见的目标文件格式就是COFF文件格式
微软的编译器产生的目标文件都是这种格式. COFF文件有一个很有意思的段叫 .drectve段, 这个段中保存的是编译器传递给链接器
的命令行参数, 可以通过这个段实现指定运行库等功能.
2): Windows下的可执行文件, 动态链接库等都使用PE文件格式, PE文件格式是COFF文件格式的改进版本, 增加了PE文件头,
数据目录等一些结构, 使得能够满足程序执行时的需求.
"""