如何在没有MOV指令的情况下在寄存器之间移动数据?
How to move data between registers without MOV instruction?
如何在不使用 MOV
指令的情况下像 MOV EAX, EBX
那样移动?
首先清理一个寄存器XOR EAX, EAX
上的数据,然后执行OR EAX, EBX
您可以将 lea
与简单的寄存器寻址模式一起使用,作为较慢的 mov
(没有移动消除并且在 Ice Lake 之前的 Intel 上运行的端口更少),尽管它仍然是一条指令。
# nasm -f elf64 foo.asm && objdump -drwC -Mintel foo.o
0000000000000000 <.text>:
0: 89 c8 mov eax,ecx
2: 8d 01 lea eax,[rcx] # in 64-bit code,
4: 67 8d 01 lea eax,[ecx] # don't use 32-bit address size
除了速度较慢之外,某些寄存器 () 还需要额外的代码大小。 (我使用了 64 位操作数大小,因此它们都需要 REX 前缀,因此例如 RSP 和 R12 机器代码排队,因为它们都需要使用 SIB 字节。
10: 48 89 fe mov rsi,rdi
13: 48 8d 37 lea rsi,[rdi]
16: 48 8d 34 24 lea rsi,[rsp]
1a: 49 8d 34 24 lea rsi,[r12]
1e: 48 8d 75 00 lea rsi,[rbp+0x0] # source had just [rbp]
22: 49 8d 75 00 lea rsi,[r13+0x0]
同样的事情在其他模式下当然也可以,使用lea eax, [ecx]
作为2字节指令。 (或者对于 32 位寄存器,在 16 位模式下为 4 个字节。即使您只需要 16 位寄存器,对于 [bx|bp] + [si|di 以外的源寄存器,16 位模式也需要 32 位地址大小] 因为 16 位寻址模式编码限制。)
push
/pop
也是一个选项,可以仅用 2 个字节的机器代码复制 64 位寄存器,而不是通常的 3 个字节。
或者如果你不关心源寄存器(实际上是移动而不是复制),你可以xchg
,当EAX为两个寄存器之一
30: 52 push rdx
31: 58 pop rax
32: 48 89 d0 mov rax,rdx
35: 48 8d 02 lea rax,[rdx]
38: 87 f1 xchg ecx,esi # opcode + ModRM form
3a: 91 xchg ecx,eax # EAX special case
其他愚蠢的计算机技巧包括 imul
和来自评论的双班制建议
立即 imul
1.
对于 16 位寄存器,SHLD 或计数 = 16 的 SHRD 是可能的。 (对于 64 位以外的任何操作数大小,x86 标量整数移位用 & 31
屏蔽计数,因此这不能移位 32 位或 64 位完整寄存器的所有位,只能移位 16 位部分寄存器。并且 shld/shrd 是 386 中新增的,因此 16 位寄存器始终是“部分”的。)
40: 6b f1 01 imul esi,ecx,0x1
43: 66 0f a4 ce 10 shld si,cx,0x10 # SI = CX. CX unchanged.
48: 66 0f ac ce 10 shrd si,cx,0x10
4d: 0f a4 ce 20 shld esi,ecx,0x20 # nope, equivalent to a shift by 0
或者如果您想考虑多条指令,您可以像 Yuri 建议的那样将目标和 add
、or
或 xor
异或归零。零是 +
、|
和 ^
.
的标识元素
将 AND 转换为全一(&
的标识元素)效率更低。 (对 EAX 的旧值的错误依赖,并且它不是异或归零,所以它不能像 Sandybridge-family CPU 那样被消除(没有执行单元)。代码大小也更大)
or eax, -1
and eax, ecx
(Godbolt compiler explorer NASM 来源和反汇编,用于本答案中的反汇编)
如何在不使用 MOV
指令的情况下像 MOV EAX, EBX
那样移动?
首先清理一个寄存器XOR EAX, EAX
上的数据,然后执行OR EAX, EBX
您可以将 lea
与简单的寄存器寻址模式一起使用,作为较慢的 mov
(没有移动消除并且在 Ice Lake 之前的 Intel 上运行的端口更少),尽管它仍然是一条指令。
# nasm -f elf64 foo.asm && objdump -drwC -Mintel foo.o
0000000000000000 <.text>:
0: 89 c8 mov eax,ecx
2: 8d 01 lea eax,[rcx] # in 64-bit code,
4: 67 8d 01 lea eax,[ecx] # don't use 32-bit address size
除了速度较慢之外,某些寄存器 (
10: 48 89 fe mov rsi,rdi
13: 48 8d 37 lea rsi,[rdi]
16: 48 8d 34 24 lea rsi,[rsp]
1a: 49 8d 34 24 lea rsi,[r12]
1e: 48 8d 75 00 lea rsi,[rbp+0x0] # source had just [rbp]
22: 49 8d 75 00 lea rsi,[r13+0x0]
同样的事情在其他模式下当然也可以,使用lea eax, [ecx]
作为2字节指令。 (或者对于 32 位寄存器,在 16 位模式下为 4 个字节。即使您只需要 16 位寄存器,对于 [bx|bp] + [si|di 以外的源寄存器,16 位模式也需要 32 位地址大小] 因为 16 位寻址模式编码限制。)
push
/pop
也是一个选项,可以仅用 2 个字节的机器代码复制 64 位寄存器,而不是通常的 3 个字节。
或者如果你不关心源寄存器(实际上是移动而不是复制),你可以xchg
,当EAX为两个寄存器之一
30: 52 push rdx
31: 58 pop rax
32: 48 89 d0 mov rax,rdx
35: 48 8d 02 lea rax,[rdx]
38: 87 f1 xchg ecx,esi # opcode + ModRM form
3a: 91 xchg ecx,eax # EAX special case
其他愚蠢的计算机技巧包括 imul
和来自评论的双班制建议
立即
imul
1.对于 16 位寄存器,SHLD 或计数 = 16 的 SHRD 是可能的。 (对于 64 位以外的任何操作数大小,x86 标量整数移位用
& 31
屏蔽计数,因此这不能移位 32 位或 64 位完整寄存器的所有位,只能移位 16 位部分寄存器。并且 shld/shrd 是 386 中新增的,因此 16 位寄存器始终是“部分”的。)
40: 6b f1 01 imul esi,ecx,0x1
43: 66 0f a4 ce 10 shld si,cx,0x10 # SI = CX. CX unchanged.
48: 66 0f ac ce 10 shrd si,cx,0x10
4d: 0f a4 ce 20 shld esi,ecx,0x20 # nope, equivalent to a shift by 0
或者如果您想考虑多条指令,您可以像 Yuri 建议的那样将目标和 add
、or
或 xor
异或归零。零是 +
、|
和 ^
.
将 AND 转换为全一(&
的标识元素)效率更低。 (对 EAX 的旧值的错误依赖,并且它不是异或归零,所以它不能像 Sandybridge-family CPU 那样被消除(没有执行单元)。代码大小也更大)
or eax, -1
and eax, ecx
(Godbolt compiler explorer NASM 来源和反汇编,用于本答案中的反汇编)