c - para - rlf ensamblador
¿Cuál es el propósito del registro RBP en el ensamblador x86_64? (2)
Entonces estoy tratando de aprender un poco de ensamblaje, porque lo necesito para la clase de Arquitectura de Computadores Escribí algunos programas, como imprimir la secuencia de Fibonacci.
Reconocí que siempre que escribo un programa, uso esas 3 líneas (como aprendí al comparar el código de ensamblaje generado desde gcc
con su equivalente en C
):
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
Tengo 2 preguntas al respecto:
- En primer lugar, ¿por qué uso
%rbp
? ¿No es más fácil usar%rsp
, ya que su contenido se mueve a%rbp
en la segunda línea? - ¿Por qué tengo que restar algo de
%rsp
? Quiero decir que no siempre son16
(cuando estabaprintf
la línea 7 u 8 variables, luego restaré24
o28
Uso Manjaro de 64 bits en una máquina virtual (4 GB de RAM), procesador Intel de 64 bits
Linux usa la arquitectura ABI de System V para x86-64 (AMD64); vea System V ABI en OSDev Wiki para más detalles.
Esto significa que la pila crece hacia abajo ; Las direcciones más pequeñas están "más arriba" en la pila. Las funciones típicas de C se compilan para
pushq %rbp ; Save address of previous stack frame
movq %rsp, %rbp ; Address of current stack frame
subq $16, %rsp ; Reserve 16 bytes for local variables
; ... function ...
movq %rbp, %rsp ; / equivalent to the
popq %rbp ; / ''leave'' instruction
ret
La cantidad de memoria reservada para las variables locales es siempre un múltiplo de 16 bytes, para mantener la pila alineada con 16 bytes. Si no se necesita espacio de pila para las variables locales, no hay subq $16, %rsp
o instrucción similar.
(Tenga en cuenta que la dirección de retorno y el %rbp
anterior %rbp
en la pila tienen un tamaño de 8 bytes, 16 bytes en total).
Mientras que %rbp
apunta al marco de pila actual, %rsp
apunta a la parte superior de la pila. Debido a que el compilador conoce la diferencia entre %rbp
y %rsp
en cualquier punto dentro de la función, es libre de usar cualquiera de ellos como base para las variables locales.
Un marco de pila es solo el campo de juego de la función local: la región de pila que utiliza la función actual.
Las versiones actuales de GCC deshabilitan el marco de pila cuando se utilizan optimizaciones. Esto tiene sentido, porque para los programas escritos en C, los marcos de pila son más útiles para la depuración, pero no mucho más. ( -O2 -fno-omit-frame-pointer
puede usar, por ejemplo, -O2 -fno-omit-frame-pointer
para mantener los cuadros de la pila mientras habilita optimizaciones)
Aunque el mismo ABI se aplica a todos los binarios, sin importar en qué idioma estén escritos, algunos otros idiomas sí necesitan marcos de pila para "desenrollar" (por ejemplo, para "lanzar excepciones" a un llamante ancestral de la función actual); es decir, para "desenrollar" los cuadros de pila, una o más funciones pueden abortarse y el control pasar a una función de antecesor, sin dejar cosas innecesarias en la pila.
Cuando se omiten los marcos de pila - -fomit-frame-pointer
para GCC -, la implementación de la función cambia esencialmente a
subq $8, %rsp ; Re-align stack frame, and
; reserve memory for local variables
; ... function ...
addq $8, %rsp
ret
Debido a que no hay un marco de pila ( %rbp
se usa para otros propósitos, y su valor nunca se empuja a la pila), cada llamada de función empuja solo la dirección de retorno a la pila, que es una cantidad de 8 bytes, por lo que necesitamos restar 8 de %rsp
para mantenerlo como un múltiplo de 16. (En general, el valor que se resta y agrega a %rsp
es un múltiplo impar de 8.)
Los parámetros de la función se pasan típicamente en los registros. Vea el enlace ABI al principio de esta respuesta para obtener detalles, pero en resumen, los tipos integrales y los punteros se pasan en los registros %rdi
, %rsi
, %rdx
, %rcx
, %r8
y %r9
, con argumentos de punto flotante en los %xmm0
a %xmm7
.
En algunos casos, verás rep ret
lugar de rep
. No se confunda: la rep ret
significa exactamente lo mismo que ret
; el prefijo de rep
, aunque normalmente se usa con instrucciones de cadena (instrucciones repetidas), no hace nada cuando se aplica a la instrucción ret
. Es solo que a algunos predictores de rama de los procesadores de AMD no les gusta saltar a una instrucción ret
, y la solución recomendada es usar una rep ret
allí.
Finalmente, omití la zona roja sobre la parte superior de la pila (los 128 bytes en las direcciones menos de %rsp
). Esto se debe a que no es realmente útil para las funciones típicas: en el caso normal de tener un marco de pila, querrás que tus cosas locales estén dentro del marco de la pila, para hacer posible la depuración. En el caso de omit-stack-frame, los requisitos de alineación de la pila ya significan que necesitamos restar 8 de %rsp
, por lo que no cuesta nada incluir la memoria que necesitan las variables locales en esa resta.
rbp
es el puntero de marco en x86_64. En su código generado, obtiene una instantánea del puntero de la pila ( rsp
), de modo que cuando se realizan ajustes a rsp
(es decir, se reserva espacio para las variables locales o se insertan valores en la pila), las variables locales y los parámetros de función aún son accesibles desde un desplazamiento constante de rbp
.
Una gran cantidad de compiladores ofrecen omisión de puntero de marco como una opción de optimización; esto hará que las variables de acceso al código de ensamblaje generadas sean relativas a rsp
y liberará a rbp
como otro registro de propósito general para uso en funciones.
En el caso de GCC, que supongo que estás usando de la sintaxis del ensamblador de AT&T, ese interruptor es -fomit-frame-pointer
. Intente compilar su código con ese interruptor y vea qué código de ensamblaje obtiene. Probablemente notará que al acceder a valores relativos a rsp
lugar de rbp
, el desplazamiento del puntero varía a lo largo de la función.