149 votes

Quand et pourquoi un compilateur initialisera-t-il la mémoire à 0xCD, 0xDD, etc. sur malloc/free/new/delete?

Je sais que le compilateur initialise parfois la mémoire avec certaines séries telles que 0xCD et 0xDD. Ce que je veux savoir, c'est quand et pourquoi cela se produit.

Quand

Cela dépend-il du compilateur utilisé ?

Est-ce que malloc/new et free/delete fonctionnent de la même manière à cet égard ?

Est-ce spécifique à la plate-forme ?

Cela se produira-t-il sur d'autres systèmes d'exploitation, comme Linux ou VxWorks ?

Pourquoi

Je crois que cela n'arrive que dans la configuration de débogage Win32 et c'est utilisé pour détecter les débordements de mémoire et aider le compilateur à attraper les exceptions.

Pouvez-vous donner des exemples concrets de l'utilité de cette initialisation ?

Je me souviens avoir lu quelque chose (peut-être dans Code Complete 2) disant qu'il était bon d'initialiser la mémoire à un motif connu lors de son allocation, et certains motifs déclencheront des interruptions dans Win32 qui se traduiront par des exceptions affichées dans le débogueur.

Est-ce que c'est portable ?

214voto

Michael Burr Points 181287

Un bref résumé de ce que les compilateurs de Microsoft utilisent pour divers bits de mémoire non possédés/non initialisés lorsqu'ils sont compilés en mode débogage (le support peut varier en fonction de la version du compilateur) :

Valeur  Nom            Description 
------   --------        -------------------------
0xCD     Mémoire propre Mémoire allouée via malloc ou new mais jamais 
                         écrite par l'application. 

0xDD     Mémoire morte  Mémoire qui a été libérée avec delete ou free. 
                         Elle est utilisée pour détecter l'écriture à travers des pointeurs erronés. 

0xED ou  Barrière alignée 'Terre de personne' pour des allocations alignées. Utiliser une 
0xBD                     valeur différente ici que 0xFD permet au runtime
                         de détecter non seulement l'écriture en dehors de l'allocation,
                         mais aussi d'identifier le mélange des routines d'allocation/désallocation 
                         spécifiques à l'alignement avec les routines régulières.

0xFD     Mémoire barrière Aussi connu sous le nom de "terre de personne." Cela sert à envelopper
                         la mémoire allouée (l'entourant d'une barrière) 
                         et est utilisé pour détecter les accès aux tableaux en dehors
                         des limites ou autres accès (surtout écritures) au-delà
                         de la fin (ou du début) d'un bloc alloué.

0xFD ou  Espace tampon    Utilisé pour remplir l'espace tampon dans certains tampons mémoire 
0xFE                     (parties inutilisées de `std::string` ou le tampon utilisateur 
                         passé à `fread()`). 0xFD est utilisé dans VS 2005 (peut-être
                         également dans certaines versions antérieures), 0xFE est utilisé dans
                         VS 2008 et ultérieur.

0xCC                     Lorsque le code est compilé avec l'option /GZ,
                         les variables non initialisées sont automatiquement assignées 
                         à cette valeur (au niveau octet).

// les valeurs magiques suivantes sont faites par le système d'exploitation, pas par le runtime C :

0xAB  Bloc alloué Memory allouée par LocalAlloc(). 

0xBAADF00D Mauvaise valeur  Mémoire allouée par LocalAlloc() avec LMEM_FIXED, mais
                         pas encore écrite. 

0xFEEEFEEE               Mémoire de tas remplie par le système d'exploitation, marquée pour être utilisée,
                         mais qui n'a pas été allouée par HeapAlloc() ou LocalAlloc(). 
                         Ou cette mémoire vient d'être libérée par HeapFree(). 

Avertissement : le tableau provient de notes que j'ai traînant quelque part - elles peuvent ne pas être à 100% correctes (ou cohérentes).

Nombre de ces valeurs sont définies dans vc/crt/src/dbgheap.c :

