【问题标题】:64 bit assembly, when to use smaller size registers64 位汇编,何时使用更小的寄存器
【发布时间】:2011-09-28 11:30:44
【问题描述】:

我了解在 x86_64 程序集中有例如(64 位)rax 寄存器,但它也可以作为 32 位寄存器、eax、16 位、ax 和 8 位等来访问。在什么情况下我不会只使用完整的 64 位,为什么,会有什么优势?

以这个简单的 hello world 程序为例:

section .data
msg: db "Hello World!", 0x0a, 0x00
len: equ $-msg

section .text
global start

start:
mov rax, 0x2000004      ; System call write = 4
mov rdi, 1              ; Write to standard out = 1
mov rsi, msg            ; The address of hello_world string
mov rdx, len            ; The size to write
syscall                 ; Invoke the kernel
mov rax, 0x2000001      ; System call number for exit = 1
mov rdi, 0              ; Exit success = 0
syscall                 ; Invoke the kernel

至少,rdi 和 rdx 只需要 8 位而不是 64 位,对吧?但是,如果我将它们分别更改为 dil 和 dl(它们的低 8 位等效项),程序会进行汇编和链接,但不会输出任何内容。

但是,如果我使用 eax、edi 和 edx,它仍然有效,所以我应该使用它们而不是完整的 64 位吗?为什么或为什么不?

【问题讨论】:

  • 实际上在 Linux 中(可能还有其他一切?)系统调用的参数是 32 位宽的,因此您应该使用 EDI 和 EDX。 win.tue.nl/~aeb/linux/lk/lk-4.html#ss4.3
  • rax 怎么样,也应该改为 eax 吗?我尝试更改这 3 个并且它有效,但我想知道为什么我应该这样做以及有什么优势。
  • 在这个程序的情况下,唯一明显的区别是文字值(4、1、0 等)在 64 位时是两倍大,所以你的程序将大几个字节,理论上,从磁盘/内存加载到 CPU 可能需要更长的时间。
  • 所以没有理由在不需要时使用完整的 64 位,对吧? (我知道也没有理由手动进行代码组装,但我只是想确保......)
  • @MattyK: mov r64, sign-extended-imm32 是 7 个字节,而 mov r32, imm32 是 5 个字节。在 GAS 中,您可以使用 movabs 请求 mov r64, imm64,但 NASM/YASM 仅根据常量的大小选择该编码。 (事实上​​,当您将目标写为rdi 时,NASM 会将小常量优化为mov r32, imm32。我不确定符号地址;如果您不使用“small”,它可能会将它们保留为imm64代码模型,并且您的符号地址约为 32 位。但它不会将 mov rdi,0 优化为 xor edi,edi,因为对标志有副作用。)

标签: assembly 64-bit x86-64 nasm cpu-registers


【解决方案1】:

首先是从内存(读取字符、处理数据结构、反序列化网络数据包等)将较小的(例如 8 位)值加载到寄存器中。

MOV AL, [0x1234]

MOV RAX, [0x1234]
SHR RAX, 56
# assuming there are actually 8 accessible bytes at 0x1234,
# and they're the right endianness; otherwise you'd need
# AND RAX, 0xFF or similar...

或者,当然,将所述值写回内存。


(编辑,比如 6 年后):

因为这不断出现:

MOV AL, [0x1234]
  • 仅读取 0x1234 处的单个内存字节(相反只会覆盖单个内存字节)
  • 保留其他 56 位 RAX 中的任何内容
    • 这会在 RAX 的过去值和未来值之间产生依赖关系,因此 CPU 无法使用 register renaming 优化指令。

相比之下:

MOV RAX, [0x1234]
  • 从 0x1234 开始读取 8 个字节的内存(反之会覆盖 8 个字节的内存)
  • 覆盖所有的RAX
  • 假设内存中的字节与 CPU 具有相同的字节序(在网络数据包中通常不是这样,因此我多年前的 SHR 指令)

同样需要注意:

MOV EAX, [0x1234]

然后,如 cmets 中所述,有:

MOVZX EAX, byte [0x1234]
  • 仅读取 0x1234 处的单个内存字节
  • 扩展值以用零填充所有 EAX(以及 RAX)(消除依赖性并允许寄存器重命名优化)。

在所有这些情况下,如果您想从“A”寄存器写入到内存中,您必须选择宽度:

MOV [0x1234], AL   ; write a byte (8 bits)
MOV [0x1234], AX   ; write a word (16 bits)
MOV [0x1234], EAX  ; write a dword (32 bits)
MOV [0x1234], RAX  ; write a qword (64 bits)

【讨论】:

  • Erm... x86_64 总是 little endian,所以你的例子会产生不同的结果。
  • 这里最好的选择是movzx eax, [0x1234]
  • 彼得科尔德斯是对的。 “mov al”不会破坏依赖链。
  • 寄存器实际上没有字节序。左移乘以 2 的幂,右移乘以 2 的幂。(因此 MSB 是最左边的位。)该概念仅适用于您可以从不同地址加载字节的情况。 (在这种情况下,如果你真的想为你的答案的第一个例子发明一些奇怪的案例,你的第一个例子应该是movzx eax, [0x1234 + 7]。提示:学习汇编的人通常在没有假设大端的例子的情况下有足够的字节序问题当问题与此无关时,内存中的数据。我建议只删除该部分。)
  • 如果我没记错的话,你总是可以AND将一个值的较低部分放入包含较高部分的寄存器中,f.e.你有 0x1230 0000 并且你希望 123 在下部,你可以and rxx, 0x123。对于包含较低部分的寄存器的高值也应该是正确的,不同的是你会有一个更长的数字......如果我错了,请纠正我
