为什么我不能使用-fPIE编译,但可以使用-fPIC?

7

我有一个有趣的编译问题。 首先,请查看要编译的代码。

$ ls
Makefile main.c sub.c sub.h
$ gcc -v
...
gcc version 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC)

## Makefile
%.o: CFLAGS+=-fPIE #[2]

main.so: main.o sub.o
    $(CC) -shared -fPIC -o $@ $^

//main.c
#include "sub.h"

int main_func(void){
    sub_func();
    subsub_func();

    return 0;
}

//sub.h
#pragma once
void subsub_func(void);
void sub_func(void);

//sub.c
#include "sub.h"
#include <stdio.h>
void subsub_func(void){
    printf("%s\n", __func__);
}
void sub_func(void){
    subsub_func();//[1]
    printf("%s\n", __func__);
}

我编译代码时出现以下错误:

$ LANG=en make
cc -fPIE   -c -o main.o main.c
cc -fPIE   -c -o sub.o sub.c
cc -shared -fPIC -o main.so main.o sub.o
/usr/bin/ld: sub.o: relocation R_X86_64_PC32 against symbol `subsub_func' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: final link failed: Bad value
collect2: error: ld returned 1 exit status
make: *** [main.so] Error 1

接着,我修改了代码(删除了一行[1] / 使用-fPIC而不是-PIE [2]),然后成功编译了这些内容。

$ make #[1]
cc -fPIE   -c -o main.o main.c
cc -fPIE   -c -o sub.o sub.c
cc -shared -fPIC -o main.so main.o sub.o
$ make #[2]
cc -fPIC   -c -o main.o main.c
cc -fPIC   -c -o sub.o sub.c
cc -shared -fPIC -o main.so main.o sub.o

为什么会出现这种现象?
我听说,当一个对象内部调用函数时,如果使用-fPIC编译,则通过PLT进行调用;而如果使用-fPIE编译,则直接跳转到该函数。 我猜想,使用-fPIE时的函数调用机制避免了重定位。 但我想知道更确切和准确的解释。
你能帮我吗?
谢谢大家。

1
请注意,我已将您代码中的日元符号更改为反斜杠,因为我强烈怀疑上下文中应该使用反斜杠。将日元符号替换为反斜杠是一种非常古老的修补程序(日本国家ASCII的变体),我认为它早已灭绝了,但也许并非完全如此?您是在日本写作吗?无论如何,这不会影响您问题的实质,但可能会使其他读者感到困惑,他们没有我这么老。 - zwol
谢谢。我是在日本写的。 - nutsman
1个回答

16

对于所示代码,-fPIC-fPIE之间唯一的代码生成区别在于从sub_funcsubsub_func的调用。使用-fPIC,该调用通过PLT进行;使用-fPIE,则是直接调用。在汇编转储(cc -S)中,其看起来像这样:

--- sub.s.pic   2017-12-07 08:10:00.308149431 -0500
+++ sub.s.pie   2017-12-07 08:10:08.408068650 -0500
@@ -34,7 +34,7 @@ sub_func:
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
-   call    subsub_func@PLT
+   call    subsub_func
    leaq    __func__.2258(%rip), %rsi
    leaq    .LC0(%rip), %rdi
    movl    $0, %eax

在未链接的目标文件中,它是重定位类型的更改:

--- sub.o.dump.pic  2017-12-07 08:13:54.197775840 -0500
+++ sub.o.dump.pie  2017-12-07 08:13:54.197775840 -0500
@@ -22,7 +22,7 @@
   1f:  55                      push   %rbp
   20:  48 89 e5                mov    %rsp,%rbp
   23:  e8 00 00 00 00          callq  28 <sub_func+0x9>
-           24: R_X86_64_PLT32  subsub_func-0x4
+           24: R_X86_64_PC32   subsub_func-0x4
   28:  48 8d 35 00 00 00 00    lea    0x0(%rip),%rsi        # 2f <sub_func+0x10>
            2b: R_X86_64_PC32   .rodata+0x14
   2f:  48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # 36 <sub_func+0x17>
在这种体系结构上,当您使用cc -shared链接共享库时,链接器不允许输入目标文件包含针对全局符号的R_X86_64_PC32重定位,因此当您使用-fPIE而不是-fPIC时会出现错误。
现在,您可能会想知道为什么不允许在共享库内进行直接调用。实际上,它们是允许的,但仅当被调用者不是全局变量时才允许。例如,如果您使用static声明了subsub_func,则调用目标将由汇编器解析,并且在目标文件中根本不会有任何重定位,如果您使用__attribute__((visibility("hidden")))进行声明,则会得到一个R_X86_64_PC32重定位,但是链接器会允许它,因为被调用者不再从库中导出。但在这两种情况下,subsub_func将不再可以从库外部调用。
现在,您可能会想知道全局符号的特性是什么,这意味着您必须通过PLT从共享库中调用它们。这与ELF符号解析规则的一个方面有关,可能会让您感到惊讶:共享库中的任何全局符号都可以被可执行文件或链接顺序中的较早的库覆盖。具体而言,如果我们保留您的sub.hsub.c不变,但是将main.c编写为这样:
//main.c
#include "sub.h"
#include <stdio.h>

void subsub_func(void) {
    printf("%s (main)\n", __func__);
}

int main(void){
    sub_func();
    subsub_func();

    return 0;
}

现在它已经有了一个官方可执行入口,但也有subsub_func的第二个定义,并且我们将sub.c编译成共享库,将main.c编译成调用它的可执行文件,并像这样运行整个程序:

$ cc -fPIC -c sub.c -o sub.o
$ cc -c main.c -o main.o
$ cc -shared -Wl,-soname,libsub.so.1 sub.o -o libsub.so.1
$ ln -s libsub.so.1 libsub.so
$ cc main.o -o main -L. -lsub
$ LD_LIBRARY_PATH=. ./main

输出结果将会是

subsub_func (main)
sub_func
subsub_func (main)

简而言之,从mainsubsub_func的调用以及库中sub_funcsubsub_func的调用都会解析到可执行文件中的定义。为了实现这一点,sub_func的调用必须经过PLT。

您可以通过附加的链接器开关-Bsymbolic来更改此行为。

$ cc -shared -Wl,-soname,libsub.so.1 -Wl,-Bsymbolic sub.o -o libsub.so.1
$ LD_LIBRARY_PATH=. ./main
subsub_func
sub_func
subsub_func (main)

现在从sub_func的调用已经解析为库中的定义。在这种情况下,使用-Bsymbolic允许sub.c使用-fPIE而不是-fPIC进行编译,但我不建议您这样做。使用-fPIE代替-fPIC还会产生其他影响,例如更改如何访问线程本地存储,这些影响无法通过-Bsymbolic解决。


我想指出,这个“-Wl,-Bsymbolic”标志是我正在构建的一个C99库在从CMake项目中正确构建的gcc中的唯一方法。clang没有这样的问题。具体的错误是:“/usr/bin/ld:CMakeFiles/foo.dir/libfoo.c.o:相对于符号'foo_check_context'的重定位R_X86_64_PC32不能用于制作共享对象;重新编译使用-fPIC”。编译命令确实包含了“-fPIC”,但我不确定问题出在哪里。 - ijustlovemath
@ijustlovemath 个人认为这应该归类为“不要使用cmake”。(最有可能的解释是,进入libfoo的几个文件之一没有使用-fPIC编译,而这必须是cmake的问题。) - zwol
libfoo.c是一个带有相应头文件的单个文件,所以我不确定这是否是问题所在。无论是谁的错,感谢您提供的解决方案! - ijustlovemath

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接