为什么gcc生成PLT表,即使它显然不需要?

3

考虑以下代码:

int foo();
int main() {
    foo();
    while(1){}
}

int foo() 是在一个共享对象中实现的。

使用 gcc -o main main.c -lfoo -nostdlib -m32 -O2 -e main --no-pic -L./shared 编译此代码会得到以下 diasm:

$ objdump -d ./main

./main:     file format elf32-i386


Disassembly of section .plt:

00000240 <.plt>:
 240:   ff b3 04 00 00 00       pushl  0x4(%ebx)
 246:   ff a3 08 00 00 00       jmp    *0x8(%ebx)
 24c:   00 00                   add    %al,(%eax)
    ...

00000250 <foo@plt>:
 250:   ff a3 0c 00 00 00       jmp    *0xc(%ebx)
 256:   68 00 00 00 00          push   $0x0
 25b:   e9 e0 ff ff ff          jmp    240 <.plt>

Disassembly of section .text:

00000260 <main>:
 260:   8d 4c 24 04             lea    0x4(%esp),%ecx
 264:   83 e4 f0                and    $0xfffffff0,%esp
 267:   ff 71 fc                pushl  -0x4(%ecx)
 26a:   55                      push   %ebp
 26b:   89 e5                   mov    %esp,%ebp
 26d:   51                      push   %ecx
 26e:   83 ec 04                sub    $0x4,%esp
 271:   e8 fc ff ff ff          call   272 <main+0x12>
 276:   eb fe                   jmp    276 <main+0x16>

以下是需要重新安置的内容:

$ objdump -R ./main

./main:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE 
00000272 R_386_PC32        foo
00001ffc R_386_JUMP_SLOT   foo

请注意:
  1. 该代码使用 --no-pic 编译,因此不是 PIC。
  2. .text 段(main 函数)中调用 foo() 不通过 PLT 进行。相反,它只是一个简单的 R_386_PC32 重定位,我认为它将直接在加载时被重新定位到 foo 函数的地址。对我来说很有道理,因为代码不是 PIC,所以没有必要通过 PLT 添加额外的间接引用。
  3. 即使没有使用,PLT 仍然会被生成。其中包含一个 foo 条目,并且我们甚至有一个 R_386_JUMP_SLOT 重定位来设置加载时 GOT 中的 foo 条目(PLT 指向该条目)。
我的问题很简单:我没有看到代码中使用 PLT,也没有看到它在这里是必需的,那么为什么 gcc 会创建它呢?

1
"我没有看到 PLT 在任何地方被使用" -- foo() 的定义是从哪里来的?如果它来自于 libfoo.so(如反汇编所示),那么 PLT 是必要的。 - undefined
@EmployedRussian 是的,foo() 的定义来自 libfoo.so。我不明白为什么在这种情况下需要 PLT。对 foo() 的调用并不指向 PLT,而是直接使用了 R_386_PC32 重定位。如果我使用 -fpic 编译 main.c,那么是的,我会看到调用指向相应的 PLT 条目,然后我会看到一个单独的 R_386_JUMP_SLOT 重定位。 - undefined
此外,在调用foo之前,GOT指针没有被准备好。这似乎是在模块未使用PIC编译时的行为。原始的main.o目标文件只在使用-fpic编译时准备GOT指针并请求R_386_PLT32重定位。如果使用--no-pic编译,则不再设置GOT指针,并且会请求一个简单的R_386_PC32重定位,如问题所示。基于此,我认为动态链接器将直接将重定位添加到此情况下的调用中,我不明白它是如何使用PLT的。 - undefined
1个回答

6
--no-pic 并非像-no-pie一样,它似乎是-fno-pic-fno-pie的同义词,影响代码生成但不影响链接。假设你的发行版的GCC默认创建了一个PIE,那么你就在创建一个PIE,所以没有将对foo@plt的调用转换。
我得到了一个链接器警告/tmp/ccyRsNtd.o: warning: relocation against 'getpid@@GLIBC_2.0' in read-only section '.text.startup'/warning: creating DT_TEXTREL in a PIE。(但可执行文件确实运行了,与64位情况不同,其中call rel32无法重新定位到整个地址空间。)
另外,是的,由于某种原因,ld构建了一个未使用的PLT条目,但是您链接的方式完全不标准。
构建PLT的正常原因是:
当链接一个非PIE时,ld会将call foo转换为call foo@plt,而不是在每个调用点包含文本重定位,这将需要在每次程序加载时进行运行时修补。
使用-fno-plt可以获得更高效的汇编语言,特别是在64位模式下,即使是PIE代码也可以直接引用GOT。
为了创建一个更简单的示例,我使用了libc中的函数(getpid),而非自定义库。使用gcc -fno-pie -no-pie -m32 -O2 foo.c正常编译后,我得到了5字节的e8 d5 ff ff ff call rel32: call 8049040 <getpid@plt>
但是如果加上-fno-plt,则会得到6个字节的ff 15 f4 bf 04 08 call [disp32] - call DWORD PTR ds:0x804bff4。没有涉及PLT,只是使用绝对地址引用GOT条目。
不需要运行时重定位;这个页面的.text部分可以保持“清洁”,作为可执行文件的支持文件来私有映射。(运行时重定位会使它变为脏页,如果内核想要逐出该页,则仅由交换空间支持。)
此外,它使用一个“正常”的GOT条目,需要早期绑定。即使使用-nostdlib -lc和不明智的-e main而不是像正常人一样调用_start,也可以工作。由于它是动态链接的可执行文件,因此动态链接器会在入口点之前运行,并设置GOT。

@felipeek: 哦,是的,--no-pic 不像 -no-pie,它似乎是 -fno-pic-fno-pie 的同义词,影响代码生成但不影响链接。我没有使用它,因为 GCC 没有对其进行文档说明。但你是对的,你确实在创建一个 PIE,所以调用 foo@plt 不会被转换。相反,你会得到一个链接器警告 /tmp/ccyRsNtd.o: warning: relocation against 'getpid@@GLIBC_2.0' in read-only section '.text.startup' / warning: creating DT_TEXTREL in a PIE。所以,是的,由于某种原因,ld 构建了一个未使用的 PLT 条目,但你的链接方式完全非标准。 - undefined
@felipeek:在制作ELF共享对象(包括PIE)时,期望目标文件已经引用foo@PLT,如果这是所需的/需要的。因此,这就是GCC通常在其汇编源代码中为不在当前编译单元中的函数所做的:https://godbolt.org/z/f17vr1。如果`ld`发现目标函数在另一个`.o`中找到,它可以“放松”调用以不使用PLT。(如果您使用`-fno-plt`进行编译,则需要填充一个额外的字节;它使用`67`地址大小前缀来填充`call rel32以适应call [disp32]call [RIP + rel32]`的空间) - undefined
1
只有在链接非PIE可执行文件时,ld才会处理PLT的生成:非常有趣,我认为这解释了我所看到的奇怪行为。如果我使用-fno-pie生成main.o,重定位不会经过PLT。实际上,如果我使用-no-pie将二进制文件链接起来,链接器确实会生成PLT,并使call指令指向PLT,即使重定位不是R_386_PLT32。如果我链接二进制文件而没有使用no-pie(默认为PIE),链接器会使call指令直接指向共享对象中的函数,但是... - undefined
1
...由于某种奇怪的原因,它仍然为PLT创建了文件。这是我的最初问题,但有很多标志误导了我的理解。我没有意识到即使在向ld传递--no-pic时,最终的可执行文件仍然被编译为PIE。实际上,我认为在这种情况下,ld并不关心保留PLT,因为将目标文件编译为非PIE,然后生成带有PIE的二进制文件并不常见。非常感谢! - undefined
1
@felipeek:是的,听起来没错。如果你想在我的快速回答中补充一些细节并发表答案,现在你已经更加明确了,可以随意。否则,这些评论对于未来的读者来说应该足够了。 - undefined
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接