assembly - instruction - ¿Cuál es la función de las instrucciones push/pop usadas en los registros del ensamblaje x86?
push assembler (5)
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?
Aquí es cómo empujas un registro. Supongo que estamos hablando de x86.
push ebx
push eax
Se empuja en la pila. El valor del registro ESP
se reduce al tamaño del valor presionado a medida que la pila 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 ; restore original eax
Un push
es una instrucción única en x86, que hace dos cosas internamente.
- Almacene el valor presionado en la dirección actual del registro
ESP
. - Disminuya el registro
ESP
al tamaño del valor presionado.
Casi todas las CPU usan la pila. La pila de programas es una técnica LIFO con administración compatible con hardware.
La pila es la cantidad de memoria del programa (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 pila se reduce) en dirección opuesta. Un término estándar para insertar en la pila es PUSH y para eliminar de la pila es POP .
La pila 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 pila cargará / almacenará un registro o constante en memoria de pila y el puntero de la pila disminuirá automáticamente o aumentará según el número de palabras presionadas o apilado en (de) la pila.
A través de las instrucciones del ensamblador podemos almacenar para apilar:
- Registros de CPU y también constantes.
- Direcciones de retorno para funciones o procedimientos
- Funciones / procedimientos entrada / salida variables
- 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 sintaxis.
Usado como un par, esto le permite guardar un registro en la pila y restaurarlo más tarde. Hay otros usos, también.
presionar un valor (no necesariamente almacenado en un registro) significa escribirlo en la pila.
Hacer estallar significa restaurar todo 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
¿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 pila sensible cuando el programa comience a ejecutarse: https://.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 con aserciones .
¿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 compilar 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 pila, 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 compiladores 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 compilador. Este problema se llama asignación de registros , y es isomorfo para graficar coloreando .
Cuando el asignador del compilador 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:
- Peter Cordes menciona que las técnicas descritas en http://agner.org/optimize/microarchitecture.pdf sugieren que
push
ypop
toman una sola microoperación. - Johan menciona que, dado que el Pentium M Intel usa un "motor de pila", que almacena los valores esp + regsize y esp-regsize precomputados, lo que permite ejecutar push y pop en un único uop. También mencionado en: https://en.wikipedia.org/wiki/Stack_register
- ¿Qué es el microcódigo Intel?
- https://security.stackexchange.com/questions/29730/processor-microcode-manipulation-to-change-opcodes
- ¿Cuántos ciclos de CPU se necesitan para cada instrucción de ensamblaje?