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

问题描述 投票:0回答:1

我正在尝试将“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 编译并不能解决问题,只会给我同样的错误。

gcc assembly x86-64 att addressing-mode
1个回答
17
投票

在 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你want
R_X86_64_32S
重定位而不是
R_X86_64_64
64位绝对重定位。

您想要这种编码的唯一原因是内核代码,其中静态地址位于 64 位虚拟地址空间的高 2GiB,而不是低 2GiB。

mov
在某些 CPU 上(例如,在更多端口上运行)比 lea 具有
slight
性能优势,但通常情况下,如果您可以使用 32 位绝对值,它位于低 2GiB 的虚拟地址空间中,
mov r32, imm32
起作用.

(相关: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
指向寄存器的指针。

© www.soinside.com 2019 - 2024. All rights reserved.