Que fait Visual Studio avec un pointeur supprimé et pourquoi?

130

Un livre C ++ que j'ai lu indique que lorsqu'un pointeur est supprimé à l'aide de l' deleteopérateur, la mémoire à l'emplacement vers lequel il pointe est "libérée" et peut être écrasée. Il indique également que le pointeur continuera à pointer vers le même emplacement jusqu'à ce qu'il soit réaffecté ou défini sur NULL.

Dans Visual Studio 2012 cependant; cela ne semble pas être le cas!

Exemple:

#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;
}

Lorsque je compile et exécute ce programme, j'obtiens la sortie suivante:

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

Il est clair que l'adresse vers laquelle pointe le pointeur change lorsque la suppression est appelée!

Pourquoi cela arrive-t-il? Cela a-t-il quelque chose à voir avec Visual Studio en particulier?

Et si la suppression peut changer l'adresse vers laquelle elle pointe de toute façon, pourquoi la suppression ne définirait-elle pas automatiquement le pointeur au NULLlieu d'une adresse aléatoire?

tjwrona1992
la source
4
Supprimer un pointeur, cela ne signifie pas qu'il sera défini sur NULL, vous devez vous en occuper.
Matt
11
Je le sais, mais le livre que je lis dit spécifiquement qu'il contiendra toujours la même adresse qu'il indiquait avant la suppression, mais le contenu de cette adresse peut être écrasé.
tjwrona1992
6
@ tjwrona1992, oui, car c'est ce qui se passe habituellement. Le livre énumère simplement les résultats les plus probables, pas la règle absolue.
SergeyA
5
@ tjwrona1992 Un livre C ++ que j'ai lu - et le nom du livre est ...?
PaulMcKenzie
4
@ tjwrona1992: Cela peut être surprenant, mais c'est toute l'utilisation de la valeur de pointeur non valide qui est un comportement non défini, pas seulement le déréférencement. "Vérifier où il pointe vers" EST en utilisant la valeur d'une manière non autorisée.
Ben Voigt

Réponses:

175

J'ai remarqué que l'adresse stockée ptrétait toujours remplacée par 00008123...

Cela semblait étrange, alors j'ai fait un peu de fouille et j'ai trouvé ce billet de blog Microsoft contenant une section traitant de "Nettoyage automatisé des pointeurs lors de la suppression d'objets C ++".

... les vérifications de NULL sont une construction de code courante, ce qui signifie qu'une vérification existante de NULL combinée à l'utilisation de NULL comme valeur de nettoyage pourrait fortuitement cacher un véritable problème de sécurité de la mémoire dont la cause racine doit vraiment être abordée.

Pour cette raison, nous avons choisi 0x8123 comme valeur de nettoyage - du point de vue du système d'exploitation, il s'agit de la même page de mémoire que l'adresse zéro (NULL), mais une violation d'accès à 0x8123 se démarquera mieux pour le développeur comme nécessitant une attention plus détaillée .

Non seulement cela explique ce que Visual Studio fait avec le pointeur après sa suppression, mais cela explique également pourquoi ils ont choisi de NE PAS le définir NULLautomatiquement!


Cette "fonctionnalité" est activée dans le cadre du paramètre "Vérifications SDL". Pour l'activer / le désactiver, allez dans: PROJET -> Propriétés -> Propriétés de configuration -> C / C ++ -> Général -> Vérifications SDL

Pour confirmer ceci:

La modification de ce paramètre et la réexécution du même code produit la sortie suivante:

ptr = 007CBC10
ptr = 007CBC10

"feature" est entre guillemets car dans le cas où vous avez deux pointeurs vers le même emplacement, appeler delete ne nettoiera qu'UN d'entre eux. L'autre sera laissé pointant vers l'emplacement invalide ...


METTRE À JOUR:

Après 5 ans d'expérience en programmation C ++, je me rends compte que tout ce problème est fondamentalement un point discutable. Si vous êtes un programmeur C ++ et que vous utilisez toujours newet que deletevous gérez des pointeurs bruts au lieu d'utiliser des pointeurs intelligents (qui contournent tout ce problème), vous pouvez envisager un changement de cheminement de carrière pour devenir programmeur C. ;)

