web-dev-qa-db-fra.com

L'emballage struct est-il déterministe?

Par exemple, supposons que j'ai deux structures équivalentes a et b dans différents projets:

typedef struct _a
{
    int a;
    double b;
    char c;
} a;

typedef struct _b
{
    int d;
    double e;
    char f;
} b;

En supposant que je n'ai utilisé aucune directive comme #pragma pack et ces structures sont compilées sur le même compilateur sur la même architecture, auront-elles un remplissage identique entre les variables?

42
Govind Parmar

Le compilateur est déterministe; dans le cas contraire, une compilation séparée serait impossible. Deux unités de traduction différentes avec la même déclaration struct fonctionneront ensemble; cela est garanti par §6.2.7/1: Types compatibles et types composites .

De plus, deux compilateurs différents sur la même plate-forme devraient interopérer, bien que cela ne soit pas garanti par la norme. (C'est un problème de qualité d'implémentation.) Pour permettre l'interopérabilité, les rédacteurs du compilateur s'accordent sur une plateforme ABI (Application Binary Interface) qui inclura une spécification précise de la façon dont les types composites sont représentés. De cette façon, il est possible pour un programme compilé avec un compilateur d'utiliser des modules de bibliothèque compilés avec un compilateur différent.

Mais vous n'êtes pas seulement intéressé par le déterminisme; vous souhaitez également que la disposition de deux types différents soit identique.

Selon la norme, deux types struct sont compatibles si leurs membres (pris dans l'ordre) sont compatibles et si leurs balises et noms de membres sont identiques. Étant donné que votre exemple structs a des balises et des noms différents, ils ne sont pas compatibles même si leurs types de membres le sont, vous ne pouvez donc pas utiliser l'un là où l'autre est requis.

Il peut sembler étrange que la norme autorise les balises et les noms de membres à affecter la compatibilité. La norme exige que les membres d'une structure soient présentés dans l'ordre de déclaration, de sorte que les noms ne peuvent pas changer l'ordre des membres dans la structure. Pourquoi, alors, pourraient-ils affecter le rembourrage? Je ne connais aucun compilateur où ils le font, mais la flexibilité de la norme est basée sur le principe que les exigences doivent être le minimum nécessaire pour garantir une exécution correcte. Le crénelage de structures balisées différemment n'est pas autorisé au sein d'une unité de traduction, il n'est donc pas nécessaire de le tolérer entre différentes unités de traduction. Et donc la norme ne le permet pas. (Il serait légitime pour une implémentation d'insérer des informations sur le type dans les octets de remplissage d'un struct, même si elle devait ajouter un remplissage de manière déterministe pour fournir de l'espace pour ces informations. La seule restriction est que le remplissage ne peut pas être placé avant le premier membre d'un struct.)

Une plateforme ABI est susceptible de spécifier la disposition d'un type composite sans référence à son nom de balise ou de membre. Sur une plate-forme particulière, avec une plate-forme ABI qui a une telle spécification et un compilateur documenté pour se conformer à la plate-forme ABI, vous pourriez vous en sortir avec l'alias, bien qu'il ne soit pas techniquement correct, et évidemment les conditions préalables le rendent non portable .

55
rici

La norme C elle-même ne dit rien à ce sujet, donc en principe, vous ne pouvez tout simplement pas être sûr.

Mais: très probablement votre compilateur adhère à un ABI particulier, sinon communiquer avec d'autres bibliothèques et avec le système d'exploitation serait être un cauchemar. Dans ce dernier cas, l'ABI prescrira généralement exactement comment fonctionne l'emballage.

Par exemple:

  • sous x86_64 Linux/BSD, le SystemV AMD64 ABI est la référence. Ici (§3.1) pour chaque type de données de processeur primitif, il est détaillé la correspondance avec le type C, sa taille et ses exigences d'alignement, et il est expliqué comment utiliser ces données pour constituer la disposition de la mémoire des champs de bits, des structures et des unions; tout (en plus du contenu réel du rembourrage) est spécifié et déterministe. La même chose vaut pour de nombreuses autres architectures, voir ces liens .

  • ARM recommande son EABI pour ses processeurs, et il est généralement suivi à la fois par Linux et Windows; l'alignement des agrégats est spécifié dans "Procédure d'appel standard pour la documentation ARM Architecture), §4.3.

  • sous Windows, il n'y a pas de norme inter-fournisseurs, mais VC++ dicte essentiellement l'ABI, auquel pratiquement n'importe quel compilateur adhère; il peut être trouvé ici pour x86_64, ici pour ARM (mais pour la partie d'intérêt de cette question, il se réfère simplement à = ARM EABI).

15
Matteo Italia

Tout compilateur sensé produira une disposition de mémoire identique pour les deux structures. Les compilateurs sont généralement écrits comme des programmes parfaitement déterministes. Le non-déterminisme devrait être ajouté de manière explicite et délibérée, et pour ma part, je ne vois pas l'intérêt de le faire.

Cependant, cela pas vous permet de lancer un struct _a* à un struct _b* et accéder à ses données via les deux. Afaik, ce serait toujours une violation des règles strictes d'alias même si la disposition de la mémoire est identique, car cela permettrait au compilateur de réorganiser les accès via le struct _a* avec accès via le struct _b*, ce qui entraînerait un comportement imprévisible et indéfini.

10
cmaster

auront-ils un remplissage identique entre les variables?

En pratique, ils aiment surtout avoir la même disposition de mémoire.

En théorie, puisque la norme ne dit pas grand-chose sur la façon dont le rembourrage doit être utilisé sur les objets, vous ne pouvez pas vraiment supposer quoi que ce soit sur le rembourrage entre les éléments.

De plus, je ne vois même pas pourquoi voudriez-vous savoir/supposer quelque chose sur le remplissage entre les membres d'une structure. écrivez simplement du code C standard et conforme et tout ira bien.

8
David Haim

Vous ne pouvez pas approcher de façon déterministe la disposition d'une structure ou d'une union en langage C sur différents systèmes.

Bien que de nombreuses fois, il puisse sembler que la disposition générée par différents compilateurs soit la même, vous devez considérer les cas comme une convergence dictée par la commodité pratique et fonctionnelle de la conception du compilateur dans le cadre de la liberté de choix laissée au programmeur par la norme, et donc pas efficace.

La norme C11 ISO/IEC 9899: 2011, presque inchangée par rapport aux normes précédentes, clairement indiquée au paragraphe 6.7.2.1 Spécificateurs de structure et d'union:

Chaque membre non-bit-field d'une structure ou d'un objet union est aligné d'une manière définie par l'implémentation et appropriée à son type.

Pire encore le cas des champs de bits où une grande autonomie est laissée au programmeur:

Une implémentation peut allouer n'importe quelle unité de stockage adressable suffisamment grande pour contenir un champ de bits. S'il reste suffisamment d'espace, un champ binaire qui suit immédiatement un autre champ binaire dans une structure doit être compressé en bits adjacents de la même unité. S'il reste un espace insuffisant, si un champ binaire qui ne correspond pas est placé dans l'unité suivante ou chevauche des unités adjacentes est défini par l'implémentation. L'ordre d'allocation des champs binaires au sein d'une unité (de haut en bas ou de bas en haut) est défini par l'implémentation. L'alignement de l'unité de stockage adressable n'est pas spécifié.

Il suffit de compter le nombre de fois où les termes "défini par l'implémentation" et "non spécifié" apparaissent dans le texte.

Il est convenu que pour vérifier la version du compilateur, la machine et l'architecture cible chaque exécution avant d'utiliser la structure ou l'union générée sur un système différent est inabordable vous devriez avoir obtenu une réponse décente à votre question.

Maintenant, disons que oui, il existe un moyen de contourner le problème.

Soyez clair que ce n'est pas définitivement la solution, mais c'est une approche commune que vous pouvez trouver autour lorsque l'échange de structures de données est partagé entre différents systèmes: pack éléments de structure sur la valeur 1 (taille de caractère standard).

L'utilisation d'un emballage et d'une définition de structure précise peut conduire à une déclaration suffisamment fiable qui peut être utilisée sur différents systèmes. L'emballage force le compilateur à supprimer les alignements définis par l'implémentation, réduisant ainsi les éventuelles incompatibilités dues à la norme. De plus, en évitant d'utiliser des champs de bits, vous pouvez supprimer les incohérences résiduelles liées à l'implémentation. Enfin, l'efficacité de l'accès, en raison de l'alignement manquant, peut être recréée en ajoutant manuellement une déclaration fictive entre les éléments, conçue de manière à forcer chaque champ sur un alignement correct.

