Warum brauchen wir die Anweisung "nop", dh no operation im Mikroprozessor 8085?


19

In der Anweisung des Mikroprozessors 8085 gibt es eine Maschinensteueroperation "nop" (keine Operation). Meine Frage ist, warum wir keine Operation brauchen? Ich meine, wenn wir das Programm beenden müssen, werden wir HLT oder RST 3 verwenden. Oder wenn wir zur nächsten Anweisung übergehen wollen, werden wir die nächste Anweisung geben. Aber warum keine Operation? Was ist der Bedarf?


5
NOP wird häufig beim Debuggen und Aktualisieren Ihres Programms verwendet. Wenn Sie zu einem späteren Zeitpunkt einige Zeilen zu Ihrem Programm hinzufügen möchten, können Sie die NOP einfach überschreiben. Andernfalls müssen Sie Zeilen einfügen, und beim Einfügen wird das gesamte Programm verschoben. Auch fehlerhafte Anweisungen (falsch) können durch NOP ersetzt (einfach überschrieben) werden, wobei dieselben Argumente verwendet werden.
Plutoniumschmuggler

Ohkay. Die Verwendung von nop vergrößert jedoch auch den Platz. Während unser primäres Ziel ist, dass es wenig Platz einnimmt.
Demietra95

* Ich meine, unser Ziel ist es, ein Problem kleiner zu machen. Wird es nicht auch ein Problem?
Demietra95

2
Deshalb sollte es mit Bedacht eingesetzt werden. Andernfalls besteht Ihr gesamtes Programm nur aus ein paar NOPs.
Plutoniumschmuggler

Antworten:


34

Eine Verwendung des Befehls NOP (oder NOOP, no-operation) in CPUs und MCUs besteht darin, eine kleine, vorhersehbare Verzögerung in Ihren Code einzufügen. Obwohl NOPs keine Operation ausführen, dauert es einige Zeit, sie zu verarbeiten (die CPU muss den Opcode abrufen und dekodieren, so dass dies einige Zeit in Anspruch nimmt). Bereits 1 CPU-Zyklus wird für die Ausführung eines NOP-Befehls "verschwendet" (die genaue Anzahl kann normalerweise dem CPU / MCU-Datenblatt entnommen werden). Daher ist das Aneinanderreihen von NOPs eine einfache Möglichkeit, eine vorhersagbare Verzögerung einzufügen:

tdelay=NTclockK

Dabei ist K die Anzahl der Zyklen (am häufigsten 1), die für die Verarbeitung eines NOP-Befehls benötigt werden, und ist die Taktperiode.Tclock

Warum würdest du das tun? Es kann nützlich sein, die CPU zu zwingen, ein wenig zu warten, bis externe (möglicherweise langsamere) Geräte ihre Arbeit abgeschlossen haben und Daten an die CPU melden, dh NOP ist nützlich für Synchronisationszwecke.

Siehe auch die zugehörige Wikipedia-Seite zu NOP .

Eine andere Verwendung ist das Ausrichten von Code an bestimmten Adressen im Speicher und andere "Assembly-Tricks", wie auch in diesem Thread zu Programmers.SE und in diesem anderen Thread zu StackOverflow erläutert .

Ein weiterer interessanter Artikel zum Thema .

Dieser Link zu einer Google-Buchseite bezieht sich insbesondere auf die 8085-CPU. Auszug:

Jeder NOP-Befehl verwendet vier Takte zum Abrufen, Dekodieren und Ausführen.

EDIT (um ein in einem Kommentar geäußertes Anliegen anzusprechen)

Wenn Sie sich Gedanken über die Geschwindigkeit machen, denken Sie daran, dass (Zeit-) Effizienz nur ein Parameter ist, den Sie berücksichtigen müssen. Es hängt alles von der Anwendung ab: Wenn Sie die 10-milliardste Zahl von berechnen möchten, besteht Ihre einzige Sorge möglicherweise in der Geschwindigkeit. Auf der anderen Seite, wenn Sie von Temperatursensoren mit einer MCU über eine ADC Logdaten wollen, ist die Geschwindigkeit der Regel nicht so wichtig, aber die richtige Menge an Zeit warten , füllen Sie bitte der ADC korrekt jede Lesung zu ermöglichen , ist von wesentlicher Bedeutung . In diesem Fall besteht die Gefahr, dass die MCU, wenn sie nicht genug wartet, völlig unzuverlässige Daten erhält (ich gebe zu, dass sie diese Daten schneller erhält , obwohl: o).π


