为什么gcc强制PIC为x64共享库?

时间:2011-10-23 08:02:42

标签: linux assembly x86 shared-libraries x86-64

尝试使用gcc将非PIC代码编译到x64上的共享库中会导致错误,例如:

/usr/bin/ld: /tmp/ccQ2ttcT.o: relocation R_X86_64_32 against `a local symbol' can not be used when making a shared object; recompile with -fPIC

这个问题是关于为什么就是这样。我知道x64具有RIP相对寻址,旨在提高PIC代码的效率。但是,这并不意味着加载时重定位不能(理论上)应用于此类代码。

一些在线资源,包括this one(在此问题上被广泛引用)声称,在共享库中存在一些禁止非PIC代码的固有限制,因为的RIP相对寻址。我不明白为什么会这样。

考虑“旧的x86” - call指令也有一个IP相对操作数。然而,其中带有call的x86代码可以很好地编译成没有PIC的共享库,但使用load-time relocation R_386_PC32。对于x64中的数据RIP相对寻址,不能做同样的事情吗?

请注意,我完全理解PIC代码的好处,并且性能损失RIP相对寻址有助于缓解。不过,我很好奇不允许使用非PIC代码的原因。它背后是否有真正的技术推理,还是只是鼓励编写PIC代码?

3 个答案:

答案 0 :(得分:16)

以下是我从a post on comp.unix.programmer读到的最佳解释:

  

共享库需要x86-64上的PIC,或更准确地说,可重定位代码   必须是PIC。这是因为一个32位的立即数地址操作数   重定位后,代码中使用的代码可能需要超过32位。如果   发生这种情况,无处可写新值。

答案 1 :(得分:5)

只是说些别的话。

url provided in the question中,它提到您可以将-mcmodel=large传递给gcc,告诉编译器为您的代码生成64位立即数地址操作数。

因此,gcc -mcmodel=large -shared a.c将生成非PIC共享对象。

-

演示:

交流转换器:

#include <stdio.h>

void foo(void)
{
    printf("%p\n", main);
}

32位立即数地址操作数会阻止您生成非PIC对象。

xiami@gentoo ~ $ cc -shared -o a.so a.c
/usr/lib/gcc/x86_64-pc-linux-gnu/4.8.4/../../../../x86_64-pc-linux-gnu/bin/ld: /tmp/cck3FWeL.o: relocation R_X86_64_32 against `main' can not be used when making a shared object; recompile with -fPIC
/tmp/cck3FWeL.o: error adding symbols: Bad value
collect2: error: ld returned 1 exit status

使用-mcmodel=large来解决它。 (警告仅出现在我的系统上,因为我的PaX内核禁止对.text进行修改。)

xiami@gentoo ~ $ cc -mcmodel=large -shared -o a.so a.c
/usr/lib/gcc/x86_64-pc-linux-gnu/4.8.4/../../../../x86_64-pc-linux-gnu/bin/ld: /tmp/ccZ3b9Xk.o: warning: relocation in readonly section `.text'.
/usr/lib/gcc/x86_64-pc-linux-gnu/4.8.4/../../../../x86_64-pc-linux-gnu/bin/ld: warning: creating a DT_TEXTREL in object.

现在您可以看到重定位条目的类型为 R_X86_64_64 ,而不是R_X86_64_32,R_X86_64_PLT32,R_X86_64_PLTOFF64。

xiami@gentoo ~ $ objdump -R a.so
a.so:      file format elf64-x86-64
DYNAMIC RELOCATION RECORDS
OFFSET           TYPE              VALUE 
...
0000000000000758 R_X86_64_64       printf
...

在我的系统上,将此共享对象链接到正常代码并运行该程序将发出如下错误: ./a.out: error while loading shared libraries: ./a.so: cannot make segment writable for relocation: Permission denied

这证明了动态加载器正在对 .text 进行重定位,PIC库不会。

答案 2 :(得分:2)

问题是,PIC和非PIC代码仍然不同。

C来源:

extern int x;
void func(void) { x += 1; }

汇编,而不是PIC:

addl    $1, x(%rip)

装配,使用PIC:

movq    x@GOTPCREL(%rip), %rax
addl    $1, (%rax)

因此看起来PIC代码必须通过重定位表来访问全局变量。它实际上必须对函数执行相同的操作,但它可以通过在链接时创建的存根来执行功能。这在程序集级别是透明的,而访问全局变量则不是。 (但是,如果需要函数的地址,则PIC和非PIC不同,就像全局变量一样。)请注意,如果更改代码如下:

__attribute__((visibility("hidden"))) extern int x;

在这种情况下,由于GCC知道符号必须与代码位于同一对象中,因此它会发出与非PIC版本相同的代码。