Comme cas résiduel, vous devez considérer un remplissage à la fin de la structure que certains compilateurs ajoutent, mais comme il n'y a pas de données utiles associées, vous pouvez l'ignorer (sauf pour l'allocation dynamique de l'espace, mais encore une fois vous pouvez y faire face).

5
Frankie_C

ISO C indique que deux types struct dans différentes unités de traduction sont compatibles s'ils ont la même balise et les mêmes membres. Plus précisément, voici le texte exact de la norme C99:

6.2.7 Type compatible et type composite

Deux types ont un type compatible si leurs types sont identiques. Des règles supplémentaires permettant de déterminer si deux types sont compatibles sont décrites au 6.7.2 pour les spécificateurs de type, au 6.7.3 pour les qualificatifs de type et au 6.7.5 pour les déclarants. De plus, deux types de structure, d'union ou énumérés déclarés dans des unités de traduction distinctes sont compatibles si leurs balises et membres satisfont aux exigences suivantes: Si l'un est déclaré avec une balise, l'autre doit être déclaré avec la même balise. Si les deux sont des types complets, les exigences supplémentaires suivantes s'appliquent: il doit y avoir une correspondance biunivoque entre leurs membres de sorte que chaque paire de membres correspondants soit déclarée avec des types compatibles, et telle que si un membre d'une paire correspondante est déclaré avec un nom, l'autre membre est déclaré avec le même nom. Pour deux structures, les membres correspondants doivent être déclarés dans le même ordre. Pour deux structures ou unions, les champs binaires correspondants doivent avoir les mêmes largeurs. Pour deux énumérations, les membres correspondants doivent avoir les mêmes valeurs.

Cela semble très étrange si nous l'interprétons du point de vue de "quoi, la balise ou les noms des membres pourraient affecter le remplissage?" Mais fondamentalement, les règles sont simplement aussi strictes qu'elles peuvent l'être tout en permettant le cas commun: plusieurs unités de traduction partageant exactement texte d'une déclaration de structure via un fichier d'en-tête. Si les programmes suivent des règles plus souples, ils ne se trompent pas; ils ne dépendent tout simplement pas des exigences de comportement de la norme, mais d'ailleurs.

Dans votre exemple, vous ne respectez pas les règles de langage, en n'ayant que l'équivalence structurelle, mais pas les noms de balises et de membres équivalents. En pratique, cela n'est pas réellement appliqué; les types de structures avec différentes balises et noms de membres dans différentes unités de traduction sont de facto physiquement compatibles de toute façon. Toutes sortes de technologies en dépendent, comme les liaisons de langages non-C aux bibliothèques C.

Si vos deux projets sont en C (ou C++), cela vaudrait probablement la peine d'essayer de mettre la définition dans un en-tête commun.

C'est également une bonne idée de se défendre contre les problèmes de version, tels qu'un champ de taille:

// Widely shared definition between projects affecting interop!
// Do not change any of the members.
// Add new ones only at the end!
typedef struct a
{
    size_t size; // of whole structure
    int a;
    double b;
    char c;
} a;

L'idée est que quiconque construit une instance de a doit initialiser le champ size en sizeof (a). Ensuite, lorsque l'objet est passé à un autre composant logiciel (peut-être de l'autre projet), il peut vérifier la taille par rapport à itssizeof (a). Si le champ de taille est plus petit, il sait que le logiciel qui a construit a utilise une ancienne déclaration avec moins de membres. Par conséquent, les membres inexistants ne doivent pas être accessibles.

4
Kaz

Tout compilateur particulier doit être déterministe, mais entre deux compilateurs, ou même le même compilateur avec différentes options de compilation, ou même entre différentes versions du même compilateur, tous les paris sont désactivés.

Vous êtes beaucoup mieux si vous ne dépendez pas des détails de la structure, ou si vous le faites, vous devez incorporer du code pour vérifier au moment de l'exécution que la structure est réellement telle que vous dépendez.

Un bon exemple de ceci est le changement récent des architectures 32 à 64 bits, où même si vous n'avez pas changé la taille des entiers utilisés dans une structure, le conditionnement par défaut des entiers partiels a changé; alors qu'auparavant, 3 entiers 32 bits consécutifs s'emballaient parfaitement, maintenant ils se regroupent dans deux emplacements 64 bits.

Vous ne pouvez pas prévoir quels changements pourraient survenir à l'avenir; Si vous dépendez de détails qui ne sont pas garantis par le langage, tels que le remplissage de la structure, vous devez vérifier vos hypothèses lors de l'exécution.

2
ddyer