Pourquoi peu endianness est un problème dans bitfields?
Tout code portable qui utilise bitfields semble distinguer entre little et big-endian plates-formes. Voir la déclaration de struct iphdr dans le noyau linux pour un exemple d'un tel code. Je n'arrive pas à comprendre pourquoi peu endianness est tout un problème.
Comme je le comprends, bitfields sont purement compilateur construit, utilisé pour faciliter niveau de bits de manipulations.
Par exemple, considérez les points suivants champ de bits:
struct ParsedInt {
unsigned int f1:1;
unsigned int f2:3;
unsigned int f3:4;
};
uint8_t i;
struct ParsedInt *d = &i;
Ici, l'écriture d->f2
est tout simplement un compact et lisible façon de dire (i>>1) & (1<<4 - 1)
.
Toutefois, les opérations sur les bits sont bien définis et de travail, indépendamment de l'architecture. Alors, comment se fait-bitfields ne sont pas portables?
- Aussi longtemps que vous le lire et à écrire les bits il n'y a pas de problème. Le problème est d'une autre machine à écrire les bits ou de leur position d'être prescrite dans une norme de la propriété intellectuelle. La norme n'est pas encore fixe la taille d'un octet. Les chances que vous allez vraiment vous avez un problème n'est pas insurmontable.
- Votre hypothèse que d->f2 est la même chose que (i>>1)&(1<<4 - 1) est faux. Il est complètement compilateur-dépendante. Voir les réponses ci-dessous.
Vous devez vous connecter pour publier un commentaire.
Par la norme C, le compilateur est libre pour stocker le champ de bits à peu près dans n'importe quel chemin il veut. Vous pouvez jamais faire toutes les hypothèses où les bits sont alloués. Voici quelques bits-le domaine des choses qui ne sont pas spécifiés par la norme C:
Comportement non spécifié
De mise en œuvre définies par le comportement
Big/little endian est bien sûr également de la mise en œuvre définies. Cela signifie que votre structure ne pouvait être attribuée de la manière suivante (en supposant que 16 bits ints):
Qui s'applique? Faire une supposition, ou de lire en profondeur backend documentation de votre compilateur. Ajouter de la complexité des nombres entiers de 32 bits, en big ou little endian, à présent. Puis ajouter le fait que le compilateur est permis d'ajouter n'importe quel nombre de rembourrage octets n'importe où à l'intérieur de votre champ de bits, car il est traité comme un struct (il ne peut pas ajouter de remplissage au début de la structure, mais partout ailleurs).
Et puis je n'ai même pas mentionné ce qui se passe si vous utilisez de la plaine "int" en tant que bits type = la mise en œuvre définies par le comportement, ou si vous utilisez un autre type que (non signé) int = la mise en œuvre définies par le comportement.
Donc pour répondre à la question, il n'y a pas une telle chose comme portable bit-code de champ, parce que la norme est extrêmement vague avec combien de champs de bits doivent être mises en œuvre. La seule chose que peu de champs peuvent être de confiance, est à être des morceaux de valeurs booléennes, où le programmeur n'est pas concerné de l'emplacement de l'bits en mémoire.
La seule solution portable est d'utiliser les opérateurs bit par bit au lieu de champs de bits. Le code machine généré sera exactement le même, mais déterministe. Bit à Bit les opérateurs sont 100% portable sur n'importe quel compilateur C pour n'importe quel système.
struct iphdr s; s.version = 2; s.ihl = 3;
àuint8_t s[]; s[0] = (uint8_t)((3<<3)|(2<<0));
. Le premier est évident, à la fois de l'écrivain code et le code de la consommation, le plus tard est totalement opaque, car le code consommateur doit savoir à la disposition de la mémoire (avez-vous repérer le bug ?). Bien sûr, vous pouvez écrire une fonction qui va définir l'une de ces deux (ou les deux). Mais vous aurez à écrire un beaucoup de code, qui ne sera probablement jamais être utilisé et est sujette aux erreurs, se terminant en (inutile) du code de ballonnements et de la complexité (si l'interface est trop grand pour s'en rappeler)s[0] = VERSION | IHL;
. En théorie bits-champs est une bonne idée, mais le C standard échoue complètement à l'appui. Dans mon expérience, le code qui est à l'aide de champs de bits est beaucoup plus de bug sur le ventre, parce que le programmeur utilisant toujours faire beaucoup d'hypothèses implicites sur le champ de bits, qui ne sont pas du tout garanti dans la pratique.s[0] = VERSION | IHL_SET(val);
où IHL_SET est une macro simple:#define IHL_SET(x) ((x << IHL_OFFSET) & IHL_MASK)
. (Masque est facultatif). M'a pris 10 secondes pour écrire, aucun effort.Et c'est une partie du problème. Si l'utilisation de peu de champs a été limitée à ce que le compilateur à la "propriété", puis comment le compilateur paniers bits ou commandés seraient à peu près pas de souci à personne.
Cependant, peu de champs sont probablement beaucoup plus fréquemment utilisés pour modéliser les constructions qui sont externes au compilateur domaine du matériel registres, le "fil de fer" protocole pour les communications, ou de format de fichier de mise en page. Ces choses ont des exigences strictes en matière de comment les bits doivent être disposés, et à l'aide de bits des champs de modèle signifie que vous devez compter sur la mise en œuvre définies, et - pire encore - le comportement non spécifié de la façon dont le compilateur va la disposition de l'bits.
En bref, peu de champs ne sont pas spécifiés bien assez pour les rendre utiles pour les situations qu'ils semblent être les plus couramment utilisés pour.
ISO/IEC 9899: 6.7.2.1 /10
Il est plus sûr d'utiliser des opérations de déplacement de bits au lieu de faire des hypothèses sur le champ de bits de la commande ou de l'alignement lorsqu'il essaie d'écrire du code portable, quel que soit le système de stockage ou du nombre de bits.
Voir aussi EXP11-C. Ne pas appliquer les opérateurs attendent à un type de données d'un type incompatible.
Champ de bits accès sont mis en œuvre en termes d'opérations sur le type sous-jacent. Dans l'exemple,
unsigned int
. Donc, si vous avez quelque chose comme:Lorsque vous accédez à champ
b
, le compilateur accède à un ensemble deunsigned int
et puis décale et des masques appropriés bits de large. (Bon, il ne ont pour, mais nous pouvons prétendre qu'il n'.)Sur big endian, la mise en page sera quelque chose comme ceci (bit de poids fort en premier):
Sur little endian, la mise en page sera comme ceci:
Si vous souhaitez accéder à la big endian mise en page de little endian ou vice versa, vous aurez à faire un travail supplémentaire. Cette augmentation de la portabilité a une perte de performance, et depuis struct mise en page est déjà non-portable, la langue des réalisateurs est allé avec la version plus rapide.
Cela fait beaucoup d'hypothèses. Notez également que
sizeof(struct x) == 4
sur la plupart des plateformes.unsigned int
, sa valeur serait toujours AAAABBBBBBBBBCCCC, quelle que soit l'endianness, hein? Alors, si je voulais couper le champc
de cela, je nei & 0xff
et il serait encore portable. Pourquoi bitfields ne sont pas les mêmes?unsigned int
et de champs de bits. Dans deux affaires, des structures en mémoire sont efficaces, mais ne peut pas être copié à d'autres systèmes sans échange d'octets opérations.Les champs de bits seront stockées dans un ordre différent en fonction de la endian-ness de la machine, ce n'est pas trop grave, dans certains cas, mais dans d'autres elle peut avoir une importance. Dire par exemple que votre ParsedInt struct représenté drapeaux dans un paquet envoyé sur un réseau, un petit-boutiste de la machine et de la machine big endian lire les drapeaux dans un ordre différent de la transmission de l'octet qui est évidemment un problème.
Pour faire écho à la plupart des points saillants: Si vous êtes en utilisant ce sur un seul compilateur/HW plate-forme comme un logiciel de construire, puis de stockage ne sera pas un problème. Si vous utilisez le code ou les données à travers de multiples plates-formes OU le besoin de correspondre à un matériel peu mises en page, puis il EST un problème. Et un beaucoup de professionnel logiciel est multi-plateforme, donc il doit faire attention.
Voici l'exemple le plus simple: j'ai un code qui stocke les nombres en format binaire sur le disque. Si je n'ai pas l'écrire et lire des données sur le disque moi-même explicitement octet par octet, alors il ne sera pas la même valeur s'il est lu à partir d'une face de endian système.
Exemple concret:
Disons que mon programme est livré avec des données sur le disque et que je veux lire. Dire que je veux charger comme 4096 dans ce cas...
Ici, je l'ai lu comme une valeur de 16 bits, pas aussi explicite octets.
Cela signifie que si mon système correspond à l'endianness stockées sur le disque, je reçois 4096, et si ça ne marche pas, j'obtiens 16 !!!!!
Donc l'utilisation la plus courante de l'endianness est pour charger en masse des nombres binaires, et puis faire un bswap si vous n'avez pas de match. Dans le passé, nous avions stocker des données sur le disque comme en big endian parce que Intel était le odd man out et de haute vitesse instructions pour échanger les octets. Aujourd'hui, Intel est tellement courante que font souvent Little Endian la valeur par défaut et le swap sur un big endian système.
Un rythme plus lent, mais endian approche neutre est à faire TOUS les I/O en octets, c'est à dire:
Noter que c'est identique au code que vous voulez écrire à faire une endian swap, mais vous n'avez plus besoin de vérifier le boutisme. Et vous pouvez utiliser des macros pour rendre cela moins douloureux.
J'ai utilisé l'exemple de données stockées, utilisées par un programme.
L'autre principale de l'application mentionné, c'est d'écrire des registres matériels, où ces registres ont un absolu de la commande. Un lieu TRÈS courant c'est avec des graphiques. Obtenez de l'endianness mal et votre couleur rouge et bleue canaux obtenir inversée! Encore une fois, la question est celle de la portabilité - vous pourrait tout simplement s'adapter à une plate-forme matérielle et la carte graphique, mais si vous voulez que votre code fonctionne sur des ordinateurs différents, vous devez tester.
Voici un test classique:
Noter que le champ de bits problèmes existent, mais sont orthogonales à l'endianness questions.
Juste à point, nous avons discuté de la question de l'octet de stockage, pas de bit de stockage ou stockage dans bitfields, qui passe par l'autre question:
Si vous êtes à la rédaction de la croix-plate-forme de code, jamais il suffit d'écrire une struct comme un objet binaire. En plus de la endian octet questions décrites ci-dessus, il peut y avoir toutes sortes d'emballage et de problèmes de mise en forme entre les compilateurs. Les langages de programmation permettent pas de restrictions sur la façon dont un compilateur peut présenter des structures ou bitfields en mémoire réelle, de sorte que lors de l'enregistrement sur le disque, vous devez écrire chaque membre de données d'un struct un à la fois, de préférence dans un octet de manière neutre.
Ce conditionnement des impacts "peu boutisme" dans bitfields parce que les différents compilateurs peut stocker bitfields dans une direction différente, et le peu endianness impacts sur la façon dont ils seraient extraites.
Donc garder à l'esprit les DEUX niveaux du problème - l'octet endianness impacts de la capacité d'un ordinateur à lire une seule valeur scalaire, par exemple, d'un flotteur, tandis que le compilateur (et de construire des arguments) de l'impact d'un programme sur la capacité de lire dans une structure d'agrégation.
Ce que j'ai fait dans le passé est de sauvegarder et de charger un fichier de façon neutre et de stocker des méta-données sur la façon dont les données sont stockées dans la mémoire. Ceci me permet d'utiliser le "rapide et facile" binaire chemin de chargement où compatible.