地址无关代码(PIC)
0x00 动态链接的基本思想
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接一起形成一个完整的程序,而不是像静态链接一样把所有的程序模块都链接成一个单独的可执行文件。
0x01 关于模块
我们知道在动态链接下,一个程序被分成了若干个文件,有程序的主要部分,即可执行文件(例如Program)和程序所依赖的共享对象(例如Lib.so),很多时候我们也吧这些部分称为模块,即动态链接下的可执行文件和共享对象都可以看作是程序的一个模块。
0x02 地址无关代码概述
在计算机领域,地址无关代码(Position-independent code,即PIC)是指可在主存储器中任意位置正确地运行,而不受其绝对地址影响的一种机器码。PIC被广泛应用于共享库,这就使得共享库中的代码能够被加载到进程的任意地址空间中(这就是其不受绝对地址影响的一种表现)。下面我们可以通过分析共享对象模块中的地址引用类型来理解地址无关代码,示例代码如下:
static int a;
extern int b;
extern void ext();
void bar()
{
a = 1;
b = 2;
}
void foo()
{
bar();
ext();
}
若按照是否跨模块来分,地址引用类型可以分为两大类:
- 模块内部引用:
- 模块内部的函数调用、跳转:
在示例代码中foo函数对bar函数的调用就属于模块内部调用,这种调用情况比较简单,因为调用者和被调用者都处于同一个模块,它们之间的相对位置都是固定的。这一调用可能会产生如下代码:
8048344: 8048344: 55 push %ebp 8048345: 89 e5 mov %esp,%ebp 8048347: 5d pop %ebp 8048348: c3 ret 8048349: ... 8048357: e8 e8 ff ff ff call 8048344 804835c: b8 00 00 00 00 mov $0x0,%eax
foo函数对bar函数的调用的那条指令实际上是一条相对地址调用指令,这条指令中第一个字节
e8
是相对偏移调用指令call
的调用码,而指令中的后四个字节e8 ff ff ff
是目的地址8048344
相对于当前指令的下一条指令的偏移(0xffffffe8
是-24的补码形式,即bar的地址为0x804835c + (-24) = 0x8048344
),所以只要bar函数和foo函数的相对位置不变,这条指令是地址无关的。即无论模块被加载到哪个位置,这条指令都是有效的,且这种相对地址的方式对于jmp
指令也有效。- 模块内部的数据访问:
一个模块前面一般是若干个页的代码,后面紧跟着若干个页的数据,这些页之间的相对位置是固定的,也就是说,任何一条指令与它需要访问的模块内部数据之间的相对位置是固定的,那么只需要相对于当前指令加上固定的偏移量就可以访问模块内部数据了。
模块外部引用:
- 模块间数据访问:
相对模块内的数据访问而言,模块间的数据访问目标地址要等到装载时才决定,比如示例中的变量b,它被定义在其它模块中,并且该地址在装载时才能确定。若要使得代码地址无关,基本的思想就是把跟地址相关的部分放到数据段里面,很明显,这些其它模块的全局变量地址是跟模块装载地址有关的。ELF文件的做法是在数据段里面建立一个指向这些变量的指针数组,也被称为全局偏移表(Global Offset Table),当代码需要引用该全局变量时,可以通过 GOT 中相对应的项间接引用,它的基本原理如下图所示:
当某个指令要访问变量b时,程序首先会找到 GOT,然后根据 GOT 中变量所对应的项找到变量的目标地址。每个变量都对应一个四个字节的地址,链接器在转载模块的时候会查找每个变量所在的地址,然后填充 GOT 中的各个项,以确保每个指针所指向的地址正确。由于 GOT 本身是放在数据段的,所以它可以在模块装载时被修改,并且每个进程都可以拥有独立的副本,相互不受影响。
- 模块间的函数调用、跳转:
对于模块间的函数调用和跳转,我们可以采用类似模块间数据访问的方法来解决,与之不同的是,GOT 中相应的项保存的是目标函数的地址,当模块要调用目标函数时,可以通过 GOT 中的项进行间接跳转。
注:本文部分参考于《程序员的自我修养》,如有不正确之处,望指正!