web-dev-qa-db-fra.com

Pourquoi le bit endianness est un problème dans les champs de bits?

Tout code portable qui utilise des champs de bits semble faire la distinction entre les plates-formes grand et petit. Voir la déclaration de struct iphdr dans le noyau Linux pour un exemple de ce code. Je ne parviens pas à comprendre pourquoi l’endianité est un problème.

Autant que je sache, les champs de bits ne sont que des constructions de compilateur, utilisées pour faciliter les manipulations au niveau des bits.

Par exemple, considérons le champ binaire suivant:

struct ParsedInt {
    unsigned int f1:1;
    unsigned int f2:3;
    unsigned int f3:4;
};
uint8_t i;
struct ParsedInt *d = &i;
Voici, écrire d->f2 est simplement un moyen compact et lisible de dire (i>>1) & (1<<4 - 1).

Cependant, les opérations sur les bits sont bien définies et fonctionnent quelle que soit l'architecture. Alors, comment se fait-il que les champs de bits ne soient pas portables?

52
Leonid99

Selon la norme C, le compilateur est libre de stocker le champ de bits à peu près de la manière qu'il souhaite. Vous pouvez jamais faire des suppositions sur l'emplacement des bits. Voici quelques éléments liés au champ de bits qui ne sont pas spécifiés par le standard C:

Comportement non spécifié

  • L'alignement de l'unité de stockage adressable alloué pour contenir un champ de bits (6.7.2.1).

Comportement défini par l'implémentation

  • Indique si un champ de bits peut chevaucher une limite d'unité de stockage (6.7.2.1). 
  • L'ordre d'attribution des champs de bits dans une unité (6.7.2.1).

Big/little endian est bien sûr également défini par la mise en œuvre. Cela signifie que votre structure peut être allouée de la manière suivante (en supposant que les ints 16 bits):

PADDING : 8
f1 : 1
f2 : 3
f3 : 4

or

PADDING : 8
f3 : 4
f2 : 3
f1 : 1

or

f1 : 1
f2 : 3
f3 : 4
PADDING : 8

or

f3 : 4
f2 : 3
f1 : 1
PADDING : 8

Lequel s'applique? Devinez, ou lisez la documentation détaillée de votre compilateur. Ajoutez à cela la complexité des entiers 32 bits, en gros ou en petit endian. Ajoutez ensuite le fait que le compilateur est autorisé à ajouter un nombre quelconque de padding bytes n'importe où dans votre champ de bit, car il est traité comme une structure (il ne peut pas ajouter de remplissage au tout début de la structure, mais partout. autre).

Et puis, je n'ai même pas mentionné ce qui se passe si vous utilisez un "int" ordinaire comme type de champ de bits = comportement défini par l'implémentation, ou si vous utilisez un autre type que le comportement (non signé) int = défini par l'implémentation.

Donc, pour répondre à la question, il n’existe pas de code de champ de bits portable, car la norme C est extrêmement vague en ce qui concerne la mise en oeuvre des champs de bits. La seule chose dont on puisse faire confiance aux champs de bits est d'être des morceaux de valeurs booléennes, où le programmeur ne se soucie pas de l'emplacement des bits en mémoire.

La seule solution portable consiste à utiliser les opérateurs binaires au lieu des champs de bits. Le code machine généré sera exactement le même, mais déterministe. Les opérateurs binaires sont 100% portables sur tous les compilateurs C et sur tous les systèmes. 

64
Lundin

Pour autant que je sache, les champs de bits sont purement des constructions de compilateur

Et cela fait partie du problème. Si l'utilisation des champs de bits était limitée à ce que le compilateur "possédait", la façon dont le compilateur compressait ou commandait les bits ne préoccupait quasiment personne.

Cependant, les champs de bits sont probablement beaucoup plus souvent utilisés pour modéliser des constructions externes au domaine du compilateur: registres de matériel, protocole "fil" de communication ou format de fichier. Ces choses ont des exigences strictes sur la façon dont les bits doivent être disposés, et utiliser des champs de bits pour les modéliser signifie que vous devez compter sur le comportement non défini de l'implémentation et - pire encore - de la façon dont le compilateur va mettre en forme .

En bref, les champs de bits ne sont pas assez bien spécifiés pour les rendre utiles dans les situations pour lesquelles ils semblent le plus couramment utilisés.

12
Michael Burr

ISO/CEI 9899: 6.7.2.1/10

Une implémentation peut affecter n'importe quel unité de stockage adressable assez grande tenir un petit champ. Si suffisamment d’espace reste, un champ qui a tout de suite suit un autre champ de bits dans un la structure doit être emballée dans bits adjacents de la même unité. Si il ne reste pas beaucoup d'espace, que ce soit un le champ bit qui ne correspond pas est mis dans l'unité suivante ou se chevauche adjacente les unités sont définies par implémentation. Le ordre d'attribution des champs de bits dans une unité (ordre élevé à ordre inférieur ou ordre faible à ordre élevé) est mise en œuvre définie. L'alignement de l'unité de stockage adressable est non précisé.

Il est plus sûr d’utiliser des opérations de décalage de bits au lieu de faire des suppositions sur l’ordre ou l’alignement des champs de bits lors de la tentative d’écriture de code portable, quelles que soient l’endianisme du système ou son bitness.

Voir aussi EXP11-C. N'appliquez pas les opérateurs attendus d'un type aux données d'un type incompatible .

8
mizo

Les accès aux champs de bits sont implémentés en termes d'opérations sur le type sous-jacent. Dans l'exemple, unsigned int. Donc, si vous avez quelque chose comme:

struct x {
    unsigned int a : 4;
    unsigned int b : 8;
    unsigned int c : 4;
};

Lorsque vous accédez au champ b, le compilateur accède à un unsigned int entier, puis décale et masque la plage de bits appropriée. (Eh bien, ça ne fonctionne pas {oblige} _, mais on peut faire semblant.)

Sur big endian, la disposition ressemblera à ceci (le bit le plus significatif en premier):

AAAABBBB BBBBCCCC

Sur little endian, la disposition sera la suivante:

BBBBAAAA CCCCBBBB

Si vous voulez accéder à la disposition big endian depuis little endian ou inversement, vous devrez faire un travail supplémentaire. Cette augmentation de la portabilité a un impact négatif sur les performances et, comme struct layout est déjà non portable, les développeurs de langage ont opté pour la version plus rapide.

Cela fait beaucoup d'hypothèses. Notez également que sizeof(struct x) == 4 sur la plupart des plateformes.

5
Dietrich Epp

Les champs de bits seront stockés dans un ordre différent en fonction de l'endurance de la machine. Cela peut ne pas être important dans certains cas, mais dans d'autres, cela peut l'être. Supposons, par exemple, que votre structure ParsedInt représente des drapeaux dans un paquet envoyé sur un réseau, une machine petit endian et une machine big endian lisent ces drapeaux dans un ordre différent de celui de l'octet transmis, ce qui pose évidemment problème.

1
Charles Keepax

Pour faire écho aux points les plus saillants: Si vous utilisez ceci sur une plate-forme compilateur/matériel unique en tant que construction logicielle uniquement, alors l’endianité ne sera pas un problème. Si vous utilisez du code ou des données sur plusieurs plates-formes OR doivent correspondre aux dispositions de bits matérielles, il s'agit alors d'un problème EST. Et un lot de logiciel professionnel est multi-plateforme, il doit donc s'en soucier.

Voici l'exemple le plus simple: J'ai un code qui stocke les nombres au format binaire sur le disque. Si je n'écris pas et ne lis pas ces données sur disque moi-même explicitement octet par octet, ce ne sera pas la même valeur si elles sont lues à partir d'un système final opposé. 

Exemple concret:

int16_t s = 4096; // un numéro signé de 16 bits ...

