为什么 gcc 不为函数调用引用 PLT?
Why doesn't gcc reference the PLT for function calls?
我正在尝试通过编译简单的函数并查看输出来学习汇编。
我正在查看其他库中的调用函数。这是一个调用别处定义的函数的玩具 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 [=11=]x0,%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 [=14=], %edi
。这看起来像是在传递一个空指针——mov $address_of_string, %rdi
在这里肯定是正确的吗?
您没有在启用 symbol-interposition 的情况下构建(-fPIC
的 side-effect),因此 call
目标地址可能会在 link 时间到另一个 object 文件中的地址,该文件被静态 linked 到同一个可执行文件中。 (例如 gcc foo.o bar.o
)。
但是,如果该符号仅在您动态 linking 到 (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
但是如果您查看 linked 二进制文件:
(在 this Godbolt compiler explorer link 上,单击 "binary" 按钮可在 gcc -S
asm 输出和 objdump -dr
反汇编之间切换)
# disassembled linker output
mov [=11=]x400654,%edi
jmpq 400490 <puts@plt>
在 linking 期间,对 puts
的调用被 "magically" 替换为通过 puts@plt
的间接寻址,puts@plt
定义出现在 link编辑可执行文件。
我不知道这是如何工作的细节,但它是在 link 访问共享库时 link 完成的。至关重要的是,它不需要 header 文件中的任何内容来将函数原型标记为在共享库中。包含 <stdio.h>
的结果与您自己声明 puts
的结果相同。 (强烈不推荐这样做;对于 C 实现来说,只在 headers 中正确工作可能是合法的。它恰好在 Linux 上工作。)
在编译 position-independent 可执行文件(使用 -fPIE
)时,linked 二进制文件通过PLT,与没有 -fPIC
相同。但是,编译器asm输出不同(在上面的godboltlink上自己试试):
call_puts: # compiled with -fPIE
leaq .LC0(%rip), %rdi # RIP-relative addressing for static data
jmp puts@PLT
编译器强制通过 PLT 间接调用它看不到定义的函数。我不明白为什么。在 PIE 模式下,我们正在为可执行文件而不是共享库编译代码。 linker 应该能够 link 多个 object 文件到 position-independent 可执行文件中,并在可执行文件中定义的函数之间直接调用。我在 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 需要 position-independent 可执行文件以及共享库,但是编译器在为库编写代码与为可执行文件编写代码时可以执行的操作有所不同。
这个 Godbolt example 有一些更多的函数来演示关于 PIC 和 PIE 模式的东西,例如call_puts()
无法在 PIC 模式下内联到另一个函数,只能内联到 PIE。
另请参阅:Shared object in Linux without symbol interposition, -fno-semantic-interposition error。
puzzled by mov [=37=], %edi
您正在查看 .o
的反汇编输出,其中地址只是占位符 0,将根据重定位在 link 时间被 linker 替换ELF object 文件中的信息。这就是为什么@Leandros 建议 objdump -r
.
同理,call
机器码中的相对位移为all-zeros,因为linker还没有填写
我自己仍在研究这个链接过程,但想用我自己的话重申一下。在执行开始时,与 PLT 相关的用户函数调用可能并未全部填充正确的代码。这样做可能会在执行开始时花费很多时间;并且并非所有由 PLT 检测的函数调用都可能被使用。所以在 'lazy binding' 方法下,第一次通过 PLT 代码调用 'user' 函数时,它总是首先跳转到 PLT 'binding function'。绑定函数出去并找到 'user' 函数的正确地址(我认为来自 GOT),然后用指向 'user' 的代码替换 PLT 条目(指向绑定函数)功能。所以此后每次调用用户函数时,都不会调用 'lazy' 绑定函数; 'user' 函数被调用。这可能就是 PLT 条目乍一看很奇怪的原因;它指向绑定函数而不是 'user' 函数。
我正在尝试通过编译简单的函数并查看输出来学习汇编。
我正在查看其他库中的调用函数。这是一个调用别处定义的函数的玩具 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 [=11=]x0,%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 [=14=], %edi
。这看起来像是在传递一个空指针——mov $address_of_string, %rdi
在这里肯定是正确的吗?
您没有在启用 symbol-interposition 的情况下构建(-fPIC
的 side-effect),因此 call
目标地址可能会在 link 时间到另一个 object 文件中的地址,该文件被静态 linked 到同一个可执行文件中。 (例如 gcc foo.o bar.o
)。
但是,如果该符号仅在您动态 linking 到 (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
但是如果您查看 linked 二进制文件:
(在 this Godbolt compiler explorer link 上,单击 "binary" 按钮可在 gcc -S
asm 输出和 objdump -dr
反汇编之间切换)
# disassembled linker output
mov [=11=]x400654,%edi
jmpq 400490 <puts@plt>
在 linking 期间,对 puts
的调用被 "magically" 替换为通过 puts@plt
的间接寻址,puts@plt
定义出现在 link编辑可执行文件。
我不知道这是如何工作的细节,但它是在 link 访问共享库时 link 完成的。至关重要的是,它不需要 header 文件中的任何内容来将函数原型标记为在共享库中。包含 <stdio.h>
的结果与您自己声明 puts
的结果相同。 (强烈不推荐这样做;对于 C 实现来说,只在 headers 中正确工作可能是合法的。它恰好在 Linux 上工作。)
在编译 position-independent 可执行文件(使用 -fPIE
)时,linked 二进制文件通过PLT,与没有 -fPIC
相同。但是,编译器asm输出不同(在上面的godboltlink上自己试试):
call_puts: # compiled with -fPIE
leaq .LC0(%rip), %rdi # RIP-relative addressing for static data
jmp puts@PLT
编译器强制通过 PLT 间接调用它看不到定义的函数。我不明白为什么。在 PIE 模式下,我们正在为可执行文件而不是共享库编译代码。 linker 应该能够 link 多个 object 文件到 position-independent 可执行文件中,并在可执行文件中定义的函数之间直接调用。我在 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 需要 position-independent 可执行文件以及共享库,但是编译器在为库编写代码与为可执行文件编写代码时可以执行的操作有所不同。
这个 Godbolt example 有一些更多的函数来演示关于 PIC 和 PIE 模式的东西,例如call_puts()
无法在 PIC 模式下内联到另一个函数,只能内联到 PIE。
另请参阅:Shared object in Linux without symbol interposition, -fno-semantic-interposition error。
puzzled by
mov [=37=], %edi
您正在查看 .o
的反汇编输出,其中地址只是占位符 0,将根据重定位在 link 时间被 linker 替换ELF object 文件中的信息。这就是为什么@Leandros 建议 objdump -r
.
同理,call
机器码中的相对位移为all-zeros,因为linker还没有填写
我自己仍在研究这个链接过程,但想用我自己的话重申一下。在执行开始时,与 PLT 相关的用户函数调用可能并未全部填充正确的代码。这样做可能会在执行开始时花费很多时间;并且并非所有由 PLT 检测的函数调用都可能被使用。所以在 'lazy binding' 方法下,第一次通过 PLT 代码调用 'user' 函数时,它总是首先跳转到 PLT 'binding function'。绑定函数出去并找到 'user' 函数的正确地址(我认为来自 GOT),然后用指向 'user' 的代码替换 PLT 条目(指向绑定函数)功能。所以此后每次调用用户函数时,都不会调用 'lazy' 绑定函数; 'user' 函数被调用。这可能就是 PLT 条目乍一看很奇怪的原因;它指向绑定函数而不是 'user' 函数。