¿Qué hace Visual Studio con un puntero eliminado y por qué?

130

Un libro de C ++ que he estado leyendo dice que cuando se elimina un puntero usando el deleteoperador, la memoria en la ubicación a la que apunta se "libera" y se puede sobrescribir. También establece que el puntero continuará apuntando a la misma ubicación hasta que sea reasignado o configurado NULL.

Sin embargo, en Visual Studio 2012; este no parece ser el caso!

Ejemplo:

#include <iostream>

using namespace std;

int main()
{
    int* ptr = new int;
    cout << "ptr = " << ptr << endl;
    delete ptr;
    cout << "ptr = " << ptr << endl;

    system("pause");

    return 0;
}

Cuando compilo y ejecuto este programa obtengo el siguiente resultado:

ptr = 0050BC10
ptr = 00008123
Press any key to continue....

¡Claramente, la dirección a la que apunta el puntero cambia cuando se llama a borrar!

¿Por qué está pasando esto? ¿Tiene esto algo que ver específicamente con Visual Studio?

Y si eliminar puede cambiar la dirección a la que apunta de todos modos, ¿por qué no eliminar automáticamente establecer el puntero en NULLlugar de alguna dirección aleatoria?

tjwrona1992
fuente
44
Eliminar un puntero, no significa que se establecerá en NULL, debe ocuparse de eso.
Matt
11
Lo sé, pero el libro que estoy leyendo dice específicamente que todavía contendrá la misma dirección a la que apuntaba antes de eliminarlo, pero el contenido de esa dirección puede sobrescribirse.
tjwrona1992
66
@ tjwrona1992, sí, porque esto es lo que generalmente sucede. El libro solo enumera el resultado más probable, no la regla difícil.
SergeyA
55
@ tjwrona1992 ¿ Un libro de C ++ que he estado leyendo , y el nombre del libro es ...?
PaulMcKenzie
44
@ tjwrona1992: Puede ser sorprendente, pero todo el uso del valor de puntero no válido es un comportamiento indefinido, no solo la desreferenciación. "Verificar hacia dónde apunta" está utilizando el valor de una manera no permitida.
Ben Voigt

Respuestas:

175

Me di cuenta de que la dirección almacenada ptrsiempre se sobrescribía con 00008123...

Esto parecía extraño, por lo que investigé un poco y encontré esta publicación de blog de Microsoft que contiene una sección que trata sobre "Desinfección automática de punteros al eliminar objetos de C ++".

... las comprobaciones de NULL son una construcción de código común, lo que significa que una comprobación existente de NULL combinada con el uso de NULL como valor de desinfección podría ocultar fortuitamente un problema de seguridad de memoria genuino cuya causa raíz realmente necesita ser abordada.

Por esta razón, hemos elegido 0x8123 como valor de desinfección: desde la perspectiva del sistema operativo, está en la misma página de memoria que la dirección cero (NULL), pero una infracción de acceso en 0x8123 se destacará mejor para el desarrollador ya que necesita una atención más detallada .

No solo explica lo que Visual Studio hace con el puntero después de que se elimina, sino que también responde por qué eligieron NO configurarlo NULLautomáticamente.


Esta "función" está habilitada como parte de la configuración "Verificaciones SDL". Para habilitarlo / deshabilitarlo, vaya a: PROYECTO -> Propiedades -> Propiedades de configuración -> C / C ++ -> General -> Verificaciones SDL

Para confirmar esto:

Cambiar esta configuración y volver a ejecutar el mismo código produce el siguiente resultado:

ptr = 007CBC10
ptr = 007CBC10

"característica" está entre comillas porque en el caso de que tenga dos punteros en la misma ubicación, llamar a eliminar solo desinfectará UNO de ellos. El otro se quedará apuntando a la ubicación no válida ...


ACTUALIZAR:

