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

C 函数指针局部变量的意外值,c,linux,assembly,x86,att,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; } #包括 typedef in

我做了一些实验,创建了一个指向函数的指针类型的局部变量,该函数指向
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;
}
#包括
typedef int(*func)(常量字符*,…);
int main()
{
func x=printf;
printf(“%p\n”,x);
x(“%p\n”,x);
返回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
0x000000000000063a:推送%rbp
0x000000000000063b:mov%rsp,%rbp
0x000000000000063e:子$0x10,%rsp
0x0000000000000642:mov 0x20098f(%rip),%rax#0x200fd8
0x0000000000000649:mov%rax,-0x8(%rbp)
0x000000000000064d:mov-0x8(%rbp),%rax
0x0000000000000651:mov%rax,%rsi
0x0000000000000654:lea 0xb9(%rip),%rdi#0x714
0x000000000000065b:mov$0x0,%eax
0x0000000000000660:callq 0x520
0x0000000000000665:mov-0x8(%rbp),%rax
0x0000000000000669:mov-0x8(%rbp),%rdx
0x000000000000066d:mov%rax,%rsi
0x0000000000000670:lea 0x9d(%rip),%rdi#0x714
0x0000000000000677:mov$0x0,%eax
0x000000000000067c:callq*%rdx
0x000000000000067e:mov$0x0,%eax
0x0000000000000683:LEVEQ
0x0000000000000684:retq
对我来说奇怪的是,调用
printf
直接使用plt(正如预期的那样)但是使用局部变量调用它使用了一个完全不同的地址(正如您在程序集的第4行中看到的,存储在局部变量x中的值不是plt条目的地址)


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

反汇编的第四行和第五行对应于
func x=printf语句。
printf
的地址存储在内存中的地址
0x200fd8
,可使用
rip
-相对地址(
0x20098f(%rip)
)访问该地址。然后将其存储在局部变量中(相对于
ebp
,地址为
-0x8(%rbp)


运行时需要对存储在
0x200fd8

中的值进行任何调整。函数有一个地址,但每个共享库都有一个PLT,这将导致指向
printf
的不同指针具有不同的值

(正如您在程序集的第4行中看到的,存储在局部变量x中的值不是plt条目的地址)

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

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

为了实现这一点,
printf
符号获得“早期绑定”而不是惰性动态链接,从而避免了以后使用该函数指针时的PLT开销

Footenote 1:虽然你的推理可能是基于这样一个事实,那就是它是一个负载,而不是一个相对撕裂的LEA。这几乎告诉你这不是PLT条目;PLT的一部分要点是拥有一个地址,该地址是
调用rel32
的链路时间常数,这也使LEA具有RIP+rel32寻址模式。如果编译器希望在寄存器中使用PLT地址,那么它会使用该地址


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


对可执行文件中未定义的函数的所有调用是否都首先通过plt来获得更好的性能

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

PLT的存在是为了避免动态链接期间
调用rel32
偏移的运行时修复。在64位系统上,允许访问超过2GB的地址。并支持符号插入。参见(在
-fno-plt
存在之前编写;它基本上就像他提出的一个想法)

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

pic代码呢

您的代码是PIC,或者实际上是PIE(位置独立可执行文件),大多数发行版默认情况下配置GCC

我希望
x
指向
printf

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

(与大多数当前Linux发行版不同,Godbolt默认值为
-fno pie

vs

因此,对于函数po的重复使用,PIE可执行文件实际上具有更好的效率
# 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.
# 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