Así que 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 cada vez que escribo una función uso esas 3 líneas (como aprendí al comparar el código de ensamblaje generado desde gcc con su equivalente C):

pushq   %rbp
movq    %rsp, %rbp
subq    $16, %rsp

Tengo 2 preguntas al respecto:

  1. En primer lugar, ¿por qué necesito usar %rbp? ¿No es más sencillo usar %rsp, ya que su contenido se mueve a %rbp en la segunda línea?
  2. ¿Por qué tengo que restar algo de %rsp? Quiero decir que no siempre es 16, cuando estaba printf usando 7 u 8 variables, entonces restaría 24 o 28.

Uso Manjaro de 64 bits en una máquina virtual (4 GB de RAM), procesador Intel de 64 bits

62
user6827707 28 ene. 2017 a las 20:29

2 respuestas

La mejor respuesta

rbp es el puntero de marco en x86_64. En su código generado, obtiene una instantánea del puntero de la pila (rsp) para que cuando se realicen ajustes en rsp (es decir, reservar espacio para variables locales o push ing valores en el pila), las variables locales y los parámetros de función aún son accesibles desde un desplazamiento constante desde rbp.

Muchos compiladores ofrecen la omisión de puntero de marco como una opción de optimización; esto hará que el código de ensamblado generado acceda a las variables relativas a rsp en su lugar y liberará rbp como otro registro de propósito general para su uso en funciones.

En el caso de GCC, que supongo que estás usando de la sintaxis del ensamblador de AT&T, ese conmutador es -fomit-frame-pointer. Intente compilar su código con ese modificador y vea qué código ensamblador obtiene. Probablemente notará que al acceder a valores relativos a rsp en lugar de rbp, el desplazamiento desde el puntero varía a lo largo de la función.

65
Govind Parmar 28 ene. 2017 a las 17:42

Linux usa el System V ABI para la arquitectura x86-64 (AMD64); vea System V ABI en OSDev Wiki para más detalles.

Esto significa que la pila crece ; 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 a 16 bytes. Si no se necesita espacio de pila para las variables locales, no hay subq $16, %rsp o una instrucción similar.

(Tenga en cuenta que la dirección de retorno y el %rbp anterior enviado a la pila tienen 8 bytes de tamaño, 16 bytes en total).

Mientras %rbp apunta al marco de la 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 patio de juegos de la función local: la región de pila que usa la función actual.

Las versiones actuales de GCC deshabilitan el marco de la pila cada vez que 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. (Sin embargo, puede usar, por ejemplo, -O2 -fno-omit-frame-pointer para mantener los cuadros de la pila y, de lo contrario, habilitar optimizaciones)

Aunque el mismo ABI se aplica a todos los archivos binarios, no importa en qué idioma estén escritos, ciertos otros idiomas necesitan marcos de pila para "desenrollar" (por ejemplo, "lanzar excepciones" a un llamador ancestral de la función actual); es decir, para "desenrollar" los marcos de la pila que una o más funciones pueden ser abortadas y el control pasa a alguna función ancestral, 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 fines, y su valor nunca se empuja a la pila), cada llamada a la función empuja solo la dirección de retorno a la pila, que es una cantidad de 8 bytes, por lo que necesita restar 8 de %rsp para mantener un múltiplo de 16. (En general, el valor restado y agregado a %rsp es un múltiplo impar de 8.)

Los parámetros de función generalmente se pasan en registros. Consulte el enlace ABI al comienzo de esta respuesta para obtener detalles, pero en resumen, los tipos y punteros integrales se pasan en los registros %rdi, %rsi, %rdx, %rcx, { {X4}} y %r9, con argumentos de punto flotante en los registros %xmm0 a %xmm7.

En algunos casos, verá rep ret en lugar de rep. No se confunda: rep ret significa exactamente lo mismo que ret; el prefijo 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 los predictores de ramificación de ciertos procesadores AMD no les gusta saltar a una instrucción ret, y la solución recomendada es usar un rep ret allí.

Finalmente, he omitido la zona roja sobre la parte superior de la pila (el 128 bytes en direcciones inferiores a %rsp). Esto se debe a que no es realmente útil para las funciones típicas: en el caso normal de tener-stack-frame, querrás que tus cosas locales estén dentro del stack stack, para que sea posible la depuración. En el caso omit-stack-frame, los requisitos de alineación de la pila ya significan que necesitamos restar 8 de %rsp, por lo que incluir la memoria necesaria para las variables locales en esa resta no cuesta nada.

46
Nikos Renieris 2 ene. 2018 a las 21:32