【问题标题】:What's the difference between the x86-64 AT&T instructions movq and movabsq?x86-64 AT&T 指令 movq 和 movabsq 有什么区别?
【发布时间】:2019-02-25 07:03:00
【问题描述】:

看了this stack overflow answer,和this document,还是不明白movq和movabsq的区别。

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

可以使用 movq 指令将立即数数据移动到 64 位寄存器,该指令将对 32 位立即数进行符号扩展,或者使用 movabsq 指令,当完整的 64 位立即数是必填。

在the first reference,彼得说:

有趣的实验: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 更有优势?

我错过了什么吗?

【问题讨论】:

  • 如有疑问,请检查拆卸。我没有尝试过clang,但gas 默默地将其转换为movabsq。 movabsq 的重点是明确表示您想要 64 位立即数,例如即使文字适合 32 位或符号。
  • 也许可以尝试使用$label 作为立即数,因此汇编器在汇编时不知道标签的绝对地址是否适合 32 位立即数。直到链接时间才知道。
  • 我更新了我对另一个问题的回答,mov $symbol, %rdi 和 movabs $symbol, %rdi 等等。感谢您发现这个错误的假设。我什至测试了 2008 年的旧版本 GAS,它仍然使用 10 字节编码而不是截断,所以我的猜测可能永远不会对汇编时常量正确,只有链接时常量(地址)。

标签: assembly x86-64 att immediate-operand


【解决方案1】:

填充 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 位寄存器。

注意在程序集级别如何使用room for ambiguity,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) 被认为并不重要(但 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) 之间的过去差异更深,因为虽然 (1) 和 (3) 都有相同大小的操作数, 32 位,(3) 有一个 64 位立即数。

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

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

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

【讨论】:

  • 这里没有列出累加器寄存器的第四个特殊情况。 你的意思是从/到 64 位的 AL/AX/EAX/RAX 加载/存储绝对地址?如果您包含它,那么您还需要包含mov r64, r/m64 与正常寻址模式。
  • nops 是否会产生 µOP?我以为它们在解码器中被丢弃了。
  • @fuz 据我所知,它们仍然发给 BE。这就是我在看到 PeterCordes 对此的评论后所做的几个实验结果的解释。无论如何,我仔细选择了术语“no op”而不是“nop”来包含除nop 之外的其他事实上的 no op 指令。
  • @fuz: nop 在 Intel CPU 上采用融合域 uop 一直通过管道。运行 NOP 通常对性能并不重要,因此英特尔没有给他们特别的支持,当然除了零非融合域 uops / 没有执行单元。 nop 有可能成为分支目标,因此不在 uop 缓存中包含它会很复杂。此外,退役指令的性能计数器计数 NOP。我想从性能计数器中丢失 NOP 是可以的,如果他们想在问题/重命名时放弃它,但大概不像我们想象的那么容易得到正确的 RIP...
  • @MargaretBloom:IDK 在 ROB 中拥有 nop 是多么重要,以便在指令出现故障之前或之后得到正确的 RIP。或者,如果每条指令都知道它的开始地址和结束地址,而不必参考前一条指令的结尾。如果设置了 TF,ISA 要求它在 nop 之后进行陷阱。 (如果TF 没有被重命名,那么这仍然可以在问题/重命名中处理,也许可以通过将其翻译成伪nop 像mov rax,rax?或者只是不过滤掉nop?)我'我敢肯定,这很复杂,我们不知道。
猜你喜欢
  • 2017-03-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-01
  • 2018-06-23
  • 2012-03-10
  • 1970-01-01
  • 2018-03-22
相关资源
最近更新 更多