In computing, position-independent code (PIC) or position-independent executable (PIE) is a body of machine code that, being placed somewhere in the primary memory, executes properly regardless of its absolute address.

参考资料

  1. Position-independent code-wiki
  2. 深入理解 Linux 位置无关代码 PIC
  3. Position Independent Code (PIC) in shared libraries
  4. 稍微了解地址无关代码(Position-Independent Code)
  5. 浅谈位置无关代码
  6. 《深入理解计算机系统》- PIC(位置无关代码)
  7. MOOC-计算机系统基础(一):程序的表示、转换与链接
  8. 深入理解-位置无关代码

没有开启地址随机化(ASLR - Address Space Layout Randomization)时,系统不会随机化分配程序的虚拟地址空间,程序所有的地址都是按照固定的规则来生成。通过objdump命令反汇编后可以看到,对于全局变量和函数调用的访问,汇编指令跟的地址都是固定的,这样的代码我们就称它为位置相关的。

固定地址的方式虽然简单,但是无法实现一些高级特性比如动态库支持。动态库的代码会通过mmap()系统调用来映射到进程的虚拟地址空间,不同的进程中,同一个动态库映射的虚拟地址是不确定的。如果动态库的实现上使用位置相关的代码,则无法达到其任意地址运行的目的,这种情况下我们就需要引入位置无关代码PIC的概念了。

img

PIC,全称Position Independent Code。位置无关代码是指代码无论被加载到哪个地址上都可以正常执行。gcc选项中添加-fPIC会产生相关代码。

PIC的做法是让指令部分做到地址无关,所以可以让所有进程共享一份。但是数据部分并不地址无关,而是让所有进程在地址空间中都产生一份副本。

所以目标就是就是实现指令部分的无关,而指令中可能会包含对内部和外部的函数调用,以及内部和外部的数据访问,所以这样的划分就需要考虑四种情况。

PIC的实现

已知:当链接器将各个目标文件的所有section组合到一起的时候,链接器完全知道每个section的大小和它们之间的相对位置。因此可以计算出在.TEXT段内任意一条指令相对于.DATA段起始地址的相对偏移量。

引用模块内数据

由“已知”可得,如果知道了当前指令的地址,那么就可以计算出数据段的地址。X86平台上没有获取当前指令指针寄存器IP的值的指令(X64上可以直接访问RIP),但可以通过一个小技巧来获取:

image-20210531201257771

这段代码在实际运行时,会有以下的事情发生:

  • 当cpu执行 call STUB的时候,会将下一条指令的地址(即IP中的值)保存到stack上,然后跳到标签STUB处执行。
  • STUB处的指令是pop ebx,这样就将 “pop ebx”这条指令所在的地址从stack弹出放到了ebx寄存器中,这样就得到了IP寄存器的值。

在知道了当前的绝对地址后,根据已知的偏移量就可以得到数据段中某个变量的地址。

引用模块内函数

static 函数

对这种函数的访问是最容易解决的问题,因为一个动态库在编译成一个模块之后,其中的指令之间的相对位置是固定的,所以通过一个相对跳转指令即可访问。

img

全局函数

因为为全局函数,所以要考虑一个叫做全局符号介入的问题,什么是全局符号介入呢?

在Linux下,当动态链接器加载一个模块时,需要将这个模块的符号加入到全局符号表中,如果某个要加入的符号名已经存在时,也就是此时重复了,这时候会忽略这次的添加操作,以第一次决议的符号为准,未来运行期间访问到这个符号的所有指令,都会使用第一次决议的符号,这时候情况和下面的外部函数情况相同。

引用模块外数据

对于外部数据的访问,是通过全局偏移表global offset table(GOT)来实现的。

GOT是一张在data section中保存的一张表,里面记录了很多地址字段 (entry)。假设一条指令想要引用一个变量,并不是直接去用绝对地址,而是去引用GOT里的一个entry。GOT表在data section中的地址是明确的,GOT的entry包含了变量的绝对地址。

image-20210531201310221

但是还有一个问题,这个GOT表里存储的entry值又是怎么变成实际的绝对地址的呢?

动态加载器会解析rel.dyn段,当它看到重定向类型为R_386_GLOB_DAT的时候,会将符号var实际的地址值替换到记录的偏移处。

引用模块外函数

与数据不同,因为有新特性:延迟绑定。

对于动态库的函数来说,在没有加载到程序的地址空间前,函数的实际地址都是未知的,动态加载器会处理这些问题,解析出实际地址的过程,这个过程称之为绑定。绑定的动作会消耗一些时间,因为加载器要通过特殊的查表、替换操作。

如果动态库有成百上千个函数接口,而实际的进程只用到了其中的几十个接口,如果全部都在加载的时候进行绑定操作,没有意义并且非常耗时。因此提出了延迟绑定的概念,程序只有在使用到对应接口时才实时地绑定接口地址。

为了实现延迟绑定,就额外增加了一个间接表PLT(过程链接表)。

PLT搭配GOT实现延迟绑定的过程如下:

image-20210531201757402

首先跳到PLT表对应函数地址PLT[n],然后取出GOT中对应的entry。GOT[n]里保存了实际要跳转的函数的地址,首次执行时此值为PLT[n]的prepare resolver的地址,这里准备了要解析的函数的相关参数,然后到PLT[0]处调用resolver进行解析。

resolver函数会做几件事情:

(1)解析出代码想要调用的func函数的实际地址A

(2)用实际地址A覆盖GOT[n]保存的plt_resolve_addr的值

(3)调用func函数

在之后的函数调用中,就不需要再走resolver过程了:

image-20210531201922015

留言

⬆︎TOP