【问题标题】:How to load address of function or label into register如何将函数或标签的地址加载到寄存器中
【发布时间】:2019-12-04 07:47:49
【问题描述】:

我正在尝试将“main”的地址加载到 GNU 汇编器中的寄存器 (R10) 中。我做不到。这是我所拥有的和收到的错误消息。

main:
   lea main, %r10

我还尝试了以下语法(这次使用 mov)

main:
   movq $main, %r10

以上两种情况都会出现以下错误:

/usr/bin/ld: /tmp/ccxZ8pWr.o: relocation R_X86_64_32S against symbol `main' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: final link failed: Nonrepresentable section on output
collect2: error: ld returned 1 exit status

使用 -fPIC 编译并不能解决问题,只会给我同样的错误。

【问题讨论】:

  • 使用 rip-relative 寻址模式:lea main(%rip), %r10
  • 这可以被标记为重复,但这是一个很好的地方,可以作为关于将静态地址放入寄存器的规范答案的好地方。我已经多次将其作为其他答案的一部分,现在我可以在这里链接。

标签: gcc assembly x86-64 att addressing-mode


【解决方案1】:

在 x86-64 中,大多数立即数和位移仍然是 32 位,因为 64 位会浪费太多代码大小(I-cache 占用空间和获取/解码带宽)。

lea main, %reg 是一种绝对的disp32 寻址模式,它将阻止加载时地址随机化 (ASLR) 选择随机的 64 位(或 47 位)地址。所以it's not supported 在 Linux 上除了位置相关的可执行文件,或者在 MacOS 上,静态代码/数据总是在低 32 位之外加载。 (有关文档和指南的链接,请参阅x86 tag wiki。)在 Windows 上,您可以将可执行文件构建为“大地址感知”或不构建。如果不选择,地址将适合 32 位。


将静态地址放入寄存器的标准有效方法是相对于 RIP 的 LEA

# RIP-relative LEA always works.  Syntax for various assemblers:
  lea main(%rip), %r10       # AT&T syntax

  lea  r10, [rip+main]       # GAS .intel_syntax noprefix   equivalent
  lea  r10, [rel main]       ; NASM equivalent, or use  default rel
  lea  r10, [main]           ; FASM defaults to RIP-relative.  MASM may also

请参阅 How do RIP-relative variable references like "[RIP + _a]" in x86-64 GAS Intel-syntax work? 了解这 3 种语法的解释,并参阅 Why are global variables in x86-64 accessed relative to the instruction pointer?(和 this)了解为什么 RIP 相对是处理静态数据的标准方法。

这使用从当前指令末尾开始的 32 位相对位移,例如 jmp/call。这可以到达.data.bss.rodata 中的任何静态数据,或.text 中的函数,假设静态代码+数据的总大小限制通常为 2GiB。


在 Linux 上的位置依赖代码(例如使用gcc -fno-pie -no-pie 构建),您可以利用 32 位绝对寻址来节省代码大小。此外,mov r32, imm32 在 Intel/AMD CPU 上的吞吐量比 RIP 相对 LEA 稍好,因此乱序执行可能能够更好地与周围代码重叠。 (优化代码大小通常不如大多数其他事情重要,但当其他所有条件相同时,选择较短的指令。在这种情况下,所有其他条件 至少相等,或者mov imm32 也更好.)

请参阅32-bit absolute addresses no longer allowed in x86-64 Linux?,了解有关 PIE 可执行文件如何成为默认值的更多信息。 (这就是为什么您在使用 32 位绝对值时收到关于 -fPIC 的链接错误。)

# in a non-PIE executable,  mov imm32 into a 32-bit register is even better
# same as you'd use in 32-bit code
## GAS AT&T syntax
mov  $main, %r10d        # 6 bytes
mov  $main, %edi         # 5 bytes: no REX prefix needed for a "legacy" register

## GAS .intel_syntax
mov  edi, OFFSET main

;;  mov  edi, main     ; NASM and FASM syntax

请注意,写入任何 32 位寄存器总是零扩展至完整的 64 位寄存器(R10 和 RDI)。

lea main, %edilea main, %rdi 也可以在 Linux 非 PIE 可执行文件中工作,但切勿将 LEA 与 [disp32] 绝对寻址模式一起使用(即使在不需要 SIB 字节的 32 位代码中) ; mov 总是至少一样好。

当您有一个唯一确定它的寄存器操作数时,操作数大小后缀是多余的;我更喜欢只写mov 而不是movlmovq


愚蠢/糟糕的方式是一个 10 字节的 64 位绝对地址作为立即数:

# Inefficient, DON'T USE
movabs  $main, %r10            # 10 bytes including the 64-bit absolute address

如果你使用mov rdi, main 而不是mov edi, main,这就是你在 NASM 中得到的结果,所以很多人最终都会这样做。 Linux 动态链接确实实际上支持 64 位绝对地址的运行时修复。但它的用例是跳转表,而不是作为立即数的绝对地址。


movq $sign_extended_imm32, %reg(7 字节)仍然使用 32 位绝对地址,但在符号扩展 mov 到 64 位寄存器上浪费了代码字节,而不是从写入隐式零扩展到 64 位一个 32 位的寄存器。

通过使用 movq,您是在告诉 GAS 您想要R_X86_64_32S 重定位而不是 R_X86_64_64 64 位绝对重定位。

您想要这种编码的唯一原因是内核代码,其中静态地址位于 64 位虚拟地址空间的高 2GiB,而不是低 2GiB。 mov 在某些 CPU 上比 lea轻微的性能优势(例如在更多端口上运行),但通常如果你可以使用 32 位绝对值,它在虚拟地址空间的低 2GiB 中mov r32, imm32 有效。

(相关:Difference between movq and movabsq in x86-64


PS:我有意省略了关于“大”或“巨大”内存/代码模型的任何讨论,其中 RIP 相对 +-2GiB 寻址无法访问静态数据,甚至可能无法访问其他代码地址。以上是针对 x86-64 System V ABI 的“small”和/或“small-PIC”代码模型。对于中型和大型型号,您可能需要movabs $imm64,但这种情况非常罕见。

我不知道mov $imm32, %r32 是否适用于具有运行时修复的 Windows x64 可执行文件或 DLL,但相对于 RIP 的 LEA 肯定可以。

半相关:Call an absolute pointer in x86 machine code - 如果您正在 JIT,请尝试将 JIT 缓冲区放在现有代码附近,以便您可以 call rel32,否则 movabs 是指向寄存器的指针。

【讨论】:

  • 内存模型的回归。
  • @ecm:我在 Stack Overflow 上看到过 1 或 2 个问题,提到需要在我关注的标签中使用“大数据”代码模型(x86*,程序集,以及一些相关的)。我认为这是具有巨大静态数组的 Fortran。但是,是的,有趣的一点是 32 位 x86 GNU/Linux ABI 完全摆脱了不同大小的内存模型/代码模型。 i386 SysV ABI 文档仅在可能从 x86-64 复制的内容中提到“代码模型”一次。但是对于 64 位地址空间的 rel32 位移,AMD64 确实为选择创造了空间。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-03-24
  • 2016-06-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多