【解决方案2】:

如果您只想使用 8 位数量,那么您将使用 AL 寄存器。 AX 和 EAX 相同。

例如,您可以有一个包含两个 32 位值的 64 位值。您可以通过访问 EAX 寄存器来处理低 32 位。当您想处理高 32 位时,您可以交换两个 32 位量(反转寄存器中的 DWORD),以便高位现在在 EAX 中。

【讨论】:

  • 我将如何交换 32 位数量?
  • nasm 中的实际指令是什么?我对此有点陌生。
  • ROL 或 ROR,分别用于向左或向右旋转。在这种情况下,哪个方向都没有关系。还有 RCL 和 RCR 用于带进位的旋转,它们略有不同。
  • 只要“使用”是只读的,它就可以工作。写入 32 位寄存器会将高位 32 归零。stackoverflow.com/questions/11177137/…。由于 x86-64 包含 SSE2 作为基线,如果您希望每个寄存器的多个值的 SIMD 打包,您可以使用 XMM 寄存器。 (不过,SWAR 确实有它的位置,使用 ALU 移位进行 64 位加载和解包可能很有用。)
【解决方案3】:

64-bit 是您可以作为一个单元使用的最大内存。这并不意味着您需要使用多少。

如果您需要 8 位,请使用 8。如果您需要 16 位,请使用 16。如果您使用多少位无关紧要。

诚然,在 64 位处理器上,使用完整的 64 位几乎没有开销。但是,例如,如果您正在计算一个字节值,则使用一个字节将意味着结果已经是正确的大小。

【讨论】:

  • 32 位操作数大小通常是最好的:最小的代码大小(没有 REX 或操作数大小前缀),并且没有部分寄存器合并/错误依赖项。 (8 位也没有必要的前缀,但如果您不仔细了解 AMD CPU(无部分注册重命名)与 P6 / 早期 SnB 系列与 Haswell 的情况,可能会导致部分寄存器内容变慢及更高版本。Why doesn't GCC use partial registers?)
【解决方案4】:

您在这里问了几个问题。

如果您只加载寄存器的低 8 位,则寄存器的其余部分将保持其先前的值。这可以解释为什么你的系统调用得到了错误的参数。

在您只需要 32 位时使用 32 位的一个原因是,许多使用 EAX 或 EBX 的指令比使用 RAX 或 RBX 的指令短一个字节。这也可能意味着加载到寄存器中的常量更短。

指令集已经发展了很长时间,并且有很多怪癖!

【讨论】:

    【解决方案5】:

    如果您只需要 32 位寄存器,您可以放心地使用它们,这在 64 位下是可以的。但如果您只需要 16 位或 8 位寄存器,请尽量避免使用它们或始终使用 movzx/movsx 清除剩余位。众所周知,在 x86-64 下,使用 32 位操作数会清除 64 位寄存器的高位。这样做的主要目的是避免错误的依赖链。

    请参考Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 1的相关部分 - 3.4.1.1 -:

    32 位操作数生成 32 位结果,在目标通用寄存器中零扩展为 64 位结果

    中断依赖链允许指令以随机顺序并行执行,由自 1995 年 Pentium Pro 以来由 CPU 内部实现的Out-of-Order algorithm

    来自Intel® 64 and IA-32 Architectures Optimization Reference Manual 的引述,第 3.5.1.8 节:

    修改部分寄存器的代码序列可能会在其依赖链中遇到一些延迟,但可以通过使用依赖破坏习惯用法来避免。在基于 Intel Core 微架构的处理器中,当软件使用这些指令将寄存器内容清零时,许多指令可以帮助清除执行依赖性。通过对 32 位寄存器而不是部分寄存器进行操作,打破指令之间对部分寄存器的依赖关系。对于移动,这可以通过 32 位移动或使用 MOVZX 来完成。

    汇编/编译器编码规则 37。(M 影响,MH 通用性):通过对 32 位寄存器而不是部分寄存器进行操作,打破指令之间对部分寄存器的依赖关系。对于移动,这可以通过 32 位移动或使用 MOVZX 来完成。

    对于 x64,MOVZX 和带有 32 位操作数的 MOV 是等价的 - 它们都破坏了依赖链。

    这就是为什么如果您在使用较小寄存器时始终尝试清除较大寄存器的最高位,您的代码将执行得更快。当这些位总是被清除时,不再依赖于寄存器的先前值,CPU可以在内部重命名寄存器。

    Register renaming 是 CPU 内部使用的一种技术,它消除了由于连续指令重用寄存器而导致的错误数据依赖性,这些指令之间没有任何实际数据依赖关系。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-01-05
      • 1970-01-01
      • 2020-06-07
      • 2014-11-05
      • 1970-01-01
      • 2017-12-23
      • 1970-01-01
      • 2020-01-30
      相关资源
      最近更新 更多