c - 函数指针局部变量的意外值

标签 c linux assembly x86 att

我做了一些实验,在其中创建了一个指向类型为printf的函数的指针类型的局部变量。然后我定期调用printf并按如下方式使用该变量:

#include<stdio.h>
typedef int (*func)(const char*,...);

int main()
{
        func x=printf;
        printf("%p\n", x);
        x("%p\n", x);
        return 0;
}

我已经编译了它,并使用gdb查看了main的反汇编,并得到了:
   0x000000000000063a <+0>:     push   %rbp
   0x000000000000063b <+1>:     mov    %rsp,%rbp
   0x000000000000063e <+4>:     sub    $0x10,%rsp
   0x0000000000000642 <+8>:     mov    0x20098f(%rip),%rax        # 0x200fd8
   0x0000000000000649 <+15>:    mov    %rax,-0x8(%rbp)
   0x000000000000064d <+19>:    mov    -0x8(%rbp),%rax
   0x0000000000000651 <+23>:    mov    %rax,%rsi
   0x0000000000000654 <+26>:    lea    0xb9(%rip),%rdi        # 0x714
   0x000000000000065b <+33>:    mov    $0x0,%eax
   0x0000000000000660 <+38>:    callq  0x520 <printf@plt>
   0x0000000000000665 <+43>:    mov    -0x8(%rbp),%rax
   0x0000000000000669 <+47>:    mov    -0x8(%rbp),%rdx
   0x000000000000066d <+51>:    mov    %rax,%rsi
   0x0000000000000670 <+54>:    lea    0x9d(%rip),%rdi        # 0x714
   0x0000000000000677 <+61>:    mov    $0x0,%eax
   0x000000000000067c <+66>:    callq  *%rdx
   0x000000000000067e <+68>:    mov    $0x0,%eax
   0x0000000000000683 <+73>:    leaveq
   0x0000000000000684 <+74>:    retq

对我来说很奇怪的是,直接调用printf会使用plt(按预期方式),但是使用局部变量调用它会使用一个完全不同的地址(如程序集的第4行所示,存储在局部变量中的值x不是plt条目的地址)。

怎么可能?难道不是所有对可执行文件中未定义函数的调用都首先通过plt以获得更好的性能和图片代码吗?

最佳答案

(as you can see in line 4 of the assembly that the value stored in local variable x is not the address of the plt entry)



??该值在反汇编中不可见,仅在其加载位置可见。 (实际上,它没有加载指向PLT条目的指针,但是程序集的第4行没有告诉您1。)使用objdump -dR查看动态重定位。

这是使用相对RIP寻址模式的内存负载。在这种情况下,它将在libc中加载一个指向实际printf地址的指针。该指针存储在全局偏移表(GOT)中。

为了使此工作有效,printf符号获得“早期绑定(bind)”,而不是惰性动态链接,从而避免了PLT开销,供以后使用该函数指针使用。

脚注1:尽管您也许基于这样的事实,即它是负载而不是相对于RIP的LEA。确实可以告诉您这不是PLT条目; PLT要点的一部分是拥有一个地址,该地址是call rel32的链接时间常数,这也使LEA具有RIP + rel32寻址模式。如果编译器想要在寄存器中使用PLT地址,则将使用该地址。

顺便说一句,PLT存根本身也将GOT条目用于其内存间接跳转。对于仅用作函数调用目标的符号,GOT条目保留指向PLT存根的指针,该指针指向调用惰性动态链接器解析该PLT条目的push/jmp指令。即更新GOT条目。

Don't all the calls to functions undefined in the executable go first through the plt for better performance



不,PLT通过为每个调用添加额外级别的间接性来提高运行时性能。 gcc -fno-plt使用早期绑定(bind)而不是等待第一次调用,因此它可以通过GOT将间接call内联到每个调用站点中。

PLT的存在是为了避免在动态链接期间对call rel32偏移量进行运行时修复。在64位系统上,允许到达2GB以上的地址。并且还支持符号插入。请参阅https://www.macieira.org/blog/2012/01/sorry-state-of-dynamic-libraries-on-linux/(在-fno-plt存在之前编写;基本上就像他所建议的想法之一)。

