为什么我不能用 -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.h
和 sub.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)
也就是说, 从 main
到 subsub_func
的调用,以及从 sub_func
到 sub_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
解决。
我有一个有趣的编译问题。 首先,请看要编译的代码。
$ 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.h
和 sub.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)
也就是说, 从 main
到 subsub_func
的调用,以及从 sub_func
到 sub_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
解决。