Gestión de memoria (II)

Lecturas de la serie
Sobre este artículo
Tiempo estimado de lectura: 5 minutos
Nivel: Básico
Última actualización: 10 de mayo, 2026
1. Pila de usuario (user stack)
La pila de usuario (user stack) es la región de memoria destinada a gestionar el ciclo de vida de las funciones en ejecución. Cada vez que se invoca una función, la pila actúa como un almacén temporal y dinámico donde se alojan sus variables locales y datos de control. A diferencia de otras áreas de memoria (como el free store), la gestión es aquí automática: el espacio se reserva al entrar en la función y se libera inmediatamente al salir, lo que la convierte en un mecanismo rápido y eficiente para el manejo de datos.

El tamaño máximo habitual para la pila en un sistema Linux es de 8 MiB.

En un procesador x86-64, el registro %rsp (puntero de pila o stack pointer) referencia en todo momento a la cima/cabecera de la pila de usuario.
Es importante entender que, por diseño de la arquitectura, la pila crece hacia direcciones inferiores de memoria. Por tanto, para alojar memoria en ella, basta con restar valor al puntero, mientras que para desalojarla se debe incrementar.
La llamada a una función introduce, por lo general, un nuevo marco de pila (stack frame) en la pila de usuario, disminuyendo el valor del registro %rsp convenientemente.  Así, supongamos que una función f() invoca a otra función g() (véase la figura adjunta). En primer lugar, se introduce en el marco de f() la dirección de retorno, que indica al procesador dónde debe continuar la ejecución tras finalizar g(). Se crea entonces un nuevo marco de pila para g(), en el que se almacenan:
  • Copias de seguridad de los valores contenidos en los registros no volátiles que vayan a utilizarse (aquéllos que g() debe restaurar por contrato antes de finalizar para no corromper el estado de f()).
  • Los argumentos y las variables locales que no puedan contenerse directamente en los registros y/o los argumentos para las funciones invocadas posteriormente por g().
Al finalizar la ejecución de g(), el puntero de pila será incrementado hasta su valor original, invalidando lógicamente el marco de g(), aunque los datos permanezcan físicamente en memoria hasta que otro marco los sobrescriba.
Durante el cierre del marco de pila tiene lugar el desenrollado de la pila (stack unwinding). En C++, este proceso garantiza la destrucción automática de todos los objetos locales con almacenamiento automático que fueron creados dentro de la función. El compilador inserta llamadas a los respectivos destructores en el orden inverso al de su construcción, asegurando una liberación de recursos determinista.
Este mecanismo es la base de la técnica RAII (Resource Acquisition Is Initialization), permitiendo que la gestión de recursos sea segura y automática, incluso si la función finaliza prematuramente debido a una excepción.
Con carácter general, el puntero de pila permanece estático a lo largo del cuerpo de una función, lo que permite su uso como referencia para recorrer la pila en combinación con metadatos generados por el compilador y almacenados en el ejecutable [1]. En contraste con la arquitectura x86 de 32 bits, pues, no es necesaria la utilización del puntero de marco %rbp (frame pointer) para referenciar el fondo de la pila, lo que libera un registro adicional de propósito general para mejorar el rendimiento.

El modo de funcionamiento de la pila, que sigue el esquema LIFO (Last In, First Out), es simple y determinista. Es más, la reutilización constante de la pila tiende a mantenerla activa en la memoria caché de la CPU, por lo que el acceso a sus datos resulta enormemente eficiente [2].
2. Bugs a evitar
Cabe citar dos errores de software (bugs) comunes en la gestión de la pila, a saber:
  • Desbordamiento de pila (stack overflow): el agotamiento del espacio asignado a la pila —sea por recursión profunda o por instanciación de objetos de gran tamaño en el user stack— es detectado por el kernel mediante páginas de guarda (guard pages). Éstas son regiones de memoria virtual sin permisos de acceso situadas en el extremo de crecimiento de la pila. Cuando el proceso accede a una de estas páginas, la MMU genera una excepción de hardware que el kernel traduce en una señal SIGSEGV (segmentation fault [4]), provocando por defecto la terminación del proceso (crash) para evitar la corrupción de regiones adyacentes.
  • Corrupción de pila por desbordamiento de array: sobrescribir un array más allá de su cota superior puede corromper datos críticos como la dirección de retorno. Históricamente, este error ha constituido una vulnerabilidad de seguridad clásica difícil de diagnosticar durante procesos de depuración (debugging). Los compiladores actuales implementan mecanismos de protección como los canarios de pila (stack canaries) para detectar estas corrupciones y abortar la ejecución del proceso mediante señales SIGABRT. Asimismo, el uso de abstracciones de C++, como el contenedor std::array combinado con comprobaciones de rango (.at()), neutraliza este tipo de vulnerabilidades frente al uso de arrays tradicionales de C.
Referencias bibliográficas
  1. Stack reconstruction – https://blogs.msdn.microsoft.com/ntdebugging/2010/05/12/x64-manual-stack-reconstruction-and-stack-walking/
  2. Stack and cache memory – https://stackoverflow.com/questions/23760725/c-stack-memory-and-cpu-cache
  3. Bryant R. E. and O'Hallaron D. R., 'Computer Systems: A Programmer's Perspective', 2nd Edition. Addison-Wesley (2010).
  4. Wikipedia – Segmentation fault – https://en.wikipedia.org/wiki/Segmentation_fault