程序员的自我修养-静态链接

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文件头, 
数据目录等一些结构, 使得能够满足程序执行时的需求.
"""
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值