为什么gcc没有引用PLT进行函数调用?

Wil*_*hes 3 c assembly gcc abi

我正在尝试通过编译简单的函数和查看输出来学习汇编.

我正在考虑调用其他库中的函数.这是一个玩具C函数,它调用其他地方定义的函数:

void give_me_a_ptr(void*);

void foo() {
    give_me_a_ptr("foo");
}
Run Code Online (Sandbox Code Playgroud)

这是gcc生成的程序集:

$ gcc -Wall -Wextra -g -O0 -c call_func.c
$ objdump -d call_func.o 

call_func.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <foo>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   bf 00 00 00 00          mov    $0x0,%edi
   9:   e8 00 00 00 00          callq  e <foo+0xe>
   e:   90                      nop
   f:   5d                      pop    %rbp
  10:   c3                      retq   
Run Code Online (Sandbox Code Playgroud)

我期待着类似的东西call <give_me_a_ptr@plt>.为什么在它知道give_me_a_ptr定义的位置之前它会跳到相对位置?

我也很困惑mov $0, %edi.这看起来像是在传递一个空指针 - 这肯定mov $address_of_string, %rdi是正确的吗?

Pet*_*des 9

您没有在启用符号插入的情况下构建(副作用-fPIC),因此call目标地址可能会在链接时解析到另一个静态链接到同一可执行文件的目标文件中的地址.(例如gcc foo.o bar.o).

但是,如果符号仅在您动态链接到(gcc foo.o -lbar)的库中找到,则call必须通过PLT进行间接支持.

现在这是一个棘手的部分:没有-fPIC-fPIE,gcc仍然发出直接调用函数的asm:

int puts(const char*);         // puts exists in libc, so we can link this example
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
Run Code Online (Sandbox Code Playgroud)

但是如果你看一下链接二进制文件:(在这个Godbolt编译器浏览器链接上,单击"二进制"按钮在gcc -Sasm输出和objdump -dr反汇编之间切换)

    # disassembled linker output
    mov    $0x400654,%edi
    jmpq   400490 <puts@plt>
Run Code Online (Sandbox Code Playgroud)

在链接期间,调用puts是"神奇地"替换为间接通过puts@plt,并且puts@plt链接的可执行文件中存在定义.

我不知道它是如何工作的细节,但它是在链接到共享库时在链接时完成的.至关重要的是,它不需要头文件中的任何内容来将函数原型标记为在共享库中.您可以<stdio.h>从声明puts自己的内容中获得相同的结果.(这是非常不推荐的;对于C实现而言,只有在头文件中的声明才能正常工作可能是合法的.但它恰好在Linux上工作.)


在编译与位置无关的可执行文件(with -fPIE)时,链接的二进制文件跳转到putsPLT,与没有相同-fPIC.但是,编译器asm输出是不同的(在上面的godbolt链接上自己尝试):

call_puts:  # compiled with -fPIE
    leaq    .LC0(%rip), %rdi      # RIP-relative addressing for static data
    jmp     puts@PLT
Run Code Online (Sandbox Code Playgroud)

编译器通过PLT强制间接对任何无法看到定义的函数的调用.我不明白为什么.在PIE模式下,我们正在编译可执行文件的代码,而不是共享库.链接器应该能够将多个目标文件链接到与位置无关的可执行文件,并在可执行文件中定义的函数之间进行直接调用.我正在测试Linux(我的桌面和Godbolt),而不是OS X,我认为这gcc -fPIE是默认设置.它可能配置不同,IDK.


随着-fPIC代替-fPIE,事情更糟糕的:即使调用同一个编译单元中定义的全局函数必须要经过PLT,支持 符号插入.(例如LD_PRELOAD=intercept_some_functions.so ./a.out)

之间的差异-fPIC-fPIE主要馅饼可以假设在同一编译单元的功能没有符号插入,但PIC不能.OS X需要与位置无关的可执行文件以及共享库,但是在为库创建代码而不是为可执行文件创建代码时,编译器可以执行的操作有所不同.

这个Godbolt示例还有一些函数可以演示有关PIC和PIE模式的内容,例如,call_puts()它们无法内联到PIC模式下的另一个函数,只有PIE.

另请参见:Linux中没有符号插入的共享对象,-fno-semantic-interposition error.


困惑的 mov $0, %edi

您正在查看来自.o其中的反汇编输出,其中地址只是占位符0,将在链接时由链接器替换,基于ELF目标文件中的重定位信息.这就是@Leandros建议的原因objdump -r.

类似地,call机器代码中的相对位移是全零,因为链接器还没有填充它.