¿Cuál es la función de las instrucciones push / pop que se utilizan en los registros en el ensamblaje x86?

101

Al leer acerca de ensamblador a menudo me encuentro con gente que escribe que empujan un cierto registro del procesador y el pop de nuevo más tarde para restaurar su estado anterior.

  • ¿Cómo se puede presionar un registro? ¿Dónde se empuja? ¿Por qué es necesario?
  • ¿Esto se reduce a una sola instrucción de procesador o es más complejo?
Ars emble
fuente
3
Advertencia: todas las respuestas actuales se dan en la sintaxis de ensamblado de Intel; push-pop en la sintaxis de AT & T, por ejemplo, utiliza un post-fix como b, w, l, o qpara indicar el tamaño de la memoria que se está manipulado. Ej .: pushl %eaxypopl %eax
Hawken
5
@hawken En la mayoría de los ensambladores capaces de asimilar la sintaxis de AT&T (especialmente gas), el sufijo de tamaño puede omitirse si el tamaño del operando se puede deducir del tamaño del operando. Este es el caso de los ejemplos que ha proporcionado, ya %eaxque siempre tiene un tamaño de 32 bits.
Gunther Piez

Respuestas:

153

presionar un valor (no necesariamente almacenado en un registro) significa escribirlo en la pila.

hacer estallar significa restaurar lo que esté en la parte superior de la pila en un registro. Esas son instrucciones básicas:

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
Linus Kleen
fuente
5
El operando explícito para push y pop es r/m, no solo registrarse, por lo que puede hacerlo push dword [esi]. O incluso pop dword [esp]para cargar y luego almacenar el mismo valor en la misma dirección. ( github.com/HJLebbink/asm-dude/wiki/POP ). Solo menciono esto porque dices "no necesariamente un registro".
Peter Cordes
2
También puede hacer popposible un espacio de memoria:pop [0xdeadbeef]
SS Anne
Hola, ¿cuál es la diferencia entre push / pop y pushq / popq? Estoy en macos / intel
SteakOverflow
46

Así es como empuja un registro. Supongo que estamos hablando de x86.

push ebx
push eax

Se coloca en la pila. El valor del ESPregistro se reduce al tamaño del valor empujado a medida que la pila crece hacia abajo en los sistemas x86.

Es necesario preservar los valores. El uso general es

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 pushes una sola instrucción en x86, que hace dos cosas internamente.

  1. Disminuya el ESPregistro por el tamaño del valor empujado.
  2. Almacene el valor introducido en la dirección actual del ESPregistro.
Madhur Ahuja
fuente
@vavan acaba de enviar una solicitud para que se solucione
jgh fun-run
38

¿Dónde se empuja?

esp - 4. Más precisamente:

  • esp se resta de 4
  • el valor se empuja a esp

pop invierte esto.

El System V ABI le dice a Linux que rspseñale una ubicación de pila sensible cuando el programa comienza a ejecutarse: ¿Cuál es el estado de registro predeterminado cuando se inicia el programa (asm, linux)? que es lo que debería utilizar habitualmente.

¿Cómo se puede presionar un registro?

Ejemplo mínimo de 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.
    */

Lo anterior en GitHub con aserciones ejecutables .

¿Por qué es necesario?

Es cierto que esas instrucciones podrían implementarse fácilmente a través de mov, addy sub.

La razón por la que existen, es que esas combinaciones de instrucciones son tan frecuentes, que Intel decidió proporcionárnoslas.

La razón por la que esas combinaciones son tan frecuentes es que facilitan guardar y restaurar los valores de los registros en la memoria temporalmente para que no se sobrescriban.

Para comprender el problema, intente compilar algo de código C a mano.

Una dificultad importante es decidir dónde se almacenará cada variable.

Idealmente, todas las variables encajarían en los registros, que es la memoria más rápida para acceder (actualmente alrededor de 100 veces más rápido que la RAM).

Pero, por supuesto, podemos tener fácilmente más variables que registros, especialmente para los argumentos de funciones anidadas, por lo que la única solución es escribir en la memoria.

Podríamos escribir en cualquier dirección de memoria, pero dado que las variables locales y los argumentos de las llamadas a funciones y los retornos encajan en un patrón de pila agradable, que evita la fragmentación de la memoria , esa es la mejor manera de lidiar con eso. Compare eso con la locura de escribir un asignador de montones.

