简体   繁体   English

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

[英]Why doesn't gcc reference the PLT for function calls?

I'm trying to learn assembly by compiling simple functions and looking at the output. 我正在尝试通过编译简单的函数和查看输出来学习汇编。

I'm looking at calling functions in other libraries. 我正在考虑调用其他库中的函数。 Here's a toy C function that calls a function defined elsewhere: 这是一个玩具C函数,它调用其他地方定义的函数:

void give_me_a_ptr(void*);

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

Here's the assembly produced by gcc: 这是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   

I was expecting something like call <give_me_a_ptr@plt> . 我期待像call <give_me_a_ptr@plt> Why is this jumping to a relative position before it even knows where give_me_a_ptr is defined? 为什么在它知道give_me_a_ptr定义在哪里之前会跳到相对位置?

I'm also puzzled by mov $0, %edi . 我也对mov $0, %edi感到困惑。 This looks like it's passing a null pointer -- surely mov $address_of_string, %rdi would be correct here? 这看起来像是在传递一个空指针 - 肯定是mov $address_of_string, %rdi在这里是正确的吗?

You're not building with symbol-interposition enabled (a side-effect of -fPIC ), so the call destination address can potentially be resolved at link time to an address in another object file that is being statically linked into the same executable. 您没有使用符号插入功能构建( -fPIC的副作用),因此call目标地址可能会在链接时解析为另一个静态链接到同一可执行文件的目标文件中的地址。 (eg gcc foo.o bar.o ). (例如gcc foo.o bar.o )。

However, if the symbol is only found in a library that you're dynamically linking to ( gcc foo.o -lbar ), the call has to be indirected through the PLT to support. 但是,如果符号仅在您动态链接到的库中找到( gcc foo.o -lbar ),则必须通过PLT间接call以支持。

Now this is the tricky part: without -fPIC or -fPIE , gcc still emits asm that calls the function directly: 现在这是一个棘手的部分: 没有-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

But if you look at the linked binary: (on this Godbolt compiler explorer link , click the "binary" button to toggle between gcc -S asm output and objdump -dr disassembly) 但是如果你看一下链接的二进制文件:(在这个Godbolt编译器浏览器链接上 ,单击“二进制”按钮在gcc -S asm输出和objdump -dr反汇编之间切换)

    # disassembled linker output
    mov    $0x400654,%edi
    jmpq   400490 <puts@plt>

During linking, the call to puts was "magically" replaced with indirection through puts@plt , and a puts@plt definition is present in the linked executable. 在链接期间,对puts的调用被“神奇地”替换为通过puts@plt间接替换,并且puts@plt定义存在于链接的可执行文件中。

I don't know the details of how this works, but it's done at link time when linking to a shared library. 我不知道它是如何工作的细节,但它是在链接到共享库时在链接时完成的。 Crucially, it doesn't require anything in the header files to mark the function prototype as being in a shared library. 至关重要的是,它不需要头文件中的任何内容来将函数原型标记为在共享库中。 You get the same results from including <stdio.h> as you do from declaring puts yourself. 您可以通过包含<stdio.h>获得与通过声明puts自己相同的结果。 (This is highly not recommended; it's probably legal for a C implementation to only work properly with the declarations in headers. It happens to work on Linux, though.) (这是非常不推荐的;对于C实现而言,只有在头文件中的声明才能正常工作可能是合法的。但它恰好在Linux上运行。)


When compiling a position-independent executable (with -fPIE ), the linked binary jumps to puts through the PLT, identically to without -fPIC . 当编译一个与位置无关的可执行文件 (与-fPIE ),所链接的二进制跳到puts通过PLT,相同地没有-fPIC However, the compiler asm output is different (try it yourself on the godbolt link above): 但是,编译器asm输出是不同的(在上面的godbolt链接上自己尝试):

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

