3 votes

Est-ce qu'un pointeur vers un membre d'une structure peut être utilisé pour accéder à un autre membre de la même structure ?

Je suis en train de chercher à comprendre comment fonctionne le type-punning lorsqu'il s'agit de stocker une valeur dans un membre d'une structure ou d'une union.

Le Standard N1570 6.2.6.1(p6) spécifie que

Lorsqu'une valeur est stockée dans un objet de type structure ou union, y compris dans un objet membre, les octets de la représentation de l'objet qui correspondent à des octets de bourrage prennent des valeurs non spécifiées.

J'ai donc interprété cela comme si nous avions un objet à stocker dans un membre tel que la taille de l'objet équivaut au sizeof(type_déclaré_du_membre) + bourrage, les octets liés au bourrage auront une valeur non spécifiée (même si nous avions défini les octets dans l'objet d'origine). Voici un exemple:

struct first_member_padded_t{
    int a;
    long b;
};

int a = 10;
struct first_member_padded_t s;
char repr[offsetof(struct first_member_padded_t, b)] = //une valeur quelconque
memcpy(repr, &a, sizeof(a));
memcpy(&(s.a), repr, sizeof(repr));
s.b = 100;
printf("%d%ld\n", s.a, s.b); //affiche 10100

Sur ma machine sizeof(int) = 4, offsetof(struct first_member_padded_t, b) = 8.

Le comportement de l'affichage 10100 est-il bien défini pour un tel programme? Je pense que oui.

3voto

Eric Postpischil Points 36641

Que font les appels à memcpy?

La question est mal posée. Regardons d'abord le code :

char repr[offsetof(struct first_member_padded_t, b)] = //some value
memcpy(repr, &a, sizeof(a));
memcpy(&(s.a), repr, sizeof(repr));

Remarquez d'abord que repr est initialisé, donc tous les éléments qui le composent ont des valeurs données.

Le premier memcpy est bon - il copie les octets de a dans repr.

Si le deuxième memcpy était memcpy(&s, repr, sizeof repr);, il copierait les octets de repr dans s. Cela écrirait des octets dans s.a et, en raison de la taille de repr, dans tout padding entre s.a et s.b. Selon le C 2018 6.5 7 et d'autres parties de la norme, il est permis d'accéder aux octets d'un objet (et "accéder" signifie à la fois lire et écrire, selon 3.1 1). Ainsi, cette copie dans s est correcte, et s.a prend la même valeur que a.

Cependant, le memcpy utilise &(s.a) au lieu de &s. Il utilise l'adresse de s.a plutôt que l'adresse de s. Nous savons que convertir s.a en un pointeur vers un type de caractère nous permettrait d'accéder aux octets de s.a (6.5 7 et plus) (et le passer à memcpy a le même effet qu'une telle conversion, car memcpy est spécifié pour avoir l'effet de copier des octets), mais il n'est pas clair si cela nous permet d'accéder à d'autres octets dans s. En d'autres termes, la question est de savoir si nous pouvons utiliser &s.a pour accéder à des octets autres que ceux dans s.a.

6.7.2.1 15 nous indique que, si on convertit un pointeur vers le premier membre d'une structure de manière "appropriée", le résultat pointe vers la structure. Ainsi, si nous convertissons &s.a en un pointeur vers struct first_member_padding_t, il pointerait vers s, et nous pouvons certainement utiliser un pointeur vers s pour accéder à tous les octets dans s. Ainsi, cela serait également bien défini :

memcpy((struct first_member_padding t *) &s.a, repr, sizeof repr);