tjwrona1992
la source
12
C'est une belle trouvaille. Je souhaite que MS documente mieux le comportement de débogage comme celui-ci. Par exemple, il serait bien de savoir quelle version du compilateur a commencé à implémenter cela et quelles options activent / désactivent le comportement.
Michael Burr
5
"du point de vue du système d'exploitation, c'est dans la même page mémoire que l'adresse zéro" - hein? La taille de page standard (en ignorant les grandes pages) sur x86 n'est-elle pas toujours de 4 Ko pour Windows et Linux? Bien que je me souvienne vaguement de quelque chose à propos des premiers 64 Ko d'espace d'adressage sur le blog de Raymond Chen, je prends donc en pratique le même résultat,
Voo
12
@Voo windows réserve le premier (et dernier) 64 Ko de RAM comme espace mort pour le recouvrement. 0x8123 y tombe bien
ratchet freak
7
En fait, cela n'encourage pas les mauvaises habitudes et ne vous permet pas de ne pas définir le pointeur sur NULL - c'est la raison pour laquelle ils utilisent à la 0x8123place 0. Le pointeur n'est toujours pas valide, mais provoque une exception lors de la tentative de déréférencement (bon), et il ne passe pas les vérifications NULL (également bon, car c'est une erreur de ne pas le faire). Où est la place pour les mauvaises habitudes? C'est vraiment quelque chose qui vous aide à déboguer.
Luaan
3
Eh bien, il ne peut pas définir les deux (tous), c'est donc la deuxième meilleure option. Si vous ne l'aimez pas, désactivez simplement les vérifications SDL - je les trouve plutôt utiles, en particulier lors du débogage du code de quelqu'un d'autre.
Luaan
30

Vous voyez les effets secondaires de l' /sdloption de compilation. Activé par défaut pour les projets VS2015, il permet des contrôles de sécurité supplémentaires au-delà de ceux fournis par / gs. Utilisez Projet> Propriétés> C / C ++> Général> SDL vérifie le paramètre pour le modifier.

Citant l' article MSDN :

  • Effectue une désinfection limitée du pointeur. Dans les expressions qui n'impliquent pas de déréférences et dans les types qui n'ont pas de destructeur défini par l'utilisateur, les références de pointeur sont définies sur une adresse non valide après un appel à supprimer. Cela permet d'éviter la réutilisation de références de pointeur obsolètes.

N'oubliez pas que la définition de pointeurs supprimés sur NULL est une mauvaise pratique lorsque vous utilisez MSVC. Il va à l'encontre de l'aide que vous obtenez à la fois du tas de débogage et de cette option / sdl, vous ne pouvez plus détecter les appels libres / supprimés non valides dans votre programme.

Hans Passant
la source
1
Confirmé. Après avoir désactivé cette fonctionnalité, le pointeur n'est plus redirigé. Merci de fournir le paramètre réel qui le modifie!
tjwrona1992
Hans, est-il toujours considéré comme une mauvaise pratique de définir des pointeurs supprimés sur NULL dans le cas où vous avez deux pointeurs pointant vers le même emplacement? Lorsque vous en avez deleteun, Visual Studio laissera le deuxième pointeur pointant vers son emplacement d'origine qui est désormais invalide.
tjwrona1992
1
Je ne sais pas trop quel genre de magie vous attendez en définissant le pointeur sur NULL. Cet autre pointeur n'est pas pour ne rien résoudre, vous avez toujours besoin de l'allocateur de débogage pour trouver le bogue.
Hans Passant
3
VS ne nettoie pas les pointeurs. Cela les corrompt. Ainsi, votre programme plantera quand vous les utiliserez de toute façon. L'allocateur de débogage fait à peu près la même chose avec la mémoire de tas. Le gros problème avec NULL, ce n'est pas assez corrompu. Sinon, une stratégie courante, google "0xdeadbeef".
Hans Passant
1
Définir le pointeur sur NULL est toujours bien meilleur que de le laisser pointer vers son adresse précédente qui est maintenant invalide. Tenter d'écrire sur un pointeur NULL ne corrompra aucune donnée et fera probablement planter le programme. Tenter de réutiliser le pointeur à ce stade peut même ne pas planter le programme, cela peut simplement produire des résultats très imprévisibles!
tjwrona1992
19

