it-swarm.com.de

Zweck der NOP-Anweisung und der Align-Anweisung in der x86-Assembly

Es ist ungefähr ein Jahr her, seit ich das letzte Mal eine Versammlungsklasse besucht habe. In dieser Klasse verwendeten wir MASM mit den Irvine-Bibliotheken, um das Programmieren zu vereinfachen.

Nachdem wir die meisten Anweisungen durchgearbeitet hatten, sagte er, dass die NOP-Anweisung im Wesentlichen nichts bewirkt habe und sich keine Sorgen um die Verwendung machen müsse. Wie auch immer, es war ungefähr mittelfristig und er hat einen Beispielcode, der nicht richtig laufen würde, also sagte er uns, wir sollten eine NOP-Anweisung hinzufügen und es funktionierte gut. Ich fragte, ob ich nach dem Unterricht warum und was es tatsächlich tat, und er sagte, er wisse es nicht.

Weiß jemand Bescheid?

15
alvonellos

Oft wird NOP verwendet, um Anweisungsadressen auszurichten. Dies tritt normalerweise beispielsweise beim Schreiben von Shellcode auf, um Pufferüberlauf oder Format String Vulnerability .

Angenommen, Sie haben einen relativen Sprung zu 100 Bytes vorwärts und nehmen einige Änderungen am Code vor. Es besteht die Möglichkeit, dass Ihre Änderungen die Adresse des Sprungziels durcheinander bringen und Sie daher auch den oben genannten relativen Sprung ändern müssen. Hier können Sie NOPs hinzufügen, um die Zieladresse nach vorne zu verschieben. Wenn zwischen der Zieladresse und der Sprunganweisung mehrere NOPs liegen, können Sie die NOPs entfernen, um die Zieladresse rückwärts zu ziehen.

Dies wäre kein Problem, wenn Sie mit einem Assembler arbeiten, der Beschriftungen unterstützt. Sie können einfach JXX someLabel Ausführen (wobei JXX ein bedingter Sprung ist), und der Assembler ersetzt das someLabel durch die Adresse dieses Labels. Wenn Sie jedoch den zusammengesetzten Maschinencode (die tatsächlichen Opcodes) einfach von Hand ändern (wie dies manchmal beim Schreiben von Shellcode der Fall ist), müssen Sie die Sprunganweisung auch manuell ändern. Entweder Sie ändern es oder Sie verschieben die Zielcode-Adresse mithilfe von NOPs.

Ein anderer Anwendungsfall für die Anweisung NOP wäre ein NOP-Schlitten . Im Wesentlichen besteht die Idee darin, ein ausreichend großes Array von Anweisungen zu erstellen, die keine Nebenwirkungen verursachen (z. B. NOP oder das Inkrementieren und anschließende Dekrementieren eines Registers), aber den Befehlszeiger erhöhen. Dies ist zum Beispiel nützlich, wenn man zu einem bestimmten Code springen möchte, dessen Adresse nicht bekannt ist. Der Trick besteht darin, den besagten NOP-Schlitten vor dem Zielcode zu platzieren und dann irgendwo zum besagten Schlitten zu springen. Was passiert ist, dass die Ausführung hoffentlich von dem Array aus fortgesetzt wird, das keine Nebenwirkungen hat, und es Befehl für Befehl weiterleitet, bis es den gewünschten Code trifft. Diese Technik wird häufig in den oben genannten Pufferüberlauf-Exploits verwendet, insbesondere um Sicherheitsmaßnahmen wie ASLR entgegenzuwirken.

Eine weitere besondere Verwendung für den Befehl NOP ist das Ändern des Codes eines Programms. Beispielsweise können Sie Teile von bedingten Sprüngen durch NOPs ersetzen und als solche die Bedingung umgehen. Dies ist eine häufig verwendete Methode, wenn " Cracken" Kopierschutz von Software. Im einfachsten Fall geht es nur darum, das Assembly-Code-Konstrukt für die Codezeile if(genuineCopy) ... zu entfernen und die Anweisungen durch NOPs und .. Voilà! Es werden keine Überprüfungen durchgeführt und nicht echte Kopien funktionieren!

