我正在尝试通过编译简单的函数并查看输出来学习汇编。
我正在考虑调用其他库中的函数。这是一个玩具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
在这里是正确的吗?
答案 0 :(得分:9)
您未在启用symbol-interposition的情况下构建(-fPIC
的副作用),因此call
目标地址可能会在链接时解析为地址另一个静态链接到同一个可执行文件的目标文件。 (例如gcc foo.o bar.o
)。
但是,如果符号仅在您动态链接到(gcc foo.o -lbar
)的库中找到,则call
必须通过PLT支持。
现在这是一个棘手的部分:without -fPIC
or -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
但是如果你看一下链接的二进制文件:
(在this Godbolt compiler explorer link上,点击&#34;二进制&#34;按钮在gcc -S
asm输出和objdump -dr
反汇编之间切换
# disassembled linker output
mov $0x400654,%edi
jmpq 400490 <puts@plt>
在链接过程中,对puts
的调用是#34;神奇地&#34;通过puts@plt
替换为间接,并且链接的可执行文件中存在puts@plt
定义。
我不知道其工作原理的详细信息,但在链接到共享库时,它已在链接时完成。至关重要的是,它不需要头文件中的任何内容来将函数原型标记为在共享库中。您可以通过自己声明<stdio.h>
来获得与puts
相同的结果。 (这是非常不推荐的;对于C实现而言,只有在头文件中使用声明才能正常工作。但它恰好在Linux上工作。)
编译与位置无关的可执行文件(使用-fPIE
)时,链接的二进制文件会通过PLT跳转到puts
,与没有-fPIC
的情况相同。但是,编译器asm输出是不同的(在上面的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
是默认值。它可能配置不同,IDK。
使用-fPIC
代替-fPIE
,情况甚至更糟:即使对同一编译单元中定义的全局函数的调用也必须通过PLT,以支持 symbol interposition 即可。 (例如LD_PRELOAD=intercept_some_functions.so ./a.out
)
-fPIC
和-fPIE
之间的差异主要是PIE可以假设在同一个编译单元中没有符号插入功能,但PIC不能。 OS X需要与位置无关的可执行文件以及共享库,但是在为库创建代码和为可执行文件创建代码时,编译器可以执行的操作有所不同。
这个Godbolt example有更多功能可以演示有关PIC和PIE模式的内容,例如: call_puts()
无法在PIC模式下内联到另一个函数,只有PIE。
另请参阅:Shared object in Linux without symbol interposition, -fno-semantic-interposition error。
被
困惑mov $0, %edi
您正在查看来自.o
的反汇编输出,其中地址只是占位符0,将在链接时由链接器替换,基于ELF目标文件中的重定位信息。这就是为什么@Leandros建议objdump -r
。
同样,call
机器代码中的相对位移是全零,因为链接器还没有填充它。
答案 1 :(得分:-1)
我自己还在研究这个连接过程,但是想用自己的话来重述一些事情。在执行开始时,与PLT相关的用户函数调用可能并非都填充了正确的代码。这样做可能会在执行开始时花费大量时间;并不是所有PLT检测的函数调用都可能被使用。因此,在'延迟绑定'方法下,第一次通过PLT代码调用'用户'函数时,它总是首先跳转到PLT'绑定函数'。绑定函数熄灭并找到'user'函数的正确地址(我想从GOT中),然后用指向'user'函数的代码替换PLT条目(指向绑定函数)。因此,每次调用用户函数时,都不会调用'lazy'绑定函数;而是调用'user'函数。这可能就是为什么PLT条目乍一看看起来很奇怪;它指的是绑定功能,而不是'用户'功能。