Para proporcionar un ejemplo concreto de cómo un compilador gestiona la pila y cómo se accede a los valores en la pila, podemos ver representaciones visuales, además del código generado por GCC
un entorno Linux con i386 como la arquitectura de destino.
1. Marcos de pila
Como sabe, la pila es una ubicación en el espacio de direcciones de un proceso en ejecución que utilizan las funciones o procedimientos , en el sentido de que el espacio se asigna en la pila para las variables declaradas localmente, así como los argumentos pasados a la función ( el espacio para variables declaradas fuera de cualquier función (es decir, variables globales) se asigna en una región diferente en la memoria virtual). El espacio asignado para todos los datos de una función se refiere a un marco de pila . Aquí hay una representación visual de múltiples cuadros de pila (de Computer Systems: A Programmer's Perspective ):
2. Gestión de marcos de pila y ubicación variable
Para que los valores escritos en la pila dentro de un marco de pila particular sean administrados por el compilador y leídos por el programa, debe haber algún método para calcular las posiciones de estos valores y recuperar su dirección de memoria. Los registros en la CPU referidos como el puntero de la pila y el puntero base ayudan con esto.
El puntero base, ebp
por convención, contiene la dirección de memoria del fondo, o base, de la pila. Las posiciones de todos los valores dentro del marco de la pila se pueden calcular utilizando la dirección en el puntero base como referencia. Esto se muestra en la imagen de arriba: %ebp + 4
es la dirección de memoria almacenada en el puntero base más 4, por ejemplo.
3. Código generado por el compilador
Pero lo que no entiendo es cómo una aplicación lee las variables en la pila; si declaro y asigno x como un entero, digamos x = 3, y el almacenamiento está reservado en la pila y luego se almacena su valor de 3 allí, y luego en la misma función declaro y asigno y como, por ejemplo, 4, y luego sigo que luego uso x en otra expresión, (por ejemplo z = 5 + x), ¿cómo puede el programa leer x para evaluar z cuando está debajo de y en la pila?
Usemos un programa de ejemplo simple escrito en C para ver cómo funciona esto:
int main(void)
{
int x = 3;
int y = 4;
int z = 5 + x;
return 0;
}
Examinemos el texto de ensamblaje producido por GCC para este texto fuente C (lo limpié un poco por claridad):
main:
pushl %ebp # save previous frame's base address on stack
movl %esp, %ebp # use current address of stack pointer as new frame base address
subl $16, %esp # allocate 16 bytes of space on stack for function data
movl $3, -12(%ebp) # variable x at address %ebp - 12
movl $4, -8(%ebp) # variable y at address %ebp - 8
movl -12(%ebp), %eax # write x to register %eax
addl $5, %eax # x + 5 = 9
movl %eax, -4(%ebp) # write 9 to address %ebp - 4 - this is z
movl $0, %eax
leave
Lo que observamos es que las variables X, Y y Z se encuentran en direcciones %ebp - 12
, %ebp -8
y %ebp - 4
, respectivamente. En otras palabras, las ubicaciones de las variables dentro del marco de la pila main()
se calculan utilizando la dirección de memoria guardada en el registro de la CPU %ebp
.
4. Los datos en la memoria más allá del puntero de la pila están fuera de alcance
Claramente me estoy perdiendo algo. ¿Es que la ubicación en la pila es solo acerca de la duración / alcance de la variable, y que toda la pila es realmente accesible para el programa todo el tiempo? Si es así, ¿eso implica que hay algún otro índice que contiene las direcciones solo de las variables en la pila para permitir que se recuperen los valores? Pero luego pensé que el punto principal de la pila era que los valores se almacenaban en el mismo lugar que la dirección variable.
La pila es una región en la memoria virtual, cuyo uso es administrado por el compilador. El compilador genera código de tal manera que los valores más allá del puntero de la pila (valores más allá de la parte superior de la pila) nunca se referencian. Cuando se llama a una función, la posición del puntero de la pila cambia para crear espacio en la pila que se considera que no está "fuera de límites", por así decirlo.
A medida que se llaman y devuelven funciones, el puntero de la pila se reduce y se incrementa. Los datos escritos en la pila no desaparecen una vez que están fuera del alcance, pero el compilador no genera instrucciones que hagan referencia a estos datos porque no hay forma de que el compilador calcule las direcciones de estos datos usando %ebp
o %esp
.
5. Resumen
El compilador genera el código que puede ejecutar directamente la CPU. El compilador gestiona la pila, los marcos de la pila para las funciones y los registros de la CPU. Una estrategia utilizada por GCC para rastrear las ubicaciones de las variables en los cuadros de la pila en el código destinado a ejecutarse en la arquitectura i386 es usar la dirección de memoria en el puntero base del cuadro de la pila %ebp
, como referencia y escribir valores de variables en ubicaciones en los cuadros de la pila. en desplazamientos a la dirección en %ebp
.