53 votes

Quand est-il valide d'accéder à un pointeur vers un objet "mort"?

Tout d'abord, pour clarifier, je ne parle pas de la dereferrisation des pointeurs invalides!

Considérons les deux exemples suivants.

Exemple 1

typedef struct { int *p; } T;

T a = { malloc(sizeof(int) };
free(a.p);  // a.p est maintenant indéterminé?
T b = a;    // Accès via un type non caractère?

Exemple 2

void foo(int *p) {}

int *p = malloc(sizeof(int));
free(p);   // p est maintenant indéterminé?
foo(p);    // Accès via un type non caractère?

Question

Est-ce que l'un des deux exemples ci-dessus induit un comportement indéfini?

Contexte

Cette question est posée en réponse à cette discussion. La suggestion était que, par exemple, les arguments de pointeur peuvent être passés à une fonction via des registres de segment x86, ce qui pourrait provoquer une exception matérielle.

D'après la norme C99, nous apprenons ce qui suit (soulignement de ma part):

[3.17] valeur indéterminée - soit une valeur non spécifiée soit une représentation de piège

et ensuite:

[6.2.4 p2] La valeur d'un pointeur devient indéterminée lorsque l'objet pointé atteint la fin de sa durée de vie.

et ensuite:

[6.2.6.1 p5] Certaines représentations d'objet ne doivent pas représenter une valeur du type d'objet. Si la valeur stockée d'un objet a une telle représentation et est lue par une expression lvalue qui n'a pas de type caractère, le comportement est indéfini. Si une telle représentation est produite par un effet secondaire qui modifie tout ou partie de l'objet par une expression lvalue qui n'a pas de type caractère, le comportement est indéfini. Une telle représentation est appelée une représentation de piège.

En prenant tout ceci en compte, quelles restrictions avons-nous sur l'accès aux pointeurs vers des objets "morts"?

Addendum

Alors que j'ai cité la norme C99 ci-dessus, je serais intéressé de savoir si le comportement diffère dans l'une des normes C++.

3 votes

Vous avez cité la norme de manière excellente - ces mots me donnent clairement entendre que l'utilisation d'un pointeur invalide de quelque manière que ce soit, même sans le déréférencer, invoque un comportement indéfini.

0 votes

Je ne vois pas d'où cela devrait venir. Tant que vous passez le pointeur autour, rien ne se passe. bien sûr c'est évident, que cela n'a pas de sens, car vous ne pouvez pas utiliser ce pointeur de toute façon, mais le passer autour est pratiquement la même chose que d'avoir un pointeur non initialisé.

1 votes

@Devolus : Oui, c'était aussi mon intuition. Mais la norme semble relativement non ambiguë. Et AProgrammer a fait remarquer (dans la discussion liée) que si les registres de segment sont impliqués, cela pourrait vraiment entraîner une exception matérielle.

31voto

hvd Points 42125

Exemple 2 est invalide. L'analyse dans votre question est correcte.

Exemple 1 est valide. Un type de structure ne contient jamais de représentation piège, même si l'un de ses membres en contient. Cela signifie que l'assignation de structures, sur un système où les représentations piège poseraient problème, doit être implémentée comme une copie octet par octet, plutôt qu'une copie membre par membre.

6.2.6 Représentations des types

6.2.6.1 Généralités

6 [...] La valeur d'un objet de type structure ou union n'est jamais une représentation piège, même si la valeur d'un membre de l'objet de type structure ou union peut être une représentation piège.

0 votes

Ah, c'est intéressant. Je n'avais pas remarqué cette clause. Merci!

0 votes

Depuis le problème ne concerne pas les représentations de pièges mais des valeurs indéterminées, je ne pense pas que le problème soit résolu par le texte cité. En fonction de J.2 (bien que non normatif), UB se produit si "La valeur d'un objet avec une durée de stockage automatique est utilisée alors qu'elle est indéterminée (6.2.4, 6.7.8, 6.8)." Cependant, peut-être que dans ce cas, c'est la valeur du membre, pas la valeur de la structure, qui est indéterminée, auquel cas la valeur de l'objet avec une valeur indéterminée n'est pas utilisée.

0 votes

@R. J.2 est obsolète. Le texte normatif (de C99, de toute façon) interdit seulement la lecture d'objets contenant des représentations de pièges. S'ils sont indéterminés mais ne peuvent pas contenir de représentations de pièges, la lecture est autorisée. C'est important aussi pour, par exemple, unsigned char.

15voto

R.. Points 93718

Ma interprétation est que seules les types non caractère peuvent avoir des représentations de piège, mais que tout type peut avoir une valeur indéterminée, et que l'accès à un objet avec une valeur indéterminée de quelque manière que ce soit invoque un comportement indéfini. L'exemple le plus célèbre pourrait être l'utilisation invalide d'objets non initialisés comme graine aléatoire dans OpenSSL.

Donc, la réponse à votre question serait : jamais.

En passant, une conséquence intéressante du fait que non seulement l'objet pointé mais le pointeur lui-même devient indéterminé après un free ou un realloc est que cet idiome invoque un comportement indéfini :

void *tmp = realloc(ptr, newsize);
if (tmp != ptr) {
    /* ... */
}

1 votes

Re "accès à un objet ..."; il y a une note de bas de page dans la norme que je n'ai pas citée ci-dessus: "Ainsi, une variable automatique peut être initialisée à une représentation de piège sans entraîner un comportement indéfini, mais la valeur de la variable ne peut pas être utilisée tant qu'une valeur appropriée n'est pas stockée dedans." Cela semble être acceptable d'écrire dans un tel objet.

3 votes

@OliCharlesworth, bien sûr que c'est le cas. Sinon comment pouvez-vous faire quelque chose comme: free(x); x = NULL;?

0 votes

@Shahbaz : En effet! J'ai du mal à analyser la norme de manière à permettre ce genre de chose ;)

0voto

supercat Points 25534

Le fait de dire que la valeur du pointeur devient indéterminée, même si rien ne perturbe les bits la représentant, est probablement un effort pour accommoder la règle "comme si". S'il existe une séquence d'actions dont le comportement pourrait être observé affecté par une transformation d'optimisation utile, la règle "comme si" exige qu'au moins une action au sein de cette séquence soit caractérisée comme invoquant un comportement indéterminé justifiant toute bizarrerie observée découlant de l'optimisation.

Considérez la fonction suivante :

void test(int *p1, uint64_t ofs)
{
  int ret;
  int *p2 = malloc(sizeof (int));
  if ((uintptr_t)p1 == (uintptr_t)p2+ofs)
  {
    *p2 = 1;
    *p1 = 2;
    doSomething(*p2);
  }
  free(p2);
  return p2;
}

Dans la plupart des cas où la fonction pourrait être invoquée, remplacer l'appel à doSomething(*p2) par doSomething(2) améliorerait les performances sans affecter le comportement, sauf dans les scénarios où p1 est un pointeur vers une région morte du stockage dont l'adresse coïncide avec l'adresse de la nouvelle région renvoyée par malloc(). Considérer que p1 devient indéterminé lorsque le stockage ainsi identifié deviendrait éligible pour être réutilisé par malloc() permettrait à un compilateur d'ignorer la possibilité que l'adresse puisse correspondre à l'adresse d'une future allocation.

-1voto

curiousguy Points 2900

Discussion sur le C++

Réponse courte : En C++, il n'y a pas de notion d'accéder à "lire" une instance de classe ; vous ne pouvez "lire" qu'un objet non-classe, et cela se fait par une conversion lvalue en rvalue.

Réponse détaillée :

typedef struct { int *p; } T;

T désigne une classe non nommée. Pour les besoins de la discussion, nommons cette classe T :

struct T {
    int *p; 
};

Étant donné que vous n'avez pas déclaré de constructeur de copie, le compilateur en déclare implicitement un, donc la définition de la classe est la suivante :

struct T {
    int *p; 
    T (const T&);
};

Donc nous avons :

T a;
T b = a;    // Accès via un type non-caractère ?

Oui, en effet ; il s'agit d'une initialisation par le constructeur de copie, donc la définition du constructeur de copie sera générée par le compilateur ; la définition est équivalente à :

inline T::T (const T& rhs) 
    : p(rhs.p) {
}

Donc vous accédez à la valeur en tant que pointeur, pas un ensemble d'octets.

Si la valeur du pointeur est invalide (non initialisée, libérée), le comportement n'est pas défini.

0 votes

En fait, une conversion de lvalue en rvalue peut également être effectuée pour les lvalues de classe. Le contexte est lors du passage d'une lvalue de classe à travers les points de suspension dans un appel de fonction.

0 votes

@JohannesSchaub-litb Oui vous le pouvez. [conv.lval] "Sinon, si le glvalue a un type de classe, la conversion initialise une copie temporaire de type T à partir du glvalue et le résultat de la conversion est une prvalue pour le temporaire". Ainsi, cette conversion est définie en termes du constructeur, et nous revenons à accéder à chaque membre un par un, avec une conversion de lvalue en rvalue pour chacun.

0 votes

C'est correct. Du moins en ce qui concerne les objets de classe non union. Les unions sont copiées "bitwise".

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