Qual è la funzione delle istruzioni push / pop usate sui registri nell'assembly x86?


101

Quando leggo di assembler mi imbatto spesso in persone che scrivono che spingono un certo registro del processore e lo pop di nuovo in seguito per ripristinarne lo stato precedente.

  • Come puoi spingere un registro? Dove viene spinto? Perché è necessario?
  • Questo si riduce a una singola istruzione del processore o è più complesso?

3
Attenzione: tutte le risposte correnti sono fornite nella sintassi dell'assembly di Intel; Push-pop in AT & T sintassi per esempio usa un post-fix come b, w, l, o qper indicare la dimensione della memoria essere manipolati. Es: pushl %eaxepopl %eax
Hawken

5
@hawken Sulla maggior parte degli assemblatori in grado di inghiottire la sintassi AT&T (in particolare gas) il suffisso di dimensione può essere omesso se la dimensione dell'operando può essere dedotta dalla dimensione dell'operando. Questo è il caso degli esempi che hai fornito, poiché ha %eaxsempre una dimensione di 32 bit.
Gunther Piez

Risposte:


153

spingere un valore (non necessariamente memorizzato in un registro) significa scriverlo nello stack.

popping significa ripristinare in un registro tutto ciò che si trova in cima alla pila . Queste sono istruzioni di base:

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

5
L'operando esplicito per push e pop r/mnon è solo register, quindi puoi farlo push dword [esi]. O anche pop dword [esp]per caricare e quindi memorizzare lo stesso valore nello stesso indirizzo. ( github.com/HJLebbink/asm-dude/wiki/POP ). Lo dico solo perché dici "non necessariamente un registro".
Peter Cordes

2
Puoi anche popentrare in un'area della memoria:pop [0xdeadbeef]
SS Anne

Salve, qual è la differenza tra push / pop e pushq / popq? Sono su macos / intel
SteakOverflow

46

Ecco come spingere un registro. Presumo che stiamo parlando di x86.

push ebx
push eax

Viene messo in pila. Il valore di ESPregister viene decrementato alla dimensione del valore inserito quando lo stack cresce verso il basso nei sistemi x86.

Serve per preservare i valori. L'utilizzo generale è

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 pushè una singola istruzione in x86, che fa due cose internamente.

  1. Decrementa il ESPregistro della dimensione del valore inserito.
  2. Memorizza il valore premuto all'indirizzo corrente del ESPregistro.

@vavan ha appena inviato una richiesta per
risolverlo

38

Dove viene spinto?

esp - 4. Più precisamente:

  • esp viene sottratto per 4
  • il valore viene spinto a esp

pop inverte questo.

L'ABI di System V dice a Linux di rsppuntare a una posizione dello stack ragionevole quando il programma inizia a funzionare: Qual è lo stato del registro predefinito all'avvio del programma (asm, linux)? che è quello che dovresti usare di solito.

Come puoi spingere un registro?

Esempio minimo 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.
    */

Quanto sopra su GitHub con asserzioni eseguibili .

Perché è necessario?

È vero che queste istruzioni potrebbero essere facilmente implementate tramite mov, adde sub.

La ragione per cui esistono, è che quelle combinazioni di istruzioni sono così frequenti che Intel ha deciso di fornirle per noi.

Il motivo per cui queste combinazioni sono così frequenti è che facilitano il salvataggio e il ripristino temporaneo dei valori dei registri in memoria in modo che non vengano sovrascritti.

Per capire il problema, prova a compilare manualmente un po 'di codice C.

Una delle difficoltà maggiori è decidere dove memorizzare ogni variabile.

Idealmente, tutte le variabili si adatterebbero ai registri, che è la memoria più veloce a cui accedere (attualmente circa 100 volte più veloce della RAM).

Ma ovviamente, possiamo facilmente avere più variabili che registri, specialmente per gli argomenti delle funzioni annidate, quindi l'unica soluzione è scrivere in memoria.

Potremmo scrivere in qualsiasi indirizzo di memoria, ma poiché le variabili locali e gli argomenti delle chiamate e dei ritorni di funzione rientrano in un bel pattern di stack, che impedisce la frammentazione della memoria , questo è il modo migliore per gestirlo. Confrontalo con la follia di scrivere un allocatore di heap.

