momo zone

调核人的blog

why X86_64 share lib must be PIC

之前一个老掉牙的程序要从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位。

Advertisements

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s

%d 博主赞过: