c++ ejemplo - ¿Se puede acceder a la memoria de una variable local fuera de su alcance?




parametros creacion (17)

tengo el siguiente código.

#include <iostream>

int * foo()
{
    int a = 5;
    return &a;
}

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}

¡Y el código se está ejecutando sin excepciones de tiempo de ejecución!

La salida fue de 58

¿Cómo puede ser? ¿No es inaccesible la memoria de una variable local fuera de su función?


Answers

¿Cómo puede ser? ¿No es inaccesible la memoria de una variable local fuera de su función?

Usted alquila una habitación de hotel. Usted pone un libro en el cajón superior de la mesita de noche y se va a dormir. Echa un vistazo a la mañana siguiente, pero "olvida" devolver tu llave. ¡Robas la llave!

Una semana más tarde, regresa al hotel, no se registra, entra a escondidas en su antigua habitación con la llave robada y mira el cajón. Su libro todavía está allí. ¡Asombroso!

¿Como puede ser? ¿No se puede acceder al contenido de un cajón de la habitación de hotel si no ha alquilado la habitación?

Bueno, obviamente ese escenario puede ocurrir en el mundo real, no hay problema. No hay una fuerza misteriosa que haga que tu libro desaparezca cuando ya no estás autorizado para estar en la habitación. Tampoco hay una fuerza misteriosa que te impida entrar en una habitación con una llave robada.

La administración del hotel no está obligada a eliminar su libro. No hiciste un contrato con ellos que dijera que si dejas cosas atrás, lo destruirán por ti. Si vuelves a entrar ilegalmente a tu habitación con una llave robada para recuperarla, no es necesario que el personal de seguridad del hotel te atrape furtivamente. No hiciste un contrato con ellos que dijera "si trato de volver a colarse en mi habitación". Habitación más tarde, estás obligado a detenerme ". Más bien, firmó un contrato con ellos que decía "Prometo no volver a escabullirme a mi habitación más tarde", un contrato que usted rompió .

En esta situación puede pasar cualquier cosa . El libro puede estar ahí, tienes suerte. El libro de otra persona puede estar allí y el suyo podría estar en el horno del hotel. Alguien podría estar allí justo cuando entras, haciendo pedazos tu libro. El hotel podría haber retirado la mesa y el libro por completo y reemplazarlo con un armario. Todo el hotel podría estar a punto de ser demolido y reemplazado por un estadio de fútbol, ​​y usted morirá en una explosión mientras se escabulle.

No sabes lo que va a pasar; cuando se retiró del hotel y robó una llave para usarla ilegalmente más tarde, renunció al derecho a vivir en un mundo predecible y seguro porque eligió romper las reglas del sistema.

C ++ no es un lenguaje seguro . Con alegría le permitirá romper las reglas del sistema. Si intentas hacer algo ilegal y tonto, como volver a una habitación en la que no estás autorizado a entrar y hurgar en un escritorio que podría no estar más allí, C ++ no te detendrá. Los idiomas más seguros que C ++ resuelven este problema restringiendo su poder, por ejemplo, al tener un control mucho más estricto sobre las claves.

ACTUALIZAR

Dios santo, esta respuesta está recibiendo mucha atención. (No estoy seguro de por qué: lo consideré solo como una pequeña analogía "divertida", pero como sea).

Pensé que podría ser pertinente actualizar esto un poco con algunas ideas técnicas más.

Los compiladores están en el negocio de generar código que gestione el almacenamiento de los datos manipulados por ese programa. Existen muchas formas diferentes de generar código para administrar la memoria, pero con el tiempo dos técnicas básicas se han afianzado.

El primero es tener algún tipo de área de almacenamiento "de larga duración" en la que la "vida útil" de cada byte en el almacenamiento, es decir, el período de tiempo en que se asocia de manera válida con alguna variable del programa, no se pueda predecir fácilmente. de tiempo. El compilador genera llamadas a un "administrador de almacenamiento dinámico" que sabe cómo asignar dinámicamente el almacenamiento cuando es necesario y reclamarlo cuando ya no es necesario.

El segundo método es tener un área de almacenamiento “de corta duración” en la que se conozca la vida útil de cada byte. Aquí, las vidas siguen un patrón de "anidación". La vida más larga de estas variables de corta duración se asignará antes que cualquier otra variable de corta duración, y se liberará en último lugar. Las variables de vida más corta se asignarán después de las de mayor duración y se liberarán antes que ellas. La vida útil de estas variables de vida más corta está "anidada" dentro de la vida útil de las variables de vida más larga.

