Je suis curieux de savoir combien de façons existe-t-il de définir un registre à zéro dans x86 Assembly. En utilisant une instruction. Quelqu'un m'a dit qu'il avait réussi à trouver au moins 10 façons de le faire.
Ceux que je peux penser sont:
xor ax,ax
mov ax, 0
and ax, 0
Il y a beaucoup de possibilité de faire bouger 0 dans hache sous IA32 ...
lea eax, [0]
mov eax, 0FFFF0000h //All constants form 0..0FFFFh << 16
shr eax, 16 //All constants form 16..31
shl eax, 16 //All constants form 16..31
Et peut-être le plus étrange ... :)
@movzx:
movzx eax, byte ptr[@movzx + 6] //Because the last byte of this instruction is 0
et...
@movzx:
movzx ax, byte ptr[@movzx + 7]
Modifier:
Et pour le mode processeur 16 bits x86, non testé ...:
lea ax, [0]
et...
@movzx:
movzx ax, byte ptr cs:[@movzx + 7] //Check if 7 is right offset
Le préfixe cs: est facultatif si le registre de segments ds n'est pas égal au registre de segments cs.
Voir cette réponse pour les best registres de cheminement à zéro: xor eax,eax
(avantages en termes de performances et codage réduit).
J'examinerai simplement les moyens par lesquels une seule instruction peut mettre à zéro un registre. Si vous autorisez le chargement d'un zéro dans la mémoire, les possibilités sont beaucoup trop nombreuses. Nous allons donc exclure les instructions qui se chargent de la mémoire.
J'ai trouvé 10 instructions simples différentes qui remettent à zéro un registre 32 bits (et donc le registre complet 64 bits en mode long), sans conditions préalables ni charges à partir d'une autre mémoire. Cela ne tient pas compte des différents encodages du même insn, ni des différentes formes de mov
. Si vous comptez le chargement de la mémoire connue comme contenant un zéro, ou des registres de segments ou autres, il existe une multitude de chemins. Il existe également une multitude de façons de mettre à zéro les registres de vecteurs.
Pour la plupart d'entre elles, les versions eax et rax sont des codages distincts pour la même fonctionnalité, remettant à zéro les registres 64 bits complets, soit remettant à zéro la moitié supérieure de manière implicite ou écrivant explicitement le registre complet avec un préfixe REX.W.
# 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
@movzx:
movzx eax, byte ptr[@movzx + 6] //Because the last byte of this instruction is 0. neat hack from GJ.'s answer
.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
"Décaler tous les bits d'un côté" n'est pas possible pour les registres GP de taille normale, mais uniquement pour les registres partiels. Les nombres de postes shl
et shr
sont masqués: count &= 31;
, ce qui équivaut à count %= 32;
. (Mais 286 et les versions antérieures ne sont que de 16 bits, donc ax
est un registre "complet". La forme shr r/m16, imm8
du compte de variables variable ayant été ajoutée à l'instruction 286, il existait donc des CPU dans lesquelles une équipe pouvait mettre à zéro un registre entier.)
Notez également que les décomptes pour les vecteurs saturent au lieu d'être enveloppés.
# 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
En fonction d'autres conditions existantes (autre qu'un zéro dans un autre registre):
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
Certaines de ces instructions entières SSE2 peuvent également être utilisées sur les registres MMX (mm0
- mm7
). Encore une fois, le meilleur choix est une forme de xor. PXOR
/VPXOR
, ou XORPS
/VXORPS
.
AVX vxorps xmm0,xmm0,xmm0
remet à zéro le ymm0/zmm0 complet et vaut mieux que vxorps ymm0,ymm0,ymm0
sur les processeurs AMD . Ces instructions de remise à zéro ont trois codages: SSE hérité, AVX (préfixe VEX) et AVX512 (préfixe EVEX), bien que la version SSE ne remette que le 128 en bas, qui n'est pas le registre complet sur les processeurs prenant en charge AVX ou AVX512. Quoi qu'il en soit, en fonction de la manière dont vous comptez, chaque entrée peut être composée de trois instructions différentes (le même opcode, toutefois, juste des préfixes différents). À l'exception de vzeroall
, qu'AVX512 n'a pas modifié (et ne réinitialise pas zmm16-31).
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
PXOR xmm0, xmm0
XORPD xmm0, xmm0
XORPS xmm0, xmm0
VZEROALL
# 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 fro the same VCMPPD xmm,xmm,xmm, imm8 instruction. Same exception behaviour as LT_OQ.
SUBPS xmm0, xmm0
et similaire ne fonctionneront pas car NaN-NaN = NaN, pas zéro.
De plus, les instructions FP peuvent déclencher des exceptions sur les arguments NaN. Par conséquent, même CMPPS/PD n'est sécurisé que si vous savez que les exceptions sont masquées et que vous ne vous inquiétez pas de la possibilité de définir les bits d'exception dans MXCSR. Même la version AVX, avec son choix étendu de prédicats, lèvera #IA
sur SNaN. Les prédicats "quiet" suppriment uniquement #IA
pour QNaN. CMPPS/PD peut également déclencher l'exception Denormal.
(Voir le tableau dans l'entrée de référence insn set pour CMPPD , ou de préférence dans le PDF d'origine d'Intel puisque l'extrait HTML modifie ce tableau.)
Il y a probablement plusieurs options ici, mais je ne suis pas assez curieux pour le moment de fouiller dans la liste d'instructions en les cherchant toutes.
Il y en a un intéressant à noter, cependant: VPTERNLOGD/Q peut définir un registre sur all-one à la place, avec imm8 = 0xFF. (Mais a une dépendance fausse sur l'ancienne valeur, sur les implémentations actuelles). Puisque les instructions de comparaison se comparent toutes dans un masque, VPTERNLOGD semble être le meilleur moyen de définir un vecteur à un entier sur Skylake-AVX512 dans mes tests, bien que il ne faut pas cas spécial imm8 = 0xFF à éviter une fausse dépendance .
VPTERNLOGD zmm0, zmm0,zmm0, 0 ; inputs can be any registers you like.
Un seul choix (car sub ne fonctionne pas si l'ancienne valeur était infini ou NaN).
FLDZ ; Push +0.0
Quelques autres possibilités:
sub ax, ax
movxz, eax, ah
Edit: Je devrais noter que le movzx
ne remet pas tous les eax
à zéro - il ne remet que le ah
(plus les 16 premiers bits qui ne sont pas accessibles en tant que registres).
Pour être le plus rapide, si les services de la mémoire sont remplacés, sub
et xor
sont équivalents. Ils sont plus rapides que les autres, car ils sont suffisamment répandus pour que les concepteurs de CPU leur apportent une optimisation particulière. Spécifiquement, avec un sub
ou xor
normal, le résultat dépend de la valeur précédente dans le registre. La CPU reconnaît le xor-with-self et soustrait-de-soi spécialement pour savoir que la chaîne de dépendance y est rompue. Toute instruction ultérieure ne dépendra d'aucune valeur précédente, il pourra donc exécuter les instructions précédentes et suivantes en parallèle à l'aide de registres de renommage.
Sur les anciens processeurs, nous nous attendons à ce que «mov reg, 0» soit plus lent simplement parce qu'il contient 16 bits de données supplémentaires, et la plupart des premiers processeurs (en particulier le 8088) étaient principalement limités par leur capacité à charger le flux à partir de la mémoire - - en fait, sur un 8088, vous pouvez estimer le temps d’exécution avec assez de précision avec toutes les feuilles de référence, et faites attention au nombre d’octets impliqués. Cela ne fonctionne pas pour les instructions div
et idiv
name__, mais c'est à peu près tout. OTOH, je devrais probablement me taire, car le 8088 n’a vraiment que peu d’intérêt pour quiconque (depuis au moins une décennie maintenant).
Vous pouvez définir le registre CX sur 0 avec LOOP $
.
Bien entendu, des cas spécifiques disposent de moyens supplémentaires pour définir un registre sur 0: par exemple. Si vous avez défini eax
sur un entier positif, vous pouvez définir edx
sur 0 avec un cdq/cltd
(cette astuce est utilisée sur un célèbre shellcode de 24 octets, qui apparaît dans "Programmation non sécurisée par exemple").
Ce fil est vieux mais quelques autres exemples. Les plus simples:
xor eax,eax
sub eax,eax
and eax,0
lea eax,[0] ; it doesn't look "natural" in the binary
combinaisons plus complexes:
; 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
not eax ;++eax = 0
mov 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
...