/*
 * Les valeurs suivantes sont non nulles, constantes, impaires, grandes et atypiques
 *      Les valeurs non nulles aident à trouver des bugs en supposant des données remplies de zéros.
 *      Les valeurs constantes sont bonnes, de sorte que le remplissage de mémoire soit déterministe
 *          (pour aider à rendre les bugs reproductibles). Bien sûr, il est mauvais si
 *          le remplissage constant de valeurs étranges masque un bug.
 *      Les nombres mathématiquement impairs sont bons pour trouver des bugs en supposant un bit de poids faible effacé.
 *      Les grands nombres (valeurs d'octet au moins) sont moins typiques et sont bons
 *          pour trouver des mauvaises adresses.
 *      Les valeurs atypiques (c'est-à-dire pas trop souvent) sont bonnes car elles provoquent généralement
 *          une détection précoce dans le code.
 *      Dans le cas de la terre de personne et des blocs libres, si vous stockez dans l'un de ces
 *          emplacements, le vérificateur d'intégrité de mémoire le détectera.
 *
 *      _bAlignLandFill a été changé de 0xBD à 0xED, pour s'assurer que
 *      4 octets de cela (0xEDEDEDED) donneraient une adresse inaccessible en dessous de 3gb.
 */

static unsigned char _bNoMansLandFill = 0xFD;   /* remplir la terre de personne avec ceci */
static unsigned char _bAlignLandFill  = 0xED;   /* remplir la terre de personne pour les routines alignées */
static unsigned char _bDeadLandFill   = 0xDD;   /* remplir les objets libres avec ceci */
static unsigned char _bCleanLandFill  = 0xCD;   /* remplir les nouveaux objets avec ceci */

Il y a aussi quelques cas où le runtime débogage remplira des tampons (ou des parties de tampons) avec une valeur connue, par exemple, l'espace 'slack' dans l'allocation de std::string ou le tampon passé à fread(). Ces cas utilisent une valeur nommée _SECURECRT_FILL_BUFFER_PATTERN (définie dans crtdefs.h). Je ne sais pas exactement quand elle a été introduite, mais elle était présente dans le runtime débogage au moins à partir de VS 2005 (VC++8).

Initialement, la valeur utilisée pour remplir ces tampons était 0xFD - la même valeur utilisée pour la terre de personne. Cependant, dans VS 2008 (VC++9) la valeur a été changée en 0xFE. Je suppose que c'est parce qu'il pourrait y avoir des situations où l'opération de remplissage déborderait de la fin du tampon, par exemple, si l'appelant passait une taille de tampon qui était trop grande à fread(). Dans ce cas, la valeur 0xFD peut ne pas déclencher la détection de ce dépassement puisque si la taille du tampon était trop grande de seulement un, la valeur de remplissage serait la même que la valeur de terre de personne utilisée pour l'initialisation de ce chas. Aucun changement dans la terre de personne signifie que le dépassement ne serait pas remarqué.

Ainsi, la valeur de remplissage a été changée dans VS 2008 de sorte que dans un tel cas le chas de la terre de personne changerait, entraînant la détection du problème par le runtime.

Comme d'autres l'ont noté, l'une des propriétés clés de ces valeurs est que si une variable pointeur avec l'une de ces valeurs est déréférencée, cela entraînera une violation d'accès, puisque dans une configuration Windows 32 bits standard, les adresses en mode utilisateur n'iront pas au-delà de 0x7fffffff.

1 votes

Je ne sais pas si cela se trouve sur MSDN - je l'ai rassemblé ici et là ou peut-être je l'ai trouvé sur un autre site web.

2 votes

Oh ouais - une partie vient de la source CRT dans DbgHeap.c.

0 votes

Une partie de celui-ci se trouve sur MSDN (msdn.microsoft.com/en-us/library/bebs9zyz.aspx), mais pas tout. Bonne liste.

40voto

Adam Rosenfield Points 176408