3
Viele Dinge (insbesondere Ausgänge, die ICs außerhalb von uC ansteuern) unterliegen zeitlichen Einschränkungen wie "Die Mindestzeit zwischen D und der Taktflanke beträgt 100 us" oder "Die IR-LED muss mit 1 MHz blinken". Daher sind häufig (genaue) Verzögerungen erforderlich.
Wouter van Ooijen

6
NOPs können hilfreich sein, um das Timing beim Bit-Banging eines seriellen Protokolls zu optimieren. Sie können auch nützlich sein, um ungenutzten Codeplatz zu füllen, gefolgt von einem Sprung zum Kaltstartvektor in seltenen Situationen, wenn der Programmzähler beschädigt wird (z. B. Netzteilstörung, Auswirkung eines seltenen Gammastrahlenereignisses usw.) und die Ausführung von Code in beginnt Ansonsten leeren Sie einen Teil des Code-Raums.
Techydude

6
Auf dem Atari 2600 Video Computer System (der zweiten Videospielkonsole, auf der Programme ausgeführt werden, die auf Kassetten gespeichert sind) würde der Prozessor genau 76 Zyklen pro Scanzeile ausführen, und viele Vorgänge müssten nach dem Start von a in einer bestimmten Anzahl von Zyklen ausgeführt werden Zeile scannen. Auf diesem Prozessor dauert der dokumentierte "NOP" -Befehl zwei Zyklen, aber es ist auch üblich, dass der Code einen ansonsten unbrauchbaren Befehl mit drei Zyklen verwendet, um eine Verzögerung auf eine genaue Anzahl von Zyklen aufzufüllen. Schnellerer Code hätte eine völlig verstümmelte Anzeige ergeben.
Supercat

1
Die Verwendung von NOPs für Verzögerungen kann auch auf Nicht-Echtzeitsystemen in Fällen sinnvoll sein, in denen ein E / A-Gerät eine minimale Zeit zwischen aufeinander folgenden Vorgängen, jedoch keine maximale Zeit festlegt. Bei vielen Controllern dauert das Verschieben eines Bytes aus einem SPI-Port beispielsweise acht CPU-Zyklen. Code, der nichts anderes tut, als Bytes aus dem Speicher abzurufen und an den SPI-Port auszugeben, könnte etwas zu schnell ausgeführt werden. Das Hinzufügen von Logik zum Testen, ob der SPI-Port für jedes Byte bereit ist, würde ihn jedoch unnötig verlangsamen. Durch Hinzufügen von ein oder zwei
NOPs

1
... wenn es keine Interrupts gibt. Wenn ein Interrupt auftritt, verschwenden die NOPs unnötigerweise Zeit, aber die Zeit, die von einem oder zwei NOPs verschwendet wird, ist geringer als die Zeit, die erforderlich ist, um zu bestimmen, ob ein Interrupt sie unnötig gemacht hat.
Supercat