Beachten Sie, dass im Wesentlichen beide Beispiele für Shellcode und Cracking dasselbe tun. Ändern Sie vorhandenen Code, ohne die relativen Adressen von Vorgängen zu aktualisieren, die auf der relativen Adressierung beruhen.

37
zxcdw

Ein NOP kann in einem Verzögerungsschlitz verwendet werden, wenn kein anderer Befehl neu angeordnet werden darf, um dort platziert zu werden.

lw   v0,4(v1)
jr   v0

In MIPS wäre dies ein Fehler, da zum Zeitpunkt des Lesens des Registers v0 durch jr das Register v0 noch nicht mit dem Wert aus der vorherigen Anweisung geladen wurde.

Der Weg, dies zu beheben, wäre:

lw   v0,4(v1)
nop
jr   v0
nop

Dies füllt die Dealy-Slots nach dem Laden von Word- und Sprungregisterbefehlen mit einem NOP, so dass der Ladewortbefehl abgeschlossen ist, bevor der Sprungregisterbefehl ausgeführt wird.

Weiterführende Literatur - ein bisschen zu SPARC Ausfüllen von Verzögerungsschlitzen . Aus diesem Dokument:

Was kann in den Delay-Slot gesteckt werden?

  • Einige nützliche Anweisungen, die ausgeführt werden sollten, unabhängig davon, ob Sie verzweigen oder nicht.
  • Einige Anweisungen, die nützlich sind, funktionieren nur, wenn Sie verzweigen (oder wenn Sie nicht verzweigen), schaden aber nicht, wenn sie im anderen Fall ausgeführt werden.
  • Wenn alles andere fehlschlägt, eine NOP-Anweisung

Was darf NICHT in den Delay-Slot gesteckt werden?

  • Alles, was den CC festlegt, von dem die Verzweigungsentscheidung abhängt. Der Verzweigungsbefehl entscheidet sofort, ob verzweigt werden soll oder nicht, aber er verzweigt erst nach dem Verzögerungsbefehl. (Nur der Zweig ist verzögert, nicht die Entscheidung.)
  • Eine weitere Verzweigungsanweisung. (Was passiert, wenn Sie dies tun, ist nicht einmal definiert! Das Ergebnis ist unvorhersehbar!)
  • Eine "Set" -Anweisung. Dies sind wirklich zwei Anweisungen, nicht eine, und nur die Hälfte davon befindet sich im Verzögerungsschlitz. (Der Assembler warnt Sie davor.)

Beachten Sie die dritte Option im Feld für die Verzögerung. Der Fehler, den Sie gesehen haben, war wahrscheinlich jemand, der eines der Dinge füllte, die nicht in den Verzögerungsschlitz gelegt werden dürfen. Wenn Sie an dieser Stelle ein NOP platzieren, wird der Fehler behoben.

Hinweis: Nach dem erneuten Lesen der Frage war dies für x86, das keine Verzögerungssteckplätze hat (Verzweigung blockiert stattdessen nur die Pipeline). Das wäre also nicht die Ursache/Lösung für den Fehler. Auf RISC-Systemen hätte dies die Antwort sein können.

10
user40980

mindestens ein Grund für die Verwendung von NOP ist die Ausrichtung. x86-Prozessoren lesen Daten aus dem Hauptspeicher in ziemlich großen Blöcken, und der Anfang des zu lesenden Blocks ist immer ausgerichtet. Wenn also ein Codeblock vorhanden ist, der viel gelesen wird, sollte dieser Block ausgerichtet werden. Dies führt zu einer geringen Beschleunigung.

6
permeakra

Es gibt einen x86-spezifischen Fall, der in anderen Antworten noch nicht beschrieben ist: Interrupt-Behandlung. Für einige Stile kann es Codeabschnitte geben, wenn Interrupts deaktiviert sind, da der Hauptcode mit einigen Daten funktioniert, die von Interrupt-Handlern gemeinsam genutzt werden. Es ist jedoch sinnvoll, Interrupts zwischen solchen Abschnitten zuzulassen. Wenn man naiv schreibt


    STI
    CLI

