我很好奇在x86汇编中有多少种方法可以使用一个指令将寄存器设置为零。有人告诉我他至少找到了10种方法。
我能想到的方法有:
xor ax,ax
mov ax, 0
and ax, 0
有很多种可能的方法可以在IA32下将0移动到ax中...
lea eax, [0]
mov eax, 0FFFF0000h //All constants form 0..0FFFFh << 16
shr ax, 16 //All constants form 16..31
shl eax, 16 //All constants form 16..31
也许这是最奇怪的... :)
@movzx:
movzx eax, byte ptr[@movzx + 6] //Because the last byte of this instruction is 0
同时在32位模式下(指令更长,最终(最高有效)地址字节后移)也是如此...
@movzx:
movzx ax, byte ptr[@movzx + 7]
编辑:
对于16位x86 CPU模式,未经测试...:
lea ax, [0]
并且...
@movzx:
movzx ax, byte ptr cs:[@movzx + 7] //Check if 7 is right offset
cs:前缀在ds段寄存器不等于cs段寄存器的情况下是可选的。
shr eax, 16
指令不能保证清除AX
寄存器!使用shr ax, 16
即可。您的movzx ax, byte ptr cs:[@movzx + 7]
指令用于16位实地址模式,使用了正确的偏移量7,但我认为您应该明确说明需要一个地址大小前缀(67h)才能使32位位移的高字为零。 - Sep Rolandeax >>= 16
会使低16位(AX)非零。它会使EAX的高16位为0,但那不是AX。 - Peter Cordesmovzx eax,byte ptr[@movzx + 7]
导致它出现错误(除非你指的是在16位模式下使用67h前缀,但这并没有暗示,并且你在注释中也没有提到)。该指令只有7个字节长,这就是为什么前一个代码块中相同的指令使用$ + 6
而不是$ + 7
来访问disp32的最后一个字节。 - Peter Cordesmovzx
是386+指令,因此在所有x86-16机器上均无法使用。 - ecm请参考这个回答了解寄存器清零的最佳方法:xor eax,eax
(具有性能优势和更小的编码)。
# Works on any reg unless noted, usually of any size. eax/ax/al as placeholders
and eax, 0 ; three encodings: imm8, imm32, and eax-only imm32
andn eax, eax,eax ; BMI1 instruction set: dest = ~s1 & s2
imul eax, any,0 ; eax = something * 0. two encodings: imm8, imm32
lea eax, [0] ; absolute encoding (disp32 with no base or index). Use [abs 0] in NASM if you used DEFAULT REL
lea eax, [rel 0] ; YASM supports this, but NASM doesn't: use a RIP-relative encoding to address a specific absolute address, making position-dependent code
mov eax, 0 ; 5 bytes to encode (B8 imm32)
mov rax, strict dword 0 ; 7 bytes: REX mov r/m64, sign-extended-imm32. NASM optimizes mov rax,0 to the 5B version, but dword or strict dword stops it for some reason
mov rax, strict qword 0 ; 10 bytes to encode (REX B8 imm64). movabs mnemonic for AT&T. normally assemblers choose smaller encodings if the operand fits, but strict qword forces the imm64.
sub eax, eax ; recognized as a zeroing idiom on some but maybe not all CPUs
xor eax, eax ; Preferred idiom: recognized on all CPUs
; 2 same-size encodings each: r/m, r vs. r, r/m
@movzx:
movzx eax, byte ptr[@movzx + 6] //Assuming the high byte of the absolute address is 0. Not position-independent, and x86-64 RIP+rel32 would load 0xFF
.l: loop .l ; clears e/rcx... eventually. from I. J. Kennedy's answer. To operate on only ECX, use an address-size prefix.
; rep lodsb ; not counted because it's not safe (potential segfaults), but also zeros ecx
指令xor reg,reg
可以有两种不同的编码方式(可参考此链接)。在GAS AT&T语法中,我们可以请求汇编器选择哪个操作码。这仅适用于允许两种形式的reg,reg整数指令,即追溯到8086年的指令。因此不适用于SSE / AVX。
{load} xor %eax, %eax # 31 c0
{store} xor %eax, %eax # 33 c0
shl
和 shr
的移位计数会被掩码 (详见链接): count & 31;
即取模 32。ax
是一个“全”寄存器。还有一些 CPU 可以通过移位将整个整数寄存器清零。)# Zeroing methods that only work on 16bit or 8bit regs:
shl ax, 16 ; shift count is still masked to 0x1F for any operand size less than 64b. i.e. count %= 32
shr al, 16 ; so 8b and 16b shifts can zero registers.
# zeroing ah/bh/ch/dh: Low byte of the reg = whatever garbage was in the high16 reg
movxz eax, ah ; From Jerry Coffin's answer
根据其他现有条件(除了在另一个寄存器中为零):
bextr eax, any, eax ; if al >= 32, or ah = 0. BMI1
BLSR eax, src ; if src only has one set bit
CDQ ; edx = sign-extend(eax)
sbb eax, eax ; if CF=0. (Only recognized on AMD CPUs as dependent only on flags (not eax))
setcc al ; with a condition that will produce a zero based on known state of flags
PSHUFB xmm0, all-ones ; xmm0 bytes are cleared when the mask bytes have their high bit set
其中一些SSE2整数指令也可以用于MMX寄存器(mm0
- mm7
)。我不会单独展示它们。
同样,最好的选择是某种形式的异或运算。可以使用PXOR
/ VPXOR
或XORPS
/ VXORPS
。有关详细信息,请参见在x86汇编中将寄存器设置为零的最佳方法:xor、mov还是and?。
AVX vxorps xmm0,xmm0,xmm0
可以将整个ymm0 / zmm0清零,并且在AMD CPU上比vxorps ymm0,ymm0,ymm0
更好。
vzeroall
之外,AVX512没有改变其他指令(也不会清零zmm16-31)。PXOR xmm0, xmm0 ;; recommended
XORPS xmm0, xmm0 ;; or this
XORPD xmm0, xmm0 ;; longer encoding for zero benefit
PXOR mm0, mm0 ;; MMX, not show for the rest of the integer insns
ANDNPD xmm0, xmm0
ANDNPS xmm0, xmm0
PANDN xmm0, xmm0 ; dest = ~dest & src
PCMPGTB xmm0, xmm0 ; n > n is always false.
PCMPGTW xmm0, xmm0 ; similarly, pcmpeqd is a good way to do _mm_set1_epi32(-1)
PCMPGTD xmm0, xmm0
PCMPGTQ xmm0, xmm0 ; SSE4.2, and slower than byte/word/dword
PSADBW xmm0, xmm0 ; sum of absolute differences
MPSADBW xmm0, xmm0, 0 ; SSE4.1. sum of absolute differences, register against itself with no offset. (imm8=0: same as PSADBW)
; shift-counts saturate and zero the reg, unlike for GP-register shifts
PSLLDQ xmm0, 16 ; left-shift the bytes in xmm0
PSRLDQ xmm0, 16 ; right-shift the bytes in xmm0
PSLLW xmm0, 16 ; left-shift the bits in each word
PSLLD xmm0, 32 ; double-word
PSLLQ xmm0, 64 ; quad-word
PSRLW/PSRLD/PSRLQ ; same but right shift
PSUBB/W/D/Q xmm0, xmm0 ; subtract packed elements, byte/word/dword/qword
PSUBSB/W xmm0, xmm0 ; sub with signed saturation
PSUBUSB/W xmm0, xmm0 ; sub with unsigned saturation
;; SSE4.1
INSERTPS xmm0, xmm1, 0x0F ; imm[3:0] = zmask = all elements zeroed.
DPPS xmm0, xmm1, 0x00 ; imm[7:4] => inputs = treat as zero -> no FP exceptions. imm[3:0] => outputs = 0 as well, for good measure
DPPD xmm0, xmm1, 0x00 ; inputs = all zeroed -> no FP exceptions. outputs = 0
VZEROALL ; AVX1 x/y/zmm0..15 not zmm16..31
VPERM2I/F128 ymm0, ymm1, ymm2, 0x88 ; imm[3] and [7] zero that output lane
# Can raise an exception on SNaN, so only usable if you know exceptions are masked
CMPLTPD xmm0, xmm0 # exception on QNaN or SNaN, or denormal
VCMPLT_OQPD xmm0, xmm0,xmm0 # exception only on SNaN or denormal
CMPLT_OQPS ditto
VCMPFALSE_OQPD xmm0, xmm0, xmm0 # This is really just another imm8 predicate value for the same VCMPPD xmm,xmm,xmm, imm8 instruction. Same exception behaviour as LT_OQ.
SUBPS xmm0, xmm0
等类似指令无法正常工作,因为NaN-NaN = NaN,而不是零。
此外,FP指令可能会在NaN参数上引发异常,因此即使是CMPPS/PD,只有当您知道异常被屏蔽,并且您不关心在MXCSR中可能设置的异常位时,才是安全的。即使是AVX版本,由于其扩展的谓词选择,也会在SNaN上引发#IA
。 “静默”谓词仅对QNaN抑制#IA
。 CMPPS/PD还可能引发Denormal异常。(AVX512 EVEX编码可以抑制FP异常,对于512位向量,同时覆盖舍入模式)
(请参见CMPPD指令集参考条目中的表格,或更好的是在英特尔原始PDF中查看,因为HTML提取会破坏该表格。)
AVX1/2 和 AVX512 EVEX 版本中的 PXOR 操作:这些操作都会将 ZMM 目标寄存器的所有位清零。PXOR 有两个 EVEX 版本:VPXORD 或 VPXORQ,允许使用 dword 或 qword 元素进行掩码操作。(XORPS/PD 已经在助记符中区分了元素大小,所以 AVX512 没有改变这一点。在传统的 SSE 编码中,对于所有 CPU,XORPD 都是一个毫无意义的代码浪费(较大的操作码),与 XORPS 相比。)VPXOR xmm15, xmm0, xmm0 ; AVX1 VEX
VPXOR ymm15, ymm0, ymm0 ; AVX2 VEX, less efficient on some CPUs
VPXORD xmm31, xmm0, xmm0 ; AVX512VL EVEX
VPXORD ymm31, ymm0, ymm0 ; AVX512VL EVEX 256-bit
VPXORD zmm31, zmm0, zmm0 ; AVX512F EVEX 512-bit
VPXORQ xmm31, xmm0, xmm0 ; AVX512VL EVEX
VPXORQ ymm31, ymm0, ymm0 ; AVX512VL EVEX 256-bit
VPXORQ zmm31, zmm0, zmm0 ; AVX512F EVEX 512-bit
VPXORD xmm16{k1}{z}, xmm0, xmm0
AVX512:有一个有趣的值得一提,即:VPTERNLOGD/Q可以使用imm8 = 0xFF将寄存器设置为全1, 但是在当前实现中对旧值存在虚假依赖。由于比较指令都会比较成掩码,根据我的测试,在Skylake-AVX512上,VPTERNLOGD似乎是将向量设置为全1的最佳方法,尽管它不会特殊处理imm8 = 0xFF的情况以避免虚假依赖。
VPTERNLOGD zmm0, zmm0,zmm0, 0 ; inputs can be any registers you like.
kxorB/W/D/Q k0, k0, k0 ; narrow versions zero extend to max_kl
kshiftlB/W/D/Q k0, k0, 100 ; kshifts don't mask/wrap the 8-bit count
kshiftrB/W/D/Q k0, k0, 100
kandnB/W/D/Q k0, k0, k0 ; x & ~x
; compare into mask
vpcmpB/W/D/Q k0, x/y/zmm0, x/y/zmm0, 3 ; predicate #3 = always false; other predicates are false on equal as well
vpcmpuB/W/D/Q k0, x/y/zmm0, x/y/zmm0, 3 ; unsigned version
vptestnmB/W/D/Q k0, x/y/zmm0, x/y/zmm0 ; x & ~x test into mask
只有一种选择(因为如果旧值为无穷大或NaN,则sub函数无法工作)。
FLDZ ; push +0.0
mov al, 0
或xor ax,ax
。 - Peter Cordessub ax, ax
movxz, eax, ah
编辑:我应该指出,movzx
并没有将eax
的所有位都清零,它只是清零了ah
(以及作为寄存器本身不可访问的前16位)。
至于速度最快的问题,如果我没记错,sub
和xor
是等效的。它们比(大多数)其他指令更快,因为它们很常见,CPU设计者为它们添加了特殊优化。具体来说,对于普通的sub
或xor
,结果取决于寄存器中的先前值。CPU会特别识别与自身异或和自身相减,以便知道依赖关系链在那里被打破。之后的任何指令都不会依赖于任何先前的值,因此它可以使用重命名寄存器并行执行先前和后续指令。
特别是在旧处理器上,我们预计'mov reg, 0'会更慢,因为它有额外的16位数据,而早期处理器(尤其是8088)主要受限于从内存加载流的能力。实际上,在8088上,您可以使用任何参考表格相当准确地估计运行时间,只需注意所涉及的字节数即可。这对于div
和idiv
指令确实会失效,但仅限于此。另一方面,我应该保持沉默,因为8088对大多数人来说真的没有什么兴趣(至少已经有十年了)。
这个帖子很老了,但还有一些其他的例子。其中简单的有:
xor eax,eax
sub eax,eax
and eax,0
lea eax,[0] ; it doesn't look "natural" in the binary
更复杂的组合:
; flip all those 1111... bits to 0000
or eax,-1 ; eax = 0FFFFFFFFh
not eax ; ~eax = 0
; XOR EAX,-1 works the same as NOT EAX instruction in this case, flipping 1 bits to 0
or eax,-1 ; eax = 0FFFFFFFFh
xor eax,-1 ; ~eax = 0
; -1 + 1 = 0
or eax,-1 ; eax = 0FFFFFFFFh or signed int = -1
inc eax ;++eax = 0
使用LOOP $
可以将寄存器CX设置为0。
repe cmpsb
和repne cmpsb
都可能导致rcx/ecx/cx非零;同样的,scas
也是如此。前缀rep
可以与lods
、movs
、stos
、ins
或outs
一起使用,以将计数器寄存器设置为零。正如您正确提到的那样,这些指令具有超出更改计数器寄存器的效果。正如Peter Cordes所提到的,由于访问内存,rep lods
指令可能会出现故障。 - ecm当然,特定情况下还有其他将寄存器设置为0的方法:例如,如果您将eax
设置为正整数,则可以使用cdq/cltd
将edx
设置为0(这个技巧在一种著名的24字节shellcode中使用,该shellcode出现在“通过示例学不安全编程”中)。
因此,在x86上清除寄存器的答案可以处理上述任何寄存器中的零,当然不能清除FLAGS寄存器,因为其结构定义在第二位始终保持为1。
接下来是单个指令列表,可以在8086上清除寄存器,并且不依赖于任何现有条件。列表按字母顺序排列:
encoding instruction register cleared displacement
-------------- --------------- ----------------------- ------------
25 00 00 and ax, 0 AX
83 E0 00 and ax, 0 AX BX CX DX SI DI BP SP
81 E0 00 00 and ax, 0 AX BX CX DX SI DI BP SP
E8 -- -- call 0000h IP -($+3)
9A 00 00 xx yy call yyxxh:0000h IP
9A xx yy 00 00 call 0000h:yyxxh CS
9A 00 00 00 00 call 0000h:0000h (*) IP and CS
E9 -- -- jmp 0000h IP -($+3)
EA 00 00 xx yy jmp yyxxh:0000h IP
EA xx yy 00 00 jmp 0000h:yyxxh CS
EA 00 00 00 00 jmp 0000h:0000h (*) IP and CS
8D 06 00 00 lea ax, [0000h] AX BX CX DX SI DI BP SP
F3 AC rep lodsb CX
F3 AD rep lodsw CX
E2 FE loop $ CX
B8 00 00 mov ax, 0 AX BX CX DX SI DI BP SP
C7 C0 00 00 mov ax, 0 AX BX CX DX SI DI BP SP
F3 A4 rep movsb (*) CX
F3 A5 rep movsw (*) CX
F3 AA rep stosb (*) CX
F3 AB rep stosw (*) CX
29 C0 sub ax, ax AX BX CX DX SI DI BP SP
2B C0 sub ax, ax AX BX CX DX SI DI BP SP
31 C0 xor ax, ax AX BX CX DX SI DI BP SP
33 C0 xor ax, ax AX BX CX DX SI DI BP SP
call
和jmp
正常工作,您需要在目标位置拥有可执行代码。xor reg, reg
,如果您不想更改任何标志,则使用mov reg, 0
。jmp/call 0000h:0000h
比靠近的 jmp/call 0
到段的开头略微不太可用。你需要在 IVT 通常开始的线性地址 0 处有机器码,但是在代码序列中间跳转到 IP=0 不是你可以做的事情。(除非有一个 ret
在那里从调用返回)。实际上,第一个 IVT 条目是 #DE 除法异常,所以只要你不触发它,你就可以在线性地址 0 处有一个 retf
。(如果你不跳回,你会想在用代码覆盖 IVT 的其余部分之前禁用中断。) - Peter Cordes根据DEF CON 25 - XlogicX - Assembly Language is Too High Level所述:
基于0的立即操作数执行AAD指令将始终将AH清零,并保留AL不变。根据Intel's pseudocode,其伪代码如下:
AL ← (oldAL + (oldAH ∗ imm8)) AND FFH;
在汇编源码中:
AAD 0 ; assemblers like NASM accept this
db 0xd5,0x00 ; others many need you to encode it manually
bswap eax
前面加上一个66操作数大小前缀(即尝试编码bswap ax
为66 0F C8
)会将AX清零。AAD
或 AAM
指令在某些机器上不被支持,例如 NEC V20/V30。 - ecmmov al, 0
只有2个字节的机器代码,但是https://www2.math.uni-wuppertal.de/~fpf/Uebungen/GdR-SS02/opcode_i.html将其列为8088上的4个周期(对于8位或16位的mov reg,imm),尽管该表列出的机器代码大小是错误的,应该是1 + i(1,2)
用于非ModRM短编码。无论如何,该表格没有考虑到code-fetch,这是8088的主要瓶颈,也是8086的一部分。 - Peter Cordesmov eax,0
shl eax,32
shr eax,32
imul eax,0
sub eax,eax
xor eax,eax
and eax,0
andn eax,eax,eax
loop $ ;ecx only
pause ;ecx only (pause="rep nop" or better="rep xchg eax,eax")
;twogether:
push dword 0
pop eax
or eax,0xFFFFFFFF
not eax
xor al,al ;("mov al,0","sub al,al",...)
movzx eax,al
...
rep
的工作方式;对于除“字符串”指令(它不适用于该指令)之外的指令,它将被忽略,或者在像 pause
或 tzcnt
这样的指令中,它实际上是指令码的一部分,而这些指令是针对了解它们的 CPU。此外,x86 使用 & 0x1f
(模32)屏蔽标量移位的移位计数,或使用模64屏蔽64位操作数大小。您只能移出16或8位寄存器的所有位。请参阅我在此问题上的答案,该答案发布于此之前2年。 - Peter Cordes
这个问题只与一个小地理区域、一个特定的时间点或一个非常狭窄的情况相关,这些情况并不适用于互联网的全球受众。
- paxdiablo