8

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 RETso 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 NOPAusfü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 NOPs - zum Beispiel Windows MOV EDI, EDIfü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, AXtun 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 NOPziemlich viel. Der tatsächlich kompilierte Assembler-Code enthält diese nicht mehr. Es ist jedoch zu beachten, dass es sich nicht um x86 NOPhandelt.
  • NOPDies 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, EDIhat die beste Leistung als 2-Byte-NOP auf x86. Wenn Sie zwei NOPs 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 NOPAnweisung NOPgäbe , wäre das Schlüsselwort (Alias ​​/ Mnemonik) dennoch nützlich und würde dem entsprechen.
  • Anweisungen wie MOVKarte tatsächlich auf viele verschiedenen Opcodes, denn das auf Raum und Zeit spart - zum Beispiel, mov al, 42ist „immediate Byte zum bewegen alRegister“, was übersetzt 0xB02A( 0xB0wobei 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, rmbverwenden 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 xhcgAnweisungen ansehen . Sie werden sehen 0x86, 0x87und schließlich 0x91- 0x97. So nopmit ist es 0x90scheint 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-0x97Mikroarchitektur der Zeit war - wenn ich mich recht entsinne, war es einfach, die gesamte Bandbreite auf "xchg" abzubilden, über Register zu agieren axund 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- bxdanach dxist.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, immediateMenge. 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 nopals echte Anweisung angesehen werden oder nicht. Die ursprüngliche Mikroarchitektur behandelte es als eine Variante, xchgwenn ich mich richtig erinnere, aber es wurde tatsächlich nopin der Spezifikation benannt. Und da xchg ax, axdies 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 0x90die 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 aist 0x2Y, und 0xX8bedeutet „Register 0 direkte“, so 0x28ist 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.


Eigentlich NOPist in der Regel ein Alias MOV ax, ax, ADD ax, 0oder ähnliche Anweisungen. Warum sollten Sie eine spezielle Anweisung entwerfen, die nichts bewirkt, wenn es genügend gibt?
Dmitry Grigoryev

@DmitryGrigoryev Das geht wirklich in das Design der CPU-Sprache (naja, Mikroarchitektur) selbst ein. Die meisten CPUs (und Compiler) tendieren dazu, die Abwesenheit zu optimieren MOV ax, ax. NOPwird immer eine feste Anzahl von Zyklen für die Ausführung haben. Aber ich verstehe sowieso nicht, wie wichtig das für das ist, was ich in meiner Antwort geschrieben habe.
Luaan

CPUs können eine Abwesenheit nicht wirklich optimieren MOV ax, ax, da MOVder Befehl bereits in Vorbereitung ist, wenn sie wissen, dass es sich um eine handelt.
Dmitry Grigoryev

@DmitryGrigoryev Das hängt wirklich von der Art der CPU ab, über die Sie sprechen. Moderne Desktop-CPUs erledigen viel, nicht nur das Pipelining von Anweisungen. Zum Beispiel weiß die CPU, dass sie keine Cache-Zeilen oder ähnliches ungültig machen muss. Sie weiß, dass sie eigentlich nichts tun muss (sehr wichtig für HyperThreading und sogar für die vielen Pipes im Allgemeinen). Es würde mich nicht wundern, wenn es auch die Vorhersage von Zweigen beeinflusst (obwohl dies für NOPund wahrscheinlich auch so wäre MOV ax, ax). Moderne CPUs sind viel komplexer als die C-Compiler der
alten Schule

1
+1 für die Optimierung von noone to noöne! Ich denke, wir sollten alle zusammenarbeiten und zu dieser Schreibweise zurückkehren! Der Z80 (und wiederum der 8080) verfügt über 7 LD r, rBefehle, wobei rsich ein beliebiges einzelnes Register befindet, ähnlich wie bei Ihrem MOV ax, axBefehl. Der Grund, warum es 7 und nicht 8 ist, ist, dass eine der Anweisungen überladen ist, um zu werden HALT. So haben der 8080 und der Z80 mindestens 7 andere Anweisungen, die dasselbe tun wie NOP! Interessanterweise benötigen alle diese Befehle 4 T-Zustände, um ausgeführt zu werden, obwohl sie nicht logisch nach Bitmuster verknüpft sind, sodass es keinen Grund gibt, die LDBefehle zu verwenden!
CJ Dennis

5

In den späten 70er Jahren hatten wir (damals war ich noch ein junger Student) ein kleines Entwicklungssystem (8080, wenn Speicher zur Verfügung steht), das 1024 Byte Code enthielt (dh ein einzelnes UVEPROM) - es mussten nur vier Befehle geladen werden (L ), speichern (S), drucken (P) und etwas anderes, an das ich mich nicht erinnern kann. Es wurde mit einem echten Fernschreiber und Lochstreifen gefahren. Es war eng codiert!

Ein Beispiel für die Verwendung von NOOP war eine Interrupt-Service-Routine (ISR), die in 8-Byte-Intervallen angeordnet war. Diese Routine hatte eine Länge von 9 Byte und endete mit einem (langen) Sprung zu einer Adresse, die etwas weiter oben im Adressraum liegt. Angesichts der Little-Endian-Bytereihenfolge bedeutete dies, dass das High-Address-Byte 00h lautete und in das erste Byte des nächsten ISR eingefügt wurde, was bedeutete, dass es (der nächste ISR) mit NOOP begann, damit „wir“ passen konnten der Code in dem begrenzten Raum!

Der NOOP ist also nützlich. Außerdem denke ich, dass es für Intel am einfachsten war, es so zu codieren. Sie hatten wahrscheinlich eine Liste von Anweisungen, die sie implementieren wollten, und es begann bei '1', wie es bei allen Listen der Fall ist (dies waren die Tage von FORTRAN), also die Null NOOP-Code wurde zu einem Problem. (Ich habe noch nie einen Artikel gesehen, in dem argumentiert wurde, dass ein NOOP ein wesentlicher Bestandteil der Theorie der Informatik ist.)


Nicht alle CPUs haben NOP mit 0x00 codiert (obwohl die 8085, die 8080 und die CPU, die ich am besten kenne, die Z80, alle dies tun). Wenn ich jedoch einen Prozessor entwerfen würde, würde ich ihn dort platzieren! Etwas anderes praktisches ist, dass der Speicher normalerweise auf alle 0x00 initialisiert wird, so dass die Ausführung als Code nichts bewirkt, bis die CPU den nicht auf Null gesetzten Speicher erreicht.
CJ Dennis

@CJDennis Ich habe erklärt , warum der x86 - CPUs verwendet nicht 0x00für nopin meiner Antwort. Kurz gesagt, es spart xchg ax, axBefehlsdecodierung - ergibt sich auf natürliche Weise aus der Funktionsweise der Befehlsdecodierung, und es wird etwas "Noppy" ausgeführt. Warum also nicht das verwenden und es aufrufen nop? :) Verwendet, um einiges auf dem Silizium für die
Befehlsdecodierung

5

Auf einigen Architekturen NOPwird verwendet, um nicht verwendete Verzögerungsschlitze zu belegen . Wenn die Verzweigungsanweisung beispielsweise die Pipeline nicht löscht, werden trotzdem mehrere Anweisungen ausgeführt:

 JMP     .label
 MOV     R2, 1    ; these instructions start execution before the jump
 MOV     R2, 2    ; takes place so they still get executed

Aber was ist, wenn Sie nach dem keine nützlichen Anweisungen zum Anpassen haben JMP? In diesem Fall müssen Sie NOPs verwenden.

Verzögerungsschlitze sind nicht auf Sprünge beschränkt. In einigen Architekturen werden Datenrisiken in der CPU-Pipeline nicht automatisch behoben. Dies bedeutet, dass nach jedem Befehl, der ein Register ändert, ein Schlitz vorhanden ist, in dem der neue Wert des Registers noch nicht zugänglich ist. Wenn der nächste Befehl diesen Wert benötigt, sollte der Steckplatz mit a belegt sein NOP:

 ADD     R1, R1
 NOP               ; The new value of R1 is not ready yet
 ADD     R1, R3

Einige Anweisungen zur bedingten Ausführung ( If-True-False und ähnliches) verwenden Slots für jede Bedingung. Wenn mit einer bestimmten Bedingung keine Aktionen verknüpft sind, sollte ihr Slot mit einem belegt sein NOP:

CMP     R0, R1       ; Compare R0 and R1, setting flags
ITF     GT           ; If-True-False on GT flag 
MOV     R3, R2       ; If 'greater than', move R2 to R3
NOP                  ; Else, do nothing

+1. Natürlich tauchen diese nur auf Architekturen auf, bei denen die Abwärtskompatibilität keine Rolle spielt. Wenn x86 bei der Einführung von Anweisungs-Pipelining so etwas versuchte, würde dies fast jeder einfach als falsch bezeichnen (schließlich haben sie nur ihre CPU und ihre CPU aktualisiert) Anwendungen funktionieren nicht mehr!). X86 muss also sicherstellen, dass die Anwendung nichts davon merkt, wenn Verbesserungen wie diese in die CPU
eingebracht werden

2

Ein weiteres Beispiel für die Verwendung eines Zwei-Byte- NOP: http://blogs.msdn.com/b/oldnewthing/archive/2011/09/21/10214405.aspx

Der Befehl MOV EDI, EDI ist ein Zwei-Byte-NOP, der gerade genug Platz zum Patchen eines Sprungbefehls bietet, damit die Funktion im laufenden Betrieb aktualisiert werden kann. Die Absicht ist, dass der Befehl MOV EDI, EDI durch einen Zwei-Byte-Befehl JMP $ -5 ersetzt wird, um die Steuerung auf fünf Byte Patch-Speicherplatz umzuleiten, der unmittelbar vor dem Start der Funktion verfügbar ist. Fünf Bytes reichen für einen vollständigen Sprungbefehl aus, der die Steuerung an die an einer anderen Stelle im Adressraum installierte Ersetzungsfunktion senden kann.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.