Die anderen Antworten beziehen sich nur auf eine NOP, die tatsächlich zu einem bestimmten Zeitpunkt ausgeführt wird. Dies wird häufig verwendet, ist jedoch nicht die einzige Verwendung von NOP.
Das nicht ausgeführte NOP ist auch sehr nützlich, wenn Sie Code schreiben, der gepatcht werden kann. Im Grunde genommen füllen Sie die Funktion nach dem RET
(oder einem ähnlichen Befehl) mit ein paar NOPs auf . Wenn Sie die ausführbare Datei patchen müssen, können Sie der Funktion auf einfache Weise mehr Code hinzufügen, indem Sie vom Original aus RET
so viele dieser NOPs verwenden, wie Sie benötigen (z. B. für lange Sprünge oder sogar Inline-Code) und mit einem anderen abschließen RET
.
In diesem Anwendungsfall erwartet noöne jemals die NOP
Ausführung. Der einzige Punkt ist, das Patchen der ausführbaren Datei zuzulassen - in einer theoretischen nicht aufgefüllten ausführbaren Datei müssten Sie den Code der Funktion selbst tatsächlich ändern (manchmal passt er möglicherweise an die ursprünglichen Grenzen, aber häufig benötigen Sie trotzdem einen Sprung ) - das ist viel komplizierter, besonders wenn man die manuell geschriebene Assembly oder einen optimierenden Compiler in Betracht zieht; Sie müssen Sprünge und ähnliche Konstrukte respektieren, die auf ein wichtiges Stück Code hingewiesen haben könnten. Alles in allem ziemlich knifflig.
Natürlich wurde dies in den alten Tagen viel häufiger verwendet, als es nützlich war, solche kleinen und Online- Patches zu erstellen . Heute verteilen Sie einfach eine neu kompilierte Binärdatei und sind fertig damit. Es gibt immer noch einige, die Patch-NOPs verwenden (die ausgeführt werden oder nicht, und nicht immer wörtliche NOP
s - zum Beispiel Windows MOV EDI, EDI
für Online-Patches - auf diese Weise können Sie eine Systembibliothek aktualisieren, während das System tatsächlich ausgeführt wird, ohne dass ein Neustart erforderlich ist).
Die letzte Frage ist also, warum Sie eine spezielle Anweisung für etwas haben, das nicht wirklich etwas bewirkt.
- Dies ist eine tatsächliche Anweisung - wichtig beim Debuggen oder Handcodieren von Assemblys. Anweisungen wie
MOV AX, AX
tun genau das Gleiche, signalisieren die Absicht jedoch nicht ganz so deutlich.
- Auffüllen - "Code", der nur dazu dient, die Gesamtleistung von Code zu verbessern, der von der Ausrichtung abhängt. Es ist nie zur Ausführung gedacht. Einige Debugger verstecken bei der Demontage einfach die Padding-NOPs.
- Es gibt mehr Platz für die Optimierung von Compilern - das immer noch verwendete Muster besteht darin, dass Sie zwei Kompilierungsschritte haben, wobei der erste recht einfach ist und viele unnötige Assembler-Codes erzeugt, während der zweite die Adressverweise aufräumt, neu verkabelt und entfernt fremde Anweisungen. Dies wird häufig auch in JIT-kompilierten Sprachen beobachtet - sowohl der IL-Code von .NET als auch der Byte-Code von JVM verwenden
NOP
ziemlich viel. Der tatsächlich kompilierte Assembler-Code enthält diese nicht mehr. Es ist jedoch zu beachten, dass es sich nicht um x86 NOP
handelt.
NOP
Dies erleichtert das Online-Debuggen sowohl beim Lesen (Speicher vor dem Nullpunkt ist alles , was das Zerlegen erleichtert) als auch beim Hot-Patching (obwohl ich das Bearbeiten und Fortfahren in Visual Studio: P bei weitem bevorzuge).
Für die Ausführung von NOPs gibt es natürlich noch ein paar Punkte:
- Leistung natürlich - das war nicht der Grund, warum es 8085 war, aber selbst der 80486 verfügte bereits über eine Pipeline-Befehlsausführung, was das "Nichtstun" etwas schwieriger macht.
- Wie man sieht
MOV EDI, EDI
, gibt es andere effektive NOPs als das Literal NOP
. MOV EDI, EDI
hat die beste Leistung als 2-Byte-NOP auf x86. Wenn Sie zwei NOP
s verwenden, sind dies zwei auszuführende Anweisungen.
BEARBEITEN:
Eigentlich hat mich die Diskussion mit @DmitryGrigoryev dazu gezwungen, ein bisschen mehr darüber nachzudenken, und ich denke, es ist eine wertvolle Ergänzung zu dieser Frage / Antwort, also lassen Sie mich ein paar zusätzliche Punkte hinzufügen:
Erstens, klar, warum sollte es eine Anweisung geben, die so etwas tut mov ax, ax
? Betrachten wir zum Beispiel den Fall von 8086-Maschinencode (älter als 386-Maschinencode):
- Es gibt einen speziellen NOP-Befehl mit Opcode
0x90
. Dies ist immer noch die Zeit, in der viele Leute in der Versammlung geschrieben haben, wohlgemerkt. Selbst wenn es keine spezielle NOP
Anweisung NOP
gäbe , wäre das Schlüsselwort (Alias / Mnemonik) dennoch nützlich und würde dem entsprechen.
- Anweisungen wie
MOV
Karte tatsächlich auf viele verschiedenen Opcodes, denn das auf Raum und Zeit spart - zum Beispiel, mov al, 42
ist „immediate Byte zum bewegen al
Register“, was übersetzt 0xB02A
( 0xB0
wobei der Opcode,0x2A
als "Sofortargument"). Das dauert also zwei Bytes.
- Es gibt keinen Verknüpfungs-Opcode für
mov al, al
(da dies im Grunde genommen eine dumme Sache ist), daher müssen Sie die mov al, rmb
Überladung (rmb ist "register or memory") verwenden. Das dauert eigentlich drei Bytes. (obwohl es wahrscheinlich das weniger spezifische mov rb, rmb
verwenden würde, das nur zwei Bytes aufnehmen sollte mov al, al
- das Argument Byte wird verwendet, um sowohl das Quell- als auch das Zielregister anzugeben; jetzt wissen Sie, warum 8086 nur 8 Register hatte: D). Vergleichen Sie mit NOP
, was ein Einzelbyte-Befehl ist! Dies spart Speicher und Zeit, da das Lesen des Speichers im 8086 noch recht teuer war - ganz zu schweigen vom Laden des Programms von einer Kassette oder Diskette oder so.
Wo kommt der denn her xchg ax, ax
? Sie müssen sich nur die Opcodes der anderen xhcg
Anweisungen ansehen . Sie werden sehen 0x86
, 0x87
und schließlich 0x91
- 0x97
. So nop
mit ist es 0x90
scheint wie eine ziemlich gute Passform für xchg ax, ax
(die wiederum ist keine xchg
„Überlast“ - Sie verwenden würden xchg rb, rmb
, um zwei Bytes). Und in der Tat bin ich mir ziemlich sicher, dass dies ein netter Nebeneffekt der 0x90-0x97
Mikroarchitektur der Zeit war - wenn ich mich recht entsinne, war es einfach, die gesamte Bandbreite auf "xchg" abzubilden, über Register zu agieren ax
und ax
- di
( Da der Operand symmetrisch ist, haben Sie den gesamten Bereich einschließlich des NOP erhalten xchg ax, ax
. Beachten Sie, dass die Reihenfolge ax, cx, dx, bx, sp, bp, si, di
- bx
danach dx
ist.ax
; Denken Sie daran, dass die Registernamen mnemonische und nicht geordnete Namen sind (Akkumulator, Zähler, Daten, Basis, Stapelzeiger, Basiszeiger, Quellindex, Zielindex). Der gleiche Ansatz wurde auch für andere Operanden verwendet, zum Beispiel die mov someRegister, immediate
Menge. In gewisser Weise könnte man sich das so vorstellen, als ob der Opcode tatsächlich kein vollständiges Byte wäre - die letzten paar Bits sind "ein Argument" für den "echten" Operanden.
All dies kann auf x86 nop
als echte Anweisung angesehen werden oder nicht. Die ursprüngliche Mikroarchitektur behandelte es als eine Variante, xchg
wenn ich mich richtig erinnere, aber es wurde tatsächlich nop
in der Spezifikation benannt. Und da xchg ax, ax
dies als Anweisung nicht wirklich sinnvoll ist, können Sie sehen, wie die Designer des 8086 bei der Anweisungsdecodierung Transistoren und Pfade eingespart haben, indem sie die Tatsache ausnutzen, dass 0x90
die Zuordnung auf natürliche Weise zu etwas erfolgt, das vollständig "Noppy" ist.
Auf der anderen Seite verfügt der i8051 über einen vollständig integrierten Opcode für nop
- 0x00
. Ein bisschen praktisch. Der Befehl Design ist im Grunde das hohe Halbbyte für den Betrieb mit und den niedrigen Nibble für die Operanden der Auswahl - zum Beispiel, add a
ist 0x2Y
, und 0xX8
bedeutet „Register 0 direkte“, so 0x28
ist add a, r0
. Spart viel Silizium :)
Ich könnte noch weitermachen, da CPU-Design (ganz zu schweigen von Compiler-Design und Sprachdesign) ein ziemlich breites Thema ist, aber ich denke, ich habe viele verschiedene Standpunkte gezeigt, die in das Design ganz gut eingegangen sind, wie es ist.