Disons que mon programme est livré avec des données sur le disque que je veux lire. Disons que je veux le charger en 4096 dans ce cas ...

fread ((vide *) & s, 2, fp); // lecture du disque en binaire ...

Ici, je le lis comme une valeur de 16 bits et non comme des octets explicites… .. Cela signifie que si mon système correspond à l’endianité enregistrée sur le disque, j’obtiens 4096, et si ce n’est pas le cas, j’en ai 16 !!!!!

Donc, l’utilisation la plus courante de l’endianisme est de charger en bloc des nombres binaires, puis d’effectuer un bswap si vous ne correspondez pas. Auparavant, nous stockions les données sur disque en tant que big endian, car Intel était un homme étrange et fournissait des instructions très rapides pour échanger les octets. De nos jours, Intel est si courant que Little Endian est souvent utilisé par défaut et est échangé sur un système big endian.

Une approche plus lente mais neutre est d’effectuer TOUTES LES E/S par octets, c’est-à-dire:

uint_8 ubyte; int_8 sbyte; int16_t s; // lit les s de manière neutre

// Choisissons little endian comme ordre d'octet choisi:

fread ((vide *) & ubyte, 1, fp); // Ne lit qu'un octet à la fois Fread ((void *) & sbyte, 1, fp); // Ne lit qu'un octet à la fois

// Reconstruct s

s = ubyte | (sByte << 8);

Notez que ceci est identique au code que vous auriez écrit pour faire un échange endian, mais vous n’avez plus besoin de vérifier l’endianness. Et vous pouvez utiliser des macros pour rendre cela moins douloureux.

J'ai utilisé l'exemple des données stockées utilisées par un programme . L'autre application principale mentionnée consiste à écrire des registres matériels, dans lesquels ces registres ont un ordre absolu. Un endroit TRÈS COMMUN que cela se présente est avec des graphiques. Faites fausse route et vos canaux de couleurs rouge et bleu sont inversés! Là encore, le problème est lié à la portabilité: vous pouvez simplement vous adapter à une plate-forme matérielle et à une carte graphique données, mais si vous souhaitez que votre même code fonctionne sur des machines différentes, vous devez effectuer un test.

Voici un test classique:

typedef union {uint_16 s; uint_8 b [2]; } EndianTest_t;

EndianTest_t test = 4096;

if (test.b [0] == 12) printf ("Big Endian détecté!\n");

Notez que les problèmes de champs de bits existent également, mais sont orthogonaux aux problèmes de finalité.

0
user2465201

Signalons simplement que nous avons discuté de la question de l’endianisme des octets, et non de l’endianité des bits ou des endianités dans les champs de bits, qui se confond avec l’autre question:

Si vous écrivez du code multiplateforme, n'écrivez jamais une structure en tant qu'objet binaire. Outre les problèmes de octets finaux décrits ci-dessus, il peut y avoir toutes sortes de problèmes d'emballage et de formatage entre les compilateurs. Les langages n'imposent aucune restriction quant à la manière dont un compilateur peut disposer des structures ou des champs de bits dans la mémoire réelle; par conséquent, lors de la sauvegarde sur disque, vous devez écrire chaque membre de données d'une structure, une par une, de préférence de manière neutre en octets.

Cette compression a une incidence sur "l'endianité des bits" dans les champs de bits, car différents compilateurs peuvent stocker les champs de bits dans une direction différente, et que l'extrémité des bits affecte la manière dont ils seront extraits.

Pensez donc aux DEUX niveaux du problème - l’endianité des octets affecte la capacité d’un ordinateur à lire une seule valeur scalaire, par exemple un float, tandis que le compilateur (et les arguments de construction) affectent la capacité d’un programme à lire dans une structure globale.

Ce que j’ai fait par le passé est de sauvegarder et de charger un fichier de manière neutre et de stocker des métadonnées sur la manière dont les données sont disposées en mémoire. Cela me permet d'utiliser le chemin de chargement binaire "rapide et facile" lorsqu'il est compatible.

0
user2465201