Tester les pointeurs de validité (C / C ++)


90

Existe-t-il un moyen de déterminer (par programme, bien sûr) si un pointeur donné est "valide"? Vérifier NULL est facile, mais qu'en est-il des choses comme 0x00001234? Lorsque vous essayez de déréférencer ce type de pointeur, une exception / un crash se produit.

Une méthode multiplateforme est préférable, mais spécifique à la plate-forme (pour Windows et Linux) est également acceptable.

Mise à jour pour clarification: le problème ne vient pas des pointeurs périmés / libérés / non initialisés; à la place, j'implémente une API qui prend des pointeurs de l'appelant (comme un pointeur vers une chaîne, un descripteur de fichier, etc.). L'appelant peut envoyer (intentionnellement ou par erreur) une valeur non valide comme pointeur. Comment éviter un crash?



Je pense que la meilleure réponse positive pour Linux est donnée par George Carrette. Si cela ne suffit pas, envisagez de créer la table des symboles de fonction dans la bibliothèque, ou même un autre niveau de table des bibliothèques disponibles avec leurs propres tables de fonctions. Ensuite, comparez ces tableaux exacts. Bien sûr, ces réponses négatives sont également correctes: vous ne pouvez pas vraiment être sûr à 100% si un pointeur de fonction est valide ou non à moins que vous n'imposiez de nombreuses restrictions supplémentaires à l'application utilisateur.
minghua

La spécification API spécifie-t-elle réellement une telle obligation à respecter par l'implémentation? Au fait, je prétends n'avoir pas supposé que vous êtes à la fois le développeur et le concepteur. Mon point étant, je ne pense pas qu'une API spécifierait quelque chose comme "En cas de passage d'un pointeur invalide comme argument, la fonction doit gérer le problème et renvoie NULL.". Une API s'engage à fournir un service dans des conditions d'utilisation appropriées, et non par des hacks. Néanmoins, il ne fait aucun mal d'être un peu stupide. En utilisant une référence, ces cas se propagent moins. :)
Poniros

Réponses:


75

Mise à jour pour clarification: Le problème ne vient pas des pointeurs périmés, libérés ou non initialisés; à la place, j'implémente une API qui prend des pointeurs de l'appelant (comme un pointeur vers une chaîne, un descripteur de fichier, etc.). L'appelant peut envoyer (intentionnellement ou par erreur) une valeur non valide comme pointeur. Comment éviter un crash?

Vous ne pouvez pas faire ce chèque. Il n'y a tout simplement aucun moyen de vérifier si un pointeur est "valide". Vous devez avoir confiance que lorsque les gens utilisent une fonction qui prend un pointeur, ces gens savent ce qu'ils font. S'ils vous transmettent 0x4211 comme valeur de pointeur, vous devez alors faire confiance qu'il pointe vers l'adresse 0x4211. Et s'ils heurtaient "accidentellement" un objet, alors même si vous utilisiez une fonction du système d'exploitation effrayante (IsValidPtr ou autre), vous vous glisseriez toujours dans un bogue et n'échoueriez pas rapidement.

Commencez à utiliser des pointeurs nuls pour signaler ce genre de chose et dites à l'utilisateur de votre bibliothèque qu'il ne doit pas utiliser de pointeurs s'ils ont tendance à passer accidentellement des pointeurs invalides, sérieusement :)


C'est probablement la bonne réponse mais je pense qu'une fonction simple qui vérifie les emplacements de mémoire hexspeak communs serait utile pour le débogage général ... En ce moment, j'ai un pointeur qui pointe parfois vers 0xfeeefeee et si j'avais une fonction simple que je pourrais utiliser pour poivrer des affirmations autour de Cela faciliterait beaucoup la recherche du coupable ... EDIT: Bien qu'il ne soit pas difficile d'en écrire un vous-même, je suppose ..
quant

@quant le problème est que certains codes C et C ++ pourraient faire de l'arithmétique de pointeur sur une adresse invalide sans vérification (basé sur le principe garbage-in, garbage-out) et passeront donc un pointeur "arithmétiquement modifié" de l'un de ces puits -adresses invalides connues. Cas courants de recherche d'une méthode à partir d'une vtable inexistante basée sur une adresse d'objet non valide ou d'un type incorrect, ou simplement la lecture de champs à partir d'un pointeur vers une structure qui ne pointe pas vers un.
rwong

Cela signifie essentiellement que vous ne pouvez prendre des indices de tableau que du monde extérieur. Une API qui doit se défendre contre l'appelant ne peut tout simplement pas avoir de pointeurs dans l'interface. Cependant, il serait toujours bon d'avoir des macros à utiliser dans les assertions sur la validité des pointeurs (que vous êtes obligé d'avoir en interne). S'il est garanti qu'un pointeur pointe à l'intérieur d'un tableau dont le point de départ et la longueur sont connus, cela peut être vérifié explicitement. Il vaut mieux mourir d'une violation d'assert (erreur documentée) que d'un deref (erreur non documentée).
Rob

