00 前言

在计算机领域中,地址无关代码(position-independent code,PIC),又称地址无关可执行文件(position-independent executable,PIE),是指可在主存储器中任意位置正确地运行,而不受其绝对地址影响的一种机器码。PIC广泛使用于共享库,使得同一个库中的代码能够被加载到不同进程的地址空间中。PIC还用于缺少内存管理单元的计算机系统中,使得操作系统能够在单一的地址空间中将不同的运行程序隔离开来。

通过这种方式,无限多个进程可以共享一个共享模块的代码段的单一副本,用户可以对 GCC 使用 -fpic 选项指示 GNU 编译系统生成 PIC 代码。

01 PIC 数据引用

编译器在数据段开始的地方创建了一个表,叫做全局偏移量表(Global Offset Table, GOT) 。在 GOT 中,每个被这个目标模块引用的全局数据目标(过程或全局变量)都有一个 8 字节条目。编译器还为 GOT 中每个条目生成一个重定位记录。在加载时,动态链接器会重定位 GOT 中的每个条目,使得它包含目标的正确的绝对地址。每个引用全局目标的目标模块都有自己的 GOT。

用GOT引用全局变量

02 PIC 函数调用

使用了“延迟绑定”解决这个问题,将过程地址的绑定推迟到了第一次调用该过程时

该技术的动机:

  • 对于共享库,应用程序通常只会使用其中很少的一部分,因此该方法可以避免不需要的重定位。

第一次调用,进行绑定的时候开销很大,但是其后的每次调用都只会花费一条指令和一个间接的内存引用。

使用了两个数据结构:

  • GOT 全局偏移量表——数据段的一部分

    • GOT 是一个数组,每个条目都是 8 字节

  • PLT 过程链接表(Procedure Linkage Table)——代码段的一部分

    • PLT 是一个数组,每个条目都是 16 字节

    • 每个被可执行程序调用的库函数都有他自己的 PLT 条目

    • PLT[0] 跳转到动态链接器中

    • PLT[1] 调用系统启动函数 __libc_start_main,初始化执行环境,调用 main 函数并处理其返回值

    • PLT[2] 开始,调用用户代码调用的函数

调用addvec

上图展示了调用 addvec 的原理:

  1. 程序调用进入 PLT[2]

  2. 第一次跳转到下一条指令,将 addvec 的 ID 压入栈中,然后跳转到 PLT[0]

  3. 跳转到动态链接器中,使用两个栈条目确定 addvec 的运行时位置,重写 GOT[4],再跳回 addvec

  4. 后续再调用进入 PLT[2] 就可以直接跳转到 addvec


参考资料:

  1. 地址无关代码 https://baike.baidu.com/item/地址无关代码/22702477

  2. 《深入理解计算机系统》P489