为什么不允许从内存移动到内存?



我想知道这在汇编中是否允许,

movl (%edx) (%eax) 

我猜它会在第一个操作数中访问内存并放入第二个操作数的内存,类似于*a = *b,但我没有看到任何处理这样的例子,所以我猜这是不允许的。另外,我被告知这是不允许的

leal %esi (%edi)

为什么?最后,还有其他类似的函数是不允许的吗?

从内存复制到内存的正常/有效的方法是加载到临时寄存器中。选择一个;如果拷贝后不需要在寄存器中保存加载地址,你甚至可以使用movl (%ecx), %ecx/movl %ecx, (%eax)

还有pushl (%ecx)/popl (%edx)movsd设置RSI/ESI和RDS/EDI等方法,但速度较慢;通常最好释放一个临时寄存器,即使这意味着稍后重新加载一些东西,或者甚至存储/重新加载一些其他不太常用的值。


为什么x86不能为一条指令使用两个显式内存操作数:

movl (mem), (mem)         # AT&T syntax
mov dword [eax], [ecx]    ; or the equivalent in Intel-syntax

无效,因为x86机器码没有两个地址的mov编码. (事实上,x86指令不可能有两个任意寻址模式。)

它有mov r32, r/m32mov r/m32, r32。regg -reg移动可以使用mov r32, r/m32操作码或mov r/m32, r32操作码进行编码。许多其他指令也有两个操作码,一个是dest必须是寄存器,另一个是src必须是寄存器。

(还有一些特殊的形式,比如op r/m32, imm32,或者对于mov来说,movabs r64, [64bit-absolute-address])

参见x86指令集参考手册(HTML刮;x86标记wiki中的其他链接)。我在这里使用了Intel/NASM语法,因为这是Intel和AMD的参考手册所使用的。

很少有指令可以执行一次加载并存储到两个不同的地址,例如movs(string-move)和push/pop (mem)(什么x86指令需要两个(或更多)内存操作数?)在所有这些情况下,至少有一个内存地址是隐式的(由操作码暗示),而不是任意选择[eax][edi + esi*4 + 123]或其他任何地址。

许多ALU指令可以与内存目的地一起使用。这是对单个内存位置的读-修改-写操作,对加载和存储使用相同的寻址模式。这表明限制不是8086不能加载和存储,而是解码复杂性(和机器代码紧凑性/格式)的限制。


没有任意两个有效地址的指令(即使用灵活的寻址模式指定)。movs有隐式的源操作数和dest操作数,push有隐式的dest操作数(esp)。

一个x86指令最多有一个ModRM字节,一个ModRM只能编码一个reg/memory操作数(模式2位,基寄存器3位)和另一个寄存器操作数(3位)。使用转义码,ModRM可以向SIB字节发出信号,以编码内存操作数的基数+缩放索引,但仍然只有空间编码一个内存操作数。

如上所述,同一指令(asm源助记符)的内存源形式和内存目标形式使用两种不同的操作码。就硬件而言,它们是不同的指令。


选择这种设计的部分原因可能是实现的复杂性:如果一条指令可能需要来自AGU(地址生成单元)的两个结果,那么接线必须在那里使其成为可能。这种复杂性的一部分在于解码器,它要找出操作码是哪条指令,并解析剩下的位/字节来找出操作数是什么。由于没有其他指令可以有多个r/m操作数,因此需要额外的晶体管(硅面积)来支持对两个任意寻址模式进行编码的方法。对于逻辑来说,它必须计算出一条指令有多长,这样它就知道从哪里开始解码下一条指令。

它还可能为指令提供五个输入依赖项(存储地址的双寄存器寻址模式,加载地址的双寄存器寻址模式,如果是adcsbb,则为FLAGS)。但是当8086/80386被设计时,超标量/乱序/依赖跟踪可能还没有出现在雷达上。386增加了很多新指令,所以mov的memto - memm编码本来可以实现,但是没有实现。如果386已经开始将结果直接从ALU输出转发到ALU输入和类似的东西(与总是将结果提交到寄存器文件相比,为了减少延迟),那么这个原因可能是它没有实现的原因之一。

如果它存在,Intel P6可能会将其解码为两个独立的up,一个load和一个store。现在引入它当然没有意义,或者在1995年之后的任何时候,当P6被设计出来时,简单的指令比复杂的指令获得了更多的速度优势。(请参阅http://agner.org/optimize/了解如何使代码运行得更快。)

我看不出这有多大用处,至少与代码密度的成本相比没有多大用处。如果你想这样做,你可能没有充分利用寄存器。如果可能的话,弄清楚如何在复制时动态地处理数据。当然,有时你只需要做一次加载,然后再做一次存储,例如在一个排序例程中,在基于一个成员进行比较后交换结构体的其余部分。在更大的块中移动(例如使用xmm寄存器)是一个好主意。


leal %esi, (%edi)
这是AT&T语法,lea src, dst。因此,lea (%edi), %esimov %edi, %esi的低效等价物,但在另一个顺序中存在两个问题: 首先,寄存器没有地址。一个空的%esi不是有效的有效地址,所以不是lea的有效源

第二,lea的目标必须是一个寄存器。没有编码需要第二个有效地址来将目标存储到内存中。


您遗漏了两个操作数之间的,这是在你了解操作数的限制之前的一个亮点。
答案的其余部分只讨论修复该语法错误后的代码。

valid-asm.s:2: Error: number of operands mismatch for `lea'

无效。除了一组有限的操作数外,您可能不会在我所熟悉的任何体系结构上直接执行内存到内存的移动。例如,在英特尔兼容处理器上通过SIDI寄存器的字符串move和类似的字符串是例外,尽管这些应该避免(见下文)。大多数体系结构确实有一些东西可以帮助这些有限的内存到内存的移动。

如果考虑硬件,这是很有意义的。有地址线和数据线。处理器在地址线上发出要访问哪个内存地址的信号,然后通过数据线读取或写入数据。因此,数据必须通过缓存或处理器到达其他内存。事实上,如果你看一下145页的引用,你会看到MOVS和它的朋友绝对不能被使用的强烈声明:

注意,当REP MOVS指令将一个单词写入目的地,它从同一时钟的源读入下一个单词周期。如果第2-4位相同,则可能存在缓存库冲突这两个地址在P2和P3。换句话说,你会得到如果ESI+WORDSIZE-EDI是能被32整除。避免缓存库冲突的最简单方法是将源和目标对齐8。千万不要使用MOVSB或MOVSW优化代码,即使在16位模式下也不行。

在许多处理器上,REP MOVS和REP STOS可以通过移动来快速执行16字节或一次一整条缓存行。这种情况只有在满足一定的条件。取决于处理器,条件对于快速字符串指令,通常计数必须为高,源和目的都必须对齐,方向也必须一致前进时,起点和终点之间的距离必须在最小缓存行大小,以及源和源的内存类型目标必须是回写或组合写(您可以(通常假定满足后一个条件)。

在这些条件下,速度是你能获得的最高速度矢量寄存器在某些处理器上移动甚至更快。而字符串指令可以非常方便,这一点必须强调其他解决方案在很多情况下更快。如果符合上述条件对于快速移动不满足,那么有很多获得使用其他方法。

在某种意义上,这也解释了为什么寄存器到寄存器的移动是可以的(尽管还有其他原因)。也许我应该说,这解释了为什么它们不需要非常特殊的硬件。寄存器都在处理器中;不需要访问总线来通过地址进行读写。

最新更新