8

在阅读了这个堆栈溢出答案这个文档之后,我仍然不明白和之间的movq区别movabsq

我目前的理解是,在 中movabsq,第一个操作数是 64 位立即操作数,而movq符号扩展了 32 位立即操作数。从上面引用的第二个文件:

将立即数数据移动到 64 位寄存器可以使用movq指令来完成,该指令将对 32 位立即数进行符号扩展,或者movabsq在需要完整的 64 位立即数时使用指令。

第一个参考文献中,彼得说:

有趣的实验:movq $0xFFFFFFFF, %rax可能是不可编码的,因为它不能用符号扩展的 32 位立即数表示,并且需要 imm64 编码或%eax目标编码。

(编者注:这个错误的假设已在该答案的当前版本中得到修复)。

但是,当我组装/运行它时,它似乎工作正常:

        .section .rodata
str:
        .string "0x%lx\n"
        .text
        .globl  main
main:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    $str, %edi
        movq    $0xFFFFFFFF, %rsi
        xorl    %eax, %eax
        call    printf
        xorl    %eax, %eax
        popq    %rbp
        ret

$ clang file.s -o file && ./file

打印0xffffffff。(这同样适用于较大的值,例如,如果您添加几个额外的“F”)。movabsq生成相同的输出。

Clang 是在推断我想要什么吗?movabsq如果是的话,还有好处movq吗?

我错过了什么?

4

1 回答 1

8

填充 64 位寄存器的动作有以下三种:

  1. 移动到低 32 位部分: B8 +rd id, 5 个字节
    示例:mov eax, 241/mov[l] $241, %eax
    移动到低 32 位部分将使高部分归零。

  2. 移动 64 位立即数: 48 B8 +rd io, 10 个字节
    示例:mov rax, 0xf1f1f1f1f1f1f1f1/mov[abs][q] $0xf1f1f1f1f1f1f1f1, %rax
    移动一个完整的 64 位立即数。

  3. 使用带符号扩展的 32 位立即数移动: 48 C7 /0 id, 7 个字节
    示例:mov rax, 0xffffffffffffffff/mov[q] $0xffffffffffffffff, %rax 将带符号的 32 位立即数移动到完整的 64 位寄存器。

请注意在装配级别如何存在歧义movq用于第二种和第三种情况。

对于每个立即值,我们有:

  • (a) [0, 0x7fff_ffff]中的值可以用 (1)、(2) 和 (3) 编码。
  • (b) [0x8000_0000, 0xffff_ffff]中的值可以用 (1) 和 (2) 编码。
  • (c) [0x1_0000_0000, 0xffff_ffff_7fff_ffff]中的值可以用 (2) 编码
  • (d) [0xffff_ffff_8000_0000, 0xffff_ffff_ffff_ffff]中的值可以用 (2) 和 (3) 编码。

除第三种情况外,所有情况都至少有两种可能的编码。
如果有多个编码可用,汇编器通常会选择最短的一个,但情况并非总是如此。

对于 GAS:
movabs[q]始终对应于 (2)。
mov[q]对于情况(a)和(d),对应于(3),对于其他情况,对应于(2)。
它永远不会为移动到 64 位寄存器生成 (1)。

为了让它拾起(1),我们必须使用mov[l] $0xffffffff, %edi等价的(我相信即使这是等价的,GAS 也不会将移动到 64 位寄存器转换为低 32 位寄存器的移动)。


在 16/32 位时代,区分 (1) 和 (3) 并不重要(但在 GAS 中,可以选择一种特定形式),因为它不是符号扩展操作,而是原始编码的产物在 8086 中。

mov指令从未被分成两种形式来解释(1)和(3),相反mov,汇编器几乎总是选择(1)而不是(3)。

使用具有 64 位立即数的新 64 位寄存器会使代码过于稀疏(并且很容易违反当前 16 字节的最大指令长度),因此将 (1) 扩展为始终采用 64 位是不值得的即时。
相反,(1) 仍然具有 32 位立即数和零扩展(以打破任何虚假数据依赖性),并且 (2) 是针对实际需要 64 位立即数操作数的罕见情况引入的。
借此机会,(3) 也被更改为仍然采用 32 位立即数,但也对其进行符号扩展。
(1) 和 (3) 应该足以满足最常见的立即数(如 1 或 -1)。

然而,(1)/(3) 和 (2) 之间的差异比 (1) 和 (3) 之间的过去差异更深,因为虽然 (1) 和 (3) 都具有相同大小的 32 位操作数, (3) 有一个 64 位立即数操作数。

为什么要人为地延长指令
如链接答案中所述,一个用例可以是填充,以便下一个循环的顶部是 16/32 字节的倍数,而不需要任何 NOP 指令。
这牺牲了代码密度(指令缓存中的更多空间)和循环外的解码效率,以提高每次循环迭代的前端效率。但是对于前端来说,较长的指令通常仍然比解码一些 NOP 更便宜。

另一个更常见的用例是只需要生成一个机器代码模板。
例如,在 JIT 中,可能需要准备指令序列以仅在运行时使用和填充立即数。
在这种情况下,使用 (2) 将大大简化处理,因为所有可能的值总是有足够的空间。

另一种情况是某些修补功能,在软件的调试版本中,可以使用刚刚加载 (2) 的寄存器中的地址间接进行特定调用,以便调试器可以轻松地将调用劫持到任何新目标。

于 2018-09-21T00:39:18.830 回答