前言

如何理解DLL不是地址无关的?DLL与ELF的对比分析中我们仔细分析了ELF的PIC实现,并且提到

但是数据的相对寻址往往没有相对于当前指令地址(PC)的寻址方式,也就是说虽然同一模块内变量的相对位置也是固定的,但我们不能像函数调用那样指定一个相对偏移就得到变量的值,因为没有这样的指令。

而不得不大费周章先调用一个函数拿到PC,再加上偏移地址来获得变量的地址或GOT的位置。也感慨

设想如果系统支持数据相对寻址,那么a = 1只需要像函数调用那样指定一个偏移量然后赋值就好了,但很可惜现代的系统都不支持。

幸运的是,在x64下终于支持这种新的寻址方式了:RIP-relative addressing,RIP相对寻址。

x86下PIC的尴尬

在介绍这种新的寻址方式前,还是先回顾一下x86下的解决方法,有对比才能看得出来效果。

模块内部数据访问

static int a;

void bar(){
  a = 1;
}

编译生成32位的共享对象:

gcc -fPIC -shared -m32 lib.c -o libx86.so

我们知道变量a被放到了.bss段,并且不参与全局符号(static限制)。那么a = 1这个对a的赋值,就是找到a的地址,然后赋值就好了。因为模块内数据相对位置不变,我们只需要在赋值处设定一个相对偏移值就好了,事实是怎样的呢?看看反汇编:

000004d0 <bar>:
 4d0:   55                      push   %ebp
 4d1:   89 e5                   mov    %esp,%ebp
 4d3:   e8 12 00 00 00          call   4ea <__x86.get_pc_thunk.ax>
 4d8:   05 28 1b 00 00          add    $0x1b28,%eax
 4dd:   c7 80 14 00 00 00 01    movl   $0x1,0x14(%eax)
 4e4:   00 00 00
 4e7:   90                      nop
 4e8:   5d                      pop    %ebp
 4e9:   c3                      ret

000004ea <__x86.get_pc_thunk.ax>:
 4ea:   8b 04 24                mov    (%esp),%eax
 4ed:   c3                      ret

4d3是一个相对地址调用,调用__x86.get_pc_thunk.ax,这个函数直接将返回地址(下一条指令地址,就是PC啦)放到ecx寄存器(执行call指令函数调用时,下一指令地址会被压到栈顶,而esp寄存器始终指向栈顶,(%esp)即获取栈顶值,494即将此值交给ecx),然后454给PC加上一个偏移量0x118c,45a则继续添加偏移量0x28,然后将1赋值到此偏移地址,即a = 1

为什么不直接像4d3的call相对地址调用那样,相对地址获取a的地址,而要大费周章调用函数来拿到PC?就像最开始说的,系统不支持。

模块间,GOT

我们知道PIC对于模块间的数据访问和函数调用都是通过GOT,即调用时首先查找GOT,从GOT获得真实地址后转跳。那么GOT中数据也相当于是一个变量的值,怎么首先找到GOT呢?

void bar(){
  foo();
}

反汇编:

000003b0 <foo@plt>:
 3b0:   ff a3 0c 00 00 00       jmp    *0xc(%ebx)
 3b6:   68 00 00 00 00          push   $0x0
 3bb:   e9 e0 ff ff ff          jmp    3a0 <_init+0x2c>

00000500 <bar>:
 500:   55                      push   %ebp
 501:   89 e5                   mov    %esp,%ebp
 503:   53                      push   %ebx
 504:   83 ec 04                sub    $0x4,%esp
 507:   e8 13 00 00 00          call   51f <__x86.get_pc_thunk.ax>
 50c:   05 f4 1a 00 00          add    $0x1af4,%eax
 511:   89 c3                   mov    %eax,%ebx
 513:   e8 98 fe ff ff          call   3b0 <foo@plt>
 518:   90                      nop
 519:   83 c4 04                add    $0x4,%esp
 51c:   5b                      pop    %ebx
 51d:   5d                      pop    %ebp
 51e:   c3                      ret

0000051f <__x86.get_pc_thunk.ax>:
 51f:   8b 04 24                mov    (%esp),%eax
 522:   c3                      ret

毫无悬念还是和“模块内部数据访问”类似,通过函数调用得到PC,再加上偏移得到GOT。

RIP相对寻址

RIP可以理解成Relative Instruction-Pointer。Intel对RIP有个简单的解释:

The 64-bit instruction pointer RIP points to the next instruction to be executed, and supports a 64-bit flat memory model.

即RIP指针指向下一条指令,是不是就是PC?

Intel还谈到了RIP相对寻址的用处:

RIP-relative addressing: this is new for x64 and allows accessing data tables and such in the code relative to the current instruction pointer, making position independent code easier to implement.

明确说了让PIC更容易实现。下面就介绍x64下使用了RIP的PIC实现。

x64下PIC

模块内部数据访问

还是刚才那个例子,生成64位的共享对象。看看反汇编:

0000000000000650 <bar>:
 650:   55                      push   %rbp
 651:   48 89 e5                mov    %rsp,%rbp
 654:   c7 05 c6 09 20 00 01    movl   $0x1,0x2009c6(%rip)        # 201024 <a>
 65b:   00 00 00
 65e:   90                      nop
 65f:   5d                      pop    %rbp
 660:   c3                      retq

654行直接给rip寄存器添加0x2009c6的偏移得到变量a的地址,然后赋值1。是不是和模块内函数调用的call一样简单方便了?相比于32位下减少了一次函数调用和一次加法,RIP相对寻址的效果就是这么显著。

模块间,GOT

再看看模块间函数调用查找GOT,还是之前的例子,看看反汇编:

0000000000000570 <foo@plt>:
 570:   ff 25 a2 0a 20 00       jmpq   *0x200aa2(%rip)        # 201018 <_GLOBAL_OFFSET_TABLE_+0x18>
 576:   68 00 00 00 00          pushq  $0x0
 57b:   e9 e0 ff ff ff          jmpq   560 <_init+0x20>

0000000000000690 <bar>:
 690:   55                      push   %rbp
 691:   48 89 e5                mov    %rsp,%rbp
 694:   b8 00 00 00 00          mov    $0x0,%eax
 699:   e8 d2 fe ff ff          callq  570 <foo@plt>
 69e:   90                      nop
 69f:   5d                      pop    %rbp
 6a0:   c3                      retq

同样也是用到了RIP相对寻址,省去了之前的函数调用和添加偏移。

总结

现在的Linux发行版几乎全都是x64了,像之前那样通过函数调用获取PC的方法也基本见不到了。x64带给我们的不仅仅是更大的寻址空间和更快的计算速度,同样还有这些不容易看见的改进。我们在之前谈到DLL不是地址地址无关的时候,也狠狠抨击了ELF下PIC的大量开销,随着x64的到来,PIC是不是又显示出了优越性呢?

参考