Cependant, memcpy(&s.a, repr, sizeof repr); convertit seulement &s.a en void * (parce que memcpy est déclaré pour prendre un void *, donc &s.a est automatiquement converti lors de l'appel de la fonction) et non en un pointeur vers le type de structure. Est-ce une conversion appropriée? Notez que si nous faisions memcpy(&s, repr, sizeof repr);, cela convertirait &s en void *. 6.2.5 28 nous dit qu'un pointeur vers void a la même représentation qu'un pointeur vers un type de caractère. Considérez donc ces deux déclarations :

memcpy(&s.a, repr, sizeof repr);
memcpy(&s,   repr, sizeof repr);

Les deux déclarations passent un void * à memcpy, et ces deux void * ont la même représentation l'un que l'autre et pointent vers le même octet. Maintenant, nous pourrions interpréter la norme de manière pédestre et stricte de sorte qu'ils sont différents en ce sens que le second peut être utilisé pour accéder à tous les octets de s et le premier ne le peut pas. Alors il est étrange que nous ayons deux pointeurs nécessairement identiques qui se comportent différemment.

Une telle interprétation stricte de la norme C semble possible en théorie - la différence entre les pointeurs pourrait survenir pendant l'optimisation plutôt que dans l'implémentation réelle de memcpy - mais je ne connais aucun compilateur qui ferait cela. Notez qu'une telle interprétation est en contradiction avec la section 6.2 de la norme, qui nous parle des types et des représentations. Interpréter la norme de telle sorte que (void *) &s.a et (void *) &s se comportent différemment signifie que deux choses de même valeur et type peuvent se comporter différemment, ce qui signifie qu'une valeur consiste en quelque chose de plus que sa valeur et son type, ce qui ne semble pas être l'intention de 6.2 ou de la norme en général.

Type Casting

La question déclare :

J'essaie de comprendre comment fonctionne le type casting lorsqu'il s'agit de stocker une valeur dans un membre d'une structure ou d'une union.

Ce n'est pas du type casting tel que le terme est communément utilisé. Techniquement, le code accède à s.a en utilisant des lvalues d'un type différent de sa définition (parce qu'il utilise memcpy, qui est défini pour copier comme avec un type de caractère, alors que le type défini est int), mais les octets proviennent d'un int et sont copiés sans modification, et ce genre de copie des octets d'un objet est généralement considéré comme une procédure mécanique ; elle est faite pour effectuer une copie et non pour réinterpréter les octets dans un nouveau type. Le "type casting" fait généralement référence à l'utilisation des différentes lvalues dans le but de réinterpréter la valeur, comme écrire un unsigned int et lire un float.

Quoi qu'il en soit, le type casting n'est pas vraiment le sujet de la question.

Valeurs Dans les Membres

Le titre pose la question :

Quelles valeurs pouvons-nous stocker dans les membres d'une structure ou d'une union?

Ce titre semble ne pas correspondre au contenu de la question. La question posée dans le titre est facilement répondue : Les valeurs que nous pouvons stocker dans un membre sont celles que le type du membre peut représenter. Mais la question explore ensuite le padding entre les membres. Le padding n'affecte pas les valeurs dans les membres.

Padding Prend des Valeurs Non Spécifiées

La question cite la norme :

Lorsqu'une valeur est stockée dans un objet de type structure ou union, y compris dans un objet membre, les octets de la représentation de l'objet qui correspondent à tout padding prennent des valeurs non spécifiées.

et dit :

Donc je l'ai interprété comme si nous avons un objet à stocker dans un membre tel que la taille de l'objet égale à sizeof(type_déclaré_du_membre) + padding les octets relatifs au padding auront une valeur non spécifiée...

Le texte cité dans la norme signifie que, si les octets de padding dans s ont été définis sur certaines valeurs, comme avec memcpy, et que nous faisons ensuite s.a = quelque_chose;, alors les octets de padding ne sont plus tenus de conserver leurs valeurs précédentes.

Le code dans la question explore une situation différente. Le code memcpy(&(s.a), repr, sizeof(repr)); ne stocke pas une valeur dans un membre de la structure au sens de 6.2.6.1 6. Il ne stocke ni dans s.a ni dans s.b. Il copie des octets, ce qui est une chose différente de ce qui est discuté en 6.2.6.1.

6.2.6.1 6 signifie que, par exemple, si nous exécutons ce code :

char repr[sizeof s] = { 0 };
memcpy(&s, repr, sizeof s); // Met tous les octets de s à des valeurs connues.
s.a = 0; // Stocke une valeur dans un membre.
memcpy(repr, &s, sizeof s); // Obtient tous les octets de s pour les examiner.
for (size_t i = sizeof s.a; i < offsetof(struct first_member_padding_t, b); ++i)
    printf("Octet %zu = %d.\n", i, repr[i]);

alors il n'est pas nécessaire que tous les zéros soient imprimés - les octets dans le padding peuvent avoir changé.

1voto

supercat Points 25534

Dans de nombreuses implémentations du langage pour lequel le standard C a été écrit, une tentative d'écrire un objet de taille N octets dans une structure ou union affecterait la valeur d'au plus N octets dans la structure ou union. En revanche, sur une plateforme prenant en charge les écritures sur 8 bits et 32 bits, mais pas sur 16 bits, si quelqu'un déclare un type comme suit :

struct S { uint32_t x; uint16_t y;} *s;

et ensuite exécute s->y = 23; sans se soucier de ce qui se passe avec les deux octets suivant y, il serait plus rapide d'effectuer un écriture sur 32 bits sur y, en écrasant aveuglément les deux octets qui le suivent, que d'effectuer une paire d'écritures sur 8 bits pour mettre à jour les moitiés supérieures et inférieures de y. Les auteurs du Standard n'ont pas voulu interdire un tel traitement.

Il aurait été utile si le Standard avait inclus un moyen par lequel les implémentations pourraient indiquer si les écritures sur des membres de structures ou unions pourraient perturber le stockage au-delà d'eux, et les programmes qui seraient affectés par de telles perturbations pourraient refuser de s'exécuter sur des implémentations où cela pourrait se produire. Cependant, les auteurs du Standard s'attendaient probablement à ce que les programmeurs intéressés par de tels détails sachent quels types de matériel leur programme était censé exécuter, et sachent donc si de telles perturbations de mémoire seraient un problème sur un tel matériel.

Malheureusement, les rédacteurs de compilateurs modernes semblent interpréter les libertés qui étaient censées aider les implémentations pour des matériels inhabituels comme une invitation ouverte à être "créatifs", même lorsqu'ils ciblent des plateformes qui pourraient traiter le code efficacement sans de telles concessions.

0voto

Cacahuete Frito Points 1200

Comme l'a dit @user694733, si on suppose qu'il y a un remplissage entre s.a et s.b, memcpy() accède à une zone mémoire qui ne peut pas être accédée par &a:

int a = 1;
int b;
b = *((char *)&a + sizeof(int));

C'est un comportement indéfini, et c'est essentiellement ce qui se passe à l'intérieur de memcpy().

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