Después de 5 años más de experiencia en programación en C ++, me doy cuenta de que todo este problema es básicamente un punto discutible. Si usted es un programador de C ++ y todavía usa newy deleteadministra punteros sin procesar en lugar de usar punteros inteligentes (que evitan todo este problema), es posible que desee considerar un cambio en la carrera profesional para convertirse en un programador de C. ;)

tjwrona1992
fuente
12
Ese es un buen hallazgo. Deseo que MS documente mejor el comportamiento de depuración como este. Por ejemplo, sería bueno saber qué versión del compilador comenzó a implementar esto y qué opciones habilitan / deshabilitan el comportamiento.
Michael Burr
55
"desde la perspectiva del sistema operativo esto está en la misma página de memoria que la dirección cero" - ¿eh? ¿No es el tamaño de página estándar (ignorando las páginas grandes) en x86 todavía 4kb tanto para Windows como para Linux? Aunque recuerdo vagamente algo sobre los primeros 64kb de espacio de direcciones en el blog de Raymond Chen, en la práctica tomo el mismo resultado
Voo
12
@Voo windows reserva el primer (y último) valor de 64kB de RAM como espacio muerto para la captura. 0x8123 cae bien allí
monstruo de trinquete
77
En realidad, no fomenta los malos hábitos, y no le permite omitir la configuración del puntero en NULL, esa es la razón por la que están usando en 0x8123lugar de 0. El puntero sigue siendo inválido, pero causa una excepción al intentar desreferenciarlo (bueno), y no pasa las comprobaciones NULL (también bueno, porque es un error no hacerlo). ¿Dónde está el lugar para los malos hábitos? Realmente es algo que te ayuda a depurar.
Luaan
3
Bueno, no puede establecer ambos (todos), por lo que esta es la segunda mejor opción. Si no le gusta, simplemente desactive las comprobaciones de SDL; las encuentro bastante útiles, especialmente al depurar el código de otra persona.
Luaan
30

Verá los efectos secundarios de la /sdlopción de compilación. Activado de forma predeterminada para proyectos VS2015, permite verificaciones de seguridad adicionales más allá de las proporcionadas por / gs. Use Proyecto> Propiedades> C / C ++> General> Configuración de comprobaciones SDL para modificarlo.

Citando del artículo de MSDN :

  • Realiza una desinfección limitada del puntero. En las expresiones que no implican desreferencias y en tipos que no tienen destructor definido por el usuario, las referencias de puntero se establecen en una dirección no válida después de una llamada para eliminar. Esto ayuda a evitar la reutilización de referencias de puntero obsoleto.

Tenga en cuenta que configurar punteros eliminados en NULL es una mala práctica cuando usa MSVC. Derrota la ayuda que obtienes tanto de Debug Heap como de esta opción / sdl, ya no puedes detectar llamadas inválidas gratis / eliminar en tu programa.

Hans Passant
fuente
1
Confirmado. Después de deshabilitar esta función, el puntero ya no se redirige. ¡Gracias por proporcionar la configuración real que lo modifica!
tjwrona1992 27/10/2015
Hans, ¿todavía se considera una mala práctica establecer punteros eliminados en NULL en un caso en el que tienes dos punteros apuntando a la misma ubicación? Cuando deleteuno, Visual Studio dejará el segundo puntero apuntando a su ubicación original que ahora no es válida.
tjwrona1992
1
No me queda claro qué tipo de magia esperas que suceda al establecer el puntero en NULL. Ese otro puntero no es para que no resuelva nada, aún necesita el asignador de depuración para encontrar el error.
Hans Passant
3
VS no limpia los punteros. Los corrompe. Por lo tanto, su programa se bloqueará cuando los use de todos modos. El asignador de depuración hace lo mismo con la memoria de almacenamiento dinámico. El gran problema con NULL, no es lo suficientemente corrupto. De lo contrario, una estrategia común, google "0xdeadbeef".
Hans Passant
1
Establecer el puntero en NULL es mucho mejor que dejarlo apuntando a su dirección anterior que ahora no es válida. Intentar escribir en un puntero NULL no dañará ningún dato y probablemente bloqueará el programa. Intentar reutilizar el puntero en ese punto puede que ni siquiera bloquee el programa, ¡puede producir resultados muy impredecibles!
tjwrona1992
19

