Pregunta ¿Se puede acceder a la memoria de una variable local fuera de su alcance?


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 58

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


872
2018-06-22 20:01


origen


Respuestas:


¿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. Pones un libro en el cajón superior de la mesita de noche y te vas a dormir. Usted echa un vistazo a la mañana siguiente, pero "olvide" devolver su llave. ¡Robaste la llave!

Una semana más tarde, regresa al hotel, no se registre, ingrese sigilosamente en su habitación anterior con su llave robada y mire en el cajón. Tu libro todavía está allí. ¡Asombroso!

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

Bueno, obviamente ese escenario puede suceder en el mundo real sin problemas. No existe una fuerza misteriosa que haga que su libro desaparezca cuando ya no esté autorizado a estar en la sala. Tampoco hay una fuerza misteriosa que te impida ingresar a una habitación con una llave robada.

La administración del hotel no es necesario para eliminar tu libro No hiciste un contrato con ellos que dijera que si dejas las cosas atrás, te destrozarán. Si vuelve a ingresar ilegalmente a su habitación con una llave robada para recuperarla, el personal de seguridad del hotel no lo hará. necesario para atraparte furtivamente. No hiciste un contrato con ellos que dijera "si trato de colarme en mi habitación más tarde, debes detenerme". Por el contrario, firmó un contrato con ellos que decía "Prometo no volver a entrar a hurtadillas en mi habitación más tarde", un contrato que rompiste.

En esta situación cualquier cosa puede suceder. El libro puede estar allí, tienes suerte. El libro de otra persona puede estar allí y el tuyo podría estar en el horno del hotel. Alguien podría estar allí cuando entres, destrozando tu libro. El hotel podría haber eliminado la mesa y el libro por completo y lo reemplazó con un armario. Todo el hotel podría estar a punto de ser demolido y reemplazado por un estadio de fútbol, ​​y vas a morir en una explosión mientras sigues furtivamente.

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

C ++ no es un lenguaje seguro. Le permitirá alegremente romper las reglas del sistema. Si tratas de hacer algo ilegal y tonto como regresar a una habitación en la que no estás autorizado a estar y hurgando en un escritorio que quizás ya no esté allí, C ++ no te detendrá. Los lenguajes más seguros que C ++ resuelven este problema al restringir su poder, por ejemplo, al tener un control mucho más estricto sobre las teclas.

ACTUALIZAR

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

Pensé que podría ser pertinente actualizar esto un poco con algunos pensamientos más técnicos.

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

El primero es tener algún tipo de área de almacenamiento de "larga vida" donde la "vida útil" de cada byte en el almacenamiento, es decir, el período de tiempo cuando está asociado de forma válida con alguna variable de programa, no puede predecirse fácilmente por adelantado de tiempo. El compilador genera llamadas en un "heap manager" que sabe cómo asignar dinámicamente el almacenamiento cuando es necesario y reclamarlo cuando ya no es necesario.

El segundo es tener algún tipo de área de almacenamiento de "vida corta" donde la vida útil de cada byte en el almacenamiento es bien conocida y, en particular, los tiempos de vida de los almacenamientos siguen un patrón de "anidación". Es decir, la asignación de las variables de vida más corta de mayor duración superpone estrictamente las asignaciones de variables de vida más corta que vienen después.

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 las vidas útiles de los almacenamientos asociados con variables locales se puede resolver antes de tiempo.

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 empuje será lo último que salga.

Es como si el hotel decidiera alquilar solo habitaciones secuencialmente, y no puedes pagar hasta que todos los que tienen un número de habitación superior al tuyo se hayan ido.

Así que pensemos en la pila. En muchos sistemas operativos obtienes una pila por hilo y la pila se asigna a un determinado tamaño fijo. Cuando llamas a un método, las cosas se envían a la pila. Si luego le devuelve un puntero a la pila de su método, como lo hace el cartel original aquí, eso es solo un puntero al centro de algún bloque de memoria de un millón de bytes completamente válido. En nuestra analogía, sales del hotel; cuando lo haces, acabas de salir de la habitación ocupada con el número más alto. Si nadie más entra después de ti y vuelves a tu habitación ilegalmente, todas tus cosas estarán garantizadas. en este hotel en particular.

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

No se requiere una implementación de C ++ para dejar intacta la basura que dejó en la pila para que pueda volver a buscarla más tarde de manera ilegal; es perfectamente legal que el compilador genere código que devuelve a cero todo en la "sala" que acaba de desocupar. No lo hace porque, de nuevo, eso sería costoso.

No se requiere una implementación de C ++ para garantizar que cuando la pila se reduce de forma lógica, las direcciones que solían ser válidas todavía se mapean en la memoria. La implementación permite decirle al sistema operativo que "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 previamente válida". De nuevo, las implementaciones en realidad no hacen eso porque es lento e innecesario.

En cambio, las implementaciones te permiten cometer errores y salirte con la tuya. La mayor parte del tiempo Hasta que un día algo realmente horrible sale mal y el proceso explota.

Esto es problemático Hay muchas reglas y es muy fácil romperlas accidentalmente. Ciertamente lo he hecho muchas veces. Y lo que es peor, el problema a menudo solo aparece cuando se detecta que la memoria está corrupta, miles de millones de nanosegundos después de que se produjo la corrupción, cuando es muy difícil determinar quién cometió el error.

Más idiomas seguros para la memoria resuelven este problema al restringir su poder. En C # "normal" simplemente no hay forma de tomar la dirección de un local y devolverla o almacenarla para más adelante. Puede tomar la dirección de un local, pero el lenguaje está inteligentemente diseñado para que sea imposible de usar después de la duración de los fines locales. Para tomar la dirección de un local y devolverla, debe colocar el compilador en un modo especial "inseguro", y ponga 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 lectura adicional:


4577
2018-06-23 05:43



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

Cuando regrese 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 usarlo de nuevo, entonces te saldrás con la suya. Lo más probable es que termines escribiendo sobre cualquier otra cosa que termine con esa dirección.

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

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


260
2018-05-19 02:33



Porque el espacio de almacenamiento no fue pisoteado todavía. No cuentes con ese comportamiento.


134
2018-06-22 14:15



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

si haces algo como eso:

#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 con boo (). Si desmontas el ejecutable lo verás claramente.


71
2018-06-22 14:15



En C ++, tú poder acceder a cualquier dirección, pero eso no significa que debería. La dirección a la que está accediendo ya no es válida. Eso trabajos porque nada más revolvió el recuerdo después de que Foo regresó, pero podría colapsar bajo muchas circunstancias. Intenta analizar tu programa con Valgrind, o incluso compilarlo optimizado, y ver ...


62
2018-06-22 14:12



Nunca arroja una excepción de C ++ al acceder a la memoria no válida. Simplemente está dando un ejemplo de la idea general de hacer referencia a una ubicación de memoria arbitraria. Podría hacer lo mismo así:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Aquí simplemente estoy tratando 123456 como la dirección de un doble y escribo en él. Cualquier cantidad de cosas podría pasar:

  1. q de hecho, podría ser realmente una dirección válida de un doble, p. double p; q = &p;.
  2. q podría señalar en algún lugar dentro de la memoria asignada y simplemente sobrescribir 8 bytes allí.
  3. q puntos 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, causando que el tiempo de ejecución lo termine.
  4. Tú ganas la lotería.

La forma de configurarlo es un poco más razonable que la dirección devuelta apunte a un área de memoria válida, 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 se puede acceder en un moda determinista.

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


58
2018-06-23 04:45



¿Compiló su programa con el optimizador habilitado?

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

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


27
2018-05-19 02:33