Quindi lasciamo che i compilatori ottimizzino l'allocazione dei registri per noi, poiché questo è NP completo e una delle parti più difficili della scrittura di un compilatore. Questo problema è chiamato allocazione dei registri ed è isomorfo alla colorazione del grafico .

Quando l'allocatore del compilatore è costretto a memorizzare le cose in memoria invece che solo nei registri, ciò è noto come fuoriuscita .

Questo si riduce a una singola istruzione del processore o è più complesso?

Tutto quello che sappiamo per certo è che Intel documenta un'istruzione pushe popun'istruzione, quindi sono un'istruzione in questo senso.

Internamente, potrebbe essere espanso a più microcodici, uno da modificare espe uno per eseguire l'IO della memoria e richiedere più cicli.

Ma è anche possibile che una singola pushsia più veloce di una combinazione equivalente di altre istruzioni, poiché è più specifica.

Questo è per lo più sotto (der) documentato:


4
Non è necessario indovinare come push/ popdecodifica in uops. Grazie ai contatori delle prestazioni, è possibile eseguire test sperimentali e Agner Fog lo ha fatto e ha pubblicato tabelle di istruzioni . Le CPU Pentium-M e successive hanno single-uop push/ popgrazie allo stack engine (vedere il pdf del microarch di Agner). Ciò include le recenti CPU AMD, grazie all'accordo di condivisione dei brevetti Intel / AMD.
Peter Cordes

@PeterCordes fantastico! Quindi i contatori delle prestazioni sono documentati da Intel per contare le micro-operazioni?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Inoltre, le variabili locali fuoriuscite da regs saranno tipicamente ancora calde nella cache L1 se qualcuno di loro viene effettivamente utilizzato. Ma la lettura da un registro è effettivamente gratuita, zero latenza. Quindi è infinitamente più veloce della cache L1, a seconda di come vuoi definire i termini. Per i locali di sola lettura riversati nello stack, il costo principale è solo un carico aggiuntivo (a volte operandi di memoria, a volte con movcarichi separati ). Per le variabili non const sparse, i round trip di inoltro del negozio sono un sacco di latenza extra (un ~ 5c extra rispetto all'inoltro diretto e le istruzioni del negozio non sono economiche).
Peter Cordes

Sì, ci sono contatori per gli uops totali in alcune diverse fasi della pipeline (emissione / esecuzione / ritiro), quindi puoi contare il dominio fuso o il dominio non fuso. Vedi questa risposta per esempio. Se dovessi riscrivere quella risposta ora, ocperf.pyuserei lo script wrapper per ottenere nomi simbolici facili per i contatori.
Peter Cordes

23

I registri push e popping sono dietro le quinte equivalenti a questo:

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

Nota che questa è la sintassi At & t x86-64.

Utilizzato in coppia, consente di salvare un registro nello stack e ripristinarlo in un secondo momento. Ci sono anche altri usi.


4
Sì, quelle sequenze emulano correttamente push / pop. (eccetto push / pop non influisce sui flag).
Peter Cordes

2
Faresti meglio a usare al lea rsp, [rsp±8]posto di add/ subper emulare meglio l'effetto di push/ popsui flag.
Ruslan

12

Quasi tutte le CPU utilizzano lo stack. Lo stack del programma è la tecnica LIFO con gestione supportata dall'hardware.

Lo stack è la quantità di memoria del programma (RAM) normalmente allocata nella parte superiore dell'heap di memoria della CPU e cresce (all'istruzione PUSH il puntatore dello stack viene diminuito) nella direzione opposta. Un termine standard per l'inserimento nello stack è PUSH e per rimuovere dallo stack è POP .

Lo stack è gestito tramite il registro della CPU previsto dallo stack , chiamato anche stack pointer, quindi quando la CPU esegue POP o PUSH il puntatore dello stack caricherà / memorizzerà un registro o una costante nella memoria dello stack e il puntatore dello stack verrà automaticamente diminuito xo aumentato in base al numero di parole spinte o inserito in (da) stack.

Tramite le istruzioni dell'assemblatore possiamo memorizzare per impilare:

  1. Registri della CPU e anche costanti.
  2. Indirizzi di ritorno per funzioni o procedure
  3. Funzioni / procedure in / out variabili
  4. Funzioni / procedure variabili locali.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.