为什么我们不能将 64 位立即数移动到内存中?

amj*_*jad 4 assembly x86-64 instruction-set cpu-architecture immediate-operand

首先,我对movq和之间的区别有点困惑movabsq,我的教科书说:

常规movq指令只能具有可以表示为 32 位二进制补码的立即数源操作数。然后对该值进行符号扩展以生成目标的 64 位值。所述movabsq指令可以具有任意的64位立即值作为其源操作数,并且只能有一个寄存器作为目的地。

我有两个问题。

问题 1

movq指令只能具有可以表示为 32 位二进制补码的立即数源操作数。

所以这意味着我们不能做

movq    $0x123456789abcdef, %rbp
Run Code Online (Sandbox Code Playgroud)

我们必须这样做:

movabsq $0x123456789abcdef, %rbp
Run Code Online (Sandbox Code Playgroud)

但是为什么movq被设计为不适用于 64 位立即数,这确实违背了q(四分字)的目的,我们需要movabsq为此目的而设置另一个,这不是很麻烦吗?

问题2

由于目标movabsq必须是寄存器,而不是内存,所以我们不能将 64 位立即数移动到内存中:

movabsq $0x123456789abcdef, (%rax)
Run Code Online (Sandbox Code Playgroud)

但有一个解决方法:

movabsq $0x123456789abcdef, %rbx
movq    %rbx, (%rax)   // the source operand is a register, not immediate constant, and the destination of movq can be memory
Run Code Online (Sandbox Code Playgroud)

那么为什么这条规则旨在让事情变得更难呢?

Pet*_*des 7

是的,与-1aka不同,移动到寄存器然后移动到内存以获取不适合符号扩展的 32 位的立即数0xFFFFFFFFFFFFFFFF。不过,为什么部分是一个有趣的问题:


请记住,asm 只允许您执行机器代码中可能执行的操作。因此,这实际上是一个关于 ISA 设计的问题。此类决策通常涉及硬件易于解码的内容,以及编码效率方面的考虑。(在很少使用的指令上使用操作码会很糟糕。)

它不是为了让事情变得更难,而是为了不需要任何新的mov. 并且还将 64 位立即数限制为一种特殊指令格式。 mov是可以唯一的指令以往使用64位立即在所有(或64位的绝对地址,AL / AX / EAX / RAX的加载/存储)。

查看英特尔的手册以了解以下形式mov(请注意,它使用英特尔语法,首先使用目标,我的答案也是如此。)我还在x86-64 中的 movq 和 movabsq 之间的差异中总结了形式(及其指令长度),就像@MargaretBloom 回答x86-64 AT&T 指令 movq 和 movabsq 之间有什么区别?.

允许 imm64 与 ModR/M 寻址模式一起也可以很容易地遇到指令长度的 15 字节上限,例如 REX + opcode + imm64 是 10 字节,而 ModRM+SIB+disp32 是 6。所以mov [rdi + rax*8 + 1234], imm64即使有一个操作码也无法编码mov r/m64, imm64

这就是假设它们另作它在64位模式中通过使一些指令释放了1个字节的操作码中的一个无效(例如aaa),这可能是因为在其他模式的解码器不方便(和指令长度的预解码器)这些操作码不需要 ModRM 字节或立即数。


movq用于mov以普通 ModRM 字节的形式允许任意寻址模式作为目标。 (或作为 的来源movq r64, r/m64)。AMD 选择将这些立即数保留为​​ 32 位,与 32 位操作数大小1 相同

