Comment puis *i
et u.i
affiche différents nombres dans ce code, même si i
est défini comme int *i = &u.i;
? Je ne peux que supposer que je déclenche UB ici, mais je ne vois pas exactement comment.
( démo de l'idéone réplique si je sélectionne 'C' comme langue. Mais comme l'a souligné @ 2501, pas si 'C99 strict' est la langue. Mais là encore, j'ai le problème avec gcc-5.3.0 -std=c99
!)
// gcc -fstrict-aliasing -std=c99 -O2
union
{
int i;
short s;
} u;
int * i = &u.i;
short * s = &u.s;
int main()
{
*i = 2;
*s = 100;
printf(" *i = %d\n", *i); // prints 2
printf("u.i = %d\n", u.i); // prints 100
return 0;
}
(gcc 5.3.0, avec -fstrict-aliasing -std=c99 -O2
, également avec -std=c11
)
Ma théorie est que 100
est la réponse "correcte", car l'écriture sur le membre de l'union via short
- lvalue *s
est défini comme tel (pour cette plate-forme/endianness/peu importe). Mais je pense que l'optimiseur ne se rend pas compte que l'écriture dans *s
peut alias u.i
, et donc il pense que *i=2;
est la seule ligne qui peut affecter *i
. Est-ce une théorie raisonnable?
Si *s
peut alias u.i
, et u.i
peut alias *i
, alors le compilateur devrait sûrement penser que *s
peut alias *i
? Le crénelage ne devrait-il pas être "transitif"?
Enfin, j'ai toujours supposé que les problèmes d'alias strict étaient dus à un mauvais casting. Mais il n'y a pas de casting là-dedans!
(Mon expérience est C++, j'espère que je pose ici une question raisonnable à propos de C. Ma compréhension (limitée) est qu'en C99, il est acceptable d'écrire via un membre de l'union puis de lire un autre membre d'un autre type.)
L'écart est émis par -fstrict-aliasing
option d'optimisation. Son comportement et ses éventuels pièges sont décrits dans documentation GCC :
Portez une attention particulière au code comme celui-ci:
union a_union { int i; double d; }; int f() { union a_union t; t.d = 3.0; return t.i; }
La pratique de lire à partir d'un autre membre du syndicat que celui sur lequel on a écrit le plus récemment (appelé "punition de type") est courante. Même avec
-fstrict-aliasing
, la punition de type est autorisée, à condition que la mémoire soit accessible via le type d'union . Ainsi, le code ci-dessus fonctionne comme prévu. Voir Structuration des énumérations d'unions et implémentation des champs binaires . Cependant, ce code peut ne pas :int f() { union a_union t; int* ip; t.d = 3.0; ip = &t.i; return *ip; }
Notez que l'implémentation conforme est parfaitement autorisée à tirer parti de cette optimisation, comme le montre le deuxième exemple de code comportement indéfini . Voir Olaf's et les réponses des autres pour référence.
norme C (c'est-à-dire C11, n1570), 6.5p7 :
Un objet doit avoir sa valeur stockée accessible uniquement par une expression lvalue qui a l'un des types suivants:
- ...
- 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.
Les expressions lvalue de vos pointeurs ne sont pas pas union
, donc cette exception ne s'applique pas. Le compilateur exploite correctement ce comportement non défini.
Faites en sorte que les types des pointeurs pointent vers le type union
et déréférencez avec le membre respectif. Cela devrait fonctionner:
union {
...
} u, *i, *p;
Le crénelage strict n'est pas spécifié dans la norme C, mais l'interprétation habituelle est que le crénelage syndical (qui remplace le crénelage strict) n'est autorisé que lorsque les membres du syndicat sont directement accessibles par leur nom.
Pour les raisons derrière cela, considérez:
void f(int *a, short *b) {
L'intention de la règle est que le compilateur peut supposer que a
et b
n'aliasent pas, et générer du code efficace dans f
. Mais si le compilateur devait tenir compte du fait que a
et b
pouvaient être des membres d'union qui se chevauchaient, il ne pouvait en fait pas faire ces hypothèses.
Que les deux pointeurs soient ou non des paramètres de fonction est sans importance, la règle d'aliasing stricte ne fait pas de différence en fonction de cela.
Ce code invoque en effet UB, car vous ne respectez pas la règle stricte d'alias. Le projet n1256 de C99 indique dans 6.5 Expressions §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.
Entre le *i = 2;
Et la printf(" *i = %d\n", *i);
seul un objet court est modifié. Avec l'aide de la règle d'alias stricte, le compilateur est libre de supposer que l'objet int pointé par i
n'a pas été modifié, et il peut directement utiliser une valeur mise en cache sans la recharger depuis la mémoire principale.
Ce n'est manifestement pas ce à quoi un être humain normal pourrait s'attendre, mais la règle de pseudonyme stricte a été précisément écrite pour permettre aux compilateurs d'optimisation d'utiliser des valeurs mises en cache.
Pour la deuxième impression, les unions sont référencées dans la même norme au 6.2.6.1 Représentations de types/Général §7:
Lorsqu'une valeur est stockée dans un membre d'un objet de type union, les octets de la représentation d'objet qui ne correspondent pas à ce membre mais correspondent à d'autres membres prennent des valeurs non spécifiées.
Donc, comme u.s
A été stocké, u.i
A pris une valeur non spécifié par la norme
Mais nous pouvons lire plus loin dans 6.5.2.3 Structure et syndicalistes §3 note 82:
Si le membre utilisé pour accéder au contenu d'un objet union n'est pas le même que le dernier membre utilisé pour stocker une valeur dans l'objet, la partie appropriée de la représentation d'objet de la valeur est réinterprétée en tant que représentation d'objet dans le nouveau type comme décrit en 6.2.6 (un processus parfois appelé "type punning"). Cela pourrait être une représentation piège.
Bien que les notes ne soient pas normatives, elles permettent une meilleure compréhension de la norme. Lorsque u.s
A été stocké via le pointeur *s
, Les octets correspondant à un court-circuit ont été remplacés par la valeur 2. En supposant un petit système endian, comme 100 est plus petit que la valeur d'un court, la représentation en tant qu'int devrait maintenant être 2 car les octets de poids fort étaient 0.
TL/DR: même si ce n'est pas normatif, la note 82 devrait exiger que sur un petit système endien des familles x86 ou x64, printf("u.i = %d\n", u.i);
affiche 2. Mais selon la règle stricte d'alias, le compilateur est toujours autorisé à supposait que la valeur pointée par i
n'avait pas changé et pouvait imprimer 100
Vous explorez un domaine quelque peu controversé de la norme C.
Ceci est la règle stricte d'alias:
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),
- un type de caractère.
(C2011, 6,5/7)
L'expression lvalue *i
A le type int
. L'expression lvalue *s
A le type short
. Ces types ne sont pas compatibles entre eux, ni les deux compatibles avec un autre type particulier, ni la règle stricte d'aliasing n'offre aucune autre alternative permettant aux deux accès de se conformer si les pointeurs sont aliasés.
Si au moins l'un des accès n'est pas conforme, le comportement n'est pas défini, de sorte que le résultat que vous signalez - ou tout autre résultat - est tout à fait acceptable. En pratique, le compilateur doit produire du code qui réordonne les affectations avec les appels printf()
, ou qui utilise une valeur précédemment chargée de *i
À partir d'un registre au lieu de le relire de la mémoire, ou quelque chose de similaire.
La controverse susmentionnée survient parce que les gens pointent parfois note de bas de page 95:
Si le membre utilisé pour lire le contenu d'un objet union n'est pas le même que le dernier membre utilisé pour stocker une valeur dans l'objet, la partie appropriée de la représentation d'objet de la valeur est réinterprétée en tant que représentation d'objet dans le nouveau type comme décrit en 6.2.6 (un processus parfois appelé '' type punning ''). Cela pourrait être une représentation piège.
Les notes de bas de page sont informatives, cependant, non normatives, donc il n'y a vraiment aucune question sur le texte qui l'emporte en cas de conflit. Personnellement, je prends la note de bas de page simplement comme un guide de mise en œuvre, clarifiant le sens du fait que le stockage pour les membres du syndicat se chevauche.
On dirait que c'est le résultat de l'optimiseur faisant sa magie.
Avec -O0
, les deux lignes affichent 100 comme prévu (en supposant le petit-boutien). Avec -O2
, il y a une réorganisation en cours.
gdb donne la sortie suivante:
(gdb) start
Temporary breakpoint 1 at 0x4004a0: file /tmp/x1.c, line 14.
Starting program: /tmp/x1
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x2aaaaaaab000
Temporary breakpoint 1, main () at /tmp/x1.c:14
14 {
(gdb) step
15 *i = 2;
(gdb)
18 printf(" *i = %d\n", *i); // prints 2
(gdb)
15 *i = 2;
(gdb)
16 *s = 100;
(gdb)
18 printf(" *i = %d\n", *i); // prints 2
(gdb)
*i = 2
19 printf("u.i = %d\n", u.i); // prints 100
(gdb)
u.i = 100
22 }
(gdb)
0x0000003fa441d9f4 in __libc_start_main () from /lib64/libc.so.6
(gdb)
La raison pour laquelle cela se produit, comme d'autres l'ont dit, est qu'il s'agit d'un comportement indéfini d'accéder à une variable d'un type via un pointeur vers un autre type même si la variable en question fait partie d'une union. L'optimiseur est donc libre de faire ce qu'il veut dans ce cas.
La variable de l'autre type ne peut être lue directement que via une union qui garantit un comportement bien défini.
Ce qui est curieux, c'est que même avec -Wstrict-aliasing=2
, gcc (à partir de 4.8.4) ne se plaint pas de ce code.
Que ce soit par accident ou par conception, C89 comprend un langage qui a été interprété de deux manières différentes (ainsi que diverses interprétations intermédiaires). La question est de savoir quand un compilateur doit être tenu de reconnaître que le stockage utilisé pour un type peut être accessible via des pointeurs d'un autre. Dans l'exemple donné dans la justification C89, l'aliasing est considéré entre une variable globale qui ne fait clairement partie d'aucune union et un pointeur vers un type différent, et rien dans le code ne suggère qu'un aliasing pourrait se produire .
Une interprétation paralyse horriblement le langage, tandis que l'autre restreindrait l'utilisation de certaines optimisations aux modes "non conformes". Si ceux qui n'avaient pas obtenu leurs optimisations préférées étant donné le statut de deuxième classe avaient écrit C89 pour correspondre sans ambiguïté à leur interprétation, ces parties de la norme auraient été largement dénoncées et il y aurait eu une sorte de reconnaissance claire d'un non-cassé dialecte de C qui honorerait l'interprétation non rédhibitoire des règles données.
Malheureusement, ce qui s'est produit à la place est que les règles n'exigent clairement pas que les écrivains du compilateur appliquent une interprétation paralysante, la plupart des rédacteurs du compilateur ont simplement interprété les règles pendant des années d'une manière qui conserve la sémantique qui a rendu le C utile pour la programmation des systèmes; les programmeurs n'avaient aucune raison de se plaindre du fait que la norme n'imposait pas aux compilateurs de se comporter raisonnablement, car de leur point de vue, il semblait évident à tout le monde qu'ils devraient le faire malgré le manque de rigueur de la norme. Pendant ce temps, cependant, certaines personnes insistent sur le fait que, puisque la norme a toujours permis aux compilateurs de traiter un sous-ensemble sémantiquement affaibli du langage de programmation des systèmes de Ritchie, il n'y a aucune raison pour qu'un compilateur conforme à la norme soit censé traiter autre chose.
La solution raisonnable à ce problème serait de reconnaître que C est utilisé à des fins suffisamment variées pour qu'il y ait plusieurs modes de compilation - un mode requis traiterait tous les accès de tout ce dont l'adresse a été prise comme s'ils lisaient et écrivaient directement le stockage sous-jacent. , et serait compatible avec le code qui attend tout niveau de prise en charge de punning de type pointeur. Un autre mode pourrait être plus restrictif que C11, sauf lorsque le code utilise explicitement des directives pour indiquer quand et où le stockage qui a été utilisé comme un type devrait être réinterprété ou recyclé pour être utilisé comme un autre. D'autres modes permettraient certaines optimisations mais prennent en charge un code qui se briserait sous des dialectes plus stricts; les compilateurs sans prise en charge spécifique d'un dialecte particulier pourraient en remplacer un avec des comportements d'alias plus définis.