The compiler forces indirection through the PLT for any calls to functions it can't see the definition for. 编译器通过PLT强制间接对任何无法看到定义的函数的调用。 I don't understand why. 我不明白为什么。 In PIE mode, we're compiling code for an executable, not a shared library. 在PIE模式下,我们正在编译可执行文件的代码,而不是共享库。 The linker should be able to link multiple object files into a position-independent executable with direct calls between functions defined in the executable. 链接器应该能够将多个目标文件链接到与位置无关的可执行文件,并在可执行文件中定义的函数之间进行直接调用。 I'm testing on Linux (my desktop and godbolt), not OS X, where I assume gcc -fPIE is the default. 我正在测试Linux(我的桌面和Godbolt),而不是OS X,我认为gcc -fPIE是默认的。 It might be configured differently, IDK. 它可能配置不同,IDK。


With -fPIC instead of -fPIE , things are even worse: even calls to global functions defined within the same compilation unit have to go through the PLT, to support symbol interposition . 使用-fPIC而不是-fPIE ,事情甚至更糟:即使对同一编译单元中定义的全局函数的调用也必须通过PLT,以支持符号插入 (eg LD_PRELOAD=intercept_some_functions.so ./a.out ) (例如LD_PRELOAD=intercept_some_functions.so ./a.out

The differences between -fPIC and -fPIE are mainly that PIE can assume no symbol interposition for functions in the same compilation unit, but PIC can't. -fPIC-fPIE之间的差异主要是PIE可以假设在同一编译单元中没有符号插入函数,但PIC不能。 OS X requires position-independent executables, as well as shared libraries, but there is a difference in what the compiler can do when making code for a library vs. making code for an executable. OS X需要与位置无关的可执行文件以及共享库,但是在为库创建代码和为可执行文件创建代码时,编译器可以执行的操作有所不同。

This Godbolt example has some more functions that demonstrate stuff about PIC and PIE mode, eg that call_puts() can't inline into another function in PIC mode, only PIE. 这个Godbolt示例还有一些函数可以演示有关PIC和PIE模式的内容,例如, call_puts()无法内联到PIC模式下的另一个函数,只有PIE。

See also: Shared object in Linux without symbol interposition, -fno-semantic-interposition error . 另请参见: Linux中没有符号插入的共享对象,-fno-semantic-interposition error


puzzled by mov $0, %edi mov $0, %edi困惑

You're looking at disassembly output from the .o , where addresses are just placeholder 0s that will be replaced by the linker at link time, based on the relocation information in the ELF object file. 您正在查看.o中的反汇编输出,其中地址只是占位符0,将在链接时由链接器替换,基于ELF目标文件中的重定位信息。 That's why @Leandros suggested objdump -r . 这就是@Leandros建议objdump -r的原因。

Similarly, the relative displacement in the call machine code is all-zeros, because the linker hasn't filled it in yet. 类似地, call机器代码中的相对位移是全零,因为链接器还没有填充它。

I'm still studying this linking process myself, but wanted to restate something in my own words. 我自己还在研究这个连接过程,但是想用自己的话来重述一些事情。 The PLT-related user function calls might not all be stuffed with the proper code by the time execution starts. 在执行开始时,与PLT相关的用户函数调用可能并非都填充了正确的代码。 Doing so could take a lot of time at the start of execution; 这样做可能会在执行开始时花费大量时间; and not all the function calls instrumented by the PLT might even be used. 并不是所有PLT检测的函数调用都可能被使用。 So under a 'lazy binding' method, the very first time a 'user' function is called through the PLT code, it always jumps to the PLT 'binding function' first. 因此,在'延迟绑定'方法下,第一次通过PLT代码调用'用户'函数时,它总是首先跳转到PLT'绑定函数'。 The binding function goes out and finds the right address for the 'user' function (I think from the GOT) and then replaces the PLT entry (that points to the binding function) with the code pointing to the 'user' function. 绑定函数熄灭并找到'user'函数的正确地址(我想从GOT中),然后用指向'user'函数的代码替换PLT条目(指向绑定函数)。 So thereafter every time the user function is called, the 'lazy' binding function is not called; 因此,每次调用用户函数时,都不会调用'lazy'绑定函数; the 'user' function is called instead. 而是调用'user'函数。 This might be why the PLT entry looks odd at first blush; 这可能就是为什么PLT条目乍一看看起来很奇怪; it's pointing to the binding function and not to the 'user' function. 它指的是绑定功能而不是'用户'功能。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM