对符号`stdout@@GLIBC_2.2.5'的R_X86_64_32S重定位不能用于制作PIE对象。

3

问题

我正在阅读这本书,其中有一章关于动态链接和以下代码:

link_example.s

.globl main

.section .data

output:
    .ascii "Yeet\n\0"

.section .text
main:
    enter $0, $0
    movq stdout, %rdi
    movq $output, %rsi
    call fprintf

    movq $0, %rax
    leave
    ret

现在根据书中的指示,我需要按照以下方式编译以动态链接C库:
gcc -rdynamic link_example.s -o link_example

但我收到了以下错误信息:
/usr/bin/ld: /tmp/cchUlvqS.o: relocation R_X86_64_32S against symbol `stdout@@GLIBC_2.2.5' can not be used when making a PIE object; recompile with -fPIE
/usr/bin/ld: failed to set dynamic section sizes: bad value
collect2: error: ld returned 1 exit status

我做错了什么?

你尝试过什么?

添加-fPIE标志

我尝试了编译器建议的方法,添加了-fPIE标志:

gcc -rdynamic -fPIE link_example.s -o link_example

但我仍然再次遇到了相同的错误。

查找类似的帖子

我发现了一个类似的帖子,它说我只需要使用-shared标志:

gcc -shared link_example.s -o link_example

但是这给我带来了:
/usr/bin/ld: /tmp/ccxktZan.o: relocation R_X86_64_32S against symbol `stdout@@GLIBC_2.2.5' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: failed to set dynamic section sizes: bad value
collect2: error: ld returned 1 exit status

如果我加上-fPIC标志:

gcc -shared -fPIC link_example.s -o link_example

然后我得到了这个:
/usr/bin/ld: /tmp/ccKIQ9sl.o: relocation R_X86_64_32S against symbol `stdout@@GLIBC_2.2.5' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: failed to set dynamic section sizes: bad value
collect2: error: ld returned 1 exit status

3
你实际上需要添加-no-pie(或重写代码为PIC)。 - Jester
哦,现在它可以工作了。谢谢!您能否将您的评论写成答案,这样我就可以勾选此问题已得到解答@Jester?如果您能详细说明为什么我需要使用此标志而不是“-rdynamic”,我会很高兴的,因为根据“man gcc”关于“-no-pie”的说明:不生成动态链接的位置无关可执行文件。这个标志在这里具体起到了什么作用?这听起来像是禁用了动态链接。 - TornaxO7
2
@TornaxO7 PIE 意味着主二进制文件(而不仅仅是共享库)是位置无关的。这使代码对某些攻击具有更强的防御能力,但也需要特殊的汇编编程技术才能实现。-fpie指示编译器生成此类代码,但如果您使用汇编语言编写,则必须自己完成。-no-pie关闭 PIE,允许您在主二进制文件中编写传统的位置相关代码。 - fuz
1个回答

1

让我向您展示如何修复您的书中的汇编语言,使其与编译器的默认设置兼容。

正如问题评论所述,问题在于编译器默认生成位置无关可执行文件。这意味着stdoutfprintfoutput的地址在链接时未知,因此链接器无法“重定位”引用它们的指令。

然而,在链接时已知的是这些地址与程序计数器之间的偏移量。这意味着,如果您稍微改变一下汇编代码,它就可以正常工作。像这样:

.globl main

.section .data

output:
    .ascii "Yeet\n\0"

.section .text
main:
    enter $0, $0
    movq stdout(%rip), %rdi
    leaq output(%rip), %rsi
    call fprintf@PLT

    movq $0, %rax
    leave
    ret

注意,对于这三个指令,变化略有不同。`mov stdout, %rdi` 变成了 `mov stdout(%rip), %rdi` -- 这只是相同指令的不同寻址模式。从固定地址 `stdout` 中加载变成了从 RIP 寄存器(也称为程序计数器)中的固定 位移 `stdout` 加载。另一方面,使用 `mov $output, %rsi` 加载 固定地址 `output` 则变成了 `lea output(%rip), %rsi`。我建议您将其视为始终执行的加载有效地址操作,但旧代码在可执行文件处于固定地址时能够使用移动立即数代替实际的 lea 指令来表达该操作。最后,`call fprintf` 变成了 `call fprintf@PLT`。这告诉链接器调用需要通过 过程链接表 进行 -- 您的书应该解释了这是什么以及为什么需要。
顺便说一下,我看到这个汇编语言还有几个问题,其中最重要的是:
  • 字符串 `"Yeet\n\0"` 应该放在 只读 数据段中。
  • x86-64 ABI 规定,像 `fprintf` 这样的可变参数函数需要通过适当设置 `eax` 来告知它们接收到的浮点参数数量。
  • x86-64 上不需要使用 `enter` 和 `leave` 指令。(此外,`enter` 是一个极其缓慢的微码指令,根本不应该使用。)
我会写成这样:
    .section .rodata, "a", @progbits
.output:
    .string "Yeet\n"

    .section .text, "ax", @progbits
    .globl main
    .type  main, @function
main:
    sub    $8, %rsp
    mov    stdout(%rip), %rdi
    lea    .output(%rip), %rsi
    xor    %eax, %eax
    call   fprintf@PLT
    xor    %eax, %eax
    add    $8, %rsp
    ret

在函数开始时,您需要从%rsp中减去8,并在之后加回来,因为ABI规定,在call指令的位置%rsp必须始终是16的倍数--这意味着在进入任何函数时它不是16的倍数,而是%rsp mod 16等于8,因为call会推送8个字节(返回地址)。您可以通过enterleave的副作用来免费获得这个结果,但是如果将它们删除,您就必须手动完成。


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