También indica que el puntero continuará apuntando a la misma ubicación hasta que se reasigne o se establezca en NULL.

Esa es definitivamente información engañosa.

¡Claramente, la dirección a la que apunta el puntero cambia cuando se llama a borrar!

¿Por qué está pasando esto? ¿Tiene esto algo que ver específicamente con Visual Studio?

Esto está claramente dentro de las especificaciones del idioma. ptrno es válido después de la llamada a delete. Usar ptrdespués de que ha sido deleted es causa de comportamiento indefinido. No lo hagas El entorno de tiempo de ejecución es libre de hacer lo que quiera ptrdespués de la llamada delete.

Y si eliminar puede cambiar la dirección a la que apunta de todos modos, ¿por qué no eliminar automáticamente establecer el puntero en NULL en lugar de alguna dirección aleatoria?

Cambiar el valor del puntero a cualquier valor antiguo está dentro de la especificación del lenguaje. En cuanto a cambiarlo a NULL, diría que sería malo. El programa se comportaría de manera más sensata si el valor del puntero se estableciera en NULL. Sin embargo, eso ocultará el problema. Cuando el programa se compila con diferentes configuraciones de optimización o se transfiere a un entorno diferente, es probable que el problema aparezca en el momento más inoportuno.

R Sahu
fuente
1
No creo que responda la pregunta de OP.
SergeyA
No estoy de acuerdo incluso después de editar. Establecerlo en NULL no ocultará el problema; de hecho, lo EXPONERÁ en más casos que sin eso. Hay una razón por la cual las implementaciones normales no hacen esto, y la razón es diferente.
SergeyA
44
@SergeyA, la mayoría de las implementaciones no lo hacen en aras de la eficiencia. Sin embargo, si una implementación decide establecerlo, es mejor establecerlo en algo que no sea NULL. Revelaría los problemas antes que si estuviera configurado en NULL. Está establecido en NULL, llamar deletedos veces al puntero no causaría ningún problema. Eso definitivamente no es bueno.
R Sahu
No, no es la eficiencia, al menos, no es la principal preocupación.
SergeyA
77
@SergeyA Establecer un puntero a un valor que no sea, NULLpero que también esté definitivamente fuera del espacio de direcciones del proceso, expondrá más casos que las dos alternativas. Dejarlo colgando no necesariamente causará una falla predeterminada si se usa después de ser liberado; configurarlo NULLno causará una segfault si es deleted nuevamente.
Blacklight Shining
10
delete ptr;
cout << "ptr = " << ptr << endl;

En general, incluso la lectura (como lo hace anteriormente, tenga en cuenta: esto es diferente de la desreferenciación) de los punteros no válidos (el puntero se vuelve inválido, por ejemplo, cuando lo deletehace) es un comportamiento definido por la implementación. Esto se introdujo en CWG # 1438 . Ver también aquí .

Tenga en cuenta que antes de que la lectura de valores de punteros no válidos fuera un comportamiento indefinido, lo que tiene arriba sería un comportamiento indefinido, lo que significa que cualquier cosa podría suceder.

