当前位置: 首页 > 知识库问答 >
问题:

MOVZX缺少32位寄存器至64位寄存器

庾波光
2023-03-14

基本上指令有8->16、8->32、8->64、16->32和16->64。

32->64的转换在哪里?我必须使用签名版本吗?
如果是的话,您如何使用完整的64位来表示无符号整数?

共有1个答案

曹经业
2023-03-14

如果您不能保证RDI的高位全部为零,请使用mov eax,EDI将EDI从零扩展到RAX。参见:为什么32位寄存器上的x86-64指令会将整个64位寄存器的上半部分归零?

最好使用不同的源/目标寄存器,因为在Intel和AMD CPU上的mov eax、eax的MOV消除都失败。当移动到一个不同的寄存器时,不需要执行单元就会产生零延迟。(gcc显然不知道这一点,并且通常是零延伸。)不过,不要花费额外的指令来实现这一点。

摘要:movzx和movsx的每一个不同的源宽度都需要一个不同的操作码。目标宽度由前缀控制。由于mov可以完成该工作,因此movzx dst,r/m32的新操作码将是多余的。

在设计AMD64汇编程序语法时,AMD选择不让movzx rax,edx作为mov eax,edx的伪指令工作。这很可能是一件好事,因为知道写一个32位寄存器使上面的字节为零对于为x86-64编写高效的代码是非常重要的。

在AMD64之前,不需要读取32位源的操作码,因为最大目的地宽度为32位,而“符号扩展”到相同大小只是一个副本。请注意,movsxd eax,eax是合法的,但不推荐使用。您甚至可以使用66前缀对其进行编码,以读取32位源并写入16位目标2

不鼓励在64位模式下使用不带Rex.w的MOVSXD。应该使用常规MOV,而不是使用没有REX.W的MOVSXD。

可以使用cdq完成32->64位符号扩展,将EAX符号扩展为edx:EAX(例如,在32位idiv之前)。这是x86-64之前的唯一方法(当然不是复制和使用算术右移广播符号位)。

但AMD64已经从32零扩展到64,任何写入32位寄存器的指令都可以免费使用。这就避免了无序执行的错误依赖,这就是为什么AMD打破了8086/386的传统,即在写入部分寄存器时不触及上位字节。(为什么GCC不使用部分寄存器?)

由于每个源宽度需要一个不同的操作码,因此没有任何前缀可以使两个movzx操作码中的任何一个读取32位源。

有时确实需要花费一个指令来零扩展一些东西。这在小函数的html" target="_blank">编译器输出中很常见,因为x86-64 SysV和Windows x64调用约定允许在参数和返回值中产生大量垃圾。

#include <stdint.h>

uint64_t zext(uint32_t a) { return a; }
uint64_t extract_low(uint64_t a) { return a & 0xFFFFFFFF; }
    # both compile to
    mov     eax, edi

int use_as_index(int *p, unsigned a) { return p[a]; }
   # gcc
    mov     esi, esi         # missed optimization: mov same,same can't be eliminated on Intel
    mov     eax, DWORD PTR [rdi+rsi*4]

   # clang
    mov     eax, esi         # with signed int a, we'd get movsxd
    mov     eax, dword ptr [rdi + 4*rax]


uint64_t zext_load(uint32_t *p) { return *p; }
    mov     eax, DWORD PTR [rdi]

uint64_t zext_add_result(unsigned a, unsigned b) { return a+b; }
    lea     eax, [rdi+rsi]

在x86-64中,默认地址大小为64。高垃圾不会影响加法的低位,因此这会节省一个字节,而LEA eax,[EDI+ESI]需要67个地址大小的前缀,但对每个输入都给出相同的结果。当然,添加edi,ESI会在RDI中产生零扩展的结果。

uint64_t zext_mul_result(unsigned a, unsigned b) { return a*b; }
   # gcc8.1
    mov     eax, edi
    imul    eax, esi

   # clang6.0
    imul    edi, esi
    mov     rax, rdi    # silly: mov eax,edi would save a byte here

Intel建议在您有选择时立即销毁mov的结果,释放mov-消除占用的微架构资源,并提高mov-消除的成功率(与AMD Ryzen不同,这在Sandybridge-family上不是100%)。GCC选择mov/imul是最好的。

此外,在没有MOV消除的CPU上,如果其他输入尚未就绪(即,如果关键路径通过未得到MOVED的输入),则imul之前的MOV可能不在关键路径上。但是movimul之后依赖于这两个输入,所以它总是处于关键路径上。

当然,当这些函数内联时,编译器通常会知道寄存器的完整状态,除非它们来自函数返回值。而且它不需要在特定的寄存器(RAX返回值)中生成结果。但是,如果您的源代码草率地将unsignedsize_tuint64_t混合,编译器可能会被迫发出截断64位值的指令。(查看编译器asm输出是一个很好的方法,可以捕捉到这一点,并找出如何调整源代码以让编译器保存指令。)

脚注1:有趣的事实:AT&T语法(它使用不同的助记符,如movswl(符号扩展字->long(dword)或movzbl)可以从寄存器(如movzb%al,%ecx)推断目标大小,但不会汇编movz%al,%ecx,即使没有歧义。所以它将movzb作为自己的助记符,使用通常的操作数大小后缀,可以推断或显式。这意味着在AT&T语法中,每个不同的操作码都有自己的助记符。

有关EAX的CDQE->RAX和任何寄存器的MOVSXD之间冗余的历史教训,请参见汇编cltq和movslq差异。看看cltq在汇编中做什么?或者AT&T vs.Intel menmonics的零/符号扩展的GAS文档。

脚注2:使用movsxd ax,[rsi]:

汇编程序拒绝汇编movsxd eax,eaxmovsxd ax,eax,但可以手动对其进行编码。NDISASM甚至不对其进行反汇编(只是DB 0x63),但GNUObjDump会进行反汇编。实际的CPU也会对其进行解码。我试过Skylake只是为了确定:

 ; NASM source                           ; register value after stepi in GDB
mov     rdx, 0x8081828384858687
movsxd  rax, edx                         ; RAX = 0xffffffff84858687
db 0x63, 0xc2        ;movsxd  eax, edx   ; RAX = 0x0000000084858687
xor     eax,eax                          ; RAX = 0
db 0x66, 0x63, 0xc2  ;movsxd  ax, edx    ; RAX = 0x0000000000008687

我们知道movsxd eax,edx会将RAX的高位归零,因此它实际上并没有使用它所等待的目标寄存器中的任何位,但可能在内部对16位和32位进行类似的处理会简化解码,并简化对这种任何人都不应该使用的拐角情况编码的处理。16位表单总是必须实际合并到目标中,因此它确实依赖于输出reg。(Skylake不会将16位寄存器与完全寄存器分开重命名。)

GNU binutils反汇编不正确:gdb和objdump将源操作数显示为32位,如

  4000c8:       66 63 c2                movsxd ax,edx
  4000cb:       66 63 06                movsxd ax,DWORD PTR [rsi]

在应该的时候

  4000c8:       66 63 c2                movsxd ax,dx
  4000cb:       66 63 06                movsxd ax,WORD PTR [rsi]

在AT&T语法中,objdump仍然有趣地使用movslq。所以我想它是把它作为一个整体的助记符来处理,而不是作为一个带有q操作数大小的movsl指令来处理。或者,这只是没有人关心gas无论如何也不会组装的特殊情况的结果(它拒绝movsll,并检查movslq)。

在查看手册之前,我用NASM在Skylake上进行了实际测试,看看负载是否会出错。它当然不是:

section .bss
    align 4096
    resb 4096
unmapped_page: 
 ; When built into a static executable, this page is followed by an unmapped page on my system,
 ; so I didn't have to do anything more complicated like call mmap

 ...
_start:
    lea     rsi, [unmapped_page-2]
    db 0x66, 0x63, 0x06  ;movsxd  ax, [rsi].  Runs without faulting on Skylake!  Hardware only does a 2-byte load

    o16 movsxd  rax, dword [rsi]  ; REX.W prefix takes precedence over o16 (0x66 prefix); this faults
    mov      eax, [rsi]            ; definitely faults if [rsi+2] isn't readable

注意,movsx al,ax是不可能的:字节操作数大小需要一个单独的操作码。前缀只能在32(默认)、16位(0x66)和长模式下64位(REX.W)之间选择。movs/zx ax,word[mem]从386年起就可以使用了,但是读取源比目标更宽是x86-64中的新情况,并且仅用于符号扩展。(结果表明,16位目标编码实际上只读取16位源。)

如果AMD是这样设计的,他们就需要movzxd而不是movsxd。我认为这种设计的主要缺点是在将位字段打包到更宽的寄存器中时需要额外的指令。例如,对于编写edxeaxrdtsc之后的SHL rax,32/或rax,RDX来说,免费零扩展很方便。如果是sign-extension,则需要一条指令来将之前的RDX的上位字节归零。

其他ISA做出了不同的选择:MIPS III(~1995年)在没有引入新模式的情况下将体系结构扩展到64位。与x86非常不同的是,在固定宽度的32位指令字格式中,有足够的操作码空间未使用。

MIPS一开始是一个32位体系结构,它从来没有像32位x86那样从16位8086继承到部分寄存器,从8086完全支持8位操作数大小,带有AX=ah:al部分寄存器等等,以便于8080源代码的移植。

64位CPU上的MIPS 32位算术指令(如addu)要求它们的输入正确地进行符号扩展,并产生符号扩展的输出。(在运行遗留的32位代码而不知道更宽的寄存器时,一切都正常工作,因为移位是特殊的。)

添加rd、rs、RT(摘自MIPS III手册,A-31页)

限制:
在64位处理器上,如果GPR rt或GPR rs不包含符号扩展的32位值(位63..31相等),则操作结果未定义。

操作:

  if (NotWordValue(GPR[rs]) or NotWordValue(GPR[rt])) then UndefinedResult() endif
  temp ←GPR[rs] + GPR[rt]
  GPR[rd]← sign_extend(temp31..0)

(注意,addu中的U表示unsigned实际上是一个用词不当的地方,正如手册所指出的那样。除非您真的想让add在带符号的溢出中陷进去,否则您也可以将它用于带符号的算术。)

有一个用于双字ADDU的daddu指令,它可以执行您所期望的操作。类似地,DDIV/DMULT/DSUBU、DSLL和其他转换。

操作:

s ← sa
temp ← GPR[rt] (31-s)..0 || 0 s
GPR[rd]← sign_extend(temp)

编程注意:
与几乎所有其他字操作不同,输入操作数不必是正确的符号扩展的字值,以产生有效的符号扩展的32位结果。结果字总是符号扩展到64位目的寄存器中;这个移位量为零的指令将一个64位的值截断为32位,并对其进行符号扩展。

我认为SPARC64和PowerPC64在维护狭义结果的符号扩展方面与MIPS64类似。Code-gen for(a&0x80000000)+-12315forint a(带有-FWRAPV这样编译器就不能假设a是非负的,因为有符号溢出UB)显示PowerPC64维护或重做符号扩展的clang,并显示clang-target sparc64ANDing然后ORing以确保仅设置了低32中的右位,再次维护符号扩展。将返回类型或arg类型更改为long或在AND mask常量上添加l后缀会导致MIPS64和PowerPC64的代码差异,有时还会导致SPARC64的代码差异;可能只有MIPS64在32位指令上的输入没有正确的符号扩展,而在其他指令上,这只是一个软件调用约定的要求。

但是AArch64采用的方法更像x86-64,其中w0..31寄存器是x0..31的下半部分,指令有两种操作数大小。

我在上面的Godbolt链接中包含了MIPS64编译器输出,用于这些示例函数。(还有一些其他的,告诉我们关于调用约定和编译器的更多信息。)经常需要dext从32位零扩展到64位;但该指令直到MIPS64R2才被添加。对于-march=mips3返回P[a]对于无符号a必须使用两个双字移位(左移然后右移32位)来扩展零!它还需要一个额外的指令来对add结果进行零扩展,即实现从unsigned到uint64_t的转换。

所以我想我们可以庆幸x86-64是用免费的零扩展设计的,而不是只为某些事情提供64位操作数大小。(就像我说的,x86的传统是非常不同的;对于使用前缀的相同操作码,它已经有了可变的操作数大小。)当然,更好的位字段指令会更好。其他一些ISA(如ARM和PowerPC)因高效的位字段插入/提取而使x86蒙羞。

 类似资料:
  • 本文向大家介绍Intel x86 Assembly& Microarchitecture 32位寄存器,包括了Intel x86 Assembly& Microarchitecture 32位寄存器的使用技巧和注意事项,需要的朋友参考一下 示例 英特尔生产80386时,他们从16位处理器升级到了32位处理器。32位处理意味着两件事:要处理的数据都是32位,而要访问的内存地址是32位。为此,他们仍然

  • 本文向大家介绍verilog 移位寄存器,包括了verilog 移位寄存器的使用技巧和注意事项,需要的朋友参考一下 示例 具有异步复位功能的N位深移位寄存器。            

  • 我试图比较Peter Cordes在回答“将CPU寄存器中的所有位设置为1”的问题时提到的方法。 因此,我编写了一个基准测试,将所有13个寄存器设置为除、和之外的所有位1。 代码如下所示<代码>乘以32 nop用于避免DSB和LSD影响。 我测试了他提到的以下方法,以及这里的完整代码 为了使这个问题更简洁,我将使用替换下表中的。 下表显示,从组1到组3,当使用64位寄存器时,每个循环多1个周期。

  • 我正在寻找一种方法来移动arch64寄存器x1中的任何32位常数。 是否有一种方法可以执行

  • 注意:如果被称为,的零/非零状态与不相同,那么直接使用32位是行不通的 目前,我一直在寻找比(1c tput、3c延迟、5字节代码大小)更好的方法,这些方法可以适用于所有值。 有没有更好更聪明的方法? 编辑:删除编辑,因为它们没有添加到问题中。

  • 本文向大家介绍Intel x86 Assembly& Microarchitecture 8位寄存器,包括了Intel x86 Assembly& Microarchitecture 8位寄存器的使用技巧和注意事项,需要的朋友参考一下 示例 前四个16位寄存器可以将其高低字节和低半字节直接作为自己的寄存器进行访问: AH和AL是AX寄存器的上半部分和下半部分。 BH和BL是BX寄存器的上半部分和下