Welche Funktion haben die Push / Pop-Anweisungen, die für Register in der x86-Assembly verwendet werden?


93

Wenn man über das Lese Assembler ich über die Menschen kommt oft zu schreiben , dass sie schieben ein bestimmtes Register des Prozessors und Pop es später wieder wiederherstellen vorheriger Zustand.

  • Wie kann man ein Register schieben? Wo wird es weitergeschoben? Warum wird das benötigt?
  • Kommt es auf einen einzelnen Prozessorbefehl an oder ist es komplexer?

3
Warnung: Alle aktuellen Antworten sind in der Assembly-Syntax von Intel angegeben. Push-Pop in AT & T - Syntax zum Beispiel verwendet eine post-fix wie b, w, loder qdie Größe des Speichers manipuliert zu bezeichnen. Beispiel: pushl %eaxundpopl %eax
Hawken

5
@hawken Bei den meisten Assemblern, die die AT & T-Syntax (insbesondere Gas) verschlucken können, kann das Größenfixfix weggelassen werden, wenn die Operandengröße aus der Operandengröße abgeleitet werden kann. Dies gilt für die von Ihnen angegebenen Beispiele, da diese %eaximmer 32 Bit groß sind.
Gunther Piez

Antworten:


147

Wenn Sie einen Wert verschieben (der nicht unbedingt in einem Register gespeichert ist), müssen Sie ihn in den Stapel schreiben.

Poppen bedeutet, alles, was sich oben auf dem Stapel befindet, in einem Register wiederherzustellen . 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

3
Der explizite Operand für Push und Pop lautet r/mnicht nur "Registrieren", sondern auch "Registrieren" push dword [esi]. Oder sogar pop dword [esp], um denselben Wert zu laden und dann wieder an dieselbe Adresse zu speichern. ( github.com/HJLebbink/asm-dude/wiki/POP ). Ich erwähne dies nur, weil Sie "nicht unbedingt ein Register" sagen.
Peter Cordes

Sie können auch popin einen Bereich der Erinnerung:pop [0xdeadbeef]
SS Anne

Hallo, was ist der Unterschied zwischen Push / Pop und Pushq / Popq? Ich bin auf macos / intel
SteakOverflow

43

So schieben Sie ein Register. Ich nehme an, wir sprechen über x86.

push ebx
push eax

Es wird auf Stapel geschoben. Der Wert vonESP Registers wird auf die Größe des Push-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

A pushist eine einzelne Anweisung in x86, die intern zwei Dinge ausführt.

  1. Speichern Sie den Push-Wert an der aktuellen Adresse des ESPRegisters.
  2. Dekrementieren Sie das ESPRegister auf die Größe des Push-Werts.

7
1. und 2. sollten neu angeordnet werden
Vavan

@vavan hat gerade eine Anfrage gesendet, um das
Problem

38

Wo wird es weitergeschoben?

esp - 4. Etwas präziser:

  • esp wird von 4 abgezogen
  • Der Wert wird auf verschoben esp

pop kehrt dies um.

Das System V ABI weist Linux an, rspauf einen sinnvollen Stapelspeicherort hinzuweisen, wenn das Programm gestartet wird: Wie lautet der Standardregisterstatus beim Programmstart (asm, linux)? Welches ist, was Sie normalerweise verwenden sollten.

Wie kann man ein Register schieben?

Beispiel für ein minimales GNU-GAS:

.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 Assertions .

Warum wird das benötigt?

Es ist wahr, dass diese Anweisungen leicht über und implementiert werden movkönnen .addsub

Der Grund dafür ist, dass diese Kombinationen von Anweisungen so häufig sind, dass Intel beschlossen hat, sie für uns bereitzustellen.

Der Grund, warum diese Kombinationen so häufig sind, besteht darin, dass sie das Speichern und Wiederherstellen der Werte von Registern im Speicher erleichtern, damit sie nicht überschrieben werden.

Versuchen Sie, C-Code von Hand zu kompilieren, um das Problem zu verstehen.

Eine große Schwierigkeit besteht darin, zu entscheiden, wo jede Variable gespeichert wird.

Im Idealfall passen alle Variablen in Register, auf die am schnellsten zugegriffen werden kann (derzeit etwa 100-mal schneller als RAM).

Aber natürlich können wir leicht mehr Variablen als Register haben, insbesondere für die Argumente verschachtelter Funktionen. Die einzige Lösung besteht 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 schönes Stapelmuster passen, wird eine Speicherfragmentierung verhindert , ist dies der beste Weg, 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 ist und einer der schwierigsten Teile beim Schreiben eines Compilers ist. Dieses Problem wird als Registerzuordnung bezeichnet und ist isomorph zur Diagrammfärbung .

Wenn der Allokator des Compilers gezwungen ist, Dinge im Speicher zu speichern, anstatt nur Register, wird dies als Überlauf bezeichnet .

Kommt es auf einen einzelnen Prozessorbefehl an oder ist es komplexer?

Wir wissen nur, dass Intel a pushund a dokumentiertpop Anweisung , also sind sie eine Anweisung in diesem Sinne.

Intern könnte es auf mehrere Mikrocodes erweitert werden, einen zum Ändern espund einen zum Ausführen der Speicher-E / A, und mehrere Zyklen dauern.

Es ist aber auch möglich, dass eine einzelne pushschneller ist als eine äquivalente Kombination anderer Anweisungen, da sie spezifischer ist.

Dies ist meist un (der) dokumentiert:


4
Sie müssen nicht raten, wie push/ popin Uops dekodieren. Dank Leistungsindikatoren sind experimentelle Tests möglich, und Agner Fog hat dies getan und Anweisungstabellen veröffentlicht . Pentium-M- und spätere CPUs verfügen dank der Stack-Engine über Single-Uop push/ pop(siehe Agners Microarch- PDF). Dies schließt aktuelle AMD-CPUs dank der Intel / AMD-Patentfreigabevereinbarung ein.
Peter Cordes

@ PeterCordes super! Die Leistungsindikatoren werden also von Intel dokumentiert, um Mikrooperationen zu zählen?
Ciro Santilli 法轮功 冠状 病 六四 事件 22

Außerdem sind lokale Variablen, die aus Regs verschüttet werden, im L1-Cache normalerweise immer noch heiß, wenn eine von ihnen tatsächlich verwendet wird. Das Lesen aus einem Register ist jedoch praktisch kostenlos und hat keine Latenz. Es ist also unendlich schneller als der L1-Cache, je nachdem, wie Sie Begriffe definieren möchten. Für schreibgeschützte Einheimische, die auf den Stapel verschüttet werden, sind die Hauptkosten nur zusätzliche Ladevorgänge (manchmal Speicheroperanden, manchmal mit separaten movLadevorgängen). Für verschüttete nicht konstante Variablen sind die Roundtrips für die Weiterleitung des Geschäfts mit einer hohen zusätzlichen Latenz verbunden (zusätzliche ~ 5c im Vergleich zur direkten Weiterleitung, und die Anweisungen für das Geschäft sind nicht billig).
Peter Cordes

Ja, es gibt Zähler für die Gesamtzahl der Uops in einigen verschiedenen Pipeline-Phasen (Issue / Execute / Retirement), sodass Sie die Fused-Domain oder die Unfused-Domain zählen können. Siehe diese Antwort zum Beispiel. Wenn ich diese Antwort jetzt umschreiben würde, würde ich das ocperf.pyWrapper-Skript verwenden, um einfache symbolische Namen für die Zähler zu erhalten.
Peter Cordes

21

Pushing- 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.

Wenn Sie es als Paar verwenden, können Sie ein Register auf dem Stapel speichern und später wiederherstellen. Es gibt auch andere Verwendungszwecke.


4
Ja, diese Sequenzen emulieren Push / Pop korrekt. (außer Push / Pop haben keine Auswirkungen auf Flags).
Peter Cordes

2
Sie sollten besser lea rsp, [rsp±8]anstelle von add/ verwenden sub, um die Wirkung von push/ popauf Flags besser zu emulieren .
Ruslan

12

Fast alle CPUs verwenden Stack. Der Programmstapel ist eine LIFO- Technik mit hardwareunterstützter Verwaltung.

Der Stapel ist die Größe des Programmspeichers (RAM), der normalerweise oben auf dem CPU-Speicherhaufen zugewiesen ist und in entgegengesetzter Richtung wächst (bei PUSH-Anweisung wird der Stapelzeiger verringert). Ein Standardbegriff für das Einfügen in einen Stapel ist PUSH und für das Entfernen vom Stapel ist POP .

Der Stapel wird über das vom Stapel vorgesehene CPU-Register verwaltet, das auch als Stapelzeiger bezeichnet wird. Wenn die CPU also POP oder PUSH ausführt, lädt / speichert der Stapelzeiger ein Register oder eine Konstante in den Stapelspeicher und der Stapelzeiger wird automatisch verringert oder entsprechend der Anzahl der geschobenen Wörter erhöht oder in (von) Stapel eingeblendet.

Über Assembler-Anweisungen können wir speichern, um zu stapeln:

  1. CPU-Register und auch Konstanten.
  2. Rückgabeadressen für Funktionen oder Prozeduren
  3. Funktionen / Prozeduren in / out Variablen
  4. Funktionen / Prozeduren lokale Variablen.
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.