为什么gcc在函数调用时不引用PLT?

6

我正在尝试通过编译简单的函数并查看输出来学习汇编语言。

我正在研究如何调用其他库中的函数。以下是一个玩具C函数,它调用了在其他地方定义的函数:

void give_me_a_ptr(void*);

void foo() {
    give_me_a_ptr("foo");
}

这是由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   

我原本期望看到类似于call <give_me_a_ptr@plt>的内容,为什么在还不知道give_me_a_ptr定义位置的情况下就跳转到了相对位置?
我也对mov $0, %edi感到困惑。这看起来像是传递了一个空指针——难道不应该在此处使用mov $address_of_string, %rdi吗?

使用 objdump -dr 显示重定位条目。 - Jester
1
FYI,clang -S 会发出这个而不是你的 mov 0x0/callq 对:leaq L_.str(%rip), %rdi; callq _give_me_a_ptr - John Zwinck
2
关于 PLT,只有在编译 PIC 时使用 -fPIC 标志才会用到。 - Jester
3
在Linux上,gcc默认不使用位置无关代码(PIC),而在Darwin上则是这样,例如clang。这就是为什么你会看到绝对地址引用。学习编译器生成的内容的好方法是使用objdump -Sr命令,它会将汇编语言和源代码混合显示,并显示重定位项。 - Leandros
@JohnZwinck:你在使用OS X吗?在那里可执行文件必须是位置无关的吗?对于允许绝对寻址的代码,clang使用mov-immediate,就像gcc一样。 - Peter Cordes
@PeterCordes:确实,我在上面的评论中使用了OS X。谢谢你指出来。 - John Zwinck
2个回答

12
你没有启用symbol-interposition-fPIC的副作用),因此call目标地址可能在链接时被解析为静态链接到同一可执行文件中的另一个对象文件中的地址。(例如gcc foo.o bar.o)
但是,如果该符号仅在您动态链接到的库中找到(gcc foo.o -lbar),则必须通过PLT进行间接调用以支持。
现在这是棘手的部分:没有-fPIC-fPIE, gcc仍会发出直接调用函数的汇编代码:
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

但是如果你查看链接的二进制文件: (在这个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:  # compiled with -fPIE
    leaq    .LC0(%rip), %rdi      # RIP-relative addressing for static data
    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机器码中的相对位移都是零,因为链接器尚未填充它们。

-1

我自己还在学习这个链接过程,但想用自己的话重新表述一些内容。与 PLT 相关的用户函数调用可能在执行开始时并没有全部填充正确的代码。这样做可能需要在执行开始时花费很长时间;而且 PLT 仪器化的不是所有函数调用都会被使用。因此,在“惰性绑定”方法下,第一次通过 PLT 代码调用“用户”函数时,它总是首先跳转到 PLT 的“绑定函数”。绑定函数会查找“用户”函数的正确地址(我认为是从 GOT 中),然后用指向“用户”函数的代码替换指向绑定函数的 PLT 条目。因此,每次调用用户函数时,都不会调用“惰性”绑定函数;而是直接调用“用户”函数。这可能就是为什么 PLT 条目乍一看起来很奇怪的原因;它指向的是绑定函数,而不是“用户”函数。


2
延迟绑定发生在第一次调用 PLT 条目之后,不影响编译器是否使用 call putscall puts@PLT。它也不修改 PLT,而是修改 GOT。PLT 是只读的,并使用间接 JMP 指令的形式 puts@PLT: jmp [puts@GOTPLT]。最初,GOT 中的 puts@GOTPLT 条目指向调用惰性绑定代码的代码,共享库绑定后,它指向共享库中的 puts - Ross Ridge

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接