如何将函数或标签的地址加载到寄存器中

How to load address of function or label into register

我正在尝试将 '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 编译没有解决问题,只是给我同样的错误。

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

lea main, %reg 是一种绝对 disp32 寻址模式,它将阻止加载时地址随机化 (ASLR) 选择随机 64 位(或 47 位)地址。因此 it's not supported on Linux except in position-dependent executables, or at all on MacOS where static code/data are always loaded outside the low 32 bits. (See the x86 tag wiki for links 到文档和指南。)在 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? for an explanation of the 3 syntaxes, and (and ) 以了解为什么相对 RIP 是解决静态数据的标准方法的原因。

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


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

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

# 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] 绝对寻址模式一起使用(即使在 32-不需要 SIB 字节的位代码); mov 总是至少一样好。

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


stupid/bad方式是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 动态 linking 实际上支持 64 位绝对地址的运行时修正。但它的用例是用于跳转表,而不是作为立即数的绝对地址。


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

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

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

(相关:


PS:我故意省略了任何关于“大”或“巨大”内存/代码模型的讨论,其中 RIP 相关 +-2GiB 寻址无法访问静态数据,或者可能无法访问甚至到达其他代码地址。以上是针对 x86-64 System V ABI 的“small”and/or“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 一个指向寄存器的指针.