目标模块:你要加载进内存的模块
--加载位置无关代码(PIC)--
位置无关代码(PIC):
可以加载,无需重定位的代码。
在内存中加载目标模块时,数据段与代码段的距离总是保持不变。
与绝对内存地址无关 (不然程序就难以在代码段中引用数据段的数据)
利用这一点,
编译器会在目标模块的数据段的开始创建一个表,用于存放(被加载的)目标模块需要的全局数据目标(函数或变量)。
这个表叫全局偏移量表(GOT)。
GOT中每个被目标模块引用的全局数据目标都有一个8个字节的条目(全局数据目标的绝对地址)。
编译器会为每个条目生成重定位记录,使得它(指条目)为正确的绝对地址。
我们写的代码会通过GOT间接访问全局数据目标。
这样的话全局数据目标即使在其他模块也能被正确引用。
示例:尝试加载某个共享模块中的某个函数
链接完成后的情况:
其他模块也可以通过访问GOT[4] 使用 全局变量 addr
--调用共享库的函数--
程序调用共享库定义的函数时,编译器无法预测函数的运行地址
(共享库加载地址无法被预测)
解决问题的方法:
1.为这个引用生成一条重定位记录,动态链接器在程序加载时解析。
2.延迟绑定:第一次调用函数时再绑定。
GNU编译系统:"我们使用延迟绑定"
为什么延迟绑定:
一个库装了一堆函数,程序可能就用两个……
延迟绑定可以避免动态链接器加载成千上万用不到的重定位。
第一次调用开销不小,但是之后的每次调用都只会花费一条指令和一个间接的内存引用。
延迟绑定通过两个数据结构完成:
GOT 与 过程链接表(PLT) //听名字就知道这个表是干嘛的
PLT:代码段的一部分
PLT是一个数组,其中每个条目占16字节。
每个目标模块都有一个GOT与PLT
每个被调用的函数都有一个PLT 条目
实例: 看看第一次 调用 qwq_function 会发生什么
代码调用函数其实是在
call 函数对应的PLT表的位置。
# 对应图中的第一步:程序调用进入PLT[3] (qwq_function 对应的PLT条目)
开始执行PLT的代码:
(GOT[5] 是 qwq_function 对应的GOT条目)
跳转到 *GOT[5] (GOT[5] 中存储的地址)
第一次调用时 *GOT[5] 的值为 qwq_function 对应的PLT条目中的第二条指令。
# 这个间接跳转在 第一次调用之后的每次调用 中都有着巨大作用
函数id入栈
调用动态链接器从共享库中寻找函数 qwq_function
动态链接器用函数id与GOT[1] 确认 qwq_function 的运行时位置
然后把这个位置(qwq_function 的运行时位置)写回 GOT[5]
动态链接器把控制权移交给 qwq_function
# 执行 qwq_function
第二次call 的时候就是:
两步搞定!
// 如有问题,还麻烦各位读者(dalao)斧正。