在 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, %edi 或 lea main, %rdi 也可以在 Linux 非 PIE 可执行文件中工作,但切勿将 LEA 与 [disp32] 绝对寻址模式一起使用(即使在不需要 SIB 字节的 32 位代码中) ; mov 总是至少一样好。
当您有一个唯一确定它的寄存器操作数时,操作数大小后缀是多余的;我更喜欢只写mov 而不是movl 或movq。
愚蠢/糟糕的方式是一个 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 是指向寄存器的指针。