引用动态链接库的原理

引用动态链接库的原理

位置无关代码

可以架在你而无需重定位的代码成为位置无关代码(Position-Independent Code,PIC)。用户对GCC使用-fpic选项指示GNU编译系统生成PIC代码。动态链接库的编译必须总是使用该选项。

在x86-64系统中,对同一个目标模块中符号的引用是不需要特殊处理使之成为PIC。可以用PC相对寻址来编译这些引用,构造目标文件时由静态链接器重定位。然而,对动态链接库定义的外部过程和对全局变量的引用需要一些技巧。

PIC数据引用

无论我们在内存中何处加载一个目标模块,数据段与代码段的距离总是保持不变。因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量,与代码段和数据段的绝对内存位置是无关的。

所以想要生成对全局变量PIC引用,可以在数据段开始的地方构造一张全局偏移量(Global Offset Table,GOT)。在GOT中,每个被这个目标模块引用的全局数据都有一个由编译器生成的重定位记录。在加载时,动态链接库会重定位GOT中的每个条目,使它包含目标的正确的绝对地址。运行的时候,根据数据段和代码段之间距离保持不变的特点,使用PC相对引用得到GOT表中的绝对地址即可。

引用动态链接库的原理_第1张图片

PIC函数调用

假设程序调用有一个动态链接库定义的函数。编译器无法预测这个函数的运行时地址,因为定义它的共享模块再运行时可以加载到任意位置。GNU编译系统使用一种称为延迟绑定的技术将过程地址的绑定推迟到第一次调用该过程时。

使用延迟绑定的动机是对于一个像libc.so这样的动态链接库中包含很多函数,而一个应用程序往往只会使用到其中的小部分。把函数地址的解析推迟到它实际被调用的地方,能避免动态链接库在加载时对没有使用的函数进行不必要的重定位。第一次调用过程运行时开销很大,但是其后的每次调用只会花费一条指令和一个间接的内存引用。

延迟绑定技术使用到两个数据结构:

  1. 过程链接表(PLT)。PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接库中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
  2. 全局偏移量表(GOT)。GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息,GTO[2]是动态链接器在ld-linux.so模块中的入口地址。其余的每个条目对应一个被调用的函数,其地址需要在运行时被解析,每个条目都有一个相匹配的PLT条目。

引用动态链接库的原理_第2张图片

上图展示了,延迟绑定的具体步骤:

  1. 代码段中调用addvec函数,指令跳转到PLT[2],这是addvec函数的PLT条目。
  2. 接着,指令跳转到GOT[4]指向的地址,GOT[4]中应该包含addvec函数的地址。但是第一次进入时,GOT表中没有addvec的地址信息,只是简单地控制传送回PLT[2]中的下一条指令。
  3. 在把addvec的ID压入栈中只有,PLT[2]跳转到PLT[0]。
  4. PLT[0]通过GOT[1]把动态链接库的一个参数压入栈中,然后通过GOT[2]间接跳转进动态链接器中。动态链接库使用两个参数来确定addvec的运行时位置,并将addvec的地址写入GOT[4]中,再把控制传递给addvec。

参考书籍:《深入理解计算机系统(第三版)》

你可能感兴趣的:(C++,Linux,编程语言,linux)