计算机系统篇之链接(7):位置无关代码(上)
Author: stormQ
Created: Wednesday, 15. April 2020 04:35PM
Last Modified: Thursday, 05. November 2020 11:02PM
本文描述了 Linux 系统中引入位置无关代码(PIC)的动机、工作原理和优缺点。
共享库的一个主要目的就是允许无限多个正在运行的进程共享内存中相同的库代码(即共享库的 .text Section)。为了实现这一目的,现代系统以这样一种方式编译共享模块的代码段,使得可以把它们(即共享库的 .text Section)加载到内存的任何位置而无需链接器修改。可以加载而无需重定位的代码称为位置无关代码(Position Independent Code, PIC)。
PIC 数据引用的实现原理
编译器通过运用以下事实来生成对全局变量的PIC引用:无论我们在内存中的何处加载一个目标模块(包括共享目标模块),数据段和代码段的距离总是保持不变。因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量,与代码段和数据段的绝对内存位置是无关的。
编译器在数据段开始的地方创建了一个表,叫做全局偏移表(Global Offset Table, GOT)。在GOT中,每个被这个目标模块引用的全局数据目标(过程或全局变量)都有一个8字节条目。编译器还为GOT中每个条目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。每个引用全局目标的目标模块都有自己的GOT。GOT属于数据段的一部分。
PIC 函数调用的实现原理
如果程序调用一个由共享库定义的函数。编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。不过,这种方法并不是PIC,因为它需要链接器修改调用模块的代码段,GNU编译系统使用了一种很有趣的技术来解决这个问题,称为延迟绑定(lazy binding),将过程地址的绑定推迟到第一次调用该过程时。
使用延迟绑定的动机是对于一个像libc.so这样的共享库输出的成百上千个函数中,一个典型的应用程序只会使用其中很少的一部分。把函数地址的解析推迟到它实际被调用的地方,能避免动态链接器在加载时进行成百上千个其实并不需要的重定位。第一次调用过程的运行时开销很大,但是其后的每次调用都只花费一条指令和一个间接的内存引用。
延迟绑定是通过两个数据结构之间简洁但又有些复杂的交互来实现的,这两个数据结构是:GOT和过程链接表(Procedure Linkage Table, PLT)。如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT。GOT属于数据段的一部分,而PLT属于代码段的一部分。
过程链接表(PLT)。PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
全局偏移表(GOT)。GOT是一个数组,其中每个条目是8字节地址。与PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。初始时,每个GOT条目都指向对应PLT条目的第二条指令。
PLT和GOT如何协作在运行时解析函数的地址:在“由共享库定义的函数”被第一次调用时,延迟解析它的运行时地址,具体步骤为:1)不直接调用该函数,程序调用进入该函数的PLT条目。2)该PLT条目的第一条指令通过对应的GOT条目进行间接跳转。因为每个GOT条目初始时指向它对应的PLT条目的第二条指令,这个间接跳转只是简单地把控制权传送回该PLT条目的第二条指令。3)在把该函数的ID压入栈中之后,该PLT条目跳转到PLT[0]。4)PLT[0]通过GOT[1]间接地把动态链接器的一个参数压入栈中,然后通过GOT[2]间接跳转进动态链接器中。动态链接器使用两个栈条目来确定该函数的运行时位置,用这个地址重写其GOT条目,再把控制传递给该函数。后续调用该函数时的步骤:1)和前面一样,控制传递到该函数的PLT条目。2)不过这次通过对应的GOT条目的间接跳转会将控制权直接转移到该函数。
PIC 的优点
节省内存资源。实现了无限多个正在运行的进程共享内存中相同的库代码。
提高安全性。相比动态链接技术中的代码段可写,PIC 技术中的代码段只读。
PIC 的缺点
下一篇:计算机系统篇之链接(8):位置无关代码(中)——真正理解 PIC 数据引用的工作原理(Linux X86-64 示例)