之前一个老掉牙的程序要从X86移植到X86_64上面, 编译的时候出错,提示很多要加-fpic去编译。
ld: /tmp/18cd9ee.o: relocation R_X86_64_32 against `XXXX' can not be used when making a shared object; recompile with -fPIC
呃,看了一下Makefile竟然原程序的作者都没有把动态链接库编译成位置无关,难道这个老程序用的时候那个平台的pic导致性能下降太多? 折腾一下总算都跑了起来。不过为啥X86-64 必须要求share lib要用pic编译呢?
首先回顾一下PIC
PIC和非PIC的最核心不同在于前者在编译链接时做了重定位操作,而后者是在运行时做重定位操作。
先看一下32位非PIC代码的情况
比如下列代码:
extern int _g1;
static int _g2;
int func()
{
int i = _g2++;
return _g1+i;
}
编译成gcc汇编码:
gcc -m32 –share -S pic.c -o pic-nopic.s
.file "pic.c"
.local _g2
.comm _g2,4,4
.text
.globl func
.type func, @function
func:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $16, %esp
movl _g2, %eax
movl %eax, -4(%ebp)
addl $1, %eax
movl %eax, _g2
movl _g1, %edx
movl -4(%ebp), %eax
addl %edx, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
编译成so:
gcc -m32 –share pic.c -o pic-nopic.so
然后反汇编:
0000055c :
55c: 55 push %ebp
55d: 89 e5 mov %esp,%ebp
55f: 83 ec 10 sub $0x10,%esp
562: a1 10 20 00 00 mov 0x2010,%eax
567: 89 45 fc mov %eax,-0x4(%ebp)
56a: 83 c0 01 add $0x1,%eax
56d: a3 10 20 00 00 mov %eax,0x2010
572: 8b 15 00 00 00 00 mov 0x0,%edx
578: 8b 45 fc mov -0x4(%ebp),%eax
57b: 01 d0 add %edx,%eax
57d: c9 leave
57e: c3 ret
57f: 90 nop
可以发现_g2的地址是0x2010, 而_g1的地址是0x0,这样的结果是因为_g1是一个外部的全局变量,在编译时根本无法确定其确切地址,所以这里就填0(估计填其他也行)。在运行加载时,ld会对_g1的地址进行修正,届时就不是0了。同样对于_g2也会修正,因为代码加载到内存后的地址肯定不是从0开始的,实际_g2的地址应该是某个偏移+0x2010。加载时ld是怎么知道_g1需要地址修正,而不是像_g2那样加个偏移就ok呢?答案在readelf -d :
readelf --dyn-syms pic-nopic.so
Symbol table '.dynsym' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 00000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.1.3 (2)
3: 00000000 0 NOTYPE GLOBAL DEFAULT UND _g1
4: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
5: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
6: 00000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
7: 0000200c 0 NOTYPE GLOBAL DEFAULT ABS _edata
8: 00002014 0 NOTYPE GLOBAL DEFAULT ABS _end
9: 0000200c 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
10: 000003dc 0 FUNC GLOBAL DEFAULT 10 _init
11: 00000580 0 FUNC GLOBAL DEFAULT 13 _fini
12: 0000055c 35 FUNC GLOBAL DEFAULT 12 func
32位PIC代码的情况
.file "pic.c"
.local _g2
.comm _g2,4,4
.text
.globl func
.type func, @function
func:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $16, %esp
call __x86.get_pc_thunk.cx
addl $_GLOBAL_OFFSET_TABLE_, %ecx
movl _g2@GOTOFF(%ecx), %eax
movl %eax, -4(%ebp)
addl $1, %eax
movl %eax, _g2@GOTOFF(%ecx)
movl _g1@GOT(%ecx), %eax
movl (%eax), %edx
movl -4(%ebp), %eax
addl %edx, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size func, .-func
.section .text.__x86.get_pc_thunk.cx,"axG",@progbits,__x86.get_pc_thunk.cx,comdat
.globl __x86.get_pc_thunk.cx
.hidden __x86.get_pc_thunk.cx
.type __x86.get_pc_thunk.cx, @function
__x86.get_pc_thunk.cx:
.LFB1:
.cfi_startproc
movl (%esp), %ecx
ret
.cfi_endproc
看到不同了吗,首先多了一个__x86.get_pc_thunk.cx函数,在调整好esp后它首先被调用,addl $_GLOBAL_OFFSET_TABLE_, %ecx 这条指令的地址入栈。该函数实际是把addl $_GLOBAL_OFFSET_TABLE_, %ecx这条指令的地址传到ecx寄存器中,该寄存器从此被挪作他用。该函数返回后ecx又加上了一个$_GLOBAL_OFFSET_TABLE的变量值。该值指的是addl这个指令到GOT表的偏移。由ld填写,所以这里还只是个标号。加上了以后ecx实际指向了一个叫GOT的动态符号偏移表。movl _g2@GOTOFF(%ecx), %eax 意思是在这个偏移表内根据_g2在表内的偏移最终算出_g2变量的绝对地址。一语道破天机其实这种PIC机制就是基于位置无关代码的位置与数据位置偏移是一个定值。这样在编译时只要数据操作数用偏移去表示,运行时用代码位置+GOT+符号偏移即可间接找到数据位置,而无须进行数据位置的重定位。
64位非PIC代码的情况
gcc –share -S pic.c -o pic-nopic.s
.file "pic.c"
.local _g2
.comm _g2,4,4
.text
.globl func
.type func, @function
func:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl _g2(%rip), %eax
movl %eax, -4(%rbp)
addl $1, %eax
movl %eax, _g2(%rip)
movl _g1(%rip), %eax
addl -4(%rbp), %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
对比32位的版本发现好像即使不用-fpic编译实际上_g2和_g1的地址都是相对rip的偏移。这也算是位置无关代码吗?
64位PIC代码的情况
gcc –share -fpic -S pic.c -o pic-pic.s
.file "pic.c"
.local _g2
.comm _g2,4,4
.text
.globl func
.type func, @function
func:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl _g2(%rip), %eax
movl %eax, -4(%rbp)
addl $1, %eax
movl %eax, _g2(%rip)
movq _g1@GOTPCREL(%rip), %rax
movl (%rax), %eax
addl -4(%rbp), %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
发现_g2的情况没有变化,但_g1改用GOT查表来寻址了。_g1和_g2不同的是,前者是一个外部全局变量,而后者是一个全局静态变量,也就是说这个文件编译成so后,_g2位于.bss段它和调用他的代码相对偏移是已知的。所以gcc这里干脆就用位置无关的方式来寻址得了。但_g1就不行了,道理前文讲过了。编译成so再反汇编看看:
00000000000005dc :
5dc: 55 push %rbp
5dd: 48 89 e5 mov %rsp,%rbp
5e0: 8b 05 3a 0a 20 00 mov 0x200a3a(%rip),%eax # 201020 <_g2>
5e6: 89 45 fc mov %eax,-0x4(%rbp)
5e9: 83 c0 01 add $0x1,%eax
5ec: 89 05 2e 0a 20 00 mov %eax,0x200a2e(%rip) # 201020 <_g2>
5f2: 48 8b 05 e7 09 20 00 mov 0x2009e7(%rip),%rax # 200fe0 <_DYNAMIC+0x1a8>
5f9: 8b 00 mov (%rax),%eax
5fb: 03 45 fc add -0x4(%rbp),%eax
5fe: 5d pop %rbp
5ff: c3 retq
说了半天似乎还是没有搞清楚为什么64位下为什么必须pic。其实答案在于mov指令。mov在32和64位下操作数只能是32位。利用它在32位环境下使用绝对地址寻址当然没有问题。但在64位环境下就不行了,32位的mov无法表示这么大的虚拟地址空间。而只能用它在有限的32位范围内寻址到GOT表,再通过查表去得到变量的64位绝对地址。
有没有其他什么奇技淫巧,只要能用类似mov的,但支持更长操作数的指令,或者压缩一下64位下虚拟地址空间就可以搞定了。
当然有!用如下方法去编译:
gcc pic.c -mcmodel=large –share -S -o pic-nopic.s
.file "pic.c"
.local _g2
.comm _g2,4,4
.text
.globl func
.type func, @function
func:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movabsq $_g2, %rax
movl (%rax), %eax
movl %eax, -4(%rbp)
leal 1(%rax), %edx
movabsq $_g2, %rax
movl %edx, (%rax)
movabsq $_g1, %rax
movl (%rax), %eax
addl -4(%rbp), %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
movabsq 允许操作一个64位的立即数
000000000000060c :
60c: 55 push %rbp
60d: 48 89 e5 mov %rsp,%rbp
610: 48 b8 20 10 20 00 00 movabs $0x201020,%rax
617: 00 00 00
61a: 8b 00 mov (%rax),%eax
61c: 89 45 fc mov %eax,-0x4(%rbp)
61f: 8d 50 01 lea 0x1(%rax),%edx
622: 48 b8 20 10 20 00 00 movabs $0x201020,%rax
629: 00 00 00
62c: 89 10 mov %edx,(%rax)
62e: 48 b8 00 00 00 00 00 movabs $0x0,%rax
635: 00 00 00
638: 8b 00 mov (%rax),%eax
63a: 03 45 fc add -0x4(%rbp),%eax
63d: 5d pop %rbp
63e: c3 retq
63f: 90 nop
实际编译出来的代码觉得movabs 40位的立即数就足够了,不需要64位。