Luego dejamos que los compiladores optimicen la asignación de registros para nosotros, ya que es NP completo y una de las partes más difíciles de escribir un compilador. Este problema se denomina asignación de registros y es isomorfo para colorear los gráficos .

Cuando el asignador del compilador se ve obligado a almacenar cosas en la memoria en lugar de solo registros, eso se conoce como derrame .

¿Esto se reduce a una sola instrucción de procesador o es más complejo?

Todo lo que sabemos con certeza es que Intel documenta una pushy una popinstrucción, por lo que son una instrucción en ese sentido.

Internamente, podría expandirse a múltiples microcódigos, uno para modificar espy otro para hacer la E / S de memoria, y tomar múltiples ciclos.

Pero también es posible que una sola pushsea ​​más rápida que una combinación equivalente de otras instrucciones, ya que es más específica.

Esto está en su mayoría sub (der) documentado:

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fuente
4
No necesita adivinar cómo push/ popdecodificar en uops. Gracias a los contadores de rendimiento, es posible realizar pruebas experimentales, y Agner Fog lo ha hecho y ha publicado tablas de instrucciones . Las CPU Pentium-M y posteriores tienen un solo uop push/ popgracias al motor de pila (consulte el pdf del microarchivo de Agner). Esto incluye CPU AMD recientes, gracias al acuerdo de intercambio de patentes entre Intel / AMD.
Peter Cordes
@PeterCordes ¡increíble! Entonces, ¿Intel documenta los contadores de rendimiento para contar las microoperaciones?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
Además, las variables locales extraídas de los registros normalmente seguirán estando activas en la caché L1 si alguna de ellas se está utilizando. Pero leer desde un registro es efectivamente gratis, latencia cero. Por lo tanto, es infinitamente más rápido que la caché L1, dependiendo de cómo desee definir los términos. Para los locales de solo lectura que se derraman en la pila, el costo principal es solo uops de carga adicional (a veces operandos de memoria, a veces con movcargas separadas ). Para las variables no constantes que se derraman, los viajes de ida y vuelta de reenvío a la tienda tienen mucha latencia adicional (un ~ 5c adicional frente al reenvío directo, y las instrucciones de la tienda no son baratas).
Peter Cordes
Sí, hay contadores de uops totales en algunas etapas de canalización diferentes (emitir / ejecutar / retirar), por lo que puede contar el dominio fusionado o el dominio no fusionado. Vea esta respuesta por ejemplo. Si estuviera reescribiendo esa respuesta ahora, usaría el ocperf.pyscript de envoltura para obtener nombres simbólicos fáciles para los contadores.
Peter Cordes
23

Los registros de empuje y estallido están detrás de escena equivalentes a esto:

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

Tenga en cuenta que esta es la sintaxis x86-64 de At & t.

Usado como un par, esto le permite guardar un registro en la pila y restaurarlo más tarde. También hay otros usos.

gowrath
fuente
4
Sí, esas secuencias emulan correctamente push / pop. (excepto que push / pop no afecta a las banderas).
Peter Cordes
2
Será mejor que use en lea rsp, [rsp±8]lugar de add/ subpara emular mejor el efecto de push/ popen las banderas.
Ruslan
12

Casi todas las CPU usan pila. La pila de programas es la técnica LIFO con gestión de hardware compatible.

La pila es la cantidad de memoria de programa (RAM) que normalmente se asigna en la parte superior del montón de memoria de la CPU y crece (en la instrucción PUSH, el puntero de la pila disminuye) en la dirección opuesta. Un término estándar para insertar en la pila es PUSH y para quitar de la pila es POP .

La pila se gestiona a través del registro de la CPU previsto en la pila, también llamado puntero de pila, por lo que cuando la CPU realiza POP o PUSH, el puntero de pila cargará / almacenará un registro o constante en la memoria de pila y el puntero de pila disminuirá automáticamente o aumentará según el número de palabras empujadas o aparecieron en (desde) la pila.

A través de las instrucciones del ensamblador, podemos almacenar para apilar:

  1. Registros de CPU y también constantes.
  2. Direcciones de retorno para funciones o procedimientos
  3. Funciones / procedimientos variables de entrada / salida
  4. Funciones / procedimientos variables locales.
GJ.
fuente