与早期绑定(bind)相比,PLT的延迟绑定(bind)可以提高启动性能,但是在高速缓存命中率非常重要的现代系统上,在启动过程中一次完成所有符号扫描工作非常好。

and for pic code?



您的代码是PIC,或实际上是PIE(与位置无关的可执行文件),大多数发行版都将GCC配置为默认执行。

I expected x to point to the address of the PLT entry of printf



如果使用-fno-pie ,则PLT条目的地址是链接时常量,并且在编译时,编译器不知道您是要静态链接还是动态链接libc。因此,它使用mov $printf, %eax将功能指针的地址获取到寄存器中,并且在链接时只能转换为mov $printf@plt, %eax

See it on Godbolt(Godbolt的默认值为-fno-pie,与当前大多数Linux发行版不同。)
# gcc9.2 -O3 -fpie    for your first block
        movq    printf@GOTPCREL(%rip), %rbp
        leaq    .LC0(%rip), %rdi
        xorl    %eax, %eax
        movq    %rbp, %rsi        # saved for later in rbp
        call    printf@PLT


# gcc9.2 -O3 -fno-pie
        movl    $printf, %esi          # linker converts this symbol reference to printf@plt
        movl    $.LC0, %edi
        xorl    %eax, %eax
        call    printf                 # will convert at link-time to printf@plt
      # next use also just uses mov-immediate to rematerialize, instead of saving a load result in a register.

因此,对于重复使用指向标准库中函数的函数指针而言,PIE可执行文件实际上具有更高的效率:指针是最终地址,而不仅仅是PLT条目。
-fno-plt -fno-pie的工作方式更像是PIE模式,用于获取函数指针。除非它仍然可以使用$foo 32位立即数作为同一文件中符号的地址,而不是相对于RIP的LEA。
# gcc9.2 -O3 -fno-plt -fno-pie
        movq    printf@GOTPCREL(%rip), %rbp    # saved for later in RBP
        movl    $.LC0, %edi
        xorl    %eax, %eax
        movq    %rbp, %rsi
        call    *printf@GOTPCREL(%rip)
  # pointers to static functions can use  mov $foo, %esi

似乎您需要int foo(const char*,...) __attribute__((visibility("hidden")));来告诉编译器,它绝对不需要通过pie-fno-plt来通过该符号进行GOT转换。

直到链接器将链接时间转换为symbol到链接时间(如有必要)时,它才允许编译器始终使用有效的32位绝对立即数或RIP相对寻址,并且最终只能通过PLT间接寻址才能实现共享功能图书馆。但是随后您将获得指向PLT条目的指针,而不是指向最终地址的指针。

如果您使用的是Intel语法,那么在查看asm而不是反汇编时,它将在GCC的输出中为symbol@plt

查看编译器输出将为您提供更多的信息,这些信息仅仅是纯mov rbp, QWORD PTR printf@GOTPCREL[rip]输出中与RIP的数字偏移量。 objdump显示重定位符号会有所帮助,但是编译器输出通常更好。 (除非您看不到-r被重写为printf)

关于c - 函数指针局部变量的意外值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56760086/

相关文章:

c++ - 使用 SFML 在第二台显示器上打开全屏窗口?

c - 函数序言中的 `PUSH 0xFFFFFFFF` 是什么意思?

linux - IMUL指令寄存器

x86 - 用于 x86 (MASM) 汇编的免费 IDE + 汇编器 + 软件模拟器?

无法定义共享内存对象的大小

c - 在 Linux 编程中通过管道在进程之间发送链表结构的最佳方法是什么

c - 在 printf ( "%.*f ", a, b ) 中,如果 'a' 为负数,结果会是什么?

linux - NGINX 未获取 Vagrant 同步文件夹中的更改

python - 在 Amazon EC2 中安装 'lxml'

c++ - 使用 C++ 排队多个 system() 命令