34

Voici trois façons simples pour un programme C sous Linux de s'introspecter sur l'état de la mémoire dans laquelle il s'exécute, et pourquoi la question a des réponses sophistiquées appropriées dans certains contextes.

  1. Après avoir appelé getpagesize () et arrondi le pointeur à une limite de page, vous pouvez appeler mincore () pour savoir si une page est valide et si elle fait partie de l'ensemble de travail du processus. Notez que cela nécessite des ressources du noyau, vous devez donc le comparer et déterminer si l'appel de cette fonction est vraiment approprié dans votre api. Si votre API doit gérer des interruptions ou lire des ports série en mémoire, il est approprié de l'appeler pour éviter les comportements imprévisibles.
  2. Après avoir appelé stat () pour déterminer s'il existe un répertoire / proc / self disponible, vous pouvez ouvrir et lire / proc / self / maps pour trouver des informations sur la région dans laquelle se trouve un pointeur. Étudiez la page de manuel de proc, le pseudo-système de fichiers des informations de processus. De toute évidence, cela coûte relativement cher, mais vous pourrez peut-être vous en sortir avec la mise en cache du résultat de l'analyse dans un tableau que vous pouvez rechercher efficacement à l'aide d'une recherche binaire. Considérez également le / proc / self / smaps. Si votre API est destinée au calcul haute performance, alors le programme voudra connaître le / proc / self / numa qui est documenté sous la page de manuel de numa, l'architecture de mémoire non uniforme.
  3. L'appel get_mempolicy (MPOL_F_ADDR) est approprié pour le travail des api de calcul haute performance où il existe plusieurs threads d'exécution et que vous gérez votre travail pour avoir une affinité pour la mémoire non uniforme en ce qui concerne les cœurs du processeur et les ressources de socket. Une telle API vous dira bien sûr également si un pointeur est valide.

Sous Microsoft Windows, il y a la fonction QueryWorkingSetEx qui est documentée sous l'API d'état du processus (également dans l'API NUMA). Comme corollaire de la programmation sophistiquée de l'API NUMA, cette fonction vous permettra également de faire de simples «tests de pointeurs de validité (C / C ++)», en tant que telle, il est peu probable qu'elle soit obsolète pendant au moins 15 ans.


12
Première réponse qui n'essaye pas d'être moralement sur la question elle-même et y répond parfaitement. Les gens ne réalisent parfois pas que l'on a vraiment besoin de ce type d'approche de débogage pour trouver des bogues, par exemple dans des bibliothèques tierces ou dans du code hérité, car même valgrind ne trouve que des pointeurs sauvages en y accédant réellement, pas par exemple si vous voulez vérifier régulièrement la validité des pointeurs dans une table de cache qui a été écrasée d'un autre endroit dans votre code ...
lumpidu

Cela devrait être la réponse acceptée. Je l'ai fait de manière similaire sur une plate-forme non Linux. Fondamentalement, il expose les informations de processus au processus lui-même. Avec cet aspect, il semble que Windows fasse un meilleur travail que Linux en exposant les informations les plus significatives via l'API d'état du processus.
minghua

31

Empêcher un crash causé par l'appelant qui envoie un pointeur non valide est un bon moyen de créer des bogues silencieux difficiles à trouver.

N'est-il pas préférable que le programmeur utilisant votre API reçoive un message clair indiquant que son code est faux en le plantant plutôt que de le cacher?


8
Dans certains cas, cependant, la recherche d'un mauvais pointeur immédiatement lorsque l'API est appelée est la façon dont vous échouez tôt. Par exemple, que se passe-t-il si l'API stocke le pointeur dans une structure de données où il ne serait déféré que plus tard? Le fait de passer ensuite à l'API un mauvais pointeur provoquera un crash à un moment ultérieur aléatoire. Dans ce cas, il serait préférable d'échouer plus tôt, lors de l'appel d'API où la mauvaise valeur a été initialement introduite.
peterflynn

28

Sur Win32 / 64, il existe un moyen de le faire. Tentative de lecture du pointeur et attrape l'exception SEH résultante qui sera lancée en cas d'échec. S'il ne lance pas, c'est un pointeur valide.

Le problème avec cette méthode est cependant qu'elle renvoie simplement si vous pouvez ou non lire les données du pointeur. Il ne donne aucune garantie sur la sécurité du type ou sur un certain nombre d'autres invariants. En général, cette méthode ne sert à rien d'autre que de dire "oui, je peux lire cet endroit particulier en mémoire à un moment qui est maintenant passé".

Bref, ne faites pas ça;)

Raymond Chen a un article de blog sur ce sujet: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx


3
@Tim, il n'y a aucun moyen de faire cela en C ++.
JaredPar

