web-dev-qa-db-fra.com

Pourquoi ce prétendu déréférencement de pointeur punencé par type est-il spécifique au compilateur?

J'ai lu diverspostsonDébordement de pile RE: l'erreur de pointeur punencée par type de déréférencement. Ma compréhension est que l'erreur est essentiellement l'avertissement du compilateur du danger d'accéder à un objet via un pointeur d'un type différent (bien qu'une exception semble être faite pour char*), Qui est un avertissement compréhensible et raisonnable.

Ma question est spécifique au code ci-dessous: pourquoi la conversion de l'adresse d'un pointeur en un void** Remplit les conditions pour cet avertissement (promu en erreur via -Werror) ?

De plus, ce code est compilé pour plusieurs architectures cibles, dont une seule génère l'avertissement/l'erreur - cela pourrait-il impliquer qu'il s'agit légitimement d'une déficience spécifique à la version du compilateur?

// main.c
#include <stdlib.h>

typedef struct Foo
{
  int i;
} Foo;

void freeFunc( void** obj )
{
  if ( obj && * obj )
  {
    free( *obj );
    *obj = NULL;
  }
}

int main( int argc, char* argv[] )
{
  Foo* f = calloc( 1, sizeof( Foo ) );
  freeFunc( (void**)(&f) );

  return 0;
}

Si ma compréhension, énoncée ci-dessus, est correcte, un void**, N'étant toujours qu'un pointeur, cela devrait être un casting sûr.

Existe-t-il une solution de contournement n'utilisant pas lvalues qui pacifierait cet avertissement/erreur spécifique au compilateur? C'est à dire. Je comprends cela et pourquoi cela résoudra le problème, mais je voudrais éviter cette approche car je veux profiter de freeFunc() [~ # ~] null [~ # ~] ing un out-arg prévu:

void* tmp = f;
freeFunc( &tmp );
f = NULL;

Compilateur de problèmes (un parmi):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc --version && /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-fc3-linux-gnu-gcc (GCC) 3.4.5
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

./main.c: In function `main':
./main.c:21: warning: dereferencing type-punned pointer will break strict-aliasing rules

user@8d63f499ed92:/build$

Compilateur non conforme (l'un des nombreux):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc --version && /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-rh73-linux-gnu-gcc (GCC) 3.2.3
Copyright (C) 2002 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

user@8d63f499ed92:/build$

Mise à jour: j'ai en outre découvert que l'avertissement semble être généré spécifiquement lors de la compilation avec -O2 (Toujours avec le "compilateur de problèmes" noté uniquement)

38
StoneThrow

Une valeur de type void** Est un pointeur sur un objet de type void*. Un objet de type Foo* N'est pas un objet de type void*.

Il existe une conversion implicite entre valeurs de type Foo* Et void*. Cette conversion peut modifier la représentation de la valeur. De même, vous pouvez écrire int n = 3; double x = n; Et cela a le comportement bien défini de définir x à la valeur 3.0, Mais double *p = (double*)&n; a un comportement non défini (et en pratique, ne définira pas p sur un "pointeur vers 3.0" sur une architecture commune).

Les architectures où différents types de pointeurs vers des objets ont des représentations différentes sont rares de nos jours, mais elles sont autorisées par la norme C. Il existe de (rares) vieilles machines avec pointeurs Word qui sont les adresses d'un mot en mémoire et pointeurs octets qui sont les adresses d'un mot avec un décalage d'octet dans ce mot; Foo* Serait un pointeur Word et void* Serait un pointeur octet sur de telles architectures. Il existe de (rares) machines avec gros pointeurs qui contiennent des informations non seulement sur l'adresse de l'objet, mais aussi sur son type, sa taille et ses listes de contrôle d'accès; un pointeur vers un type défini peut avoir une représentation différente d'un void* qui a besoin d'informations de type supplémentaires lors de l'exécution.

Ces machines sont rares, mais autorisées par la norme C. Et certains compilateurs C profitent de la permission de traiter les pointeurs de type punned comme distincts pour optimiser le code. Le risque d'alias de pointeurs est une limitation majeure à la capacité d'un compilateur à optimiser le code, de sorte que les compilateurs ont tendance à tirer parti de ces autorisations.

Un compilateur est libre de vous dire que vous faites quelque chose de mal, ou de faire tranquillement ce que vous ne vouliez pas, ou de faire tranquillement ce que vous vouliez. Un comportement indéfini autorise tout cela.

Vous pouvez créer freefunc une macro:

#define FREE_SINGLE_REFERENCE(p) (free(p), (p) = NULL)

Cela vient avec les limitations habituelles des macros: manque de sécurité de type, p est évalué deux fois. Notez que cela ne vous donne la sécurité de ne pas laisser de pointeurs pendants si p était le seul pointeur vers l'objet libéré.

UNE void * est traité spécialement par la norme C en partie parce qu'il fait référence à un type incomplet. Ce traitement ne pas s'étend à void ** car pointe vers un type complet, en particulier void *.

Les règles strictes d'alias disent que vous ne pouvez pas convertir un pointeur d'un type en un pointeur d'un autre type et par la suite déréférencer ce pointeur car cela signifie réinterpréter les octets d'un type comme un autre. La seule exception est lors de la conversion en un type de caractère qui vous permet de lire la représentation d'un objet.

Vous pouvez contourner cette limitation en utilisant une macro de type fonction au lieu d'une fonction:

#define freeFunc(obj) (free(obj), (obj) = NULL)

Que vous pouvez appeler comme ceci:

freeFunc(f);

Cela a cependant une limitation, car la macro ci-dessus évaluera obj deux fois. Si vous utilisez GCC, cela peut être évité avec certaines extensions, en particulier les expressions de mots clés et d'instructions typeof:

#define freeFunc(obj) ({ typeof (&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
12
dbush

Déréférencer un pointeur de type punished est UB et vous ne pouvez pas compter sur ce qui va se passer.

Différents compilateurs génèrent des avertissements différents et, à cet effet, différentes versions du même compilateur peuvent être considérées comme des compilateurs différents. Cela semble une meilleure explication de la variance que vous voyez qu'une dépendance à l'architecture.

Un cas qui peut vous aider à comprendre pourquoi la punition de type dans ce cas peut être mauvais est que votre fonction ne fonctionnera pas sur une architecture pour laquelle sizeof(Foo*) != sizeof(void*). Cela est autorisé par la norme, bien que je n'en connaisse aucune pour laquelle cela soit vrai.

Une solution de contournement consisterait à utiliser une macro au lieu d'une fonction.

Notez que free accepte les pointeurs nuls.

9
AProgrammer

Ce code n'est pas valide selon la norme C, il peut donc fonctionner dans certains cas, mais il n'est pas nécessairement portable.

La "règle d'aliasing stricte" pour accéder à une valeur via un pointeur qui a été transtypé en un type de pointeur différent se trouve dans 6.5 paragraphe 7:

Un objet doit avoir sa valeur stockée accessible uniquement par une expression lvalue qui a l'un des types suivants:

  • un type compatible avec le type effectif de l'objet,

  • une version qualifiée d'un type compatible avec le type effectif de l'objet,

  • un type qui est le type signé ou non signé correspondant au type effectif de l'objet,

  • un type qui est le type signé ou non signé correspondant à une version qualifiée du type effectif de l'objet,

  • un type d'agrégat ou d'union qui inclut l'un des types susmentionnés parmi ses membres (y compris, récursivement, un membre d'une union sous-agrégée ou contenue), ou

  • un type de caractère.

Dans votre *obj = NULL; instruction, l'objet a le type effectif Foo* mais est accessible par l'expression lvalue *obj avec le type void*.

Au 6.7.5.1, paragraphe 2, nous avons

Pour que deux types de pointeurs soient compatibles, les deux doivent être qualifiés de manière identique et les deux doivent être des pointeurs vers des types compatibles.

Alors void* et Foo* ne sont pas des types compatibles ou compatibles avec des qualificatifs ajoutés, et ne correspondent certainement à aucune des autres options de la règle d'aliasing stricte.

Bien que ce ne soit pas la raison technique pour laquelle le code n'est pas valide, il convient également de noter la section 6.2.5, paragraphe 26:

Un pointeur vers void doit avoir les mêmes exigences de représentation et d'alignement qu'un pointeur vers un type de caractère. De même, les pointeurs vers des versions qualifiées ou non qualifiées de types compatibles doivent avoir les mêmes exigences de représentation et d'alignement. Tous les pointeurs vers les types de structure doivent avoir les mêmes exigences de représentation et d'alignement les uns que les autres. Tous les pointeurs vers les types d'union doivent avoir les mêmes exigences de représentation et d'alignement les uns que les autres. Les pointeurs vers d'autres types n'ont pas besoin d'avoir les mêmes exigences de représentation ou d'alignement.

En ce qui concerne les différences d'avertissements, ce n'est pas un cas où la norme nécessite un message de diagnostic, il s'agit simplement de savoir dans quelle mesure le compilateur ou sa version est capable de remarquer les problèmes potentiels et de les signaler de manière utile. Vous avez remarqué que les paramètres d'optimisation peuvent faire une différence. Cela est souvent dû au fait que davantage d'informations sont générées en interne sur la façon dont les différents éléments du programme s'emboîtent réellement dans la pratique, et que des informations supplémentaires sont donc également disponibles pour les vérifications d'avertissement.

4
aschepler

Bien que C ait été conçu pour des machines qui utilisent la même représentation pour tous les pointeurs, les auteurs de la norme ont voulu rendre le langage utilisable sur des machines qui utilisent différentes représentations pour les pointeurs vers différents types d'objets. Par conséquent, ils n'exigeaient pas que les machines qui utilisent différentes représentations de pointeurs pour différents types de pointeurs prennent en charge un type "pointeur vers n'importe quel type de pointeur", même si de nombreuses machines pouvaient le faire à un coût nul.

Avant la rédaction de la norme, les implémentations pour les plates-formes qui utilisaient la même représentation pour tous les types de pointeurs permettaient à l'unanimité un void** à utiliser, au moins avec une fonte appropriée, comme "pointeur vers n'importe quel pointeur". Les auteurs de la norme ont presque certainement reconnu que cela serait utile sur les plates-formes qui la prenaient en charge, mais comme elle ne pouvait pas être universellement prise en charge, ils ont refusé de la rendre obligatoire. Au lieu de cela, ils s'attendaient à ce que la mise en œuvre de qualité traite des constructions telles que ce que la justification décrirait comme une "extension populaire", dans les cas où cela serait logique.

2
supercat

En plus de ce que les autres réponses ont dit, c'est un anti-modèle classique en C, et un qui devrait être brûlé avec le feu. Il apparaît dans:

  1. Fonctions de suppression et de suppression comme celle dans laquelle vous avez trouvé l'avertissement.
  2. Fonctions d'allocation qui évitent l'idiome C standard de retourner void * (Qui ne souffre pas de ce problème car il implique une conversion de valeur au lieu de type punning), renvoyant à la place un indicateur d'erreur et stockant le résultat via un pointeur vers pointeur.

Pour un autre exemple de (1), il y avait un cas tristement célèbre de longue date dans la fonction av_free De ffmpeg/libavcodec. Je crois que cela a finalement été corrigé avec une macro ou une autre astuce, mais je ne suis pas sûr.

Pour (2), cudaMalloc et posix_memalign Sont des exemples.

Dans aucun des cas, l'interface n'est intrinsèquement nécessite une utilisation non valide, mais elle l'encourage fortement et n'admet une utilisation correcte qu'avec un objet temporaire supplémentaire de type void * Qui va à l'encontre de l'objectif du Fonctionnalité gratuite et null-out, et rend l'allocation maladroite.