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



我正在尝试将"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 缓存占用空间和获取/解码带宽)。

lea main, %reg是一种绝对disp32寻址模式,它将阻止加载时地址随机化 (ASLR) 选择随机 64 位(或 47 位)地址。 因此,在Linux上不受支持,除非在与位置相关的可执行文件中,或者在MacOS上根本不支持静态代码/数据,其中静态代码/数据总是在低32位之外加载。 (请参阅 x86 标记 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

请参阅 x86-64 GAS 英特尔语法中的 RIP相对变量引用(如"[RIP + _a]")如何工作?了解这 3 种语法的说明,以及 为什么相对于指令指针访问 x86-64 中的全局变量?(以及这个)为什么RIP相对是处理静态数据的标准方法的原因。

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


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

请参阅 x86-64 Linux 中不再允许的 32 位绝对地址?,了解有关 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可执行文件,但绝不使用具有[disp32]绝对寻址模式的LEA(即使在不需要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 位寄存器上,而不是从写入 32 位寄存器隐式零扩展到 64 位。

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

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

(相关:x86-64 中 movq 和 movabsq 的区别)


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

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

半相关:在 x86 机器代码中调用绝对指针 - 如果要 JITing,请尝试将 JIT 缓冲区放在现有代码附近,以便可以call rel32,否则将指针movabs到寄存器中。

最新更新