La era de la informatica

Tuesday, February 27, 2007

Solucion Problema 1

Hola, hoy cumplía el plazo para resolver el problema 1. No he recibido muchas respuestas, pero aquí os voy a poner la solución, si lo sabiais bien y si no pues seguro que os parece interesante. El código, que era muy sencillo, realizaba una llamada a la función ptr, que devolvía un puntero a una variable local de la función. El valor de esta variable era 3. Imprimiamos el valor de esta variable por pantalla y nos devolvía 3, pero luego lo volvíamos a imprimir y obteniamos "basura", un número que dependiendo de cada sistema y de cada momento cambia.

La pregunta que os formulé era: ¿Por qué sucede esto?

Lo más lógico sería pensar que si imprimimos un valor por pantalla, y luego sin haberlo modificado en nuestro código lo volvemos a imprimir, este valor no cambie. Pero si que cambia. Para entender esto debemos comprender como se estructura un programa en memoria cuando se está ejecutando. Y para que sirve cada zona de la memoria. Un proceso (programa en ejecución) utiliza dentro de su espacio de memoria 4 zonas bien diferenciadas:

- Zona de código: Donde se aloja el código fuente del programa. Solo se puede ejecutar una instrucción si está en memoria.
- Zona estática: Donde se alojan las variables globales del programa, que pueden ser accedidas desde cualquier punto del programa.
- Heap: Zona de memoria dinámica. Zona de memoria que debe reservarse para su uso mediante llamadas a funciones como malloc, que a su vez piden memoria al sistema operativo.
- Stack: También conocido como pila de llamadas, esta zona es la zona relevante a nuestro problema. Contiene información necesaria para las llamadas a las funciones dentro de un programa. Esta zona es de rápido acceso ya que es dinámica y no necesita ser reservada, el sistema operativo se encarga de hacerlo automáticamente.
En el siguiente gráfico se pueden ver las distintas zonas de la memoria. Dependiendo del sistema operativo y de la arquitectura el heap y el stack pueden estar intercambiados respecto a este gráfico, pero es indiferente para el funcionamiento de los programas.


Como decía el Stack es el que toma el papel protagonista en nuestro problema número 1. El stack, también conocido como pila de llamadas, es la zona encargada de albergar la información necesaria para las llamadas a las funciones de nuestro programa. La típica información que se guarda en el stack es:
- Dirección de retorno de la función. Para saber donde seguir ejecutando una vez que la función termine
- Parametros de entrada de la funcion
- Registros internos que necesiten ser almacenados
- Variables locales de la funcion

En un lenguage como C, a la hora de llamar a la función simplemente escribimos el nombre de la función y nos olvidamos de la complejidad que hay debajo. El compilador se encarga de traducir esa llamada en almacenamiento de lo necesario en el stack, dejar espacio suficiente en el stack para variables locales y de llamar finalmente a la funcion.

Vamos a ver por tanto que pasa en nuestro programa. Cuando llamamos a la función "ptr", en la pila se almacena la dirección de retorno y espacio para una variable de tipo "int" llamada "y". Cuando la función termina, devuelve un puntero a la variable "y". Es decir, devuelve un puntero a la dirección física en memoria dentro del stack. Luego metemos ese valor dentro de la variable content y lo imprimimos por pantalla.

Para imprimir ese valor por pantalla llamamos a la función printf. Esa función como todas las funciones utiliza el stack para almacenar parametros de entrada, dirección de retorno, variables locales, etc. De forma que la función printf sobreescribe la zona de memoria en el stack donde antes estaba "y". Y perdemos el valor de esta variable. Ahora referenciamos de nuevo el puntero que apuntaba a la zona donde antes estaba "y", pero ahora encontramos otro valor distinto, que nos lleva a una zona de memoria que puede contener cualquier cosa. Por tanto la salida del segundo printf es "aleatoria".

En Windows con el código original no pasó, ya que printf no sobreescribe el stack, por eso introduje una llamada a otra función que no hacía nada en especial pero que utilizaba variables locales, de forma que me aseguré de que la llamada a esa función sobreescribiese el stack y el segundo printf sería distinto al primero.

Como veis era muy sencillo. Simplemente había que entender que cada vez que llamamos a una función se utiliza la misma zona de memoria para toda la información necesaria para la función. Por eso es una mala práctica devolver referencias a variables locales. En lugar de devolver una referencia a una variable local se debe usar la función malloc para reservar memoria en el heap, y ésta ya es inamovible. Utilizar el stack es rápido y sencillo, pero una llamada posterior a una función sobreescribirá esa memoria, así que de usarlo se debe usar con mucho cuidado.

0 Comments:

Post a Comment

<< Home