6

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

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

你能帮我吗?

谢谢你们。

4

1 回答 1

15

显示的代码之间唯一的代码生成差异在于对 from的调用。使用,该调用通过 PLT;,这是一个直接调用。在程序集转储 ( ) 中,如下所示:-fPIC-fPIEsub_funcsubsub_func-fPIC-fPIEcc -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.

现在,您可能想知道为什么不允许在共享库中直接调用。实际上,它们允许的,但仅当被调用者不是全局对象时。例如,如果您声明subsub_funcwith static,那么调用目标将由汇编程序解析,并且目标文件中根本不会有重定位,如果您声明它,__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)

也就是说,库中对 tomain的调用subsub_func和对 的调用都被解析为可执行文件中的定义。为此,来自的呼叫必须通过 PLT。sub_funcsubsub_funcsub_func

您可以使用附加的链接器开关更改此行为,-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)

现在调用 fromsub_func被解析为库中的定义。在这种情况下, using-Bsymbolic允许使用而不是sub.c编译,但我不建议您这样做。使用instead of还有其他效果,例如更改需要访问线程本地存储的方式,而这些无法使用.-fPIE-fPIC-fPIE-fPIC-Bsymbolic

于 2017-12-07T13:41:52.257 回答