ELF重定位

链接重定位

在通过编译和汇编后,就生成了目标文件,链接就是把这些目标文件加工后合并成一个输出文件的过程。

链接过程可以分为两步:

指令修正

typedef struct {
    Elf64_Addr       r_offset;
    Elf64_Word       r_info;
} Elf64_Rel;

typedef struct {
    Elf64_Addr    r_offset;
    Elf64_Word    r_info;
    Elf64_Sword   r_addend;
} Elf64_Rela;

上面是重定义节区的数据结构。

重定位类型表

名称 大小 计算公式 说明
R_X86_64_NONE 0 none none  
R_X86_64_64 1 word64 S + A  
R_X86_64_PC32 2 word32 S + A - P  
R_X86_64_GOT32 3 word32 G + A  
R_X86_64_PLT32 4 word32 L + A - P  
R_X86_64_COPY 5 none none  
R_X86_64_GLOB_DAT 6 word64 S  
R_X86_64_JUMP_SLOT 7 word64 S  
R_X86_64_RELATIVE 8 word64 B + A  
R_X86_64_GOTPCREL 9 word32 G + GOT + A - P  
R_X86_64_32S 11 word32 S + A  
R_X86_64_16 12 word16 S + A  
R_X86_64_PC16 13 word16 S + A - P  
R_X86_64_8 14 word8 S + A  
R_X86_64_PC8 15 word8 S + A - P  
R_X86_64_DPTMOD64 16 word64    
R_X86_64_DTPOFF64 17 word64    
R_X86_64_TPOFF64 18 word64    
R_X86_64_TLSGD 19 word32    
R_X86_64_TLSLD 20 word32    
R_X86_64_DTPOFF32 21 word32    
R_X86_64_GOTTPOFF 22 word32    
R_X86_64_TPOFF32 23 word32    

我们这里讲编译中的链接过程的重定位,一般是R_X86_64_PC32R_X86_64_PLT32类型

例子

下面我们以具体的例子举例并进一步的说明

/***************main.c***************/
int add(int first, int second);
int main() {
    int a,b;
    a = 3;
    b = 4;
    int ret = add(a,b);
    return 0;
}

int add(int first, int second) {
    return first+second;
}

我们首先编译一下main.c

✗ gcc -c main.c -o main.o

然后用objdump看一下代码段内容

➜  relocate git:(master) ✗ objdobjdump -d main.o

main.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:	f3 0f 1e fa          	endbr64
   4:	55                   	push   %rbp
   5:	48 89 e5             	mov    %rsp,%rbp
   8:	48 83 ec 10          	sub    $0x10,%rsp
   c:	c7 45 f4 03 00 00 00 	movl   $0x3,-0xc(%rbp)
  13:	c7 45 f8 04 00 00 00 	movl   $0x4,-0x8(%rbp)
  1a:	8b 55 f8             	mov    -0x8(%rbp),%edx
  1d:	8b 45 f4             	mov    -0xc(%rbp),%eax
  20:	89 d6                	mov    %edx,%esi
  22:	89 c7                	mov    %eax,%edi
  24:	e8 00 00 00 00       	callq  29 <main+0x29>
  29:	89 45 fc             	mov    %eax,-0x4(%rbp)
  2c:	b8 00 00 00 00       	mov    $0x0,%eax
  31:	c9                   	leaveq
  32:	c3                   	retq

0000000000000033 <add>:
  33:	f3 0f 1e fa          	endbr64
  37:	55                   	push   %rbp
  38:	48 89 e5             	mov    %rsp,%rbp
  3b:	89 7d fc             	mov    %edi,-0x4(%rbp)
  3e:	89 75 f8             	mov    %esi,-0x8(%rbp)
  41:	8b 55 fc             	mov    -0x4(%rbp),%edx
  44:	8b 45 f8             	mov    -0x8(%rbp),%eax
  47:	01 d0                	add    %edx,%eax
  49:	5d                   	pop    %rbp
  4a:	c3                   	retq

我们可以看到,在 24: e8 00 00 00 00 callq 29 <main+0x29>代码段,这里是调用add函数的指令,函数地址是00 00 00 00。

我们看一下main.o的重定位表

➜  relocate git:(master) ✗ readelf -r main.o

Relocation section '.rela.text' at offset 0x250 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000025  000a00000004 R_X86_64_PLT32    0000000000000033 add - 4

...

根据上文提到的,add符号的重定位类型是R_X86_64_PLT32,计算公式是L + A - P

