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

Why I cannot compile with -fPIE but can with -fPIC?

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

$ 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的函数调用机制避免了重定位。 但我想知道对此的准确解释。

你愿意帮我吗?

谢谢大家

显示的代码在 -fPIC-fPIE 之间的 只有 代码生成差异在从 sub_func 到 [=19 的调用中=].使用 -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    [=10=], %eax

在未link编辑的目标文件中,这是重定位类型的改变:

--- 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>

而且,在这个架构上,当你 link 使用 cc -shared 的共享库时,linker 不允许输入目标文件包含 R_X86_64_PC32 重定位针对全局符号,因此您在使用 -fPIE 而不是 -fPIC 时观察到的错误。

现在,您可能想知道 为什么 不允许在共享库中直接调用。事实上,它们 允许的,但前提是被调用者不是全局的。例如,如果您用 static 声明 subsub_func,那么调用目标将由汇编程序解析,目标文件中根本不会有重定位,如果您用 [=29 声明它=] 然后你会得到一个 R_X86_64_PC32 重定位但是 linker 会允许它,因为被调用者不再从库中导出。但是在这两种情况下,subsub_func 将不再可以从库外调用。

现在您可能想知道全局符号是什么意思,这意味着您必须通过共享库中的 PLT 调用它们。这与您可能会感到惊讶的 ELF 符号解析规则的一个方面有关:共享库中的任何全局符号都可以被可执行文件或link订单。具体来说,如果我们不理会你的 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_funcsub_func 的调用,在库中,到 subsub_func,被解析为可执行文件中的定义。为此,来自 sub_func 的呼叫必须通过 PLT。

您可以使用额外的 linker 开关更改此行为,-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 还有其他影响,例如更改需要完成对 thread-local storage 的访问方式,而这些无法通过 -Bsymbolic 解决。