¿Cuál es la función de las instrucciones push / pop usadas en los registros del ensamblaje x86?

Cuando leo sobre ensamblador, a menudo me encuentro con personas que escriben que presionan un cierto registro del procesador y lo vuelven a abrir más tarde para restaurar su estado anterior.

  • ¿Cómo puedes presionar un registro? ¿Dónde se empuja? ¿Por qué es esto necesario?
  • ¿Esto se reduce a una sola instrucción de procesador o es más complejo?

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

Hacer estallar significa restaurar todo lo que está en la parte superior de la stack 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 

Aquí es cómo empujas un registro. Supongo que estamos hablando de x86.

 push ebx push eax 

Se empuja en la stack. El valor del registro ESP se reduce al tamaño del valor presionado a medida que la stack crece hacia abajo en los sistemas x86.

Es necesario para 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 ; restre original eax 

Un push es una instrucción única en x86, que hace dos cosas internamente.

  1. Almacene el valor presionado en la dirección actual del registro ESP .
  2. Disminuya el registro ESP al tamaño del valor presionado.

¿Dónde se empuja?

esp - 4 . Más precisamente:

  • esp es restado por 4
  • el valor se empuja a esp

pop invierte esto.

El sistema V ABI le dice a Linux que haga que rsp apunte a una ubicación de stack sensible cuando el progtwig comience a ejecutarse: https://stackoverflow.com/a/32967009/895245, que es lo que generalmente debería usar.

¿Cómo puedes 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 esto necesario?

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

Ellos razonan que existen, es que esas combinaciones de instrucciones son tan frecuentes, que Intel decidió proporcionarlas para nosotros.

La razón por la cual esas combinaciones son tan frecuentes es que hacen que sea más fácil guardar y restaurar temporalmente los valores de los registros en la memoria para que no se sobrescriban.

Para comprender el problema, intente comstackr un código C a mano.

Una gran dificultad 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 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 de función y devoluciones encajan en un buen patrón de stack, lo que evita la fragmentación de la memoria , esa es la mejor manera de manejarlo. Compare eso con la locura de escribir un asignador de montón.

Luego permitimos que los comstackdores optimicen la asignación de registros para nosotros, ya que es NP completa y una de las partes más difíciles de escribir un comstackdor. Este problema se llama asignación de registros , y es isomorfo para graficar coloreando .

Cuando el asignador del comstackdor se ve obligado a almacenar cosas en la memoria en lugar de solo registros, eso se conoce como un 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 instrucción push y pop , por lo que son una instrucción en ese sentido.

Internamente, podría expandirse a múltiples microcódigos, uno para modificar esp y uno para hacer la memoria IO, y tomar múltiples ciclos.

Pero también es posible que un solo push sea ​​más rápido que una combinación equivalente de otras instrucciones, ya que es más específico.

Esto es mayormente un (der) documentado:

Casi todas las CPU usan la stack. La stack de progtwigs es una técnica LIFO con administración compatible con hardware.

La stack es la cantidad de memoria del progtwig (RAM) normalmente asignada en la parte superior del montón de la memoria de la CPU y crece (en la instrucción PUSH el puntero de la stack se reduce) en dirección opuesta. Un término estándar para insertar en la stack es PUSH y para eliminar de la stack es POP .

La stack se gestiona mediante stack stack CPU register, también llamada stack puntero, de modo que cuando la CPU realiza POP o PUSH, el puntero de stack cargará / almacenará un registro o constante en memoria de stack y el puntero de la stack disminuirá automáticamente o boostá según el número de palabras presionadas o astackdo en (de) la stack.

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

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

Los registros de empujar y hacer estallar están detrás de escenas 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 esto es x86-64 At & t syntax.

Usado como un par, esto le permite guardar un registro en la stack y restaurarlo más tarde. Hay otros usos, también.