gpt4 book ai didi

assembly - MOVZX缺少32位寄存器到64位寄存器

转载 作者:行者123 更新时间:2023-12-04 22:39:43 28 4
gpt4 key购买 nike

这是复制(转换)无符号寄存器的指令:http://www.felixcloutier.com/x86/MOVZX.html

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

32-> 64转换在哪里?我必须使用签名版本吗?
如果是这样,如何将全64位用于无符号整数?

最佳答案

简短答案

如果您不能保证RDI的高位全部为零,请使用mov eax, edi将EDI零扩展到RAX。参见:Why do x86-64 instructions on 32-bit registers zero the upper part of the full 64-bit register?

最好使用不同的源/目标寄存器,因为在Intel和AMD CPU上均使用mov-elimination fails for mov eax,eax。当转移到另一个寄存器时,不需要任何执行单元就可以实现零延迟。 (gcc显然不知道这一点,通常零扩展。)但是,不要花费额外的指令来实现这一目标。



长答案

使用32位源的movzx没有编码的机器代码原因

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

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



AMD64确实需要使用32位源操作数进行符号扩展的新操作码。他们出于某种原因将助记符命名为movsxd,而不是将其作为movsx助记符的第3个操作码。英特尔将它们全部记录在one ISA ref manual entry中。他们重新调整了32位模式下ARPL的1字节操作码的用途,因此实际上movsxd比8或16位源中的movsx短1字节(假设您仍然需要REX前缀以扩展到64)位)。

不同的目标大小将相同的操作码用于不同的操作数大小1。 (66REX.W前缀为16位或64位,而不是默认的32位。)例如movsx eax, blmovsx rax, bl仅REX前缀有所不同;相同的操作码。 (movsx ax, bl也相同,但是前缀为66,以使操作数大小为16位。)

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


不建议在64位模式下使用不带REX.W的MOVSXD。应该使用常规的MOV代替没有REX.W的MOVSXD。


可以使用cdq完成32-> 64位符号扩展,以将EAX符号扩展到EDX:EAX(例如32位idiv之前)。这是x86-64之前的唯一方法(当然,除了复制和使用算术右移之外,还必须广播符号位)。



但是,AMD64已经可以通过任何写入32位寄存器的指令免费将32从零扩展到64。 This avoids false dependencies用于乱序执行,这就是为什么AMD打破了8086/386的传统,即在编写部分寄存器时不影响高位字节的传统。 (Why doesn't GCC use partial registers?

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



有时您确实需要花费一条指令对某些内容进行零扩展。在小型函数的编译器输出中很常见,因为x86-64 SysV和Windows x64调用约定允许args和返回值中有大量垃圾。

像往常一样,询问编译器是否想知道如何在asm中执行某些操作,尤其是在没有看到所需指令的情况下。我在每个函数末尾都省略了ret

Source + asm from the Godbolt compiler explorer, for the System V calling convention (args in RDI, RSI, RDX, ...)

#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个地址大小的前缀,但每个输入都得到相同的结果。当然, add 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


英特尔建议您在有选择时立即销毁 mov的结果,释放 mov消除所占用的微体系结构资源,并释放 increasing the success-rate of mov-elimination (which isn't 100% on Sandybridge-family, unlike AMD Ryzen)。 GCC选择 mov / imul是最好的。

同样,在没有消除运动的CPU上,如果其他输入尚未准备好,则imul之前的 mov可能不在关键路径上(即,如果关键路径经过了未得到 mov的输入) )。但是 mov之后的 imul取决于两个输入,因此它始终位于关键路径上。

当然,当这些函数内联时,编译器通常将知道寄存器的完整状态,除非它们来自函数返回值。并且它也不需要在特定的寄存器中产生结果(RAX返回值)。但是,如果将 unsignedsize_tuint64_t混合使用时,源代码很草率,则可能会迫使编译器发出指令以截断64位值。 (查看编译器的asm输出是捕获该错误并弄清楚如何调整源代码以使编译器保存指令的好方法。)



脚注1:有趣的事实:AT&T语法(使用 movswl(符号扩展字-> long(dword)或 movzbl等不同的助记符))可以从寄存器中推断出目标大小,例如 movzb %al, %ecx,但是 won't assemble movz %al, %ecx even though there's no ambiguity因此,它把 movzb当作自己的助记符,通常的操作数大小后缀可以推断或显式表示,这意味着每个不同的操作码在AT&T语法中都有自己的助记符。

另请参阅 assembly cltq and movslq difference,以获取有关EAX-> RAX的CDQE和任何寄存器的MOVSXD之间的冗余的历史课程。参见 What does cltq do in assembly?the GAS docs以了解AT&T与Intel规范的零/符号扩展。

脚注2: movsxd ax, [rsi]的愚蠢计算机技巧:

汇编程序拒绝汇编 movsxd eax, eaxmovsxd ax, eax,但是可以对其进行手动编码。 ndisasm甚至都没有反汇编(只是 db 0x63),但是GNU objdump可以反汇编。实际的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


那么,CPU如何在内部对其进行处理?它是否实际读取32位然后截断为操作数大小?事实证明,英特尔的ISA参考手册将16位格式记录为 63 /r MOVSXD r16, r/m16,因此 movsxd ax, [unmapped_page - 2]不会出错。 (但是它错误地记录了非REX格式在兼容/旧版模式下有效;实际上 0x63在那里被解码为ARPL。这不是Intel手册中的第一个错误。)

这很合情合理:当没有REX.W前缀时,硬件可以简单地将其解码为与 mov r16, r/m16mov r32, r/m32相同的uop。或不!
Skylake的 movsxd eax,edx(但不是 movsxd rax, edx)对目标寄存器具有输出依赖性,就像它正在合并到目标中一样!包含 times 4 db 0x63, 0xc2 ; movsx eax, edx的循环在每次迭代中以4个时钟运行(每个 movsxd 1个循环,因此有1个周期延迟)。微指令相当均匀地分布到所有4个整数ALU执行端口。具有 movsxd eax,edx / movsxd ebx,edx / 2个其他目标的循环在每次迭代中以〜1.4个时钟运行(如果使用普通的4x mov eax, edx或4x movsxd rax, edx,则略小于每个迭代前端瓶颈1.25个时钟)。在i7-6700k的Linux上使用 perf计时。

我们知道 movsxd eax, edx会将RAX的高位清零,因此实际上并没有使用它正在等待的目标寄存器中的任何位,而是大概在内部对16位和32位进行了类似的处理,从而简化了解码,并简化了这种特殊情况编码的处理没有人应该使用。 16位格式始终必须实际上合并到目标中,因此它确实对输出reg有真正的依赖性。 (Skylake不会将全名寄存器单独重命名16位reg。)

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。因此,我想它会将其视为一个整体助记符,而不是具有 movsl操作数大小的 q指令。或这仅仅是因为没人关心气体不会聚集的特殊情况(它拒绝 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)之间选择。从386开始,就可以使用 movs/zx ax, word [mem]了,但是读取比目标更宽的源代码是x86-64中的一个特例,仅用于符号扩展。 (事实证明,16位目标编码实际上仅读取16位源。)



AMD选择不做的其他ISA设计可能性:

顺便说一句,AMD可以(但不是)将AMD64设计为始终对32位寄存器写入进行符号扩展,而不是始终进行零扩展。在大多数情况下,这对于软件来说不太方便,并且可能还需要使用一些额外的晶体管,但仍然可以避免错误地依赖寄存器中的旧值。这可能会在某处增加额外的门控延迟,因为结果的高位取决于低位,与零扩展不同,零扩展只取决于它是32位操作。 (但这可能并不重要。)

如果AMD以这种方式设计,他们将需要一个 movzxd而不是 movsxd。我认为,将位域打包到更宽的寄存器中时,此设计的主要缺点是需要额外的说明。例如,在写有 shl rax,32or rax, rdxrdtsc之后, edx / eax的自由零扩展很方便。如果是符号扩展名,则需要一条指令将 rdx之前的 or高字节清零。



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

MIPS最初是32位体系结构,从没有16位8086传统和32位x86完全支持8位操作数大小(AX = AH)的32位x86遗留部分寄存器。 :AL部分法规,等等 for easy porting of 8080 source code

MIPS 32位算术指令(例如64位CPU上的 addu)要求其输入正确进行符号扩展,并产生符号扩展的输出。 (由于运行移位是特殊的,因此在运行传统的32位代码而没有意识到更宽的寄存器时,所有方法都适用。)


ADDU rd, rs, rtfrom the MIPS III manual, page 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确实是一个错误的名词。除非确实希望 add捕获有符号的溢出,否则也将其用于有符号的算术。)

