En C, le compilateur disposera les membres d'une structure dans l'ordre dans lequel ils sont déclarés, avec d'éventuels octets de remplissage insérés entre les membres, ou après le dernier membre, pour s'assurer que chaque membre est correctement aligné.
gcc fournit une extension de langage __attribute__((packed))
, qui indique au compilateur de ne pas insérer de remplissage, ce qui permet aux membres de structure d'être mal alignés. Par exemple, si le système exige normalement que tous les int
objets aient un alignement sur 4 octets, les membres de structure __attribute__((packed))
peuvent int
être alloués à des décalages impairs.
Citant la documentation gcc:
L'attribut «compressé» spécifie qu'un champ de variable ou de structure doit avoir le plus petit alignement possible - un octet pour une variable et un bit pour un champ, sauf si vous spécifiez une valeur plus grande avec l'attribut «aligné».
De toute évidence, l'utilisation de cette extension peut entraîner des exigences de données plus petites mais un code plus lent, car le compilateur doit (sur certaines plates-formes) générer du code pour accéder à un membre mal aligné un octet à la fois.
Mais y a-t-il des cas où cela n'est pas sûr? Le compilateur génère-t-il toujours du code correct (bien que plus lent) pour accéder aux membres mal alignés des structures compactées? Est-il même possible de le faire dans tous les cas?
la source
Réponses:
Oui,
__attribute__((packed))
est potentiellement dangereux sur certains systèmes. Le symptôme n'apparaîtra probablement pas sur un x86, ce qui rend simplement le problème plus insidieux; les tests sur les systèmes x86 ne révéleront pas le problème. (Sur le x86, les accès mal alignés sont gérés par le matériel; si vous déréférencer unint*
pointeur qui pointe vers une adresse impaire, il sera un peu plus lent que s'il était correctement aligné, mais vous obtiendrez le résultat correct.)Sur certains autres systèmes, tels que SPARC, la tentative d'accès à un
int
objet mal aligné provoque une erreur de bus, bloquant le programme.Il existe également des systèmes dans lesquels un accès mal aligné ignore silencieusement les bits de poids faible de l'adresse, ce qui l'amène à accéder au mauvais bloc de mémoire.
Considérez le programme suivant:
Sur Ubuntu x86 avec gcc 4.5.2, il produit la sortie suivante:
Sur SPARC Solaris 9 avec gcc 4.5.1, il produit les éléments suivants:
Dans les deux cas, le programme est compilé sans options supplémentaires, juste
gcc packed.c -o packed
.(Un programme qui utilise une structure unique plutôt qu'un tableau ne présente pas le problème de manière fiable, car le compilateur peut allouer la structure sur une adresse impaire afin que le
x
membre soit correctement aligné. Avec un tableau de deuxstruct foo
objets, au moins l'un ou l'autre aura unx
membre mal aligné .)(Dans ce cas,
p0
pointe vers une adresse mal alignée, car elle pointe vers unint
membre compact qui suit unchar
membre.p1
Se trouve être correctement alignée, car elle pointe vers le même membre dans le deuxième élément du tableau, donc il y a deuxchar
objets qui le précèdent - et sur SPARC Solaris, la baiearr
semble être allouée à une adresse paire, mais pas un multiple de 4.)Lorsqu'il fait référence au membre
x
d'unstruct foo
par son nom, le compilateur sait qu'ilx
est potentiellement mal aligné et générera du code supplémentaire pour y accéder correctement.Une fois que l'adresse de
arr[0].x
ouarr[1].x
a été stockée dans un objet pointeur, ni le compilateur ni le programme en cours d'exécution ne savent qu'il pointe vers unint
objet mal aligné . Il suppose simplement qu'il est correctement aligné, ce qui entraîne (sur certains systèmes) une erreur de bus ou une autre panne similaire.Corriger cela dans gcc serait, je crois, irréalisable. Une solution générale exigerait, pour chaque tentative de déréférencer un pointeur vers n'importe quel type avec des exigences d'alignement non triviales, soit (a) prouvant au moment de la compilation que le pointeur ne pointe pas vers un membre mal aligné d'une structure compressée, ou (b) générer un code plus volumineux et plus lent qui peut gérer des objets alignés ou mal alignés.
J'ai soumis un rapport de bogue gcc . Comme je l'ai dit, je ne pense pas qu'il soit pratique de le réparer, mais la documentation devrait le mentionner (ce n'est actuellement pas le cas).
MISE À JOUR : à partir du 20/12/2018, ce bogue est marqué comme FIXE. Le patch apparaîtra dans gcc 9 avec l'ajout d'une nouvelle
-Waddress-of-packed-member
option, activée par défaut.Je viens de construire cette version de gcc à partir des sources. Pour le programme ci-dessus, il produit ces diagnostics:
la source
Comme je l'ai dit ci-dessus, ne prenez pas de pointeur vers un membre d'une structure compressée. C'est simplement jouer avec le feu. Lorsque vous dites
__attribute__((__packed__))
ou#pragma pack(1)
, ce que vous dites vraiment, c'est "Hé gcc, je sais vraiment ce que je fais." Quand il s'avère que vous ne le faites pas, vous ne pouvez pas blâmer à juste titre le compilateur.Nous pouvons peut-être blâmer le compilateur pour sa complaisance. Bien que gcc ait une
-Wcast-align
option, elle n'est pas activée par défaut ni avec-Wall
ou-Wextra
. Ceci est apparemment dû au fait que les développeurs de gcc considèrent ce type de code comme une " abomination " de mort cérébrale indigne d'être adressée - un dédain compréhensible, mais cela n'aide pas quand un programmeur inexpérimenté s'y engouffre.Considérer ce qui suit:
Ici, le type de
a
est une structure compressée (comme défini ci-dessus). De même,b
est un pointeur vers une structure compressée. Le type de l'expressiona.i
est (fondamentalement) une valeur l int avec un alignement de 1 octet.c
etd
sont tous deux normauxint
. Lors de la lecturea.i
, le compilateur génère du code pour un accès non aligné. Quand vous lisezb->i
,b
le type de s sait toujours qu'il est emballé, donc pas de problème non plus.e
est un pointeur vers un entier aligné sur un octet, de sorte que le compilateur sait également comment le déréférencer correctement. Mais lorsque vous effectuez l'affectationf = &a.i
, vous stockez la valeur d'un pointeur int non aligné dans une variable de pointeur int aligné - c'est là que vous vous êtes trompé. Et je suis d'accord, gcc devrait avoir cet avertissement activé parpar défaut (même pas dans-Wall
ou-Wextra
).la source
__attribute__((aligned(1)))
c'est une extension gcc et n'est pas portable. À ma connaissance, le seul moyen vraiment portable de faire un accès non aligné en C (avec n'importe quelle combinaison compilateur / matériel) est avec une copie mémoire octet par octet (memcpy ou similaire). Certains matériels n'ont même pas d'instructions pour un accès non aligné. Mon expertise est avec arm et x86 qui peuvent faire les deux, bien que l'accès non aligné soit plus lent. Donc, si jamais vous avez besoin de le faire avec des performances élevées, vous devrez renifler le matériel et utiliser des astuces spécifiques à l'arche.__attribute__((aligned(x)))
semble maintenant être ignoré lorsqu'il est utilisé pour les pointeurs. :( Je n'ai pas encore tous les détails à ce sujet, mais l'utilisation__builtin_assume_aligned(ptr, align)
semble permettre à gcc de générer le code correct. Lorsque j'aurai une réponse plus concise (et, espérons-le, un rapport de bogue), je mettrai à jour ma réponse.uint32_t
membre donnera unuint32_t packed*
; essayer de lire à partir d'un tel pointeur sur un Cortex-M0, par exemple, appellera IIRC un sous-programme qui prendra ~ 7x aussi longtemps qu'une lecture normale si le pointeur n'est pas aligné ou ~ 3x aussi longtemps s'il est aligné, mais se comportera de manière prévisible dans les deux cas [le code en ligne prendrait 5 fois plus de temps, qu'il soit aligné ou non].C'est parfaitement sûr tant que vous accédez toujours aux valeurs via la structure via le
.
(point) ou la->
notation.Ce qui n'est pas sûr, c'est de prendre le pointeur de données non alignées, puis d'y accéder sans en tenir compte.
De plus, même si chaque élément de la structure est connu pour être non aligné, il est connu pour être non aligné d'une manière particulière , de sorte que la structure dans son ensemble doit être alignée comme le compilateur l'attend ou il y aura des problèmes (sur certaines plates-formes, ou à l'avenir si une nouvelle méthode est inventée pour optimiser les accès non alignés).
la source
L'utilisation de cet attribut est définitivement dangereuse.
Une chose particulière qu'il rompt est la capacité d'un
union
qui contient deux ou plusieurs structures à écrire un membre et à en lire un autre si les structures ont une séquence initiale commune de membres. La section 6.5.2.3 de la norme C11 stipule:Quand
__attribute__((packed))
est introduit, il brise cela. L'exemple suivant a été exécuté sur Ubuntu 16.04 x64 à l'aide de gcc 5.4.0 avec les optimisations désactivées:Production:
Même si
struct s1
etstruct s2
ont une "séquence initiale commune", le compactage appliqué au premier signifie que les membres correspondants ne vivent pas au même décalage d'octet. Le résultat est que la valeur écrite sur le membrex.b
n'est pas la même que la valeur lue depuis le membrey.b
, même si la norme dit qu'elles devraient être identiques.la source
(Ce qui suit est un exemple très artificiel concocté pour illustrer.) Une utilisation majeure des structures compactées est lorsque vous avez un flux de données (par exemple 256 octets) auquel vous souhaitez donner un sens. Si je prends un exemple plus petit, supposons que j'ai un programme en cours d'exécution sur mon Arduino qui envoie via série un paquet de 16 octets qui ont la signification suivante:
Ensuite, je peux déclarer quelque chose comme
puis je peux me référer aux octets targetAddr via aStruct.targetAddr plutôt que de jouer avec l'arithmétique du pointeur.
Maintenant, avec l'alignement, prendre un pointeur void * en mémoire sur les données reçues et le convertir en myStruct * ne fonctionnera pas à moins que le compilateur ne traite la structure comme compressée (c'est-à-dire qu'il stocke les données dans l'ordre spécifié et utilise exactement 16 octets pour cet exemple). Il y a des pénalités de performance pour les lectures non alignées, donc utiliser des structures compactées pour les données avec lesquelles votre programme travaille activement n'est pas nécessairement une bonne idée. Mais lorsque votre programme est fourni avec une liste d'octets, les structures compactées facilitent l'écriture de programmes qui accèdent au contenu.
Sinon, vous finissez par utiliser C ++ et écrire une classe avec des méthodes d'accesseurs et des trucs qui font de l'arithmétique des pointeurs dans les coulisses. En bref, les structures compactées sont destinées à traiter efficacement les données compactées, et les données compactées peuvent être ce avec quoi votre programme est donné pour travailler. Pour la plupart, votre code doit lire les valeurs hors de la structure, travailler avec elles et les réécrire une fois terminé. Tout le reste doit être fait en dehors de la structure compressée. Une partie du problème est le truc de bas niveau que C essaie de cacher au programmeur, et le saut de cerceau qui est nécessaire si de telles choses importent vraiment pour le programmeur. (Vous avez presque besoin d'une construction de `` disposition des données '' différente dans le langage pour pouvoir dire `` cette chose fait 48 octets de long, foo fait référence aux données de 13 octets et doit être interprétée ainsi ''; et une construction de données structurées distincte,
la source