¿Cuándo y por qué un compilador inicializará la memoria a 0xCD, 0xDD, etc. en malloc / free / new / delete?

129

Sé que el compilador a veces inicializará la memoria con ciertos patrones como 0xCDy 0xDD. Lo que quiero saber es cuándo y por qué sucede esto.

Cuando

¿Es esto específico para el compilador utilizado?

¿Hacer malloc/newy free/deletetrabajar de la misma manera con respecto a esto?

¿Es específico de la plataforma?

¿Ocurrirá en otros sistemas operativos, como Linuxo VxWorks?

Por qué

Tengo entendido que esto solo ocurre en la Win32configuración de depuración, y se usa para detectar desbordamientos de memoria y para ayudar al compilador a detectar excepciones.

¿Puedes dar ejemplos prácticos de cómo es útil esta inicialización?

Recuerdo haber leído algo (tal vez en Code Complete 2) que decía que es bueno inicializar la memoria en un patrón conocido al asignarlo, y ciertos patrones desencadenarán interrupciones en las Win32cuales se mostrarán excepciones en el depurador.

¿Qué tan portátil es esto?

LeopardSkinPillBoxHat
fuente

Respuestas:

191

Un resumen rápido de lo que usan los compiladores de Microsoft para varios bits de memoria sin propietario / sin inicializar cuando se compila para el modo de depuración (el soporte puede variar según la versión del compilador):

Value     Name           Description 
------   --------        -------------------------
0xCD     Clean Memory    Allocated memory via malloc or new but never 
                         written by the application. 

0xDD     Dead Memory     Memory that has been released with delete or free. 
                         It is used to detect writing through dangling pointers. 

0xED or  Aligned Fence   'No man's land' for aligned allocations. Using a 
0xBD                     different value here than 0xFD allows the runtime
                         to detect not only writing outside the allocation,
                         but to also identify mixing alignment-specific
                         allocation/deallocation routines with the regular
                         ones.

0xFD     Fence Memory    Also known as "no mans land." This is used to wrap 
                         the allocated memory (surrounding it with a fence) 
                         and is used to detect indexing arrays out of 
                         bounds or other accesses (especially writes) past
                         the end (or start) of an allocated block.

0xFD or  Buffer slack    Used to fill slack space in some memory buffers 
0xFE                     (unused parts of `std::string` or the user buffer 
                         passed to `fread()`). 0xFD is used in VS 2005 (maybe 
                         some prior versions, too), 0xFE is used in VS 2008 
                         and later.

0xCC                     When the code is compiled with the /GZ option,
                         uninitialized variables are automatically assigned 
                         to this value (at byte level). 


// the following magic values are done by the OS, not the C runtime:

0xAB  (Allocated Block?) Memory allocated by LocalAlloc(). 

0xBAADF00D Bad Food      Memory allocated by LocalAlloc() with LMEM_FIXED,but 
                         not yet written to. 

0xFEEEFEEE               OS fill heap memory, which was marked for usage, 
                         but wasn't allocated by HeapAlloc() or LocalAlloc(). 
                         Or that memory just has been freed by HeapFree(). 

Descargo de responsabilidad: la tabla es de algunas notas que tengo por ahí, puede que no sean 100% correctas (o coherentes).

Muchos de estos valores se definen en vc / crt / src / dbgheap.c:

/*
 * The following values are non-zero, constant, odd, large, and atypical
 *      Non-zero values help find bugs assuming zero filled data.
 *      Constant values are good, so that memory filling is deterministic
 *          (to help make bugs reproducible).  Of course, it is bad if
 *          the constant filling of weird values masks a bug.
 *      Mathematically odd numbers are good for finding bugs assuming a cleared
 *          lower bit.
 *      Large numbers (byte values at least) are less typical and are good
 *          at finding bad addresses.
 *      Atypical values (i.e. not too often) are good since they typically
 *          cause early detection in code.
 *      For the case of no man's land and free blocks, if you store to any
 *          of these locations, the memory integrity checker will detect it.
 *
 *      _bAlignLandFill has been changed from 0xBD to 0xED, to ensure that
 *      4 bytes of that (0xEDEDEDED) would give an inaccessible address under 3gb.
 */