6
C'est seulement la "bonne réponse" si vous définissez "pointeur valide" comme "ne provoque pas de violation d'accès / de segfault". Je préférerais le définir comme "pointe vers des données significatives allouées aux fins que vous allez utiliser". Je dirais que c'est une meilleure définition de la validité du pointeur ...;)
jalf

Même si le pointeur est valide, il ne peut pas être vérifié de cette façon. Pensez à thread1 () {.. if (IsValidPtr (p)) * p = 7; ...} thread2 () {sommeil (1); supprimer p; ...}
Christopher

2
@Christopher, très vrai. J'aurais dû dire "Je peux lire cet endroit particulier en mémoire à un moment qui est maintenant passé"
JaredPar

@JaredPar: Vraiment mauvaise suggestion. Peut déclencher une page de garde, donc la pile ne sera pas développée plus tard ou quelque chose d'aussi agréable.
Deduplicator

16

AFAIK il n'y a aucun moyen. Vous devez essayer d'éviter cette situation en définissant toujours les pointeurs sur NULL après avoir libéré de la mémoire.


4
Définir un pointeur sur null ne vous donne rien, sauf peut-être un faux sentiment de sécurité.

Ce n'est pas vrai. Surtout en C ++, vous pouvez déterminer s'il faut supprimer les objets membres en vérifiant la valeur null. Notez également qu'en C ++, il est valide de supprimer les pointeurs NULL, donc la suppression inconditionnelle d'objets dans les destructeurs est populaire.
Ferdinand Beyer le

4
int * p = nouveau int (0); int * p2 = p; supprimer p; p = NULL; supprimer p2; // crash

1
zabzonk et ?? ce qu'il a dit, c'est que vous pouvez supprimer un pointeur nul. p2 n'est pas un pointeur nul, mais un pointeur invalide. vous devez le définir sur null avant.
Johannes Schaub - litb

2
Si vous avez des alias vers la mémoire pointée, un seul d'entre eux serait défini sur NULL, d'autres alias sont en suspens.
jdehaan

7

Jetez un œil à ceci et à cette question. Jetez également un œil aux pointeurs intelligents .


7

En ce qui concerne la réponse un peu plus haut dans ce fil:

IsBadReadPtr (), IsBadWritePtr (), IsBadCodePtr (), IsBadStringPtr () pour Windows.

Mon conseil est de rester loin d'eux, quelqu'un a déjà posté celui-ci: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx

Un autre article sur le même sujet et du même auteur (je pense) est celui-ci: http://blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx ("IsBadXxxPtr devrait vraiment s'appeler CrashProgramRandomly ").

Si les utilisateurs de votre API envoient de mauvaises données, laissez-les planter. Si le problème est que les données transmises ne sont utilisées que plus tard (et cela rend plus difficile la recherche de la cause), ajoutez un mode de débogage où les chaînes, etc. sont enregistrées à l'entrée. S'ils sont mauvais, ce sera évident (et probablement un crash). Si cela se produit trop souvent, cela peut valoir la peine de retirer votre API du processus et de les laisser planter le processus API au lieu du processus principal.


Une autre façon est probablement d'utiliser _CrtIsValidHeapPointer . Cette fonction retournera TRUE si le pointeur est valide et lèvera une exception lorsque le pointeur est libéré. Comme documenté, cette fonction n'est disponible que dans debug CRT.
Crend King

6

Premièrement, je ne vois aucun intérêt à essayer de vous protéger de l'appelant qui tente délibérément de provoquer un crash. Ils pourraient facilement le faire en essayant d'accéder eux-mêmes via un pointeur invalide. Il existe de nombreuses autres façons - elles pourraient simplement écraser votre mémoire ou la pile. Si vous avez besoin de vous protéger contre ce genre de choses, vous devez exécuter un processus séparé en utilisant des sockets ou un autre IPC pour la communication.

Nous écrivons pas mal de logiciels qui permettent aux partenaires / clients / utilisateurs d'étendre les fonctionnalités. Inévitablement, tout bogue nous est signalé en premier, il est donc utile de pouvoir montrer facilement que le problème est dans le code du plug-in. De plus, il y a des problèmes de sécurité et certains utilisateurs sont plus fiables que d'autres.

Nous utilisons un certain nombre de méthodes différentes en fonction des exigences de performance / débit et de fiabilité. De la plus préférée:

  • processus séparés utilisant des sockets (passant souvent des données sous forme de texte).

  • processus séparés utilisant la mémoire partagée (si de grandes quantités de données doivent être transmises).

  • même processus séparer les threads via la file d'attente de messages (si des messages courts fréquents).

  • le même processus sépare les threads de toutes les données transmises allouées à partir d'un pool de mémoire.

  • même processus via un appel de procédure direct - toutes les données transmises allouées à partir d'un pool de mémoire.

