conceptAssembly y Salida del Compilador~5 min de lecturaActualizado 2026-06-30#assembly#x86-64#stack-frame#prologue#epilogue

Stack frames: prólogo y epílogo de función

Un stack frame es la parte del stack que pertenece a una llamada activa de función. Ahí una función puede guardar el frame pointer del caller, reservar storage local, spillar registros y preservar el estado que debe restaurar antes de volver. El prólogo construye ese frame; el epílogo lo desarma. Leer esos dos bordes te permite separar mecánica de call frame del cálculo real.

El reset: un stack frame no es un objeto de C. Es una región de memoria con forma de ABI alrededor de rsp, muchas veces anclada por rbp, que existe solo durante una llamada dinámica.

Cómo funciona realmente

La forma clásica x86-64 con frame pointer es:

push    rbp
mov     rbp, rsp
sub     rsp, N

Y antes de volver:

add     rsp, N
pop     rbp
ret

Leelo como un protocolo chico:

Instrucción Rol
push rbp guarda el frame pointer del caller
mov rbp, rsp crea una base estable para este frame
sub rsp, N reserva N bytes de espacio local/spill
add rsp, N libera ese espacio local/spill
pop rbp restaura el frame pointer del caller
ret popea la return address hacia rip

El compilador puede omitir parte de esto en código optimizado. Una leaf function mínima puede no necesitar storage de stack. Una función compilada omitiendo frame pointer puede usar rsp directamente y describir unwinding con metadata. Pero cuando ves esta forma, estás viendo el contrato de entrada y salida de la función escrito en instrucciones.

Un prólogo y epílogo reales

El artefacto ejecutable de esta nota vive en examples/assembly-and-compiler-output/stack-frames-function-prologue-and-epilogue/.

static NOINLINE long add_bias(long value) {
    return value + 7;
}

long framed_total(long a, long b) {
    volatile long local = a + b;
    long adjusted = add_bias(local);

    return adjusted + local;
}

volatile fuerza un slot visible de stack para local. La llamada a add_bias evita que toda la función colapse en una sola expresión aritmética.

En esta máquina, gcc es Apple clang 15 apuntando a Mach-O x86-64. Este es el cuerpo relevante de la función copiado de la salida real de:

gcc -S -O2 -fno-omit-frame-pointer -masm=intel demo.c -o demo.s
_framed_total:                          ## @framed_total
    .cfi_startproc
## %bb.0:
    push    rbp
    .cfi_def_cfa_offset 16
    .cfi_offset rbp, -16
    mov rbp, rsp
    .cfi_def_cfa_register rbp
    sub rsp, 16
    add rdi, rsi
    mov qword ptr [rbp - 8], rdi
    mov rdi, qword ptr [rbp - 8]
    call    _add_bias
    add rax, qword ptr [rbp - 8]
    add rsp, 16
    pop rbp
    ret
    .cfi_endproc

Mapeá primero los argumentos: a llega en rdi, b llega en rsi, y el valor de retorno sale en rax.

Ahora separá mecánica de negocio:

  • push rbp; mov rbp, rsp establece un frame pointer visible.
  • sub rsp, 16 reserva espacio de stack. Acá se ven 8 bytes usados, pero las reglas de alineación de la ABI hacen que 16 sea una reserva natural.
  • add rdi, rsi computa a + b.
  • mov qword ptr [rbp - 8], rdi guarda local en el frame actual.
  • mov rdi, qword ptr [rbp - 8] recarga local como primer argumento de add_bias.
  • call _add_bias pushea una return address y transfiere control.
  • add rax, qword ptr [rbp - 8] suma local al valor devuelto por el helper.
  • add rsp, 16; pop rbp; ret libera el frame y vuelve al caller.

Las directivas .cfi_* no son instrucciones de CPU. Son metadata de unwind para debuggers, profilers, maquinaria de excepciones y stack walkers. Describen dónde está la canonical frame address y los registros guardados mientras la función corre.

El helper tiene un frame más chico porque no tiene slot local de stack:

_add_bias:                              ## @add_bias
    .cfi_startproc
## %bb.0:
    push    rbp
    .cfi_def_cfa_offset 16
    .cfi_offset rbp, -16
    mov rbp, rsp
    .cfi_def_cfa_register rbp
    lea rax, [rdi + 7]
    pop rbp
    ret
    .cfi_endproc

El patrón es el mismo, pero el medio es solo lea rax, [rdi + 7]. No aparece sub rsp porque no hay storage local de stack para reservar.

Frame pointer versus stack pointer

rsp es el tope móvil del stack. Cambia cuando la función reserva storage, pushea valores temporales, llama a otra función o libera storage. rbp es un ancla estable opcional. Con frame pointer, un local como [rbp - 8] conserva la misma dirección aunque rsp cambie después.

