← Gestión de Memoria Bloque 2 · La pila y el heap · Tema 6 de 52

Tema 6

La pila y los marcos de activación

Cada vez que un programa llama a una función, el sistema guarda en la pila todo lo necesario para volver: la dirección de retorno, los parámetros y las variables locales. En este tema verás qué es un marco de activación, cómo crece y decrece la pila, qué ocurre en una recursión y cómo se manifiesta un desbordamiento de pila. Es la base para entender la pila en C y los errores más comunes.

1. Qué es la pila

La pila (stack) es una región de memoria que el proceso usa para guardar datos temporales ligados a la ejecución de funciones: parámetros, variables locales, dirección de retorno y registros salvados. Se llama "pila" porque funciona como una pila de platos: el último en entrar es el primero en salir (LIFO, last in, first out).

En C, cada vez que llamas a una función, se crea un marco de activación (stack frame) en la pila. Cuando la función termina, ese marco se destruye y la pila vuelve al estado anterior. Esto hace que la memoria de las variables locales se reutilice automáticamente.

Pila (stack) es la región de memoria que almacena los marcos de activación de las funciones en ejecución, con crecimiento hacia direcciones decrecientes y comportamiento LIFO.

2. El marco de activación

Un marco de activación contiene, típicamente:

El tamaño del marco depende del número y tipo de variables locales y parámetros. Un marco mínimo puede ser de unas pocas decenas de bytes; uno con arrays locales puede ser de varios KiB.

La pila crece hacia direcciones decrecientes; cada llamada añade un marco Marco de main Marco de f1 Marco de f2 Marco de f3 (actual) altas bajas SP (puntero)
La pila crece hacia abajo (direcciones decrecientes). El puntero de pila (SP) apunta al marco actual. Al entrar a una función se resta SP; al salir se restaura.

3. Cómo crece y decrece

En la mayoría de las arquitecturas (x86-64 incluido), la pila crece hacia direcciones decrecientes. El puntero de pila (SP, %rsp en x86-64) apunta al tope de la pila (la dirección más baja usada). Al entrar a una función, el compilador resta espacio a SP para reservar el marco; al salir, lo restaura.

Por eso, en C, las direcciones de variables locales de funciones anidadas son cada vez menores: la pila crece hacia abajo.

4. Una llamada paso a paso

Veamos qué ocurre cuando main llama a f(5):

  1. main prepara los argumentos. Si caben en registros (hasta 6 en x86-64), no toca la pila. Si no, los empuja.
  2. main ejecuta call f. La instrucción guarda la dirección de retorno en la pila (push) y salta a f.
  3. f guarda el marco anterior. Empuja el puntero de marco (push rbp) y establece rbp = rsp.
  4. f reserva espacio para locales. Resta a rsp el tamaño necesario.
  5. f ejecuta su código. Accede a locales mediante rbp - desplazamiento.
  6. Al retornar: f restaura rsp (= rbp), recupera rbp anterior (pop), y ret saca la dirección de retorno y salta a ella.

Todo esto es generado por el compilador. El programador no lo ve, pero explica por qué las variables locales "desaparecen" al salir de la función y por qué la recursión consume pila.

5. Variables locales y su dirección

Las variables locales viven en el marco de la función que las declara. Por eso su dirección cambia en cada llamada y se reutiliza al retornar. Dos llamadas a la misma función tendrán las mismas direcciones para sus locales (si no hay recursión), porque el marco se reutiliza.

pila.c
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>

void f(int n)
{
    int local = n * 2;
    printf("f(%d): &local = %p, &n = %p\n", n, (void *)&local, (void *)&n);
    if (n > 0) f(n - 1);
    printf("f(%d): retorno\n", n);
}

int main(void)
{
    int main_local = 0;
    printf("main: &main_local = %p\n", (void *)&main_local);
    f(3);
    return 0;
}
terminal · direcciones en la pila
$ gcc -std=c17 -Wall -Wextra -o pila pila.c
$ ./pila
main: &main_local = 0x7ffe2c3a1f2c
f(3): &local = 0x7ffe2c3a1f0c, &n = 0x7ffe2c3a1f10
f(2): &local = 0x7ffe2c3a1eec, &n = 0x7ffe2c3a1ef0
f(1): &local = 0x7ffe2c3a1ecc, &n = 0x7ffe2c3a1ed0
f(0): &local = 0x7ffe2c3a1eac, &n = 0x7ffe2c3a1eb0
f(0): retorno
f(1): retorno
f(2): retorno
f(3): retorno

Las direcciones de &local y &n disminuyen en cada nivel: la pila crece hacia abajo. Cada llamada reserva unos 32 bytes de marco. Al retornar, el marco se libera y la siguiente llamada (si la hubiera) reutilizaría el mismo espacio.

6. Recursión y desbordamiento de pila

La recursión es el caso en que una función se llama a sí misma. Cada llamada crea un nuevo marco, así que la profundidad de la recursión está limitada por el tamaño de la pila. Si se excede, se produce un desbordamiento de pila (stack overflow) y el programa termina con SIGSEGV.

El tamaño típico de la pila del hilo principal en Linux es 8 MiB. Un marco de 100 bytes permite ~80.000 niveles de recursión; un marco de 10 KiB (con arrays locales grandes) solo ~800.

Stack overflow no es un error de memoria "normal"