Nous essayons de ne jamais recourir à ce que vous essayez de faire lorsque vous traitez avec des logiciels tiers - en particulier lorsque les plug-ins / bibliothèque nous sont fournis sous forme de code binaire plutôt que source.

L'utilisation d'un pool de mémoire est assez facile dans la plupart des cas et n'a pas besoin d'être inefficace. Si VOUS allouez les données en premier lieu, il est trivial de vérifier les pointeurs par rapport aux valeurs que vous avez allouées. Vous pouvez également stocker la longueur allouée et ajouter des valeurs «magiques» avant et après les données pour vérifier le type de données valide et les dépassements de données.


4

J'ai beaucoup de sympathie pour votre question, car je suis moi-même dans une position presque identique. J'apprécie ce que beaucoup de réponses disent, et elles sont correctes - la routine fournissant le pointeur devrait fournir un pointeur valide. Dans mon cas, il est presque inconcevable qu'ils aient pu corrompre le pointeur - mais s'ils avaient réussi, ce serait MON logiciel qui plante, et MOI qui serait blâmé :-(

Mon exigence n'est pas de continuer après une erreur de segmentation - ce serait dangereux - je veux juste signaler ce qui est arrivé au client avant de résilier afin qu'il puisse réparer son code plutôt que de me blâmer!

Voici comment j'ai trouvé pour le faire (sous Windows): http://www.cplusplus.com/reference/clibrary/csignal/signal/

Pour donner un synopsis:

#include <signal.h>

using namespace std;

void terminate(int param)
/// Function executed if a segmentation fault is encountered during the cast to an instance.
{
  cerr << "\nThe function received a corrupted reference - please check the user-supplied  dll.\n";
  cerr << "Terminating program...\n";
  exit(1);
}

...
void MyFunction()
{
    void (*previous_sigsegv_function)(int);
    previous_sigsegv_function = signal(SIGSEGV, terminate);

    <-- insert risky stuff here -->

    signal(SIGSEGV, previous_sigsegv_function);
}

Maintenant, cela semble se comporter comme je l'espère (il affiche le message d'erreur, puis met fin au programme) - mais si quelqu'un peut repérer une faille, faites-le moi savoir!


Ne pas utiliser exit(), cela contourne RAII et peut donc provoquer des fuites de ressources.
Sebastian Mach

Intéressant - y a-t-il une autre façon de terminer proprement dans cette situation? Et l'instruction de sortie est-elle le seul problème à faire comme ça? Je remarque que j'ai acquis un "-1" - est-ce juste à cause de la "sortie"?
Mike Sadler

Oups, je me rends compte que c'est pour une situation assez exceptionnelle. Je viens de voir exit()et ma cloche d'alarme portable C ++ a commencé à sonner. Cela devrait convenir dans cette situation spécifique à Linux, où votre programme se fermerait de toute façon, désolé pour le bruit.
Sebastian Mach

1
signal (2) n'est pas portable. Utilisez sigaction (2). man 2 signalsur Linux a un paragraphe expliquant pourquoi.
rptb1

1
Dans cette situation, j'appellerais généralement abort (3) plutôt que exit (3) car il est plus susceptible de produire une sorte de backtrace de débogage que vous pouvez utiliser pour diagnostiquer le problème post-mortem. Sur la plupart des Unixen, abort (3) videra le core (si les core dumps sont autorisés) et sous Windows, il proposera de lancer un débogueur s'il est installé.
rptb1

4

Sous Unix, vous devriez pouvoir utiliser un appel système du noyau qui vérifie le pointeur et renvoie EFAULT, tel que:

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdbool.h>

bool isPointerBad( void * p )
{
   int fh = open( p, 0, 0 );
   int e = errno;

   if ( -1 == fh && e == EFAULT )
   {
      printf( "bad pointer: %p\n", p );
      return true;
   }
   else if ( fh != -1 )
   {
      close( fh );
   }

   printf( "good pointer: %p\n", p );
   return false;
}

int main()
{
   int good = 4;
   isPointerBad( (void *)3 );
   isPointerBad( &good );
   isPointerBad( "/tmp/blah" );

   return 0;
}

retour:

bad pointer: 0x3
good pointer: 0x7fff375fd49c
good pointer: 0x400793

Il y a probablement un meilleur appel système à utiliser que open () [peut-être access], car il y a une chance que cela puisse conduire à la création d'un chemin de code réel, et à une exigence de fermeture ultérieure.


C'est un hack brillant. J'adorerais voir des conseils sur différents appels système pour valider les plages de mémoire, surtout s'il est garanti qu'ils n'ont pas d'effets secondaires. Vous pouvez garder un descripteur de fichier ouvert pour écrire dans / dev / null pour tester si les tampons sont en mémoire lisible, mais il existe probablement des solutions plus simples. Le meilleur que je puisse trouver est le lien symbolique (ptr, "") qui mettra errno à 14 sur une mauvaise adresse ou 2 sur une bonne adresse, mais les changements de noyau pourraient changer l'ordre de vérification.
Preston le