static unsigned char _bNoMansLandFill = 0xFD;   /* fill no-man's land with this */
static unsigned char _bAlignLandFill  = 0xED;   /* fill no-man's land for aligned routines */
static unsigned char _bDeadLandFill   = 0xDD;   /* fill free objects with this */
static unsigned char _bCleanLandFill  = 0xCD;   /* fill new objects with this */

También hay algunas veces en que el tiempo de ejecución de depuración llenará los buffers (o partes de buffers) con un valor conocido, por ejemplo, el espacio 'slack' en std::stringla asignación o el buffer pasado fread(). Esos casos usan un valor dado el nombre _SECURECRT_FILL_BUFFER_PATTERN(definido en crtdefs.h). No estoy seguro exactamente cuándo se introdujo, pero estaba en el tiempo de ejecución de depuración al menos VS 2005 (VC ++ 8).

Inicialmente, el valor utilizado para llenar estos búferes era 0xFD: el mismo valor utilizado para la tierra de nadie. Sin embargo, en VS 2008 (VC ++ 9) el valor se cambió a 0xFE. Supongo que se debe a que podría haber situaciones en las que la operación de llenado se ejecute más allá del final del búfer, por ejemplo, si la persona que llama pasó un tamaño de búfer que era demasiado grande fread(). En ese caso, el valor 0xFDpodría no activar la detección de este desbordamiento ya que si el tamaño del búfer fuera demasiado grande en solo uno, el valor de relleno sería el mismo que el valor de la tierra de nadie utilizado para inicializar ese canario. Ningún cambio en la tierra de nadie significa que el desbordamiento no se notaría.

Por lo tanto, el valor de relleno se modificó en VS 2008 para que un caso así cambiara el canario terrestre de nadie, lo que resultaría en la detección del problema por el tiempo de ejecución.

Como otros han señalado, una de las propiedades clave de estos valores es que si se desreferencia una variable de puntero con uno de estos valores, dará como resultado una infracción de acceso, ya que en una configuración estándar de Windows de 32 bits, las direcciones de modo de usuario no irá más alto que 0x7fffffff.

Michael Burr
fuente
1
No sé si está en MSDN: lo reconstruí de aquí para allá o tal vez lo obtuve de otro sitio web.
Michael Burr
2
Ah sí, parte de la fuente de CRT en DbgHeap.c.
Michael Burr
Algunos de ellos están en MSDN ( msdn.microsoft.com/en-us/library/bebs9zyz.aspx ), pero no todos. Buena lista
Sean e
3
@seane - Para tu información, tu enlace parece estar muerto. El nuevo (el texto se ha mejorado) está disponible aquí: msdn.microsoft.com/en-us/library/974tc9t1.aspx
Simon Mourier el
¿Cuál es el nombre de estos bloques? ¿Es barrera de memoria, membar, guía de memoria o instrucción de guía ( en.wikipedia.org/wiki/Memory_barrier )?
kr85
36

Una buena propiedad sobre el valor de relleno 0xCCCCCCCC es que en el ensamblaje x86, el código de operación 0xCC es el código de operación int3 , que es la interrupción del punto de interrupción del software. Entonces, si alguna vez intenta ejecutar código en la memoria no inicializada que se ha llenado con ese valor de llenado, inmediatamente alcanzará un punto de interrupción, y el sistema operativo le permitirá adjuntar un depurador (o eliminar el proceso).

Adam Rosenfield
fuente
66
Y 0xCD es la intinstrucción, por lo que ejecutar 0xCD 0xCD generará un int CD, que también atrapará.
Tad Marshall el
2
En el mundo de hoy, la Prevención de ejecución de datos ni siquiera permite que la CPU obtenga una instrucción del montón. Esta respuesta está desactualizada desde XP SP2.
MSalters
2
@MSalters: Sí, es cierto que, por defecto, la memoria recién asignada no será ejecutable, pero alguien podría usarla fácilmente VirtualProtect()o mprotect()hacer que la memoria sea ejecutable.
Adam Rosenfield
No puede ejecutar código desde un bloque de datos. NUNCA. Adivina otra vez.
Dan
9

Es específico para el compilador y el sistema operativo, Visual Studio establece diferentes tipos de memoria en diferentes valores para que en el depurador pueda ver fácilmente si se ha superpuesto en una memoria mal asignada, una matriz fija o un objeto no inicializado. Alguien publicará los detalles mientras los busco en Google ...

http://msdn.microsoft.com/en-us/library/974tc9t1.aspx

Martin Beckett
fuente
Supongo que también se usa para verificar si se olvida de terminar sus cadenas correctamente (ya que se imprimen los 0xCD o 0xDD).
extraño
0xCC = variable local no inicializada (pila) 0xCD = variable de clase no inicializada (heap?) 0xDD = variable eliminada
FryGuy
@FryGuy Hay una razón práctica que dicta (algunos de) estos valores, como explico aquí .
Glenn Slayden
4

No es el sistema operativo, es el compilador. También puede modificar el comportamiento: consulte la parte inferior de esta publicación.

Microsoft Visual Studio genera (en modo de depuración) un binario que llena previamente la memoria de la pila con 0xCC. También inserta un espacio entre cada marco de pila para detectar desbordamientos de búfer. Aquí hay un ejemplo muy simple de dónde es útil (en la práctica, Visual Studio detectaría este problema y emitiría una advertencia):

...
   bool error; // uninitialised value
   if(something)
   {
      error = true;
   }
   return error;

Si Visual Studio no preinicializa las variables a un valor conocido, entonces este error podría ser difícil de encontrar. Con variables preinicializadas (o más bien, memoria de pila preinicializada), el problema es reproducible en cada ejecución.

Sin embargo, hay un pequeño problema. El valor que utiliza Visual Studio es VERDADERO; cualquier cosa, excepto 0, lo sería. En realidad, es bastante probable que cuando ejecute su código en modo Release, las variables unitarias puedan asignarse a un trozo de memoria de pila que contenga 0, lo que significa que puede tener un error de variable unitaria que solo se manifiesta en modo Release.

Eso me molestó, así que escribí un script para modificar el valor de pre-llenado editando directamente el binario, permitiéndome encontrar problemas de variables no inicializadas que solo aparecen cuando la pila contiene un cero. Este script solo modifica el prellenado de la pila; Nunca experimenté con el precarga de almacenamiento dinámico, aunque debería ser posible. Puede implicar la edición de la DLL en tiempo de ejecución, puede que no.

Airsource Ltd
fuente
1
¿VS no emite una advertencia al usar un valor antes de que se inicialice, como GCC?
extraño
3
Sí, pero no siempre, porque depende del análisis estático. En consecuencia, es bastante fácil confundirlo con la aritmética del puntero.
Airsource Ltd
3
"No es el sistema operativo, es el compilador". En realidad, no es el compilador, es la biblioteca de tiempo de ejecución.
Adrian McCarthy
Al depurar, el depurador de Visual Studio mostrará el valor de un bool si no es 0 o 1 con algo como verdadero (204) . Por lo tanto, es relativamente fácil ver ese tipo de error si rastrea el código.
Phil1970
4

¿Es esto específico para el compilador utilizado?

En realidad, casi siempre es una característica de la biblioteca de tiempo de ejecución (como la biblioteca de tiempo de ejecución C). El tiempo de ejecución generalmente está fuertemente correlacionado con el compilador, pero hay algunas combinaciones que puede intercambiar.

Creo que en Windows, el montón de depuración (HeapAlloc, etc.) también usa patrones de relleno especiales que son diferentes a los que provienen de malloc y las implementaciones gratuitas en la biblioteca de tiempo de ejecución C de depuración. Por lo tanto, también puede ser una característica del sistema operativo, pero la mayoría de las veces, es solo la biblioteca de tiempo de ejecución del lenguaje.

¿Malloc / new y free / delete funcionan de la misma manera con respecto a esto?

La parte de administración de memoria de new y delete generalmente se implementa con malloc y free, por lo que la memoria asignada con new y delete generalmente tiene las mismas características.

¿Es específico de la plataforma?

Los detalles son específicos del tiempo de ejecución. Los valores reales utilizados a menudo se eligen no solo para parecer inusuales y obvios cuando se observa un volcado hexadecimal, sino que están diseñados para tener ciertas propiedades que pueden aprovechar las características del procesador. Por ejemplo, a menudo se usan valores impares, ya que podrían causar una falla de alineación. Se utilizan valores grandes (en oposición a 0), ya que causan retrasos sorprendentes si realiza un bucle a un contador no inicializado. En x86, 0xCC es una int 3instrucción, por lo que si ejecuta una memoria no inicializada , quedará atrapada.

¿Ocurrirá en otros sistemas operativos, como Linux o VxWorks?

Depende principalmente de la biblioteca de tiempo de ejecución que use.

¿Puedes dar algunos ejemplos prácticos de cómo es útil esta inicialización?

Enumeré algunos arriba. Los valores generalmente se eligen para aumentar las posibilidades de que ocurra algo inusual si hace algo con porciones de memoria no válidas: retrasos largos, trampas, fallas de alineación, etc. Los administradores de montón a veces también usan valores de relleno especiales para los espacios entre asignaciones. Si esos patrones cambian alguna vez, sabe que hubo una mala escritura (como un desbordamiento del búfer) en alguna parte.

Recuerdo haber leído algo (tal vez en Code Complete 2) que es bueno inicializar la memoria a un patrón conocido al asignarlo, y ciertos patrones desencadenarán interrupciones en Win32 que darán lugar a excepciones que se mostrarán en el depurador.

¿Qué tan portátil es esto?

Escribir código sólido (y tal vez Code Complete ) habla de cosas a considerar al elegir patrones de relleno. He mencionado algunos de ellos aquí, y el artículo de Wikipedia sobre Magic Number (programación) también los resume. Algunos de los trucos dependen de los detalles del procesador que está utilizando (como si requiere lecturas y escrituras alineadas y qué valores se asignan a las instrucciones que atraparán). Otros trucos, como el uso de valores grandes y valores inusuales que se destacan en un volcado de memoria son más portátiles.

Adrian McCarthy
fuente
2

La razón obvia del "por qué" es que suponga que tiene una clase como esta:

class Foo
{
public:
    void SomeFunction()
    {
        cout << _obj->value << endl;
    }

private:
    SomeObject *_obj;
}

Y luego crea una instancia de una Fooy llama SomeFunction, dará una infracción de acceso al intentar leer0xCDCDCDCD . Esto significa que olvidó inicializar algo. Esa es la "parte del por qué". Si no, entonces el puntero podría haberse alineado con alguna otra memoria, y sería más difícil de depurar. Solo le permite saber la razón por la que obtiene una infracción de acceso. Tenga en cuenta que este caso fue bastante simple, pero en una clase más grande es fácil cometer ese error.

AFAIK, esto solo funciona en el compilador de Visual Studio cuando está en modo de depuración (a diferencia de la versión)

FryGuy
fuente
Su explicación no sigue, ya que también obtendría una infracción de acceso al intentar leer 0x00000000, lo que sería tan útil (o más, como una mala dirección). Como señalé en otro comentario en esta página, la razón real de 0xCD(y 0xCC) es que son códigos de operación x86 interpretables que desencadenan una interrupción de software, y esto permite una recuperación elegante en el depurador en un solo tipo de error específico y raro , es decir, cuando la CPU intenta ejecutar por error bytes en una región sin código. Aparte de este uso funcional, los valores de relleno son solo consejos de asesoramiento, como se observa.
Glenn Slayden
2

Es para ver fácilmente que la memoria ha cambiado desde su valor inicial, generalmente durante la depuración, pero a veces también para el código de liberación, ya que puede adjuntar depuradores al proceso mientras se está ejecutando.

Tampoco es solo memoria, muchos depuradores establecerán el contenido de los registros en un valor centinela cuando se inicie el proceso (algunas versiones de AIX establecerán algunos registros, lo 0xdeadbeefque es ligeramente humorístico).

paxdiablo
fuente
1

El compilador IBM XLC tiene una opción "initauto" que asignará a las variables automáticas un valor que usted especifique. Usé lo siguiente para mis compilaciones de depuración:

-Wc,'initauto(deadbeef,word)'

Si mirara el almacenamiento de una variable no inicializada, se establecería en 0xdeadbeef

Anthony Giorgio
fuente