Une propriété intéressante de la valeur de remplissage 0xCCCCCCCC est que en langage d'assemblage x86, l'opcode 0xCC est l'opcode int3, qui est l'interruption de point d'arrêt logiciel. Ainsi, si vous essayez d'exécuter du code dans une mémoire non initialisée remplie avec cette valeur de remplissage, vous rencontrerez immédiatement un point d'arrêt, et le système d'exploitation vous permettra de joindre un débogueur (ou d'arrêter le processus).

9 votes

Et 0xCD est l'instruction int, donc exécuter 0xCD 0xCD générera un int CD, qui provoquera également un piège.

2 votes

Dans le monde d'aujourd'hui, la Prévention de l'Exécution des Données ne permet même pas au CPU de récupérer une instruction de la pile. Cette réponse est obsolète depuis XP SP2.

2 votes

@MSalters : Oui, il est vrai qu'en général, la mémoire nouvellement allouée sera non exécutable, mais quelqu'un pourrait facilement utiliser VirtualProtect() ou mprotect() pour rendre la mémoire exécutable.

10voto

Martin Beckett Points 60406

Il est spécifique au compilateur et au système d'exploitation, Visual Studio attribue différentes valeurs à différents types de mémoire afin que dans le débogueur, vous puissiez facilement voir si vous avez débordé dans la mémoire allouée par malloc, un tableau fixe ou un objet non initialisé.

https://learn.microsoft.com/fr-fr/visualstudio/debugger/crt-debug-heap-details?view=vs-2022

0 votes

Je suppose que cela sert à vérifier si vous oubliez de terminer correctement vos chaînes aussi (car ces 0xCD's ou 0xDD's sont imprimés).

0 votes

0xCC = variable locale non initialisée (pile) 0xCD = variable de classe non initialisée (tas?) 0xDD = variable supprimée

0 votes

@FryGuy Il y a une raison pratique qui dicte (certaines de) ces valeurs, comme je l'explique ici.

4voto

Airsource Ltd Points 14291

Il ne s'agit pas du système d'exploitation - c'est le compilateur. Vous pouvez également modifier le comportement - voir en bas de ce post.

Microsoft Visual Studio génère (en mode Debug) un binaire qui remplit la mémoire de la pile avec 0xCC. Il insère également un espace entre chaque trame de pile afin de détecter les débordements de tampon. Un exemple très simple de l'utilité de cela se trouve ici (en pratique, Visual Studio détecterait ce problème et émettrait un avertissement) :

...
   bool erreur; // valeur non initialisée
   if(quelquechose)
   {
      erreur = true;
   }
   return erreur;

Si Visual Studio ne préinitialisait pas les variables à une valeur connue, ce bogue pourrait potentiellement être difficile à trouver. Avec des variables préinitialisées (ou plutôt, une mémoire de pile préinitialisée), le problème est reproductible à chaque exécution.

Cependant, il y a un léger problème. La valeur utilisée par Visual Studio est TRUE - toute valeur différente de 0 le serait. Il est en fait assez probable que lorsque vous exécutez votre code en mode Release, des variables non initialisées peuvent être allouées à une partie de la mémoire de la pile qui contient 0, ce qui signifie que vous pouvez avoir un bogue de variable non initialisée qui ne se manifeste que en mode Release.

Cela m'a ennuyé, donc j'ai écrit un script pour modifier la valeur de pré-remplissage en modifiant directement le binaire, me permettant de trouver des problèmes de variables non initialisées qui n'apparaissent que lorsque la pile contient un zéro. Ce script modifie uniquement le pré-remplissage de la pile ; je n'ai jamais expérimenté avec le pré-remplissage du tas, bien que cela soit possible. Cela pourrait impliquer la modification du DLL d'exécution, ou pas.

1 votes

Est-ce que VS n'émet pas un avertissement lors de l'utilisation d'une valeur avant son initialisation, comme GCC?

3 votes

Oui, mais pas toujours, car cela dépend de l'analyse statique. Par conséquent, il est assez facile de le confondre avec l'arithmétique des pointeurs.

3 votes

Ce n'est pas le système d'exploitation - c'est le compilateur. En fait, ce n'est pas le compilateur -- c'est la bibliothèque d'exécution.

2voto

FryGuy Points 5999

La raison évidente du "pourquoi" est que supposez que vous avez une classe comme ceci :

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

private:
    SomeObject *_obj;
}

Et ensuite vous instanciez un Foo et appelez SomeFunction, cela donnera une violation d'accès en essayant de lire 0xCDCDCDCD. Cela signifie que vous avez oublié d'initialiser quelque chose. C'est la partie "pourquoi". Sinon, le pointeur pourrait être aligné avec une autre mémoire, ce qui rendrait le débogage plus difficile. Cela vous informe simplement de la raison de la violation d'accès. Notez que ce cas était assez simple, mais dans une classe plus grande il est facile de faire cette erreur.

AFAIK, cela ne fonctionne que sur le compilateur Visual Studio en mode débogage (par opposition à la version finale)

1 votes

Votre explication ne tient pas, puisque vous obtiendriez également une violation d'accès en essayant de lire 0x00000000, ce qui serait tout aussi utile (ou même plus, car une mauvaise adresse). Comme je l'ai souligné dans un autre commentaire sur cette page, la vraie raison de 0xCD (et 0xCC) est qu'ils sont des opcodes x86 interprétables qui déclenchent une interruption logicielle, ce qui permet une récupération en douceur dans le débogueur en cas d'un seul type d'erreur spécifique et rare, à savoir, lorsque le processeur essaie par erreur d'exécuter des octets dans une région non codée. Mis à part cette utilisation fonctionnelle, les valeurs de remplissage ne sont que des indices consultatifs, comme vous l'avez noté.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X