Quelle est la sémantique des objets qui se chevauchent en C?

25

Considérez la structure suivante:

struct s {
  int a, b;
};

Typiquement 1 , cette structure aura la taille 8 et l'alignement 4.

Et si nous créons deux struct sobjets (plus précisément, nous écrivons dans le stockage alloué deux de ces objets), le deuxième objet chevauchant le premier?

char *storage = malloc(3 * sizeof(struct s));
struct s *o1 = (struct s *)storage; // offset 0
struct s *o2 = (struct s *)(storage + alignof(struct s)); // offset 4

// now, o2 points half way into o1
*o1 = (struct s){1, 2};
*o2 = (struct s){3, 4};

printf("o2.a=%d\n", o2->a);
printf("o2.b=%d\n", o2->b);
printf("o1.a=%d\n", o1->a);
printf("o1.b=%d\n", o1->b);

Y a-t-il quelque chose sur ce comportement indéfini du programme? Si oui, où devient-il indéfini? Si ce n'est pas UB, est-il garanti d'imprimer toujours ce qui suit:

o2.a=3
o2.b=4
o1.a=1
o1.b=3

En particulier, je veux savoir ce qui arrive à l'objet pointé par o1quand o2, qui le chevauche, est écrit. Est-il toujours autorisé à accéder à la partie non nettoyée ( o1->a)? L'accès à la partie encombrée est-il o1->bsimplement identique à l'accès o2->a?

Comment le type efficace s'applique-t-il ici? Les règles sont suffisamment claires lorsque vous parlez d'objets qui ne se chevauchent pas et de pointeurs pointant vers le même emplacement que le dernier magasin, mais lorsque vous commencez à parler du type effectif de parties d'objets ou d'objets qui se chevauchent, c'est moins clair.

Est-ce que quelque chose changerait si la deuxième écriture était d'un type différent? Si les membres se disaient intet shortplutôt que deux ints?

Voici un coup de foudre si vous voulez y jouer avec.


1 Cette réponse s'applique aux plates-formes où ce n'est pas le cas également: par exemple, certaines pourraient avoir la taille 4 et l'alignement 2. Sur une plate-forme où la taille et l'alignement étaient les mêmes, cette question ne s'appliquerait pas car des objets alignés et se chevauchant être impossible, mais je ne suis pas sûr qu'il existe une telle plate-forme.

BeeOnRope
la source
2
Je suis à peu près sûr que c'est UB, mais je vais laisser un avocat spécialisé en langues fournir le chapitre et le verset.
Barmar
Je pense que le compilateur C sur les anciens systèmes vectoriels Cray a forcé l'alignement et la taille pour être les mêmes, avec un modèle ILP64 et un alignement forcé 64 bits (les adresses sont des mots 64 bits - pas d'adressage d'octets). Bien sûr, cela a généré beaucoup d'autres problèmes ....
John D McCalpin

Réponses:

15

Fondamentalement, c'est toute la zone grise dans la norme; la règle d'alias stricte spécifie les cas de base et laisse le lecteur (et les éditeurs de compilateurs) remplir les détails.

Il y a eu des efforts pour écrire une meilleure règle mais jusqu'à présent, ils n'ont abouti à aucun texte normatif et je ne sais pas quel est le statut de ceci pour C2x.

Comme mentionné dans ma réponse à votre question précédente, l'interprétation la plus courante est que les p->qmoyens (*p).qet le type efficace s'appliquent à tous *p, même si nous continuons ensuite à appliquer .q.

Selon cette interprétation, printf("o1.a=%d\n", o1->a);cela entraînerait un comportement indéfini car le type effectif de l'emplacement *o1ne l'est pas s(car une partie de l'emplacement a été écrasée).

La justification de cette interprétation peut être vue dans une fonction comme:

void f(s* s1, s* s2)
{
    s2->a = 5;
    s1->b = 6;
    printf("%d\n", s2->a);
}

Avec cette interprétation, la dernière ligne pourrait être optimisée puts("5");, mais sans elle, le compilateur devrait considérer que l'appel de fonction peut avoir été f(o1, o2);et donc perdre tous les avantages prétendument fournis par la règle d'aliasing stricte.

Un argument similaire s'applique à deux types de structure non liés qui ont tous deux un intmembre à un décalage différent.

MM
la source
1
Avec f(s* s1, s* s2), sans restrict, le compilateur ne peut pas assumer s1et s2sont des pointeurs différents. Je pense , encore une fois sans restrict, qu'il ne peut même pas supposer qu'ils ne se chevauchent pas partiellement. IAC, je ne vois pas que la préoccupation d'OP soit bien démontrée par l' f()analogie. Bonne chance sans grondement. UV pour la première moitié.
chux
@ chux-ReinstateMonica sans restriction, s1 == s2serait autorisé, mais pas de chevauchement partiel. (L'optimisation dans mon exemple de code pourrait toujours être effectuée si s1 == s2)
MM
@ chux-ReinstateMonica, vous pouvez également envisager le même problème avec juste intau lieu de structs (et un système avec _Alignof(int) < sizeof(int)).
MM
3
Le statut de ce type de question concernant le type efficace pour C2x est à peu près ouvert et encore sujet à débat au sein du groupe d'étude. Soyez prudent si vous revendiquez l'équivalence de p->qet (*p).q. Cela peut être vrai pour le type d'interprétation comme vous le dites, mais ce n'est pas vrai d'un point de vue opérationnel. Il est important pour les accès simultanés à la même structure qu'un accès d'un membre n'implique pas l'accès d'un autre membre.
Jens Gustedt
Une règle d'alias stricte concerne l' accès . L'expression de gauche dans l' E1.E2expression n'effectue pas d'accès (je veux dire l' E1expression entière . Certaines de ses sous-expressions peuvent effectuer l'accès. C'est-à-dire si E1c'est (*p), puis lire la valeur du pointeur lors de l'évaluation pest l'accès, mais l'évaluation de *pou (*p)n'effectue aucune accès). La règle de repliement strict ne s'applique pas en cas d'absence d'accès.
Law Lawyer