Por eso los debug builds suelen preservar rbp: recorrer el stack e inspeccionar locals es directo. Los optimized builds muchas veces lo omiten para liberar otro registro general y se apoyan en unwind tables. El código sigue siendo válido; solo es menos prolijo visualmente.

Apéndice ARM64

El mismo demo.c se cross-compiló en esta máquina con:

clang -S -O2 -fno-omit-frame-pointer -arch arm64 demo.c -o demo.arm64.s

Cuerpo relevante de la función:

_framed_total:                          ; @framed_total
    .cfi_startproc
; %bb.0:
    sub sp, sp, #32
    .cfi_def_cfa_offset 32
    stp x29, x30, [sp, #16]             ; 16-byte Folded Spill
    add x29, sp, #16
    .cfi_def_cfa w29, 16
    .cfi_offset w30, -8
    .cfi_offset w29, -16
    add x8, x1, x0
    str x8, [sp, #8]
    ldr x0, [sp, #8]
    bl  _add_bias
    ldr x8, [sp, #8]
    add x0, x8, x0
    ldp x29, x30, [sp, #16]             ; 16-byte Folded Reload
    add sp, sp, #32
    ret
    .cfi_endproc

AArch64 usa sp como stack pointer, x29 como frame pointer convencional y x30 como link register. bl _add_bias escribe la return address en x30, así que una función que llama a otra normalmente guarda x29 y x30 juntos con stp. El epílogo recarga ambos con ldp, libera stack y vuelve con ret.

La misma idea está visible, pero cambia el vocabulario: x86-64 usa call para pushear una return address al stack; ARM64 usa link register y lo spilla cuando hace falta.

Modos de falla y trade-offs

  • Asumir que toda función tiene frame visible. Leaf functions optimizadas pueden no tener prólogo, y omitir frame pointer elimina la cadena prolija de rbp.
  • Olvidar la alineación del stack. El monto de sub rsp, N no habla solo de locals con nombre. También mantiene la alineación que esperan la ABI y las llamadas.
  • Confundir .cfi_* con instrucciones ejecutables. Las directivas CFI importan para unwinding, pero no corren en la CPU como parte del cuerpo de la función.
  • Ignorar salidas alternativas. Funciones reales pueden tener múltiples returns, caminos de error o tailcalls. Cada camino debe dejar stack y registros callee-saved en estado válido.
  • Usar red zone donde no corresponde. System V AMD64 user code puede usar red zone, pero kernels y código con interrupciones normalmente no pueden depender de eso.
  • Tratar bytes de stack como lifetime segura. Cuando la función vuelve, su frame ya no te pertenece. Los punteros hacia ahí quedan dangling.

En la práctica

  • Encontrá primero el prólogo. Te dice si hay frame pointer y cuánto stack se reserva.
  • Nombrá slots locales por offset. [rbp - 8] es un slot, no un nombre de variable. El nombre mental sale de stores y loads cercanos.
  • Separá bookkeeping de ABI del cálculo. push, mov rbp, rsp, sub rsp, CFI y el epílogo son mecánica de frame.
  • Chequeá calls antes de returns. Un call real pushea una return address y hace que la alineación del stack importe.
  • Leelo junto al modelo crudo de stack. El frame es la versión estructurada de las operaciones de stack a nivel assembly.

Conecta con: El stack a nivel assembly · La calling convention System V AMD64 · Control flow: jumps, condiciones y el registro de flags · El stack: frames y automatic storage · Stack vs heap

Fuentes

  • System V AMD64 ABI — alineación de stack, reglas de call frame, red zone, register saving y expectativas de unwind. https://gitlab.com/x86-psABIs/x86-64-ABI
  • Intel 64 and IA-32 Architectures Software Developer's Manual, Vol. 1 y Vol. 2 — semántica de push, pop, call, ret, addressing de stack e instrucciones relacionadas con frames. https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
  • Bryant & O'Hallaron — Computer Systems: A Programmer's Perspective (CS:APP), cap. 3 — llamadas a procedimiento, stack frames, register saving y C a nivel máquina. https://csapp.cs.cmu.edu/
  • Ed Jorgensen — x86-64 Assembly Language Programming with Ubuntu — ejemplos accesibles de stack frames x86-64. https://open.umn.edu/opentextbooks/textbooks/x86-64-assembly-language-programming-with-ubuntu
  • Felix Cloutier — x86 and amd64 instruction reference — lookup rápido de push, pop, call, ret y leave; verificá edge cases contra Intel SDM. https://www.felixcloutier.com/x86/
  • Arm Architecture Reference Manual + Apple ARM64 docssp, x29, x30, bl, ret y detalles de calling convention de plataformas Apple para la salida del apéndice. https://developer.arm.com/documentation/ddi0487/latest · https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms