Pourquoi cette réclamation de déréférencement de pointeur punencé par type de déréférencement est-elle spécifique au compilateur?

38

J'ai lu divers articles sur Stack Overflow RE: l'erreur de pointeur punencé par 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*), ce 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**qualifie-t-elle 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 peut-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, a void**, n'étant encore qu'un pointeur, cela devrait être un casting sûr.

Existe-t-il une solution de contournement n'utilisant pas les valeurs l qui pacifierait cet avertissement / erreur spécifique au compilateur? C'est-à-dire que je comprends cela et pourquoi cela résoudra le problème, mais je voudrais éviter cette approche parce que je veux profiter de freeFunc() NULL ing un out-arg prévu:

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

Compilateur de problèmes (l'un parmi les autres):

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)

StoneThrow
la source
1
"a void**, n'étant toujours qu'un pointeur, ce devrait être un casting sûr." Woah là skippy! On dirait que vous avez des hypothèses fondamentales en cours. Essayez de penser moins en termes d'octets et de leviers et plus en termes d'abstractions, car c'est ce que vous programmez avec
Lightness Races in Orbit
7
Tangentiellement, les compilateurs que vous utilisez ont 15 et 17 ans! Je ne compterais sur aucun des deux.
Tavian Barnes
4
@TavianBarnes Aussi, si vous devez compter sur GCC 3 pour une raison quelconque, il est préférable d'utiliser la version finale en fin de vie, qui était 3.4.6, je pense. Pourquoi ne pas profiter de tous les correctifs disponibles pour cette série avant sa mise au repos.
Kaz
Quel type de norme de codage C ++ prescrit tous ces espaces?
Peter Mortensen

Réponses:

33

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 les 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 xla valeur 3.0, mais double *p = (double*)&n;a un comportement indéfini (et dans la pratique, ne définira ppas 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 des pointeurs de mots qui sont des adresses d'un mot en mémoire et des pointeurs d'octets qui sont des adresses d'un mot avec un décalage d'octet dans ce mot; Foo*serait un pointeur de mot et void*serait un pointeur d'octet sur de telles architectures. Il existe de (rares) machines avec de 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 de celle 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 de 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 permet tout cela.

Vous pouvez créer freefuncune macro:

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

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

Gilles 'SO- arrête d'être méchant'
la source
1
Et il est bon de savoir que même si Foo*et void*ont la même représentation sur votre architecture, il n'est toujours pas défini de les punir.
Tavian Barnes
12

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 étendre à void **comme il le fait le point à 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 sera évaluée objdeux fois. Si vous utilisez GCC, cela peut être évité avec certaines extensions, en particulier les typeofexpressions de mots clés et d'instructions:

#define freeFunc(obj) ({ typeof (&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
dbush
la source
3
+1 pour fournir une meilleure implémentation du comportement souhaité. Le seul problème que je vois avec le #define, c'est qu'il évaluera objdeux fois. Je ne connais cependant pas un bon moyen d'éviter cette deuxième évaluation. Même une expression de déclaration (extension GNU) ne fera pas l'affaire comme vous devez l'attribuer objaprès avoir utilisé sa valeur.
cmaster - réintègre monica
2
@cmaster: Si vous êtes prêt à utiliser des extensions GNU telles que les expressions de déclaration, vous pouvez utiliser typeofpour éviter d' évaluer objdeux fois: #define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; }).
ruakh
@ruakh Très cool :-) Ce serait génial si dbush modifiait cela dans la réponse, donc il ne sera pas supprimé en masse avec les commentaires.
cmaster - réintègre monica
9

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 freeaccepte les pointeurs nuls.

AProgrammer
la source
2
Fascinant qu'il soit possible que sizeof Foo* != sizeof void*. Je n'ai jamais rencontré de tailles de pointeur "à l'état sauvage" dépendant du type, donc au fil des ans, je suis venu à considérer comme axiomatique que les tailles de pointeur sont toutes les mêmes sur une architecture donnée.
StoneThrow
1
@Stonethrow, l'exemple standard est les gros pointeurs utilisés pour adresser les octets dans une architecture adressable par mot. Mais je pense que les machines adressables de mots actuels utilisent l'alternative sizeof char == sizeof word .
AProgrammer
2
Notez que le type doit être entre parenthèses pour la taille de ...
Antti Haapala
@StoneThrow: quelle que soit la taille du pointeur, l'analyse d'alias basée sur le type la rend dangereuse; cela aide les compilateurs à optimiser en supposant qu'un magasin via un float*ne modifiera pas un int32_tobjet, donc par exemple un int32_t*n'a pas besoin int32_t *restrict ptrque le compilateur suppose qu'il ne pointe pas vers la même mémoire. Idem pour les magasins via un void**être supposé ne pas modifier un Foo*objet.
Peter Cordes
4

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 un type effectif Foo*mais est accessible par l'expression lvalue *objde typevoid* .

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.

Donc void*, ce Foo*ne sont pas des types compatibles ou des types compatibles avec des qualificatifs ajoutés, et ne correspondent certainement pas aux autres options de la règle d'alias 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 voiddoit 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.

aschepler
la source
2

En plus de ce que les autres réponses ont dit, ceci est un anti-modèle classique en C, et qui devrait être brûlé par 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 retour void *(qui ne souffre pas de ce problème car il implique une conversion de valeur au lieu de la punition de type ), renvoyant à la place un indicateur d'erreur et stockant le résultat via un pointeur à pointeur.

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

Pour (2), les deux cudaMalloc etposix_memalign sont des exemples.

Dans aucun des deux cas, l'interface ne nécessite intrinsèquement une utilisation non valide, mais elle l'encourage fortement et admet une utilisation correcte uniquement avec un objet temporaire supplémentaire de type void *qui va à l'encontre de l'objectif de la fonctionnalité free-and-null-out et rend l'allocation maladroite.

R .. GitHub ARRÊTEZ D'AIDER LA GLACE
la source
Avez-vous un lien expliquant davantage pourquoi (1) est un anti-modèle? Je ne pense pas que je connaisse cette situation / cet argument et j'aimerais en savoir plus.
StoneThrow
1
@StoneThrow: C'est vraiment simple - l'intention est d'empêcher toute utilisation abusive en annulant l'objet stockant le pointeur dans la mémoire qui est libérée, mais la seule façon de le faire est que l'appelant stocke réellement le pointeur dans un objet de le taper void *et le lancer / le convertir chaque fois qu'il veut le déréférencer. C'est très peu probable. Si l'appelant stocke un autre type de pointeur, la seule façon d'appeler la fonction sans invoquer UB est de copier le pointeur sur un objet temporaire de type void *et de transmettre l'adresse de celui-ci à la fonction de libération, puis il suffit ...
R .. GitHub STOP HELPING ICE
1
... annule un objet temporaire plutôt que le stockage réel où l'appelant avait le pointeur. Bien sûr, ce qui se passe réellement, c'est que les utilisateurs de la fonction finissent par effectuer un (void **)cast, produisant un comportement indéfini.
R .. GitHub STOP HELPING ICE
2

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 de "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 permettraient à l'unanimité void**d'utiliser, au moins avec une conversion appropriée, comme "pointeur vers n'importe quel pointeur". Les auteurs de la norme ont presque certainement reconnu que cela serait utile sur les plateformes qui la prenaient en charge, mais comme elle ne pouvait pas être universellement prise en charge, ils ont refusé de l'exiger. 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.

supercat
la source