由于编译器在处理该外部函数符号的时候是不知道这个符号是定义在普通对象还是共享对象里的,所以在rela表中统一将其定义为R_X86_64_PLT32类型,在随后的链接过程会根据函数符号是定义在普通对象还是共享对象进行不同的处理。

  • 该函数符号定义在普通对象中
- LD将其当做普通的R_X86_64_PC32类型进行处理,这时L+A-P = S+A-P
  • 该函数符号定义在共享对象中
- LD将其作为R_X86_64_PLT32进行处理,LD会为其create一个“函数名@plt”过程和在.got.plt表中创建一个表项(用于存储函数被加载后的实际虚拟地址),并将代码中对该函数的访问改为对该过程的访问,这些操作都要在静态链接的时候完成的,这个过程(函数名@plt)的地址就是L,所以relocate计算公式变为:L+A-P。

最后动态链接的时候会将函数的实际虚拟地址更新到.got.plt表项中,这样该过程通过.got.plt表项就可以间接跳转到实际要访问的函数了。

从符号表中知道函数符号定义在当前文件中,所以,L+A-P = S+A-P。我们再回忆一下, S表示符号的实际地址,这个在第一步空间的重新分配中就可以得到,.text段合并重新分配后,并将分配的值写入符号表,我们假设这个值是0x115c,P表示被修正(用 r_offset 计算)的存储单元的位置, (空间从新分配后,新指令位置写入新的重定位表中, 所以我们能够获取到修正后的指令位置), 我们假设是0x114e, 这里的加数是-4, 所以,我们计算

S+A-P = 0x115C - 0x114E + (-4) = 0xA

这里计算的其实就是调用者地址和函数(变量)地址的差值,在加上加数

我们看一下最后链接的结果(上述的假设数值都是我们从实际链接后的代码段中拿到的,所以很一致)

...

0000000000001129 <main>:
    1129:	f3 0f 1e fa          	endbr64
    112d:	55                   	push   %rbp
    112e:	48 89 e5             	mov    %rsp,%rbp
    1131:	48 83 ec 10          	sub    $0x10,%rsp
    1135:	c7 45 f4 03 00 00 00 	movl   $0x3,-0xc(%rbp)
    113c:	c7 45 f8 04 00 00 00 	movl   $0x4,-0x8(%rbp)
    1143:	8b 55 f8             	mov    -0x8(%rbp),%edx
    1146:	8b 45 f4             	mov    -0xc(%rbp),%eax
    1149:	89 d6                	mov    %edx,%esi
    114b:	89 c7                	mov    %eax,%edi
    114d:	e8 0a 00 00 00       	callq  115c <add>
    1152:	89 45 fc             	mov    %eax,-0x4(%rbp)
    1155:	b8 00 00 00 00       	mov    $0x0,%eax
    115a:	c9                   	leaveq
    115b:	c3                   	retq

000000000000115c <add>:
    115c:	f3 0f 1e fa          	endbr64
    1160:	55                   	push   %rbp
    1161:	48 89 e5             	mov    %rsp,%rbp
    1164:	89 7d fc             	mov    %edi,-0x4(%rbp)
    1167:	89 75 f8             	mov    %esi,-0x8(%rbp)
    116a:	8b 55 fc             	mov    -0x4(%rbp),%edx
    116d:	8b 45 f8             	mov    -0x8(%rbp),%eax
    1170:	01 d0                	add    %edx,%eax
    1172:	5d                   	pop    %rbp
    1173:	c3                   	retq
    1174:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
    117b:	00 00 00
    117e:	66 90                	xchg   %ax,%ax

...

这时候相对应的函数调用地址已经填上的正确的数值114d: e8 0a 00 00 00 callq 115c <add>,也就是我们上述计算出来的值(使用小端序),右边callq 115c <add>是工具给的解释,方便我们查看调试。

callq指令后面加上了跳转的偏移值,这是callq的相对寻址方式。

什么是加数?(我猜测的)相对寻址指向目标指令的上一条指令,所以需要加上一个偏移值,现在主流的cpu架构的指令长度都是4,所以,这里的加数就是-4 —— 指向上一条指令

这里的值采用补码表示,因为原码的补码等于自身,所以这里看起来像是原码;但是一旦add函数合并的时候,跑到main函数前面了,这里的偏移值就是一个负数,必须采用补码表示

我们这里的例子是一个文件的情况,对于多个重定位文件的链接,链接过程也是一样的。