El desbordamiento de pila ocurre cuando el puntero de pila sale de la región reservada. El núcleo detecta el acceso a la página guardia (sin permisos) y envía SIGSEGV. No hay "memoria insuficiente" recuperable: el programa muere.

7. Experimento: recursión y límite

El programa siguiente mide cuántos niveles de recursión soporta antes de desbordar la pila, usando un marco de tamaño controlado:

recursion.c
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>

static void rec(int n, char buf[1024])
{
    buf[0] = (char)n;   /* fuerza a reservar el marco */
    if (n % 1000 == 0)
        printf("nivel %d: &buf = %p\n", n, (void *)buf);
    rec(n + 1, buf);
}

int main(void)
{
    char buf[1024];
    rec(0, buf);
    return 0;
}
terminal · desbordamiento de pila
$ gcc -std=c17 -Wall -Wextra -o recursion recursion.c
$ ./recursion
nivel 0:    &buf = 0x7ffe2c3a1a80
nivel 1000: &buf = 0x7ffe2c39da80
nivel 2000: &buf = 0x7ffe2c399a80
...
nivel 7000: &buf = 0x7ffe2c389a80
Segmentation fault

Con un marco de ~1 KiB, la pila de 8 MiB soporta unos 7.000-8.000 niveles. Cada 1000 niveles la dirección baja ~4 KiB, confirmando el crecimiento hacia abajo. Al superar el límite, el acceso a la página guardia provoca SIGSEGV.

8. El tamaño de la pila

El tamaño de la pila del hilo principal se establece al crear el proceso y se puede consultar y cambiar:

MecanismoQué controla
ulimit -sTamaño máximo de la pila del shell y sus hijos (en KiB). Por defecto 8192 (8 MiB).
setrlimit(RLIMIT_STACK, ...)Lo mismo desde un programa, antes de crear hilos.
pthread_attr_setstacksizeTamaño de la pila de un hilo creado con pthreads (por defecto mucho menor, a menudo 2-8 MiB o el mínimo del sistema).
/proc/PID/limitsMuestra los límites actuales del proceso, incluido Max stack size.

Los hilos creados con pthreads tienen su propia pila, mucho más pequeña que la del hilo principal. Por eso una recursión profunda en un hilo puede desbordar con menos niveles.

9. Probar con un simulador

Este simulador muestra cómo crece la pila con llamadas anidadas: cada llamada añade un marco y el puntero de pila (SP) disminuye. Puedes elegir el tamaño del marco y el número de llamadas, y ver si se desborda el límite de 8 MiB.

Simulador · crecimiento de la pila

MedidaValor
Memoria usada por la pila
Límite
SP final (relativo)
¿Desborda?

10. Mirar la pila en Linux

terminal · pila y límites
$ ulimit -s
8192
$ cat /proc/self/limits | grep -i stack
Max stack size        8388608    8388608    bytes
$ cat /proc/self/maps | grep stack
7ffe2c380000-7ffe2c3a2000 rw-p 00000000 00:00 0  [stack]
$ ulimit -s 1024; ./recursion   # pila de 1 MiB
$ gdb ./recursion
(gdb) run
...
(gdb) bt   # backtrace: lista de marcos

11. Resumen

12. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es un marco de activación?

El bloque de memoria que se crea en la pila al llamar a una función, con la dirección de retorno, los parámetros, las variables locales y los registros salvados.

2. ¿En qué dirección crece la pila?

Hacia direcciones decrecientes (hacia abajo) en x86-64 y en la mayoría de las arquitecturas.

3. ¿Qué ocurre en un desbordamiento de pila?

El puntero de pila sale de la región reservada y accede a la página guardia (sin permisos), provocando SIGSEGV y la terminación del programa.

4. ¿Cuánto suele medir la pila del hilo principal en Linux?

8 MiB por defecto (configurable con ulimit -s).

5. ¿Por qué la recursión puede desbordar la pila?

Porque cada llamada recursiva crea un nuevo marco; si la profundidad es mayor de la que cabe en la pila, se desborda.

6. ¿Qué muestra gdb con el comando bt?

El backtrace: la lista de marcos de activación actuales, desde la función actual hasta main.

7. ¿Los hilos creados con pthreads tienen la misma pila que el hilo principal?

No: cada hilo tiene su propia pila, normalmente más pequeña que la del hilo principal.

8. ¿Qué es el puntero de marco (frame pointer)?

Un registro (rbp en x86-64) que apunta a la base del marco actual, usado para acceder a locales y restaurar la pila al salir.

Para practicar

  1. Modifica pila.c para imprimir las direcciones de dos variables locales de una función llamada dos veces (no recursiva). ¿Son iguales?
  2. Compila recursion.c con -O2 y sin optimización. ¿Cambia el número de niveles soportados? ¿Por qué?
  3. Ejecuta ulimit -s 512; ./recursion. ¿Cuántos niveles soporta ahora?
  4. Usa gdb para ejecutar recursion y, cuando se desborde, ejecuta bt. ¿Cuántos marcos muestra?
  5. Modifica recursion.c para que la función no tenga array local (solo un int). ¿Cuántos niveles más soporta?
  6. Crea un hilo con pthread_create que ejecute una recursión profunda. ¿Desborda antes que en el hilo principal? (Ajusta el tamaño de pila con pthread_attr_setstacksize.)
  7. Inspecciona /proc/PID/maps de un programa con recursión mientras está parado. ¿Dónde está la pila y cuánto mide la región?