这些形式的mov指令格式与其他指令(如add. 为了便于解码,这意味着 REX 前缀不会改变这些操作码的指令长度。 当寻址方式为变长时,指令长度译码已经够难了。

所以movq是64位的操作数大小,但其他方面相同的指令格式mov r/m64, imm32(成为符号扩展-即时形式一样,同样每隔指令,它仅具有一个直接形式),和mov r/m64, r64mov r64, r/m64

movabs是现有 no-ModRM 短格式的 64 位格式mov reg, imm32。这已经是一种特殊情况(因为 no-modrm 编码,寄存器编号来自操作码字节的低 3 位)。小的正常量可以只使用 32 位操作数大小隐式零扩展到 64 位,而不会降低效率(如32 位或 64 位模式下的5 字节mov eax, 123/AT&T mov $123, %eax)。拥有 64 位绝对值mov很有用,因此 AMD 这样做是有道理的。

由于没有 ModRM 字节,它只能编码一个寄存器目标。添加一个可以接受内存操作数的表单需要完全不同的操作码。


从一个POV,不胜感激你得到一个mov64位的立即在所有; 像 AArch64(具有固定宽度的 32 位指令)这样的 RISC ISA 需要更多的 4 条指令才能将 64 位值放入寄存器。(除非它是一个重复的位模式;AArch64 实际上很酷。不像早期的 RISC,如 MIPS64 或 PowerPC64)

如果 AMD64 要为 引入一个新的操作码movmov r/m, sign_extended_imm8那么对于节省代码大小将更加有用。 编译器发出多mov qword ptr [rsp+8], 0条指令将本地数组或结构归零的情况并不少见,每个指令都包含一个 4 字节的0立即数。在寄存器中放置一个非零的小数是相当常见的,并且会生成mov eax, 123一条 3 字节指令(从 5 减少)和mov rax, -123一条 4 字节指令(从 7 减少)。它还可以在不破坏 FLAGS 3 个字节的情况下对寄存器进行清零。

允许movimm64 进入内存很少有用,以至于 AMD 认为不值得让解码器变得更复杂。在这种情况下,我同意他们的观点,但 AMD 在添加新操作码方面非常保守。很多错过清理 x86 疣的机会,比如扩大setcc会很好。但我认为 AMD 并不确定 AMD64 会流行起来,并且如果人们不使用它,并且不想被困在需要大量额外的晶体管/电源来支持某个功能。

脚注 1
一般来说,32 位立即数显然是代码大小的一个很好的决定。想要add立即访问 +-2GiB 范围之外的内容是非常罕见的。这可能是像按位的东西很有用AND,但对于设置/清除/翻转单个比特bts/ btr/btc指令是好的(以比特位为8位立即,而不是需要一个面罩)。你不想sub rsp, 1024成为一个 11 字节的指令;7已经够糟糕了。


巨指令?效率不高

在设计 AMD64 时(2000 年代初),带有 uop 缓存的 CPU 还不是什么东西。(带有跟踪缓存的英特尔 P4 确实存在,但事后看来它被认为是一个错误。)指令提取/解码以高达 16 字节的块进行,因此拥有一条接近 16 字节的指令对于前端比movabs $imm64, %reg

当然,如果后端跟不上前端,则可以通过在阶段之间进行缓冲来隐藏此循环中仅解码 1 条指令的气泡。

跟踪一条指令的这么多数据也将是一个问题。CPU 必须将这些数据放在某个地方,如果在寻址模式中有 64 位立即数32 位位移,那就是很多位。 通常一条指令最多需要 64 位空间用于 imm32 + disp32。


顺便说一句,对于大多数带有 RAX 和立即数的操作,都有特殊的 no-modrm 操作码。(X86-64 8086,其中AX / AL是比较特殊的演化出来,看到更多的历史和解释)。对于那些add/sub/cmp/and/or/xor/... rax, sign_extended_imm32没有 ModRM 的表单来说,使用完整的 imm64将是一个合理的设计。RAX 最常见的情况是,立即数使用 8 位符号扩展的立即数(-128..127),无论如何都不是这种形式,它只为需要 4 字节立即数的指令节省 1 个字节。但是,如果您确实需要一个 8 字节的常量,将它放在寄存器或内存中以供重用会比在循环中执行 10 字节和-imm64 更好。


fcd*_*cdt 5

对于第一个问题:

来自gnu 汇编器的官方文档

在64位代码中,movabs可用于对mov具有64位位移或立即操作数的指令进行编码。

mov reg64, imm(在英特尔语法中,目标优先)是唯一接受 64 位立即值作为参数的指令。这就是为什么您不能将 64 位立即数直接写入内存,而只能写入寄存器。这种形式mov使用包含寄存器号的操作码,而不是通过 ModRM 字节指定 reg/mem 目的地。


对于第二个问题:

对于其他目标,例如内存位置,32 位立即数可以符号扩展为 64 位立即数(这意味着前 33 位是相同的)。在这种情况下,您可以使用该movq指令。

如果目标是寄存器,这也是可能的,节省 3 个字节:

48 B8 FF FF FF 7F 00 00 00 00   movabs $0x7FFFFFFF, %rax
48 C7 C0 FF FF FF 7F            movq   $0x7FFFFFFF, %rax
Run Code Online (Sandbox Code Playgroud)

在 64 位立即数处0xFFFFFFFF,高 33 位不相同(00...),因此movl不能在这里使用。这就是我选择0x7FFFFFFF这个例子的原因。但还有另一种选择:

当写入 32 位寄存器(64 位寄存器的低位部分)时,寄存器的高 32 位被清零。因此,对于高 32 位为零的 64 位立即数,movl也可以使用,这又节省了一个字节:

# with mov $imm32, reg/mem32.  Assemblers won't use this for a register destination
C7 C0 FF FF FF FF               movl   $0xFFFFFFFF, %eax
Run Code Online (Sandbox Code Playgroud)

汇编器使用特殊情况的 mov-to-register 编码保存另一个字节。(movabs-immediate 是该操作码的 REX.W 形式。)

# the mov $imm32, reg  short-form encoding with no ModRM
B8 FF FF FF FF                  movl   $0xFFFFFFFF, %eax
Run Code Online (Sandbox Code Playgroud)

GAS 和其他汇编器将自动使用您实际编写的指令的最短编码,例如它们将以mov $-1, %eax5 个字节进行编码。


但 GAS 不会自动优化%rax%eax. 例如,mov $0x00000000FFFFFFFF, %rax将使用 10-byte movabsq,而不是movl.

如果您使用,它还可以在 和 之间进行选择movabs,具体取决于立即数的大小。例如。但不会将其优化为具有 32 位操作数大小的 5 字节 mov-immediate。movqmovmov $1, %rax

但如果您使用as -Os(或 或gcc -Wa,-Os),GAS使用 5 字节movl $-1, %eax编码mov $0xFFFFFFFF, %rax。它具有相同的架构效果(一条指令使 RAX=0x00000000FFFFFFFF),但它在 asm 源中的拼写不同;使用不同的操作数大小,从而使用不同的寄存器名称。

NASM 默认执行此优化(针对不同的操作数大小)。