你没有启用
symbol-interposition(
-fPIC
的副作用),因此
call
目标地址可能在链接时被解析为静态链接到同一可执行文件中的另一个对象文件中的地址。(例如
gcc foo.o bar.o
)
但是,如果该符号仅在您动态链接到的库中找到(
gcc foo.o -lbar
),则必须通过PLT进行间接调用以支持。
现在这是棘手的部分:
没有-fPIC
或-fPIE
, gcc仍会发出直接调用函数的汇编代码:
int puts(const char*)
void call_puts(void) { puts("foo")
# gcc 5.3 -O3 (without -fPIC)
movl $.LC0, %edi # absolute 32bit addressing: slightly smaller code, because static data is known to be in the low 2GB, in the default "small" code model
jmp puts # tail-call optimization. Same as call puts/ret, except for stack alignment
但是如果你查看链接的二进制文件:
(在这个Godbolt编译器浏览器链接上,点击“binary”按钮切换gcc -S
汇编输出和objdump -dr
反汇编)
# disassembled linker output
mov $0x400654,%edi
jmpq 400490 <puts@plt>
链接时,对puts
的调用被“神奇地”替换为通过puts@plt
进行间接引用,并且在链接后的可执行文件中存在puts@plt
的定义。
我不知道这是如何工作的细节,但在链接到共享库时,在链接时完成。重要的是,它不需要头文件中标记函数原型为共享库。从包括<stdio.h>
和自己声明puts
可以获得相同的结果。(这是高度不推荐的;C实现可能只能正确处理头文件中的声明。尽管如此,它在Linux上运行正常。)
当编译一个位置无关的可执行文件(使用-fPIE
),链接二进制文件通过PLT跳转到puts
,与没有-fPIC
相同。然而,编译器汇编输出是不同的(可以在上面的godbolt链接中自行尝试):
call_puts:
leaq .LC0(%rip), %rdi
jmp puts@PLT
编译器强制间接引用通过PLT调用任何无法看到定义的函数。我不理解为什么会这样。在PIE模式下,我们将代码编译为可执行文件,而不是共享库。链接器应该能够将多个目标文件链接成一个位置无关的可执行文件,并且可直接调用在可执行文件中定义的函数。我正在Linux上进行测试(我的桌面和godbolt),而非OS X,在那里我假设
gcc -fPIE
是默认值。可能已配置不同,我不确定。
使用
-fPIC
替代
-fPIE
后,情况更糟:即使是在同一编译单元中定义的全局函数的调用也必须通过PLT进行支持以支援
symbol interposition。(例如
LD_PRELOAD=intercept_some_functions.so ./a.out
)
-fPIC
和
-fPIE
之间的区别主要在于PIE可以假定同一编译单元中的函数不会发生符号重定位,但PIC不能。OS X要求使用位置无关可执行文件以及共享库,但是在为库生成代码与为可执行文件生成代码时编译器可以做的事情有所不同。
这个
Godbolt示例包含更多函数,演示了有关PIC和PIE模式的一些内容,例如,在PIC模式下,
call_puts()
无法内联到另一个函数中,只能在PIE模式下。
另请参阅:Linux中的共享对象,无需符号重定位,-fno-semantic-interposition错误。
你正在查看来自.o文件的反汇编输出,其中地址只是占位符0,在链接时间由链接器根据ELF目标文件中的重定位信息替换。这就是为什么@Leandros建议使用objdump -r的原因。
同样地,call机器码中的相对位移都是零,因为链接器尚未填充它们。
objdump -dr
显示重定位条目。 - Jesterclang -S
会发出这个而不是你的 mov 0x0/callq 对:leaq L_.str(%rip), %rdi; callq _give_me_a_ptr
。 - John Zwinck-fPIC
标志才会用到。 - Jesterobjdump -Sr
命令,它会将汇编语言和源代码混合显示,并显示重定位项。 - Leandrosmov
-immediate,就像gcc一样。 - Peter Cordes