Wenn ich über Assembler lese, stoße ich oft auf Leute, die schreiben, dass sie Push ein bestimmtes Register des Prozessors haben und Pop es später erneut tun, um den vorherigen Zustand wiederherzustellen.
Drücken Ein Wert (der nicht unbedingt in einem Register gespeichert ist) bedeutet, dass er in den Stapel geschrieben wird.
poppen bedeutet, dass alles, was sich oben auf dem Stapel befindet, wiederhergestellt wird in ein Register. Das sind grundlegende Anweisungen:
Push 0xdeadbeef ; Push a value to the stack
pop eax ; eax is now 0xdeadbeef
; swap contents of registers
Push eax
mov eax, ebx
pop ebx
So schiebst du ein Register. Ich nehme an, wir sprechen über x86.
Push ebx
Push eax
Es wird auf Stapel geschoben. Der Wert des Registers ESP
wird auf die Größe des übertragenen Werts dekrementiert, wenn der Stapel in x86-Systemen nach unten wächst.
Es wird benötigt, um die Werte zu erhalten. Die allgemeine Verwendung ist
Push eax ; preserve the value of eax
call some_method ; some method is called which will put return value in eax
mov edx, eax ; move the return value to edx
pop eax ; restore original eax
Ein Push
ist eine einzelne Anweisung in x86, die intern zwei Aktionen ausführt.
ESP
.ESP
-Register auf die Größe des übertragenen Werts.Wo ist es angeschoben?
esp - 4
. Etwas präziser:
esp
wird um 4 subtrahiertesp
verschobenpop
kehrt dies um.
Die System V-ABI weist Linux an, rsp
auf einen sinnvollen Stack-Speicherort zu verweisen, wenn das Programm gestartet wird: Was ist der Standard-Registrierungsstatus, wenn das Programm gestartet wird (asm, linux)? Was ist das? Sie sollten normalerweise verwenden.
Wie kann man ein Register pushen?
Minimal GNU GAS-Beispiel:
.data
/* .long takes 4 bytes each. */
val1:
/* Store bytes 0x 01 00 00 00 here. */
.long 1
val2:
/* 0x 02 00 00 00 */
.long 2
.text
/* Make esp point to the address of val2.
* Unusual, but totally possible. */
mov $val2, %esp
/* eax = 3 */
mov $3, %ea
Push %eax
/*
Outcome:
- esp == val1
- val1 == 3
esp was changed to point to val1,
and then val1 was modified.
*/
pop %ebx
/*
Outcome:
- esp == &val2
- ebx == 3
Inverses Push: ebx gets the value of val1 (first)
and then esp is increased back to point to val2.
*/
Das obige auf GitHub mit ausführbaren Behauptungen .
Warum wird das benötigt?
Es ist wahr, dass diese Anweisungen einfach über mov
, add
und sub
implementiert werden könnten.
Der Grund dafür ist, dass diese Kombinationen von Anweisungen so häufig sind, dass Intel beschlossen hat, sie uns zur Verfügung zu stellen.
Der Grund, warum diese Kombinationen so häufig sind, ist, dass sie das Speichern und Wiederherstellen der Werte von Registern im Speicher vorübergehend vereinfachen, damit sie nicht überschrieben werden.
Um das Problem zu verstehen, kompilieren Sie etwas C-Code von Hand.
Eine Hauptschwierigkeit besteht darin, zu entscheiden, wo jede Variable gespeichert wird.
Im Idealfall passen alle Variablen in Register, auf die am schnellsten zugegriffen werden kann (derzeit ungefähr 100x schneller als RAM).
Aber natürlich können wir leicht mehr Variablen als Register haben, speziell für die Argumente verschachtelter Funktionen. Die einzige Lösung besteht also darin, in den Speicher zu schreiben.
Wir könnten in jede Speicheradresse schreiben, aber da die lokalen Variablen und Argumente von Funktionsaufrufen und Rückgaben in ein Nice-Stack-Muster passen, das Speicherfragmentierung verhindert, ist dies der beste Weg, um damit umzugehen. Vergleichen Sie das mit dem Wahnsinn, einen Heap-Allokator zu schreiben.
Dann lassen wir Compiler die Registerzuordnung für uns optimieren, da dies NP vollständig und einer der schwierigsten Teile beim Schreiben eines Compilers ist. Dieses Problem wird Registerzuordnung genannt , und es ist isomorph zu graph coloring .
Wenn der Allokator des Compilers gezwungen ist, Dinge im Speicher statt nur in Registern zu speichern, wird dies als spill bezeichnet.
Fällt dies auf eine einzelne Prozessoranweisung zurück oder ist es komplexer?
Wir wissen nur, dass Intel eine Anweisung Push
und eine Anweisung pop
dokumentiert, also handelt es sich um eine Anweisung in diesem Sinne.
Intern könnte es auf mehrere Mikrocodes erweitert werden, von denen einer zum Ändern von esp
und einer zum Ausführen der Speicher-E/A und für mehrere Zyklen benötigt wird.
Es ist aber auch möglich, dass ein einzelnes Push
schneller ist als eine entsprechende Kombination anderer Anweisungen, da es spezifischer ist.
Dies ist meist un (der) dokumentiert:
Push
und pop
verwendet werden eine einzige Mikrooperation.Push- und Popping-Register sind hinter den Kulissen gleichbedeutend mit:
Push reg <= same as => sub $8,%rsp # subtract 8 from rsp
mov reg,(%rsp) # store, using rsp as the address
pop reg <= same as=> mov (%rsp),reg # load, using rsp as the address
add $8,%rsp # add 8 to the rsp
Beachten Sie, dass dies die x86-64 At & t-Syntax ist.
Als Paar verwendet, können Sie ein Register auf dem Stapel speichern und später wiederherstellen. Es gibt auch andere Verwendungen.
Fast alle CPUs verwenden Stack. Der Programmstack ist LIFO Technik mit Hardware unterstützt verwalten.
Stack ist die Größe des Programmspeichers (RAM), der normalerweise am oberen Rand des CPU-Speicherheaps zugewiesen wird und in entgegengesetzter Richtung wächst (beim Push-Befehl wird der Stack-Zeiger verringert). Ein Standardbegriff zum Einfügen in einen Stapel ist Push und zum Entfernen aus dem Stapel ist [~ # ~] pop [~ # ~] .
Der Stack wird über das für den Stack vorgesehene CPU-Register verwaltet, das auch als Stack-Zeiger bezeichnet wird. Wenn die CPU also einen [~ # ~] Pop [~ # ~] oder Push ausführt Der Stapelzeiger lädt/speichert ein Register oder eine Konstante im Stapelspeicher und der Stapelzeiger wird automatisch verkleinert oder vergrößert, je nachdem, wie viele Wörter in den Stapel geschoben oder aus dem Stapel geschoben wurden.
Über Assembler-Anweisungen können wir speichern, um zu stapeln: