可执行文件和动态库格式
动态链接原理
符号重定位
必要性
C/C++程序是以文档为单位进行的。每个文档首先被编译为一个目标文件,再由链接器把这些目标文件链接为一个可执行文件或动态库。链接的过程就是解决不同C/C++文档之间符号的引用问题。
在编译时,编译器只专注于当前的编译文档,因此,此文档中引用的外部符号地址是无法确定的,也因此,目标文件中也不包含外部符号的地址;而对于当前目标文件内定义的本地符号,编译期只能知道各个符号的相对地址,但是,部分汇编指令只接收绝对地址,因此,当前目标文件内的本地符号仍有必要重定位。
注:
- 目标文件中的外部符号地址无法得知,编译器会用一个“假的”地址来占位(通常是0x0),等到链接时再进行修正
- 因此,为了在链接时知道哪些地址是“假的”需要修正,编译器在编译时会建立重定位表,以记录哪些符号的地址是“假的”
- 根据上面提到的原理,重定位表需要存储在目标文件里,以供链接器读取
- 可以使用
objdump -r file.o来查看重定位表 - 可以使用
objdump -S file.o来查看反汇编结果以及各符号地址
静态链接原理
加载时符号重定位
动态库被加载到的地址是不确定的。
地址无关代码
加载时符号重定位有两个缺点:
- 加载时可能修改代码以及数据段中的地址,因此这些指令代码无法共享(这里的共享是什么?难道是跨进程的共享?)
- 加载时重定位在符号很多时可能耗时较长
ELF使用了一种地址无关代码(PIC, Position Indedpendent Code)机制,借助这种机制,无论动态库被加载到什么地址,访问符号都无需“重定向”,即无需在加载时修正符号地址,也就解决了加载时符号重定位的两个问题。PIC的具体实现取决于指令集架构的设计。
要实现PIC,要解决两个问题:模块内部的符号访问以及模块间的符号访问。
FYI:gcc编译选项里的-fPIC,启用后将使用这种机制访问符号。
模块内符号访问
模块间符号访问
显然,不同模块的加载地址没有任何规律可言,因此任何模块内的访问技巧都无法解决模块间的符号访问问题。解决此问题的方法仍然是经典思路——套娃,即再做一层跳转。Linux/ELF的做法是在动态库的数据段添加一个表,叫做全局偏移量表(GOT, Global Offset Table),每一个外部符号都将占用该表的一个条目,该表由加载器在加载时初始化。
函数延迟绑定
PLT(Procedure Linkage Table)表