@Preston Dans DB2, je pense que nous utilisions l'accès () de unistd.h. J'ai utilisé open () ci-dessus parce que c'est un peu moins obscur, mais vous avez probablement raison de dire qu'il y a beaucoup d'appels système possibles à utiliser. Windows avait l'habitude d'avoir une API de vérification de pointeur explicite, mais il s'est avéré ne pas être thread-safe (je pense qu'il utilisait SEH pour essayer d'écrire puis de restaurer les limites de la plage de mémoire.)
Peeter Joot

2

Il n'y a aucune disposition en C ++ pour tester la validité d'un pointeur en tant que cas général. On peut évidemment supposer que NULL (0x00000000) est mauvais, et divers compilateurs et bibliothèques aiment utiliser des «valeurs spéciales» ici et là pour faciliter le débogage (par exemple, si jamais je vois un pointeur apparaître comme 0xCECECECE dans Visual Studio, je sais J'ai fait quelque chose de mal) mais la vérité est que comme un pointeur n'est qu'un index en mémoire, il est presque impossible de dire simplement en regardant le pointeur s'il s'agit du "bon" index.

Il existe diverses astuces que vous pouvez faire avec dynamic_cast et RTTI pour vous assurer que l'objet pointé est du type souhaité, mais elles nécessitent toutes que vous pointiez vers quelque chose de valide en premier lieu.

Si vous voulez vous assurer que votre programme peut détecter des pointeurs "non valides", mon conseil est le suivant: définissez chaque pointeur que vous déclarez soit sur NULL, soit sur une adresse valide immédiatement lors de la création et définissez-le sur NULL immédiatement après avoir libéré la mémoire vers laquelle il pointe. Si vous faites preuve de diligence dans cette pratique, la vérification de NULL est tout ce dont vous avez besoin.


Une constante de pointeur nul en C ++ (ou C, d'ailleurs), est représentée par une intégrale constante zéro. De nombreuses implémentations utilisent tous les zéros binaires pour le représenter, mais ce n'est pas quelque chose sur lequel compter.
David Thornley

2

Il n'y a pas de moyen portable de le faire, et le faire pour des plates-formes spécifiques peut être n'importe où entre difficile et impossible. Dans tous les cas, vous ne devriez jamais écrire de code qui dépend d'une telle vérification - ne laissez pas les pointeurs prendre des valeurs invalides en premier lieu.


2

Définir le pointeur sur NULL avant et après l'utilisation est une bonne technique. C'est facile à faire en C ++ si vous gérez des pointeurs au sein d'une classe par exemple (une chaîne):

class SomeClass
{
public:
    SomeClass();
    ~SomeClass();

    void SetText( const char *text);
    char *GetText() const { return MyText; }
    void Clear();

private:
    char * MyText;
};


SomeClass::SomeClass()
{
    MyText = NULL;
}


SomeClass::~SomeClass()
{
    Clear();
}

void SomeClass::Clear()
{
    if (MyText)
        free( MyText);

    MyText = NULL;
}



void SomeClass::Settext( const char *text)
{
    Clear();

    MyText = malloc( strlen(text));

    if (MyText)
        strcpy( MyText, text);
}

Une question mise à jour rend ma réponse erronée, bien sûr (ou au moins une réponse à une autre question). Je suis d'accord avec les réponses qui disent essentiellement, laissez-les planter s'ils abusent de l'api. Vous ne pouvez pas empêcher les gens de se frapper dans le pouce avec un marteau ...
Tim Ring

2

Ce n'est pas une très bonne politique d'accepter des pointeurs arbitraires comme paramètres d'entrée dans une API publique. Il est préférable d'avoir des types de "données simples" comme un entier, une chaîne ou une structure (je veux dire une structure classique avec des données simples à l'intérieur, bien sûr; officiellement, tout peut être une structure).

Pourquoi? Eh bien parce que, comme d'autres le disent, il n'y a pas de moyen standard de savoir si vous avez reçu un pointeur valide ou un pointant vers des fichiers indésirables.

Mais parfois, vous n'avez pas le choix - votre API doit accepter un pointeur.

Dans ces cas, il est du devoir de l'appelant de passer un bon pointeur. NULL peut être accepté comme valeur, mais pas comme pointeur vers des fichiers indésirables.

Pouvez-vous vérifier de quelque manière que ce soit? Eh bien, ce que j'ai fait dans un cas comme celui-là, c'était de définir un invariant pour le type vers lequel pointe le pointeur, et de l'appeler quand vous l'avez (en mode débogage). Au moins si l'invariant échoue (ou plante), vous savez que vous avez reçu une mauvaise valeur.

// API that does not allow NULL
void PublicApiFunction1(Person* in_person)
{
  assert(in_person != NULL);
  assert(in_person->Invariant());

  // Actual code...
}

// API that allows NULL
void PublicApiFunction2(Person* in_person)
{
  assert(in_person == NULL || in_person->Invariant());

  // Actual code (must keep in mind that in_person may be NULL)
}

re: "passer un type de données simple ... comme une chaîne" Mais en C ++, les chaînes sont le plus souvent passées sous forme de pointeurs vers des caractères, (char *) ou (const char *), vous êtes donc de retour au passage des pointeurs. Et votre exemple passe in_person comme référence, pas comme pointeur, donc la comparaison (in_person! = NULL) implique qu'il y a des comparaisons objet / pointeur définies dans la classe Person.
Jesse Chisholm

@JesseChisholm Par string, je voulais dire une chaîne, c'est-à-dire une std :: string. En aucun cas je ne recommande d'utiliser char * comme moyen de stocker des chaînes ou de les transmettre. Ne fais pas ça.
Daniel Daranas

@JesseChisholm Pour une raison quelconque, j'ai fait une erreur en répondant à cette question il y a cinq ans. De toute évidence, cela n'a pas de sens de vérifier si une personne & est NULL. Cela ne compilerait même pas. J'avais l'intention d'utiliser des pointeurs, pas des références. Je l'ai réparé maintenant.
Daniel Daranas

1

Comme d'autres l'ont dit, vous ne pouvez pas détecter de manière fiable un pointeur invalide. Considérez certaines des formes qu'un pointeur non valide peut prendre:

Vous pourriez avoir un pointeur nul. C'est celui que vous pouvez facilement vérifier et faire quelque chose.

Vous pourriez avoir un pointeur vers quelque part en dehors de la mémoire valide. Ce qui constitue une mémoire valide varie en fonction de la façon dont l'environnement d'exécution de votre système configure l'espace d'adressage. Sur les systèmes Unix, il s'agit généralement d'un espace d'adressage virtuel commençant à 0 et allant jusqu'à un grand nombre de mégaoctets. Sur les systèmes embarqués, cela peut être assez petit. Il peut ne pas commencer à 0, dans tous les cas. Si votre application s'exécute en mode superviseur ou équivalent, votre pointeur peut faire référence à une adresse réelle, qui peut ou non être sauvegardée avec de la mémoire réelle.

Vous pourriez avoir un pointeur vers quelque part dans votre mémoire valide, même à l'intérieur de votre segment de données, bss, pile ou tas, mais ne pointant pas sur un objet valide. Une variante de ceci est un pointeur qui pointait vers un objet valide, avant que quelque chose de mauvais n'arrive à l'objet. Les mauvaises choses dans ce contexte incluent la désallocation, la corruption de la mémoire ou la corruption du pointeur.

Vous pouvez avoir un pointeur illégal, tel qu'un pointeur avec un alignement illégal pour l'objet référencé.

Le problème s'aggrave encore lorsque vous considérez les architectures basées sur le segment / décalage et d'autres implémentations de pointeurs étranges. Ce genre de chose est normalement caché au développeur par de bons compilateurs et une utilisation judicieuse des types, mais si vous voulez percer le voile et essayer de déjouer le système d'exploitation et les développeurs de compilateurs, eh bien, vous pouvez, mais il n'y a pas de moyen générique. pour le faire qui gérera tous les problèmes que vous pourriez rencontrer.

La meilleure chose à faire est d'autoriser le crash et de publier de bonnes informations de diagnostic.


re: "publier de bonnes informations de diagnostic", voilà le hic. Comme vous ne pouvez pas vérifier la validité du pointeur, les informations dont vous devez vous préoccuper sont minimes. "Une exception s'est produite ici", peut être tout ce que vous obtenez. L'ensemble de la pile d'appels est agréable, mais nécessite un meilleur cadre que la plupart des bibliothèques d'exécution C ++.
Jesse Chisholm


1

En général, c'est impossible à faire. Voici un cas particulièrement méchant:

struct Point2d {
    int x;
    int y;
};

struct Point3d {
    int x;
    int y;
    int z;
};

void dump(Point3 *p)
{
    printf("[%d %d %d]\n", p->x, p->y, p->z);
}

Point2d points[2] = { {0, 1}, {2, 3} };
Point3d *p3 = reinterpret_cast<Point3d *>(&points[0]);
dump(p3);

Sur de nombreuses plates-formes, cela imprimera:

[0 1 2]

Vous forcez le système d'exécution à interpréter de manière incorrecte les bits de mémoire, mais dans ce cas, il ne va pas planter, car les bits ont tous un sens. Cela fait partie de la conception de la langue (regardez le polymorphisme C-style avec struct inaddr, inaddr_in, inaddr_in6), de sorte que vous ne pouvez pas protéger efficacement contre sur toute plateforme.


1

C'est incroyable la quantité d'informations trompeuses que vous pouvez lire dans les articles ci-dessus ...

Et même dans la documentation Microsoft MSDN, IsBadPtr est censé être interdit. Eh bien, je préfère une application qui fonctionne plutôt que de planter. Même si le travail à terme peut ne pas fonctionner correctement (tant que l'utilisateur final peut continuer avec l'application).

En googlant, je n'ai trouvé aucun exemple utile pour Windows - j'ai trouvé une solution pour les applications 32 bits,

http://www.codeproject.com/script/Content/ViewAssociatedFile.aspx?rzp=%2FKB%2Fsystem%2Fdetect-driver%2F%2FDetectDriverSrc.zip&zep=DetectDriverSrc%2FDetectDriver%2FsrcF%2Fdrobid.c. = 2

mais je dois également prendre en charge les applications 64 bits, donc cette solution n'a pas fonctionné pour moi.

Mais j'ai récolté les codes sources de wine et j'ai réussi à cuisiner un type de code similaire qui fonctionnerait également pour les applications 64 bits - en attachant le code ici:

#include <typeinfo.h>   

typedef void (*v_table_ptr)();   

typedef struct _cpp_object   
{   
    v_table_ptr*    vtable;   
} cpp_object;   



#ifndef _WIN64
typedef struct _rtti_object_locator
{
    unsigned int signature;
    int base_class_offset;
    unsigned int flags;
    const type_info *type_descriptor;
    //const rtti_object_hierarchy *type_hierarchy;
} rtti_object_locator;
#else

typedef struct
{
    unsigned int signature;
    int base_class_offset;
    unsigned int flags;
    unsigned int type_descriptor;
    unsigned int type_hierarchy;
    unsigned int object_locator;
} rtti_object_locator;  

#endif

/* Get type info from an object (internal) */  
static const rtti_object_locator* RTTI_GetObjectLocator(void* inptr)  
{   
    cpp_object* cppobj = (cpp_object*) inptr;  
    const rtti_object_locator* obj_locator = 0;   

    if (!IsBadReadPtr(cppobj, sizeof(void*)) &&   
        !IsBadReadPtr(cppobj->vtable - 1, sizeof(void*)) &&   
        !IsBadReadPtr((void*)cppobj->vtable[-1], sizeof(rtti_object_locator)))  
    {  
        obj_locator = (rtti_object_locator*) cppobj->vtable[-1];  
    }  

    return obj_locator;  
}  

Et le code suivant peut détecter si le pointeur est valide ou non, vous devez probablement ajouter une vérification NULL:

    CTest* t = new CTest();
    //t = (CTest*) 0;
    //t = (CTest*) 0x12345678;

    const rtti_object_locator* ptr = RTTI_GetObjectLocator(t);  

#ifdef _WIN64
    char *base = ptr->signature == 0 ? (char*)RtlPcToFileHeader((void*)ptr, (void**)&base) : (char*)ptr - ptr->object_locator;
    const type_info *td = (const type_info*)(base + ptr->type_descriptor);
#else
    const type_info *td = ptr->type_descriptor;
#endif
    const char* n =td->name();

Cela obtient le nom de la classe du pointeur - je pense que cela devrait suffire à vos besoins.

Une chose qui, je le crains encore, est la performance de la vérification des pointeurs - dans l'extrait de code ci-dessus, il y a déjà 3-4 appels d'API en cours - pourrait être excessive pour les applications critiques en temps.

Ce serait bien si quelqu'un pouvait mesurer la surcharge de la vérification du pointeur par rapport, par exemple, aux appels C # / c ++ gérés.


1

En effet, quelque chose pourrait être fait sous une occasion spécifique: par exemple si vous voulez vérifier si une chaîne de pointeur de chaîne est valide, l'utilisation de write (fd, buf, szie) syscall peut vous aider à faire la magie: laissez fd être un descripteur de fichier temporaire fichier que vous créez pour le test, et buf pointant sur la chaîne que vous testez, si le pointeur n'est pas valide, write () renverrait -1 et errno mis à EFAULT, ce qui indique que buf est en dehors de votre espace d'adressage accessible.


1

Ce qui suit fonctionne sous Windows (quelqu'un l'a suggéré avant):

 static void copy(void * target, const void* source, int size)
 {
     __try
     {
         CopyMemory(target, source, size);
     }
     __except(EXCEPTION_EXECUTE_HANDLER)
     {
         doSomething(--whatever--);
     }
 }

La fonction doit être une méthode statique, autonome ou statique d'une classe. Pour tester en lecture seule, copiez les données dans la mémoire tampon locale. Pour tester l'écriture sans modifier le contenu, écrivez-les. Vous ne pouvez tester que les premières / dernières adresses. Si le pointeur n'est pas valide, le contrôle sera passé à «doSomething», puis hors des crochets. N'utilisez simplement rien nécessitant des destructeurs, comme CString.


1

Sous Windows, j'utilise ce code:

void * G_pPointer = NULL;
const char * G_szPointerName = NULL;
void CheckPointerIternal()
{
    char cTest = *((char *)G_pPointer);
}
bool CheckPointerIternalExt()
{
    bool bRet = false;

    __try
    {
        CheckPointerIternal();
        bRet = true;
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
    }

    return  bRet;
}
void CheckPointer(void * A_pPointer, const char * A_szPointerName)
{
    G_pPointer = A_pPointer;
    G_szPointerName = A_szPointerName;
    if (!CheckPointerIternalExt())
        throw std::runtime_error("Invalid pointer " + std::string(G_szPointerName) + "!");
}

Usage:

unsigned long * pTest = (unsigned long *) 0x12345;
CheckPointer(pTest, "pTest"); //throws exception

0

IsBadReadPtr (), IsBadWritePtr (), IsBadCodePtr (), IsBadStringPtr () pour Windows.
Celles-ci prennent un temps proportionnel à la longueur du bloc, donc pour vérifier la cohérence, je vérifie simplement l'adresse de départ.


3
vous devez éviter ces méthodes car elles ne fonctionnent pas. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx
JaredPar

Parfois, il peut s'agir de solutions de contournement pour ne pas fonctionner: stackoverflow.com/questions/496034/…
ChrisW

0

J'ai vu diverses bibliothèques utiliser une méthode pour vérifier la mémoire non référencée et autres. Je crois qu'ils "écrasent" simplement les méthodes d'allocation de mémoire et de désallocation (malloc / free), qui ont une certaine logique qui garde la trace des pointeurs. Je suppose que c'est exagéré pour votre cas d'utilisation, mais ce serait une façon de le faire.


Cela n'aide malheureusement pas les objets alloués à la pile.
Tom

0

Techniquement, vous pouvez remplacer l'opérateur new (et supprimer ) et collecter des informations sur toute la mémoire allouée, de sorte que vous pouvez avoir une méthode pour vérifier si la mémoire du tas est valide. mais:

  1. vous avez toujours besoin d'un moyen de vérifier si le pointeur est alloué sur stack ()

  2. vous devrez définir ce qui est un pointeur `` valide '':

a) la mémoire sur cette adresse est allouée

b) la mémoire à cette adresse est l' adresse de début de l'objet (par exemple, l'adresse n'est pas au milieu d'un énorme tableau)

c) la mémoire à cette adresse est l' adresse de début de l'objet du type attendu

Conclusion : l'approche en question n'est pas une méthode C ++, vous devez définir certaines règles qui garantissent que la fonction reçoit des pointeurs valides.



0

Addendum à la (aux) réponse (s) acceptée (s):

Supposons que votre pointeur ne puisse contenir que trois valeurs - 0, 1 et -1 où 1 signifie un pointeur valide, -1 un pointeur non valide et 0 un autre non valide. Quelle est la probabilité que votre pointeur soit NULL, toutes les valeurs étant également probables? 1/3. Maintenant, retirez le cas valide, donc pour chaque cas invalide, vous avez un ratio de 50:50 pour attraper toutes les erreurs. Ça a l'air bien non? Mettez cela à l'échelle pour un pointeur de 4 octets. Il y a 2 ^ 32 ou 4294967294 valeurs possibles. Parmi ceux-ci, une seule valeur est correcte, une est NULL et il vous reste 4294967292 autres cas non valides. Recalculer: vous avez un test pour 1 cas invalide sur (4294967292+ 1). Une probabilité de 2.xe-10 ou 0 pour la plupart des raisons pratiques. Telle est la futilité du contrôle NULL.


0

Vous savez, un nouveau pilote (au moins sur Linux) capable de cela ne serait probablement pas si difficile à écrire.

D'un autre côté, ce serait une folie de construire vos programmes comme ça. À moins que vous n'ayez une utilisation vraiment spécifique et unique pour une telle chose, je ne le recommanderais pas. Si vous construisez une application volumineuse chargée de contrôles de validité de pointeur constants, elle sera probablement terriblement lente.


0

vous devez éviter ces méthodes car elles ne fonctionnent pas. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx - JaredPar 15 février 09 à 16:02

S'ils ne fonctionnent pas, la prochaine mise à jour de Windows résoudra le problème? S'ils ne fonctionnent pas au niveau du concept, la fonction sera probablement complètement supprimée de l'API Windows.

La documentation MSDN prétend qu'ils sont interdits, et la raison en est probablement un défaut de conception ultérieure de l'application (par exemple, en général, vous ne devriez pas manger de pointeurs invalides en silence - si vous êtes en charge de la conception de l'application entière bien sûr), et performances / temps de vérification du pointeur.

Mais vous ne devriez pas prétendre qu'ils ne fonctionnent pas à cause d'un blog. Dans mon application de test, j'ai vérifié qu'ils fonctionnent.


En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.