地址无关代码(Position-Independent Code)
如果共享文件(.so文件)需要加载到一个特定的地址才能运行,将造成.so文件的地址冲突问题。因此共享对象的最终装载地址在编译时是不确定的。
如下所示:
readelf --headers libc.so
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
[...]
LOAD 0x000000 0x00000000 0x00000000 0x1aef0c 0x1aef0c R E 0x1000
LOAD 0x1af23c 0x001b023c 0x001b023c 0x02c98 0x057e0 RW 0x1000
共享库的装载地址是从地址0x00000000开始的,这是一个无效地址,当使用此共享库的程序运行时,可以在进程虚拟空间分布看到最终的装载地址。
GOT表--绑定全局变量地址
让我们通过观察地址无关代码具体实现方式,来看看GOT表在其中的作用。
使用如下代码生成地址无关共享库文件:
$ cat test.c
static int a;
extern int b;
extern void ext();
void bar()
{
a=1;
b=2;
}
void foo()
{
bar();
ext();
}
gcc -shared -fPIC -o libtest.so test.c
模块内部数据访问
上述代码中静态变量a为模块内部数据,为生成地址无关代码指令不能直接包含a的绝对地址,bar函数汇编代码如下:
0000000000000710 <bar>:
710: 55 push %rbp
711: 48 89 e5 mov %rsp,%rbp
714: c7 05 16 09 20 00 01 movl $0x1,0x200916(%rip) # 201034 <a>
71b: 00 00 00
71e: 48 8b 05 b3 08 20 00 mov 0x2008b3(%rip),%rax # 200fd8 <_DYNAMIC+0x1c8>
访问数据a的对应指令为:
714: c7 05 16 09 20 00 01 movl $0x1,0x200916(%rip) # 201034 <a>
此时%rip指向当前指令的下一条指令地址:71e处。a的地址为0x200916+0x71e= 0x201034,然后将1赋值给a。
如果该共享库被加载到0x1000000000000000,那么a的实际地址为0x1000000000000000+0x200916+0x71e= 0x1000000000201034
模块间数据访问
变量b定义在其他模块中,跟模块装载地址有关,根据把地址相关的部分放到数据段的思想,ELF在数据段建立了一个指向这些变量的指针数组,也称为全局变量表(GOT Global Offset Table)
当指令访问变量b时先找到GOT表,再根据GOT表找到变量地址。
71e: 48 8b 05 b3 08 20 00 mov 0x2008b3(%rip),%rax # 200fd8 <_DYNAMIC+0x1c8>
725: c7 00 02 00 00 00 movl $0x2,(%rax)
`
b的偏移地址为0x200fd8
$objdump -h libtest.so
Sections:
Idx Name Size VMA LMA File off Algn
[...]
19 .got 00000030 0000000000200fd0 0000000000200fd0 00000fd0 2**3
CONTENTS, ALLOC, LOAD, DATA
[...]
0x200fd8属于got表范围,再看一下libtest.so的重定位项:
$objdump -R libtest.so
libtest.so: file format elf64-x86-64
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
[...]
0000000000200fd8 R_X86_64_GLOB_DAT b
[...]
可以看到b的地址需要重定位,位于0x200fd8,GOT偏移8。
所以,当动态加载器加载该so文件时,会先去看它的relocations,然后找到b的值,然后把它重定位到.got部分的0×200828地址处。当访问变量b时,直接取.got中对应地址0x200fd8处,做到了地址无关。
PLT表--延迟绑定函数
在一个程序运行过程中,可能很多函数在程序执行完都不会用到,例如,错误处理函数、很少用到的功能模块等。所以ELF引入了延迟绑定概念,当函数第一次使用时才进行绑定(符号查找、重定位等)
当我们调用外部函数时按照通常做法使用GOT表进行跳转,为实现延迟绑定,在这个过程中通过PLT表又增加一层间接跳转。
模块间调用、跳转
函数通过PLT项进行跳转,还是使用上述的libtest.so进行说明。
00000000000005f0 <ext@plt>:
5f0: ff 25 2a 0a 20 00 jmpq *0x200a2a(%rip) # 201020 <_GLOBAL_OFFSET_TABLE_+0x20>
5f6: 68 01 00 00 00 pushq $0x1
5fb: e9 d0 ff ff ff jmpq 5d0 <_init+0x20>
ext@plt的第一条指令跳转到0x201020,此地址为GOT表中保存ext函数的表项,由于延迟绑定的原因,第一次使用此函数时ext()的地址还未填入此表项:
0000000000201000 <_GLOBAL_OFFSET_TABLE_>:
201000: 10 0e
201002: 20 00
...
201018: e6 05
20101a: 00 00
20101c: 00 00
20101e: 00 00
201020: f6 05 00 00 00 00 00 # 201027 <_GLOBAL_OFFSET_TABLE_+0x27>
此时0x201020的值为0x5f6,就是执行exp@plt的第二条指令:
5f6: 68 01 00 00 00 pushq $0x1
其中1代表ext这个符号在重定位表".rel.plt"中的下标。
后续指令:
5fb: e9 d0 ff ff ff jmpq 5d0 <_init+0x20>
此指令跳转到执行符号解析和重定位工作的函数.
经过一系列过程,ext()真正的地址填入到ext@plt中。
当我们再次调用ext()时第一条指令就将跳转到真正的ext()函数中,而不再执行后续指令。
ELF将GOT表分为了两个表".got" 和 ".got.plt",其中".got"用来保存全局变量引用的地址,".got.plt"用来保存函数引用地址。
参考
https://www.freebuf.com/articles/system/135685.html
《程序员的自我修养》
网友评论