函数指针局部变量的意外值
Unexpected value of a function pointer local variable
我做了一些实验,其中我创建了一个指向函数的指针类型的局部变量,该变量指向 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 [=11=]x10,%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 [=11=]x0,%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 [=11=]x0,%eax
0x000000000000067c <+66>: callq *%rdx
0x000000000000067e <+68>: mov [=11=]x0,%eax
0x0000000000000683 <+73>: leaveq
0x0000000000000684 <+74>: retq
对我来说奇怪的是调用 printf
直接使用 plt(正如预期的那样)但是 使用局部变量调用它使用了一个完全不同的地址(正如您在程序集的第 4 行中看到的,存储在局部变量 x 中的值不是 plt 条目的地址)。
怎么可能?不是所有对可执行文件中未定义函数的调用都首先通过 plt 以获得更好的性能和 pic 代码吗?
你反汇编的第四行和第五行对应代码中的func x=printf;
语句。 printf
的地址存储在地址 0x200fd8
的内存中,使用 rip
相对地址 (0x20098f(%rip)
) 访问该地址。然后将其存储在局部变量中(相对于 ebp
,地址为 -0x8(%rbp)
)。
在 运行 时所需的任何调整都将对存储在 0x200fd8
的值进行。
一个函数在整个程序中有一个地址,但每个共享库都有一个 PLT,这会导致指向 printf
的不同指针具有不同的值。
(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)
嗯? value 在反汇编中不可见,仅在加载它的位置可见。 (实际上它没有加载指向 PLT 条目的指针,但程序集的第 4 行没有告诉你 1。)使用 objdump -dR
查看动态重定位。
这是使用 RIP 相对寻址模式从内存加载。在这种情况下,它正在加载指向 libc 中真实 printf
地址的指针。该指针存储在全局偏移量 Table (GOT) 中。
为了使这项工作有效,printf
符号得到 "early binding" 而不是惰性动态 linking,避免了以后使用该函数指针的 PLT 开销。
脚注 1:尽管您的推理可能基于这样一个事实,即它是一个负载而不是 RIP 相关的 LEA。这几乎可以告诉您这不是 PLT 条目; PLT 的部分要点是要有一个地址,该地址是 call rel32
的 link 时间常数,这也使 LEA 具有 RIP+rel32 寻址模式。如果编译器想要寄存器中的 PLT 地址,它会使用它。
顺便说一句,PLT 存根本身也使用 GOT 条目进行内存间接跳转;对于仅用作函数调用目标的符号,GOT 条目包含一个指向 PLT 存根的指针,指向 push
/ jmp
指令,这些指令调用惰性动态 linker 来解析该 PLT 条目。即更新 GOT 条目。
Don't all the calls to functions undefined in the executable go first through the plt for better performance
不,PLT 成本 运行时性能通过向每个调用添加额外的间接级别。 gcc -fno-plt
使用早期绑定而不是等待第一次调用,因此它可以通过 GOT 直接将间接 call
内联到每个调用站点。
PLT 的存在是为了避免在动态 linking 期间对 call rel32
偏移进行运行时修正。在 64 位系统上,允许访问超过 2GB 的地址。并且还支持符号插入。参见 https://www.macieira.org/blog/2012/01/sorry-state-of-dynamic-libraries-on-linux/(写于 -fno-plt
存在之前;基本上就像他提出的想法之一)。
与早期绑定相比,PLT 的惰性绑定可以提高启动性能,但在缓存命中 非常 重要的现代系统中,在启动期间立即执行所有符号扫描工作很好。
and for pic code?
您的代码是 PIC,或者实际上是 PIE(位置无关的可执行文件),大多数发行版默认配置 GCC。
I expected x
to point to the address of the PLT entry of printf
如果使用-fno-pie
,那么PLT入口的地址是一个link时间常数,在编译时编译器不会'不知道您是要静态地还是动态地 link libc。因此它使用 mov $printf, %eax
将函数指针的地址获取到寄存器中,并且在 link 时只能转换为 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")));
告诉编译器对于这个符号绝对不需要通过GOT,用pie
或-fno-plt
。
留到 link-时间让 linker 在必要时将 symbol
转换为 symbol@plt
允许编译器始终使用高效的 32 位绝对立即数或 RIP 相对寻址,并且仅以 PLT 间接寻址结束,这些功能原来位于共享库中。但是你最终会得到指向 PLT 条目的指针,而不是指向最终地址的指针。
如果您使用的是 Intel 语法,那么在 GCC 的输出中会是 mov rbp, QWORD PTR printf@GOTPCREL[rip]
,如果您查看的是 asm 而不是反汇编。
查看编译器输出可为您提供比普通 objdump
输出中 RIP 的数字偏移量更多的信息。 -r
显示重定位符号有一些帮助,但编译器输出通常更好。 (除非您没有看到 printf
被重写为 printf@plt
)
我做了一些实验,其中我创建了一个指向函数的指针类型的局部变量,该变量指向 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 [=11=]x10,%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 [=11=]x0,%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 [=11=]x0,%eax
0x000000000000067c <+66>: callq *%rdx
0x000000000000067e <+68>: mov [=11=]x0,%eax
0x0000000000000683 <+73>: leaveq
0x0000000000000684 <+74>: retq
对我来说奇怪的是调用 printf
直接使用 plt(正如预期的那样)但是 使用局部变量调用它使用了一个完全不同的地址(正如您在程序集的第 4 行中看到的,存储在局部变量 x 中的值不是 plt 条目的地址)。
怎么可能?不是所有对可执行文件中未定义函数的调用都首先通过 plt 以获得更好的性能和 pic 代码吗?
你反汇编的第四行和第五行对应代码中的func x=printf;
语句。 printf
的地址存储在地址 0x200fd8
的内存中,使用 rip
相对地址 (0x20098f(%rip)
) 访问该地址。然后将其存储在局部变量中(相对于 ebp
,地址为 -0x8(%rbp)
)。
在 运行 时所需的任何调整都将对存储在 0x200fd8
的值进行。
一个函数在整个程序中有一个地址,但每个共享库都有一个 PLT,这会导致指向 printf
的不同指针具有不同的值。
(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)
嗯? value 在反汇编中不可见,仅在加载它的位置可见。 (实际上它没有加载指向 PLT 条目的指针,但程序集的第 4 行没有告诉你 1。)使用 objdump -dR
查看动态重定位。
这是使用 RIP 相对寻址模式从内存加载。在这种情况下,它正在加载指向 libc 中真实 printf
地址的指针。该指针存储在全局偏移量 Table (GOT) 中。
为了使这项工作有效,printf
符号得到 "early binding" 而不是惰性动态 linking,避免了以后使用该函数指针的 PLT 开销。
脚注 1:尽管您的推理可能基于这样一个事实,即它是一个负载而不是 RIP 相关的 LEA。这几乎可以告诉您这不是 PLT 条目; PLT 的部分要点是要有一个地址,该地址是 call rel32
的 link 时间常数,这也使 LEA 具有 RIP+rel32 寻址模式。如果编译器想要寄存器中的 PLT 地址,它会使用它。
顺便说一句,PLT 存根本身也使用 GOT 条目进行内存间接跳转;对于仅用作函数调用目标的符号,GOT 条目包含一个指向 PLT 存根的指针,指向 push
/ jmp
指令,这些指令调用惰性动态 linker 来解析该 PLT 条目。即更新 GOT 条目。
Don't all the calls to functions undefined in the executable go first through the plt for better performance
不,PLT 成本 运行时性能通过向每个调用添加额外的间接级别。 gcc -fno-plt
使用早期绑定而不是等待第一次调用,因此它可以通过 GOT 直接将间接 call
内联到每个调用站点。
PLT 的存在是为了避免在动态 linking 期间对 call rel32
偏移进行运行时修正。在 64 位系统上,允许访问超过 2GB 的地址。并且还支持符号插入。参见 https://www.macieira.org/blog/2012/01/sorry-state-of-dynamic-libraries-on-linux/(写于 -fno-plt
存在之前;基本上就像他提出的想法之一)。
与早期绑定相比,PLT 的惰性绑定可以提高启动性能,但在缓存命中 非常 重要的现代系统中,在启动期间立即执行所有符号扫描工作很好。
and for pic code?
您的代码是 PIC,或者实际上是 PIE(位置无关的可执行文件),大多数发行版默认配置 GCC。
I expected
x
to point to the address of the PLT entry ofprintf
如果使用-fno-pie
,那么PLT入口的地址是一个link时间常数,在编译时编译器不会'不知道您是要静态地还是动态地 link libc。因此它使用 mov $printf, %eax
将函数指针的地址获取到寄存器中,并且在 link 时只能转换为 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")));
告诉编译器对于这个符号绝对不需要通过GOT,用pie
或-fno-plt
。
留到 link-时间让 linker 在必要时将 symbol
转换为 symbol@plt
允许编译器始终使用高效的 32 位绝对立即数或 RIP 相对寻址,并且仅以 PLT 间接寻址结束,这些功能原来位于共享库中。但是你最终会得到指向 PLT 条目的指针,而不是指向最终地址的指针。
如果您使用的是 Intel 语法,那么在 GCC 的输出中会是 mov rbp, QWORD PTR printf@GOTPCREL[rip]
,如果您查看的是 asm 而不是反汇编。
查看编译器输出可为您提供比普通 objdump
输出中 RIP 的数字偏移量更多的信息。 -r
显示重定位符号有一些帮助,但编译器输出通常更好。 (除非您没有看到 printf
被重写为 printf@plt
)