Las variables locales siguen el último patrón; Cuando se ingresa un método, sus variables locales cobran vida. Cuando ese método llama a otro método, las variables locales del nuevo método cobran vida. Estarán muertos antes de que las variables locales del primer método estén muertas. El orden relativo de los comienzos y finales de la vida útil de los almacenes asociados con las variables locales se puede establecer con anticipación.

Por esta razón, las variables locales generalmente se generan como almacenamiento en una estructura de datos de "pila", porque una pila tiene la propiedad de que lo primero que se presione será lo último que se desprenda.

Es como si el hotel decidiera alquilar habitaciones solo secuencialmente, y usted no puede irse hasta que todos los que tengan un número de habitación más alto que el que ha hecho la salida.

Así que pensemos en la pila. En muchos sistemas operativos, se obtiene una pila por subproceso y la pila se asigna a un determinado tamaño fijo. Cuando llamas a un método, las cosas se colocan en la pila. Si luego pasa un puntero a la pila fuera de su método, como lo hace aquí el póster original, eso es solo un puntero al medio de un bloque de memoria de un millón de bytes completamente válido. En nuestra analogía, te retiras del hotel; cuando lo hace, acaba de salir de la habitación ocupada con el número más alto. Si nadie más se registra después de usted, y usted regresa a su habitación ilegalmente, se garantiza que todas sus cosas seguirán allí en este hotel en particular .

Usamos pilas para tiendas temporales porque son realmente baratas y fáciles. No se requiere una implementación de C ++ para usar una pila para el almacenamiento de locales; Podría usar el montón. No lo hace, porque eso haría que el programa fuera más lento.

No se requiere una implementación de C ++ para dejar la basura que dejaste en la pila sin tocar, de modo que puedas volver por ella ilegalmente más tarde; es perfectamente legal que el compilador genere un código que haga que todo vuelva a cero en la "sala" que acaba de desocupar. No es porque una vez más, eso sería caro.

No se requiere una implementación de C ++ para garantizar que cuando la pila se contrae de manera lógica, las direcciones que solían ser válidas todavía se asignan a la memoria. La implementación puede decirle al sistema operativo "hemos terminado de usar esta página de pila ahora. Hasta que diga lo contrario, emita una excepción que destruya el proceso si alguien toca la página de pila que era válida anteriormente". Nuevamente, las implementaciones no lo hacen porque es lento e innecesario.

En cambio, las implementaciones le permiten cometer errores y salirse con la suya. La mayor parte del tiempo. Hasta que un día algo realmente terrible sale mal y el proceso explota.

Esto es problemático. Hay muchas reglas y es muy fácil romperlas accidentalmente. Ciertamente tengo muchas veces. Y, lo que es peor, a menudo el problema solo surge cuando se detecta que la memoria está dañada miles de millones de nanosegundos después de que ocurrió la corrupción, cuando es muy difícil averiguar quién la arruinó.

Más lenguajes seguros para la memoria resuelven este problema restringiendo su poder. En C # "normal" simplemente no hay forma de tomar la dirección de un local y devolverla o almacenarla para más tarde. Puede tomar la dirección de un local, pero el idioma está diseñado inteligentemente para que sea imposible usarlo después de la vida útil de los fines locales. Para tomar la dirección de un local y devolverla, debe poner el compilador en un modo especial "inseguro" y poner la palabra "inseguro" en su programa, para llamar la atención sobre el hecho de que probablemente esté haciendo Algo peligroso que podría estar rompiendo las reglas.

Para leer más:


Lo que estás haciendo aquí es simplemente leer y escribir en la memoria que solía ser la dirección de a . Ahora que estás fuera de foo , es solo un puntero a un área de memoria aleatoria. Da la casualidad de que en su ejemplo, esa área de memoria existe y nada más lo está usando en este momento. No rompes nada al continuar usándolo, y nada más lo ha sobrescrito aún. Por lo tanto, el 5 sigue ahí. En un programa real, esa memoria se reutilizaría casi de inmediato y se rompería algo al hacer esto (¡aunque los síntomas pueden no aparecer hasta mucho más tarde!)

Cuando regresa de foo , le dice al sistema operativo que ya no está usando esa memoria y que puede reasignarse a otra cosa. Si tienes suerte y nunca se reasigna, y el sistema operativo no te atrapa usándolo nuevamente, entonces te saldrás con la mentira. Aunque es probable que termines escribiendo sobre cualquier otra cosa que termine con esa dirección.

Ahora, si se pregunta por qué el compilador no se queja, probablemente se deba a que la optimización eliminó a foo . Por lo general, le advertirá sobre este tipo de cosas. Sin embargo, C asume que sabes lo que estás haciendo, y técnicamente no has violado el alcance aquí (no hay ninguna referencia a sí mismo fuera de foo ), solo reglas de acceso a la memoria, que solo activan una advertencia en lugar de un error.