Il indique également que le pointeur continuera à pointer vers le même emplacement jusqu'à ce qu'il soit réaffecté ou défini sur NULL.

Ce sont certainement des informations trompeuses.

Il est clair que l'adresse vers laquelle pointe le pointeur change lorsque la suppression est appelée!

Pourquoi cela arrive-t-il? Cela a-t-il quelque chose à voir avec Visual Studio en particulier?

Ceci est clairement dans les spécifications linguistiques. ptrn'est pas valide après l'appel à delete. Utiliser ptraprès qu'il ait été deleted est la cause d'un comportement indéfini. Ne fais pas ça. L'environnement d'exécution est libre de faire tout ce qu'il veut ptraprès l'appel à delete.

Et si la suppression peut changer l'adresse vers laquelle elle pointe de toute façon, pourquoi la suppression ne définirait-elle pas automatiquement le pointeur sur NULL au lieu d'une adresse aléatoire ???

La modification de la valeur du pointeur vers une ancienne valeur est conforme à la spécification du langage. Pour ce qui est de le changer en NULL, je dirais que ce serait mauvais. Le programme se comporterait d'une manière plus saine si la valeur du pointeur était définie sur NULL. Cependant, cela cachera le problème. Lorsque le programme est compilé avec différents paramètres d'optimisation ou porté dans un environnement différent, le problème apparaîtra probablement au moment le plus inopportun.

R Sahu
la source
1
Je ne pense pas que cela réponde à la question d'OP.
SergeyA
Pas d'accord même après modification. Le définir sur NULL ne masquera pas le problème - en fait, cela l'exposerait dans plus de cas que sans cela. Il y a une raison pour laquelle les implémentations normales ne font pas cela, et la raison est différente.
SergeyA
4
@SergeyA, la plupart des implémentations ne le font pas pour des raisons d'efficacité. Cependant, si une implémentation décide de le définir, il est préférable de le définir sur quelque chose qui n'est pas NULL. Cela révélerait les problèmes plus tôt que s'il était défini sur NULL. Il est défini sur NULL, un appel deletedeux fois sur le pointeur ne poserait pas de problème. Ce n'est certainement pas bon.
R Sahu
Non, pas l'efficacité - du moins, ce n'est pas la principale préoccupation.
SergeyA
7
@SergeyA Définir un pointeur sur une valeur qui n'est pas NULLmais aussi définitivement en dehors de l'espace d'adressage du processus exposera plus de cas que les deux alternatives. Le laisser en suspens ne provoquera pas nécessairement un segfault s'il est utilisé après avoir été libéré; le régler sur NULLne provoquera pas de segfault s'il est à deletenouveau d.
Blacklight Shining
10
delete ptr;
cout << "ptr = " << ptr << endl;

En général, même la lecture (comme vous le faites ci-dessus, notez: ceci est différent du déréférencement) des valeurs de pointeurs invalides (le pointeur devient invalide par exemple lorsque vous deletele faites ) est un comportement défini par l'implémentation. Cela a été introduit dans le CWG # 1438 . Voir aussi ici .

Veuillez noter qu'avant cela, la lecture des valeurs de pointeurs non valides était un comportement non défini, donc ce que vous avez ci-dessus serait un comportement non défini, ce qui signifie que tout pourrait arriver.

