我正在尝试将“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-cache 占用空间和获取/解码带宽)。
lea main, %reg
是绝对 disp32
寻址模式,它将阻止加载时地址随机化 (ASLR) 选择随机 64 位(或 47 位)地址。所以它在 Linux 上不受支持 除了在位置相关的可执行文件中,或者在静态代码/数据总是加载到低 32 位之外的 MacOS 上根本不支持。 (有关文档和指南的链接,请参阅 x86 标签 wiki。)在 Windows 上,您可以将可执行文件构建为“大地址感知”或不。如果您不选择,地址将适合 32 位。
将静态地址放入寄存器的标准有效方法是 a RIP-relative 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 Intel 语法中的 RIP 相对变量引用(如“[RIP + _a]”如何工作? 对 3 种语法的解释,以及为什么 x86-64 中的全局变量相对于指令指针?(和this)是因为 RIP-relative 是解决静态数据的标准方法。
这使用当前指令末尾的 32 位相对位移,如
jmp
/call
。这可以达到.data
、.bss
、.rodata
中的任何静态数据,或.text
中的函数,假设静态代码+数据的总大小通常为2GiB。
在 Linux 上的位置dependent代码(例如使用
gcc -fno-pie -no-pie
构建),您can利用32位绝对寻址来节省代码大小。此外,mov r32, imm32
在 Intel/AMD CPU 上的吞吐量略高于 RIP 相关的 LEA,因此乱序执行可能能够更好地与周围代码重叠。 (针对代码大小进行优化通常不如大多数其他事情重要,但是当其他一切都相同时,选择较短的指令。在这种情况下,所有其他至少相等,或者与mov imm32
一起更好。)
请参阅32 位绝对地址在 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
这就是你在 NASM 中得到的,如果你使用
mov rdi, main
而不是 mov edi, main
很多人最终会这样做。 Linux 动态链接确实实际上支持 64 位绝对地址的运行时修正。但它的用例是用于跳转表,而不是作为立即数的绝对地址。
movq $sign_extended_imm32, %reg
(7 字节)仍然使用 32 位绝对地址,但将代码字节浪费在符号扩展 mov
到 64 位寄存器上,而不是从写入 32 位到 64 位的隐式零扩展-位寄存器。
通过使用
movq
,你告诉GAS你wantR_X86_64_32S
重定位而不是R_X86_64_64
64位绝对重定位。
您想要这种编码的唯一原因是内核代码,其中静态地址位于 64 位虚拟地址空间的高 2GiB,而不是低 2GiB。
mov
在某些 CPU 上(例如,在更多端口上运行)比 lea
具有slight性能优势,但通常情况下,如果您可以使用 32 位绝对值,它位于低 2GiB 的虚拟地址空间中,
mov r32, imm32
起作用.
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
指向寄存器的指针。