En resumen: esto no suele funcionar, pero a veces lo hace por casualidad.


Es una forma 'sucia' de usar direcciones de memoria. Cuando devuelve una dirección (puntero), no sabe si pertenece al ámbito local de una función. Es sólo una dirección. Ahora que invocó la función 'foo', esa dirección (ubicación de memoria) de 'a' ya estaba asignada allí en la memoria direccionable (proceso) (de manera segura, por ahora por lo menos). Después de que se devolvió la función 'foo', la dirección de 'a' puede considerarse 'sucia' pero está ahí, no se ha limpiado, ni se ha alterado / modificado con expresiones en otra parte del programa (al menos en este caso específico). El compilador de AC / C ++ no le impide tener un acceso tan "sucio" (aunque puede advertirle, si le importa). Puede usar (actualizar) de forma segura cualquier ubicación de memoria que se encuentre en el segmento de datos de su instancia (proceso) del programa, a menos que proteja la dirección de alguna manera.


Nunca lanzas una excepción de C ++ accediendo a memoria inválida. Solo está dando un ejemplo de la idea general de hacer referencia a una ubicación de memoria arbitraria. Yo podría hacer lo mismo así:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Aquí simplemente trato a 123456 como la dirección de un doble y le escribo. Pueden pasar muchas cosas:

  1. q podría, de hecho, ser realmente una dirección válida de doble, por ejemplo, double p; q = &p; double p; q = &p; .
  2. q podría apuntar a algún lugar dentro de la memoria asignada y simplemente sobrescribo 8 bytes allí.
  3. q puntos q fuera de la memoria asignada y el administrador de memoria del sistema operativo envía una señal de falla de segmentación a mi programa, lo que hace que el tiempo de ejecución termine.
  4. Usted gana la lotería.

La forma en que lo configura es un poco más razonable que la dirección devuelta apunte a un área válida de la memoria, ya que probablemente solo estará un poco más abajo en la pila, pero sigue siendo una ubicación no válida a la que no puede acceder en una La moda determinista.

Nadie verificará automáticamente la validez semántica de las direcciones de memoria de esa manera durante la ejecución normal del programa. Sin embargo, un depurador de memoria como valgrind hará felizmente, por lo que debe ejecutar su programa a través de él y ser testigo de los errores.


Ese es un comportamiento clásico indefinido que se ha discutido aquí hace dos días: busque un poco en el sitio. En pocas palabras, tuvo suerte, pero cualquier cosa podría haber sucedido y su código está haciendo un acceso inválido a la memoria.


Este comportamiento no está definido, como lo señaló Alex. De hecho, la mayoría de los compiladores advierten contra esto, porque es una forma fácil de obtener bloqueos.

Para ver un ejemplo del tipo de comportamiento espeluznante que es probable que experimente, pruebe este ejemplo:

int *a()
{
   int x = 5;
   return &x;
}

void b( int *c )
{
   int y = 29;
   *c = 123;
   cout << "y=" << y << endl;
}

int main()
{
   b( a() );
   return 0;
}

Esto imprime "y = 123", pero sus resultados pueden variar (¡realmente!). Su puntero está golpeando otras variables locales no relacionadas.


Después de regresar de una función, todos los identificadores se destruyen en lugar de los valores guardados en una ubicación de memoria y no podemos localizar los valores sin tener un identificador. Pero esa ubicación aún contiene el valor almacenado por la función anterior.

Entonces, aquí la función foo() está devolviendo la dirección de a y a se destruye después de devolver su dirección. Y puede acceder al valor modificado a través de esa dirección devuelta.

Déjame tomar un ejemplo del mundo real:

Supongamos que un hombre esconde dinero en una ubicación y le dice la ubicación. Después de un tiempo, el hombre que te había dicho la ubicación del dinero muere. Pero todavía tienes acceso a ese dinero oculto.


Una pequeña adición a todas las respuestas:

si haces algo asi

#include<stdio.h>
#include <stdlib.h>
int * foo(){
    int a = 5;
    return &a;
}
void boo(){
    int a = 7;

}
int main(){
    int * p = foo();
    boo();
    printf("%d\n",*p);
}

la salida probablemente será: 7

Esto se debe a que después de regresar de foo (), la pila se libera y luego se reutiliza por boo (). Si desmontas el ejecutable, lo verás claramente.


