Les autres réponses ne considèrent qu'un NOP qui s'exécute à un moment donné - qui est utilisé assez couramment, mais ce n'est pas la seule utilisation de NOP.
La non-exécution NOP est également très utile pour écrire du code qui peut être patché - en gros, vous aurez pad la fonction avec quelques NOP après la RET
(ou une instruction similaire). Lorsque vous devez patcher l'exécutable, vous pouvez facilement ajouter plus de code à la fonction en commençant par l'original RET
et en utilisant autant de ces NOP que vous en avez besoin (par exemple pour les sauts longs ou même le code en ligne) et en terminant par un autre RET
.
Dans ce cas d'utilisation, noöne s'attend à ce que le soit NOP
exécuté. Le seul point est de permettre de patcher l'exécutable - dans un exécutable théorique non rembourré, vous devrez en fait changer le code de la fonction elle-même (parfois cela pourrait correspondre aux limites d'origine, mais assez souvent vous aurez quand même besoin d'un saut ) - c'est beaucoup plus compliqué, surtout si l'on considère un assemblage écrit manuellement ou un compilateur d'optimisation; vous devez respecter les sauts et les constructions similaires qui auraient pu pointer sur un morceau de code important. Dans l'ensemble, assez délicat.
Bien sûr, c'était beaucoup plus utilisé dans les temps anciens, quand il était utile de faire des patchs comme ces petits et ligne . Aujourd'hui, vous allez simplement distribuer un binaire recompilé et en finir avec lui. Il y en a encore qui utilisent les correctifs NOP (exécutés ou non, et pas toujours littéraux NOP
- par exemple, Windows utilise MOV EDI, EDI
pour les correctifs en ligne - c'est le genre où vous pouvez mettre à jour une bibliothèque système pendant que le système est en cours d'exécution, sans avoir besoin de redémarrages).
Alors la dernière question est, pourquoi avoir une instruction dédiée pour quelque chose qui ne fait vraiment rien?
- Il s'agit d'une instruction réelle - importante lors du débogage ou de l'assemblage de codage manuel. Des instructions comme
MOV AX, AX
feront exactement la même chose, mais ne signalent pas l'intention aussi clairement.
- Remplissage - "code" qui est là juste pour améliorer les performances globales du code qui dépend de l'alignement. Il n'est jamais destiné à être exécuté. Certains débogueurs masquent simplement les NOP de remplissage dans leur démontage.
- Il donne plus d'espace pour optimiser les compilateurs - le modèle toujours utilisé est que vous avez deux étapes de compilation, la première étant plutôt simple et produisant beaucoup de code d'assemblage inutile, tandis que la seconde nettoie, recâble les références d'adresse et supprime instructions étrangères. Cela se voit souvent dans les langages compilés par JIT également - l'IL de .NET et le code d'octet de la JVM utilisent
NOP
beaucoup; le code d'assemblage compilé réel n'en a plus. Il convient de noter que ce ne sont pas des x86 NOP
, cependant.
- Cela facilite le débogage en ligne à la fois pour la lecture (la mémoire pré-mise à zéro sera tout
NOP
, ce qui rend le démontage beaucoup plus facile à lire) et pour le patch à chaud (bien que je préfère de loin Modifier et Continuer dans Visual Studio: P).
Pour exécuter les NOP, il y a bien sûr quelques points supplémentaires:
- Performances, bien sûr - ce n'est pas pourquoi c'était en 8085, mais même le 80486 avait déjà une exécution d'instructions en pipeline, ce qui rend "ne rien faire" un peu plus délicat.
- Comme vu avec
MOV EDI, EDI
, il y a d'autres NOP efficaces que le littéral NOP
. MOV EDI, EDI
a les meilleures performances en tant que NOP à 2 octets sur x86. Si vous avez utilisé deux NOP
s, ce serait deux instructions à exécuter.
ÉDITER:
En fait, la discussion avec @DmitryGrigoryev m'a obligé à y réfléchir un peu plus, et je pense que c'est un ajout précieux à cette question / réponse, alors laissez-moi ajouter quelques bits supplémentaires:
Premièrement, je veux dire, évidemment - pourquoi y aurait-il une instruction qui ferait quelque chose comme ça mov ax, ax
? Par exemple, regardons le cas du code machine 8086 (plus ancien que même le code machine 386):
- Il y a une instruction NOP dédiée avec opcode
0x90
. C'est encore le moment où beaucoup de gens ont écrit en assemblée, faites attention. Donc, même s'il n'y avait pas d' NOP
instruction dédiée , le NOP
mot - clé (alias / mnémonique) serait toujours utile et correspondrait à cela.
- Des instructions telles
MOV
que mapper réellement vers de nombreux opcodes différents, car cela économise du temps et de l'espace - par exemple, mov al, 42
"déplacer l'octet immédiat dans le al
registre", ce qui se traduit par 0xB02A
( 0xB0
étant l'opcode,0x2A
étant l'argument "immédiat"). Cela prend donc deux octets.
- Il n'y a pas d'opcode de raccourci pour
mov al, al
(puisque c'est une chose stupide à faire, fondamentalement), vous devrez donc utiliser la mov al, rmb
surcharge (rmb étant "registre ou mémoire"). Cela prend en fait trois octets. (bien qu'il utiliserait probablement le moins spécifique à la mov rb, rmb
place, qui ne devrait prendre que deux octets mov al, al
- l'argument d'argument est utilisé pour spécifier à la fois le registre source et le registre cible; maintenant vous savez pourquoi 8086 n'avait que 8 registres: D). Comparez à NOP
, qui est une instruction à un octet! Cela économise de la mémoire et du temps, car la lecture de la mémoire dans le 8086 était encore assez coûteuse - sans parler du chargement de ce programme à partir d'une bande ou d'une disquette ou quelque chose, bien sûr.
Alors d'où xchg ax, ax
vient-il? Il suffit de regarder les opcodes des autres xhcg
instructions. Vous verrez 0x86
, 0x87
et enfin, 0x91
- 0x97
. Donc, nop
avec, cela 0x90
semble être un assez bon ajustement xchg ax, ax
(qui, encore une fois, n'est pas une xchg
"surcharge" - vous devrez utiliser xchg rb, rmb
, à deux octets). Et en fait, je suis presque sûr que c'était un bel effet secondaire de la micro-architecture de l'époque - si je me souviens bien, il était facile de mapper toute la gamme de 0x90-0x97
"xchg, agissant sur des registres ax
et ax
- di
" ( l'opérande étant symétrique, cela vous a donné la gamme complète, y compris le nop xchg ax, ax
; notez que l'ordre est ax, cx, dx, bx, sp, bp, si, di
- bx
est après dx
,ax
; rappelez-vous, les noms de registre sont des mnémoniques, pas des noms ordonnés - accumulateur, compteur, données, base, pointeur de pile, pointeur de base, index source, index destination). La même approche a également été utilisée pour d'autres opérandes, par exemple l' mov someRegister, immediate
ensemble. D'une certaine manière, vous pourriez penser à cela comme si l'opcode n'était pas un octet entier - les derniers bits sont "un argument" pour l'opérande "réel".
Tout cela dit, sur x86, nop
peut être considéré comme une vraie instruction ou non. La micro-architecture originale le traitait comme une variante de xchg
si je me souviens bien, mais elle était en fait nommée nop
dans la spécification. Et comme cela xchg ax, ax
n'a pas vraiment de sens en tant qu'instruction, vous pouvez voir comment les concepteurs du 8086 ont économisé sur les transistors et les voies dans le décodage des instructions en exploitant le fait que cela 0x90
correspond naturellement à quelque chose qui est entièrement "nul".
D'un autre côté, le i8051 possède un opcode entièrement conçu pour nop
- 0x00
. Un peu pratique. La conception d'instruction utilise essentiellement le demi - octet pour le fonctionnement et le bit de poids faible pour la sélection des opérandes - par exemple, add a
est 0x2Y
, et 0xX8
signifie « registre 0 direct », 0x28
c'est add a, r0
. Économise beaucoup sur le silicium :)
Je pourrais continuer, car la conception du processeur (sans parler de la conception du compilateur et de la conception du langage) est un sujet assez large, mais je pense que j'ai montré de nombreux points de vue différents qui sont entrés assez bien dans la conception.