x86-64 AT&T 指令 movq 和 movabsq 有什么区别?

What's the difference between the x86-64 AT&T instructions movq and movabsq?

看了, and this document,还是不明白movqmovabsq的区别。

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

Moving immediate data to a 64-bit register can be done either with the movq instruction, which will sign extend a 32-bit immediate value, or with the movabsq instruction, when a full 64-bit immediate is required.

中,彼得指出:

Interesting experiment: movq [=17=]xFFFFFFFF, %rax is probably not encodeable, because it's not representable with a sign-extended 32-bit immediate, and needs either the imm64 encoding or the %eax destination encoding.

(editor's note: this mistaken assumption is fixed in the current version of that answer).

然而,当我 assemble/run 这似乎工作正常:

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

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

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

Clang 是否在推断我想要什么?如果是,movabsq 是否比 movq 更有优势?

我是不是漏掉了什么?

填充64位寄存器的三种方式:

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

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

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

注意在汇编级别如何有 room for ambiguitymovq 用于第二种和第三种情况。

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

  • (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)编码。

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

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

为了让它接收 (1) 我们必须使用等价的 mov[l] [=22=]xffffffff, %edi(我相信 GAS 不会将移动到 64 位寄存器转换为移动到其较低的 32 位寄存器即使这是等效的)。


在 16/32 位时代,区分 (1) 和 (3) 并不被认为是非常重要的(但 in GAS it's possible to pick one specific form),因为它不是符号扩展操作,而是原始的人工制品编码在 8086.

mov 指令从未被分成两种形式来说明 (1) 和 (3),而是一个 mov 被用于汇编程序几乎总是选择 (1) (3).

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

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

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

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

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