giorgim
la source
3
La citation de [basic.stc.dynamic.deallocation]: "Si l'argument donné à une fonction de désallocation dans la bibliothèque standard est un pointeur qui n'est pas la valeur nulle du pointeur, la fonction de désallocation doit désallouer le stockage référencé par le pointeur, rendant invalides tous les pointeurs faisant référence à une partie du stockage désalloué »et la règle [conv.lval](section 4.1) qui dit lire (conversion lvalue-> rvalue) toute valeur de pointeur invalide est un comportement défini par l'implémentation.
Ben Voigt
Même UB peut être implémenté d'une manière spécifique par un fournisseur spécifique afin qu'il soit fiable, au moins pour ce compilateur. Si Microsoft avait décidé d'implémenter sa fonctionnalité de nettoyage des pointeurs avant le CWG # 1438, cela n'aurait pas rendu cette fonctionnalité plus ou moins fiable, et en particulier il n'est tout simplement pas vrai que "tout pourrait arriver" si cette fonctionnalité était activée. , indépendamment de ce que dit la norme.
Kyle Strand
@KyleStrand: J'ai essentiellement donné la définition d'UB ( blog.regehr.org/archives/213 ).
giorgim
1
Pour la plupart des membres de la communauté C ++ sur SO, «tout peut arriver» est pris à la lettre . Je pense que c'est ridicule . Je comprends la définition d'UB, mais je comprends aussi que les compilateurs ne sont que des morceaux de logiciels implémentés par de vraies personnes, et si ces personnes implémentent le compilateur de sorte qu'il se comporte d'une certaine manière, c'est ainsi que le compilateur se comportera , indépendamment de ce que dit la norme .
Kyle Strand
1

Je crois que vous exécutez une sorte de mode de débogage et VS tente de rediriger votre pointeur vers un emplacement connu, de sorte que toute autre tentative de déréférencement puisse être tracée et signalée. Essayez de compiler / d'exécuter le même programme en mode version.

Les pointeurs ne sont généralement pas modifiés à l'intérieur deletepour des raisons d'efficacité et pour éviter de donner une fausse idée de la sécurité. La définition du pointeur de suppression sur une valeur prédéfinie ne servira à rien dans la plupart des scénarios complexes, car le pointeur en cours de suppression ne sera probablement que l'un des nombreux pointant vers cet emplacement.

En fait, plus j'y pense, plus je trouve que VS est en faute en le faisant, comme d'habitude. Que faire si le pointeur est const? Est-ce que ça va encore le changer?

SergeyA
la source
Oui, même les pointeurs constants sont redirigés vers ce mystérieux 8123!
tjwrona1992
Il y a une autre pierre à VS :) Ce matin, quelqu'un a demandé pourquoi ils devraient utiliser g ++ au lieu de VS. Voilà.
SergeyA
7
@SergeyA mais de l'autre côté, la suppression de ce pointeur supprimé vous montrera par défaut de segmentation que vous avez essayé de déréférencer un pointeur supprimé et qu'il ne sera pas égal à NULL. Dans l'autre cas, il ne plantera que si la page est également libérée (ce qui est très peu probable). Échouer plus vite; résoudre plus tôt.
ratchet freak
1
@ratchetfreak «Échouer vite, résoudre plus tôt» est un mantra très précieux, mais «Échouer rapidement en détruisant les preuves médico-légales clés» ne lance pas un mantra aussi précieux. Dans les cas simples, cela peut être pratique, mais dans les cas plus compliqués (ceux sur lesquels nous avons le plus besoin d'aide), l'effacement d'informations précieuses diminue mes outils disponibles pour résoudre le problème.
Cort Ammon
2
@ tjwrona1992: Microsoft fait la bonne chose ici à mon avis. Il vaut mieux désinfecter un pointeur que n'en faire aucun. Et si cela vous pose un problème de débogage, placez un point d'arrêt avant l'appel de suppression incorrect. Il y a de fortes chances que sans quelque chose comme ça, vous ne repériez jamais le problème. Et si vous avez une meilleure solution pour localiser ces bogues, utilisez-la et pourquoi vous souciez-vous de ce que fait Microsoft?
Zan Lynx
0

Après avoir supprimé le pointeur, la mémoire vers laquelle il pointe peut encore être valide. Pour manifester cette erreur, la valeur du pointeur est définie sur une valeur évidente. Cela aide vraiment le processus de débogage. Si la valeur était définie sur NULL, il se peut qu'elle ne s'affiche jamais comme un bogue potentiel dans le déroulement du programme. Cela peut donc masquer un bogue lorsque vous testez ultérieurement NULL.

Un autre point est que certains optimiseurs d'exécution peuvent vérifier cette valeur et modifier ses résultats.

Dans les temps anciens, MS définissait la valeur sur 0xcfffffff.

Karsten
la source