En las implementaciones típicas de compiladores, puede pensar en el código como "imprimir el valor del bloque de memoria con una dirección que solía estar ocupada por un". Además, si agrega una nueva invocación de función a una función que contiene un int local, es muy probable que cambie el valor de a (o la dirección de memoria a la que solía apuntar). Esto sucede porque la pila se sobrescribirá con un nuevo marco que contiene datos diferentes.

Sin embargo, este es un comportamiento indefinido y no debe confiar en que funcione.


Porque el espacio de almacenamiento no estaba pisoteado todavía. No cuente con ese comportamiento.


Funciona porque la pila no ha sido alterada (aún) desde que se colocó allí. Llame a algunas otras funciones (que también están llamando a otras funciones) antes de acceder a a nuevo y probablemente ya no tenga tanta suerte ... ;-)


Tu problema no tiene nada que ver con el alcance . En el código que muestra, la función main no ve los nombres en la función foo , por lo que no puede acceder a in in foo directamente con este nombre fuera de foo .

El problema que tiene es por qué el programa no señala un error al hacer referencia a la memoria ilegal. Esto se debe a que los estándares de C ++ no especifican un límite muy claro entre la memoria ilegal y la memoria legal. Hacer referencia a algo en la pila extraída a veces causa errores y otras no. Depende. No cuente con este comportamiento. Suponga que siempre generará un error cuando programe, pero suponga que nunca señalará un error cuando realice la depuración.


Las cosas con salida de consola correcta (?) Pueden cambiar dramáticamente si usa :: printf pero no cout. Puede jugar con el depurador dentro del código siguiente (probado en x86, 32 bits, MSVisual Studio):

char* foo() 
{
  char buf[10];
  ::strcpy(buf, "TEST”);
  return buf;
}

int main() 
{
  char* s = foo();    //place breakpoint & check 's' varialbe here
  ::printf("%s\n", s); 
}

Solo está devolviendo una dirección de memoria, está permitido, pero probablemente un error.

Sí, si intenta eliminar la referencia a esa dirección de memoria, tendrá un comportamiento indefinido.

int * ref () {

 int tmp = 100;
 return &tmp;
}

int main () {

 int * a = ref();
 //Up until this point there is defined results
 //You can even print the address returned
 // but yes probably a bug

 cout << *a << endl;//Undefined results
}

En C ++, puedes acceder a cualquier dirección, pero eso no significa que debas hacerlo . La dirección a la que está accediendo ya no es válida. Funciona porque nada más revolvió la memoria después de que regresara Foo, pero podría fallar en muchas circunstancias. Intente analizar su programa con Valgrind , o incluso simplemente compilándolo optimizado, y vea ...


¿Compiló su programa con el optimizador habilitado?

La función foo () es bastante simple y podría haber sido incorporada / reemplazada en el código resultante.

Pero estoy de acuerdo con Mark B en que el comportamiento resultante no está definido.


Si las únicas operaciones son (1) verificar si el valor 'a' pertenece a A y (2) actualizar los valores en A, ¿por qué no usa una tabla hash en lugar de la matriz ordenada B? Especialmente si A no aumenta o disminuye su tamaño y los valores solo cambian, esta sería una solución mucho mejor. Una tabla hash no requiere significativamente más memoria que una matriz. (Alternativamente, B debería cambiarse no a un montón, sino a un árbol de búsqueda binario, que podría ser auto-equilibrado, por ejemplo, un árbol de distribución o un árbol rojo-negro. Sin embargo, los árboles requieren memoria adicional debido a la izquierda y la derecha punteros.)

Una solución práctica que aumenta el uso de la memoria de 6N a 8N bytes es apuntar a una tabla hash rellena exactamente al 50%, es decir, usar una tabla hash que consiste en una matriz de cortos 2N. Recomendaría implementar el mecanismo de Cuckoo Hashing (ver http://en.wikipedia.org/wiki/Cuckoo_hashing ). Siga leyendo el artículo y encontrará que puede obtener factores de carga superiores al 50% (es decir, reducir el consumo de memoria de 8N a, digamos, 7N) usando más funciones hash. " Usar solo tres funciones hash aumenta la carga al 91% " .

De Wikipedia:

Un estudio de Zukowski et al. ha demostrado que el hash cuco es mucho más rápido que el hashing encadenado para las pequeñas tablas hash residentes en caché en los procesadores modernos. Kenneth Ross ha mostrado que las versiones de hash de cuco (variantes que usan cubos que contienen más de una clave) son más rápidas que los métodos convencionales, también para tablas hash grandes, cuando la utilización del espacio es alta. El rendimiento de la tabla hash de cuco en cubos fue investigado más a fondo por Askitis, con su rendimiento comparado con esquemas de hashing alternativos.





c++ memory-management local-variables dangling-pointer