La memoria de un programa es un espacio finito dividido en regiones. El Stack maneja la ejecucion: llamadas, retornos, variables locales. El Heap almacena datos dinamicos que sobreviven a las funciones.
Stack (Pila)
Crece hacia abajo. Cada llamada a funcion crea un frame con variables locales y direccion de retorno. Al terminar la funcion, el frame desaparece automaticamente. Rapido, predecible, limitado.
Heap (Monton)
Crece hacia arriba. Tu pides memoria explicitamente (malloc,
new) y la liberas manualmente (free, delete)
o via garbage collector. Flexible, lento, propenso a errores.
Stack Frames: la anatomia de una llamada
Cada llamada a funcion crea un stack frame:
- Return address: donde continuar al terminar
- Base pointer: enlace al frame anterior
- Argumentos: parametros pasados a la funcion
- Variables locales: los datos de esta funcion
LIFO (Last In, First Out): el ultimo frame creado es el primero en destruirse. Esta propiedad hace que la recursion funcione y que la memoria se gestione automaticamente.
Heap Allocation: libertad con responsabilidad
El heap es necesario cuando:
- El tamano no se conoce en compilacion
- Los datos deben sobrevivir a la funcion
- Los datos son demasiado grandes para el stack
- Necesitas compartir datos entre funciones
Peligros: Memory leaks (olvidar free), dangling pointers (usar memoria liberada), double free (liberar dos veces), fragmentacion.
Punteros: el puente entre mundos
Un puntero es una variable que guarda una direccion de memoria:
int* p = malloc(sizeof(int));
p vive en el Stack (8 bytes, la direccion)
*p vive en el Heap (4 bytes, el entero)
Los punteros son poderosos pero peligrosos. Un puntero incorrecto puede leer/escribir cualquier parte de la memoria, causando crashes o vulnerabilidades de seguridad.
El zoo de bugs de memoria
- Stack Overflow: recursion demasiado profunda
- Memory Leak: heap nunca liberado
- Dangling Pointer: usar despues de free
- Double Free: liberar dos veces
- Buffer Overflow: escribir mas alla del limite
- Use After Free: acceder a memoria liberada
Experimentos guiados
Cada experimento revela como funciona la memoria en tiempo de ejecucion.
Visualizar la recursion
Hipotesis: Cada llamada recursiva crea un nuevo frame en el stack, y los frames se destruyen en orden inverso al retornar.
- Ejecuta una funcion factorial(5)
- Observa como el stack crece con cada llamada
- Cuenta los frames: deberia haber 5 (o 6 con main)
- Nota que cada frame tiene su propia copia de "n"
- Observa los frames desaparecer al retornar
La recursion funciona porque el stack mantiene el contexto de cada llamada separado. factorial(3) no interfiere con factorial(5) porque viven en frames distintos.
Provocar Stack Overflow
Hipotesis: Una recursion sin caso base agotara el stack, causando un crash cuando el stack colisione con el heap.
- Crea una funcion que se llame a si misma sin condicion de parada
- Ejecuta y observa el stack crecer rapidamente
- Nota cuando el stack se acerca al heap
- Observa el crash: "Stack Overflow"
- Calcula: cuantos frames cupieron?
El stack tiene un tamano maximo (tipicamente 1-8 MB). Cada frame consume espacio para variables locales, return address, etc. Recursion infinita = frames infinitos = crash garantizado.
Memory Leak: el heap que nunca se libera
Hipotesis: Si allocamos memoria en el heap sin liberarla, el heap crecera indefinidamente hasta agotar la memoria disponible.
- En un loop, haz malloc(1000) sin free
- Observa el heap crecer con cada iteracion
- El stack no crece (el puntero se sobrescribe)
- Los bloques previos quedan "huerfanos"
- Ejecuta suficientes iteraciones — que pasa?
Un memory leak clasico: el puntero se sobrescribe, perdiendo la referencia al bloque anterior. La memoria queda allocada pero inaccesible. En produccion, esto agota la RAM lentamente.
Herramientas: Valgrind, AddressSanitizer, LeakSanitizer detectan memory leaks automaticamente.
Organizacion de memoria en otros sistemas
El patron stack/heap aparece en contextos sorprendentes.
Una celula organiza sus componentes en compartimentos. El citoplasma es como el heap: espacio general para organelos. El reticulo endoplasmatico procesa proteinas en capas, como el stack procesa funciones. La membrana gestiona que entra y sale, como el sistema operativo gestiona la memoria.
Simulacion relacionada: Mitosis — la celula "duplica su memoria" antes de dividirse.
Un DFA tiene un estado actual pero no stack. Un PDA (automata de pila) agrega un stack para recordar contexto, permitiendo reconocer lenguajes mas complejos. La jerarquia de Chomsky refleja el poder que da la memoria.
Simulacion relacionada: Finite Automata — comparar DFA (sin stack) vs lo que podria un PDA.
Las capas geologicas se depositan como un stack: la mas reciente arriba. Los fosiles en capas profundas son mas antiguos. Leer la estratigrafia es como leer un stack trace: cada capa representa un momento en el tiempo.
Simulacion relacionada: Estratigrafia — capas geologicas como "stack frames" del tiempo.
Una reaccion multi-paso tiene intermediarios que existen temporalmente, como variables locales en el stack. Los catalizadores "prestan" recursos y los recuperan, similar a como el stack presta espacio y lo recupera. El estado de transicion es el "return address" quimico.
Simulacion relacionada: Cinetica Quimica — intermediarios como "variables temporales".
Mas alla del modelo clasico
- Garbage Collection (Java, Python, Go): El runtime libera automaticamente la memoria no referenciada. Mas seguro, pero con overhead y pausas impredecibles.
- Ownership (Rust): El compilador rastrea quien "posee" cada dato y libera automaticamente al salir del scope. Seguridad sin garbage collector.
- Arenas/Pools: Allocar muchos objetos juntos y liberarlos todos de una vez. Usado en juegos y servidores.
- Stack allocation extendido: Algunos lenguajes permiten VLAs (Variable Length Arrays) en el stack.
Tradeoff eterno: Control manual (C/C++) da rendimiento maximo pero riesgo de bugs. Gestion automatica (GC, Rust) da seguridad pero con costo en rendimiento o complejidad.
Por que la memoria importa para la seguridad
- Buffer Overflow: Sobrescribir return address para ejecutar codigo malicioso
- Use After Free: Reusar memoria liberada para inyectar datos
- Format String: Leer/escribir memoria via printf malformado
- Heap Spray: Llenar el heap con shellcode y saltar a el
La mayoria de exploits de bajo nivel abusan del modelo de memoria. Por eso lenguajes como Rust y tecnicas como ASLR, stack canaries, DEP existen: mitigar las consecuencias de bugs de memoria.
Preguntas para reflexionar
- Por que el stack crece hacia abajo y el heap hacia arriba? Que pasaria si crecieran en la misma direccion?
- Rust no tiene garbage collector pero previene memory leaks. Como logra esto sin overhead en runtime?
- Un servidor web procesa millones de requests. Como deberia manejar la memoria para evitar leaks acumulativos?
- Los lenguajes funcionales puros (Haskell) no tienen "variables". Tienen stack y heap? Como manejan la memoria?