双字ADDU有一条 DADDU指令,它可以完成您所期望的。类似地,DDIV / DMULT / DSUBU和DSLL等移位。

按位运算保持不变:现有的AND操作码变为64位AND;不需要64位AND,也不需要32位AND结果的免费符号扩展。

MIPS 32位移位是特殊的(SLL是32位移位。DSLL是单独的指令)。


SLL移位字左逻辑

操作方式:

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


编程说明:
与几乎所有其他字操作不同,输入操作数不
必须是正确的符号扩展字值才能产生有效的
符号扩展的32位结果。结果字始终以符号扩展
进入64位目标寄存器该指令零位移
amount将一个64位值截断为32位,并对其进行符号扩展。


我认为SPARC64和PowerPC64在保持窄结果的符号扩展方面类似于MIPS64。 Code-gen for (a & 0x80000000) +- 12315 for int a(使用 -fwrapv(因此,编译器不能假定 a是非负的,因为有符号溢出的UB))显示了用于PowerPC64的clang维护或重做符号扩展,然后对ccc -target sparc64进行“与”运算,然后进行“或”运算以确保只有低位32的右位被置1,再次保持符号扩展。将返回类型或arg类型更改为 long或在AND掩码常量上添加 L后缀会导致MIPS64和PowerPC64,有时甚至是SPARC64的代码有所不同。也许只有MIPS64实际上会在输入未正确符号扩展的32位指令上出错,而​​在其他情况下,这仅仅是软件调用约定要求。

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

整个有关MIPS的内容与x86-64无关,但是与AMD64做出的不同(更好的IMO)设计决策相比,这是一个有趣的比较。

对于那些示例函数,我在上面的Godbolt链接中包括了MIPS64编译器输出。 (以及一些其他的东西,它们告诉我们更多有关调用约定以及什么编译器的信息。)它通常需要 dext来从32位零扩展到64位。但是直到mips64r2才添加该指令。对于 -march=mips3,用于无符号 return p[a]a必须使用两个双字移位(左移然后右移32位)以零扩展!它还需要一条额外的指令来对添加结果进行零扩展,即实现从无符号到 uint64_t的转换。

因此,我认为我们很高兴x86-64设计为具有自由的零扩展名,而不是仅为某些事情提供64位操作数大小。 (就像我说的那样,x86的传统非常不同;对于使用前缀的相同操作码,它已经具有可变的操作数大小。)当然,更好的位域指令会更好。其他一些ISA(例如ARM和PowerPC)使x86显得羞愧,无法有效地进行位域插入/提取。

关于assembly - MOVZX缺少32位寄存器到64位寄存器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51387571/

28 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com