dies verarbeitet keine ausstehenden Interrupts, da unter Berufung auf Intel:

Nachdem das IF-Flag gesetzt wurde, reagiert der Prozessor auf externe, maskierbare Interrupts, nachdem der nächste Befehl ausgeführt wurde.

so wird dies mindestens wie folgt umgeschrieben:


    STI
    NOP
    CLI

In der zweiten Variante werden alle anstehenden Interrupts nur zwischen NOP und CLI verarbeitet. (Natürlich kann es viele alternative Varianten geben, die den STI-Befehl verdoppeln. Aber explizites NOP ist zumindest für mich offensichtlicher.)

3
Netch

Ein Zweck für NOP (in der Generalversammlung, nicht nur x86) ist es, Zeitverzögerungen einzuführen. Sie möchten beispielsweise einen Mikrocontroller programmieren, der mit einer Verzögerung von 1 s an einige LEDs ausgegeben werden muss. Diese Verzögerung kann mit NOP (und Zweigen) implementiert werden. Natürlich könnten Sie ADD oder etwas anderes verwenden, aber das würde den Code unleserlicher machen. oder vielleicht brauchen Sie alle Register.

3
m3th0dman

Im Allgemeinen sind beim 80x86 keine NOP-Anweisungen für die Programmkorrektheit erforderlich, obwohl gelegentlich auf einigen Computern strategisch platzierte NOPs dazu führen können, dass Code schneller ausgeführt wird. Auf dem 8086 würde beispielsweise Code in Zwei-Byte-Blöcken abgerufen, und der Prozessor hatte einen internen "Prefetch" -Puffer, der drei solcher Blöcke enthalten konnte. Einige Anweisungen würden schneller ausgeführt, als sie abgerufen werden könnten, während die Ausführung anderer Anweisungen eine Weile dauern würde. Während langsamer Anweisungen würde der Prozessor versuchen, den Prefetch-Puffer zu füllen, so dass die nächsten Anweisungen schnell ausgeführt werden könnten, wenn sie schnell wären. Wenn der Befehl, der dem langsamen Befehl folgt, an einer geraden Wortgrenze beginnt, werden die nächsten Befehle im Wert von sechs Bytes vorab abgerufen. Wenn es an einer ungeraden Bytegrenze beginnt, werden nur fünf Bytes vorabgerufen. Wenn ich mich recht erinnere, dauerte ein NOP drei Zyklen und ein Speicherabruf vier. Wenn also das Vorabrufen des zusätzlichen Bytes einen Speicherzyklus speichern würde, könnte es manchmal vorkommen, dass ein "NOP" hinzugefügt wird, um den Befehl nach einem langsamen Start an einer geraden Wortgrenze zu veranlassen Speichern Sie einen Zyklus.

Solche Speicherausrichtungsprobleme können die Programmgeschwindigkeit beeinflussen, haben jedoch im Allgemeinen keinen Einfluss auf die Korrektheit. Auf der anderen Seite gibt es einige Probleme im Zusammenhang mit dem Prefetch bei älteren Prozessoren, bei denen ein NOP die Korrektheit beeinträchtigen könnte. Wenn ein Befehl ein bereits vorab abgerufenes Codebyte ändert, führt der 8086 (und ich denke der 80286 und 80386) den vorabgerufenen Befehl aus, obwohl er nicht mehr mit dem Speicher übereinstimmt. Das Hinzufügen von ein oder zwei NOP zwischen dem Befehl, der den Speicher ändert, und dem geänderten Codebyte kann verhindern, dass das Codebyte abgerufen wird, bis es geschrieben wurde. Beachten Sie übrigens, dass viele Kopierschutzschemata diese Art von Verhalten ausnutzten. Beachten Sie jedoch auch, dass dieses Verhalten nicht garantiert ist. Verschiedene Prozessorvarianten können den Vorabruf unterschiedlich behandeln, einige können vorabgerufene Bytes ungültig machen, wenn der Speicher, aus dem sie gelesen wurden, geändert wird, und Interrupts machen den Vorabrufpuffer im Allgemeinen ungültig; Der Code wird erneut abgerufen, wenn die Interrupts zurückkehren.

3
supercat