giorgim
fuente
3
También es relevante la cita de [basic.stc.dynamic.deallocation]: "Si el argumento dado a una función de desasignación en la biblioteca estándar es un puntero que no es el valor de puntero nulo, la función de desasignación desasignará el almacenamiento al que hace referencia el puntero, invalidando todos los punteros que se refieren a cualquier parte del almacenamiento desasignado "y la regla en [conv.lval](sección 4.1) que dice que la lectura (conversión lvalue-> rvalue) de cualquier valor de puntero no válido es un comportamiento definido por la implementación.
Ben Voigt
Incluso UB puede ser implementado de manera específica por un proveedor específico de modo que sea confiable, al menos para ese compilador. Si Microsoft hubiera decidido implementar su función de desinfección de puntero antes del CWG # 1438, eso no habría hecho que esa función fuera más o menos confiable, y en particular simplemente no es cierto que "cualquier cosa podría suceder" si esa función está activada , independientemente de lo que diga el estándar.
Kyle Strand
@KyleStrand: Básicamente di la definición de UB ( blog.regehr.org/archives/213 ).
giorgim
1
Para la mayoría de la comunidad de C ++ en SO, "todo podría pasar" se toma demasiado literalmente . Creo que esto es ridículo . Entiendo la definición de UB, pero también entiendo que los compiladores son solo piezas de software implementadas por personas reales, y si esas personas implementan el compilador para que se comporte de cierta manera, así es como se comportará el compilador , independientemente de lo que diga el estándar .
Kyle Strand
1

Creo que está ejecutando algún tipo de modo de depuración y VS está intentando reorientar su puntero a una ubicación conocida, de modo que se pueda rastrear e informar más intentos de desreferenciarlo. Intente compilar / ejecutar el mismo programa en modo de lanzamiento.

Por lo general, los punteros no se cambian deletepor razones de eficiencia y para evitar dar una falsa idea de seguridad. Establecer el puntero de eliminación en un valor predefinido no servirá en la mayoría de los escenarios complejos, ya que es probable que el puntero que se elimine sea solo uno de varios que apuntan a esta ubicación.

De hecho, cuanto más lo pienso, más encuentro que VS tiene la culpa al hacerlo, como de costumbre. ¿Qué pasa si el puntero es constante? ¿Todavía lo va a cambiar?

SergeyA
fuente
¡Sí, incluso los punteros constantes son redirigidos a este misterioso 8123!
tjwrona1992
Hay otra piedra para VS :) Justo esta mañana alguien preguntó por qué deberían usar g ++ en lugar de VS. Aquí va.
SergeyA
77
@SergeyA, pero desde el otro lado, al eliminar el puntero eliminado, se mostrará por defecto que trataste de eliminar un puntero eliminado y no será igual a NULL. En el otro caso, solo se bloqueará si la página también se libera (lo cual es muy poco probable). Fallar más rápido; resuelva antes
monstruo de trinquete
1
@ratchetfreak "Fallar rápido, resolver antes" es un mantra muy valioso, pero "Fallar rápido destruyendo evidencia forense clave" no comienza un mantra tan valioso. En casos simples, puede ser conveniente, pero en casos más complicados (en los que solemos necesitar más ayuda), borrar información valiosa disminuye mis herramientas disponibles para resolver el problema.
Cort Ammon
2
@ tjwrona1992: Microsoft está haciendo lo correcto aquí en mi opinión. Desinfectar un puntero es mejor que no hacer ninguno. Y si esto le causa un problema en la depuración, coloque un punto de interrupción antes de la llamada de eliminación incorrecta. Lo más probable es que sin algo como esto nunca detectarías el problema. Y si tiene una mejor solución para localizar estos errores, utilícela y ¿por qué le importa lo que haga Microsoft?
Zan Lynx
0

Después de eliminar el puntero, la memoria a la que apunta puede seguir siendo válida. Para manifestar este error, el valor del puntero se establece en un valor obvio. Esto realmente ayuda al proceso de depuración. Si el valor se estableció en NULL, es posible que nunca se muestre como un error potencial en el flujo del programa. Por lo que puede ocultar un error cuando se prueba después contra NULL.

Otro punto es que algunos optimizadores de tiempo de ejecución pueden verificar ese valor y cambiar sus resultados.

En épocas anteriores, MS establece el valor en 0xcfffffff .

Karsten
fuente