Le __attribute __ ((compressé)) / #pragma pack de gcc est-il dangereux?

164

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 intobjets 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?

Keith Thompson
la source
1
Le rapport de bogue gcc est maintenant marqué comme FIXED avec l'ajout d'un avertissement sur l'affectation du pointeur (et une option pour désactiver l'avertissement). Détails dans ma réponse .
Keith Thompson

Réponses:

148

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 un int*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 intobjet 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:

#include <stdio.h>
#include <stddef.h>
int main(void)
{
    struct foo {
        char c;
        int x;
    } __attribute__((packed));
    struct foo arr[2] = { { 'a', 10 }, {'b', 20 } };
    int *p0 = &arr[0].x;
    int *p1 = &arr[1].x;
    printf("sizeof(struct foo)      = %d\n", (int)sizeof(struct foo));
    printf("offsetof(struct foo, c) = %d\n", (int)offsetof(struct foo, c));
    printf("offsetof(struct foo, x) = %d\n", (int)offsetof(struct foo, x));
    printf("arr[0].x = %d\n", arr[0].x);
    printf("arr[1].x = %d\n", arr[1].x);
    printf("p0 = %p\n", (void*)p0);
    printf("p1 = %p\n", (void*)p1);
    printf("*p0 = %d\n", *p0);
    printf("*p1 = %d\n", *p1);
    return 0;
}

Sur Ubuntu x86 avec gcc 4.5.2, il produit la sortie suivante:

sizeof(struct foo)      = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = 0xbffc104f
p1 = 0xbffc1054
*p0 = 10
*p1 = 20

Sur SPARC Solaris 9 avec gcc 4.5.1, il produit les éléments suivants:

sizeof(struct foo)      = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = ffbff317
p1 = ffbff31c
Bus error

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 xmembre soit correctement aligné. Avec un tableau de deux struct fooobjets, au moins l'un ou l'autre aura un xmembre mal aligné .)

(Dans ce cas, p0pointe vers une adresse mal alignée, car elle pointe vers un intmembre compact qui suit un charmembre. p1Se 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 deux charobjets qui le précèdent - et sur SPARC Solaris, la baie arrsemble être allouée à une adresse paire, mais pas un multiple de 4.)

Lorsqu'il fait référence au membre xd'un struct foopar son nom, le compilateur sait qu'il xest potentiellement mal aligné et générera du code supplémentaire pour y accéder correctement.

Une fois que l'adresse de arr[0].xou arr[1].xa été stockée dans un objet pointeur, ni le compilateur ni le programme en cours d'exécution ne savent qu'il pointe vers un intobjet 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-memberoption, activée par défaut.

Lorsque l'adresse du membre compressé de struct ou union est prise, cela peut entraîner une valeur de pointeur non alignée. Ce correctif ajoute -Waddress-of-packing-member pour vérifier l'alignement lors de l'affectation du pointeur et avertir les adresses non alignées ainsi que les pointeurs non alignés

Je viens de construire cette version de gcc à partir des sources. Pour le programme ci-dessus, il produit ces diagnostics:

c.c: In function main’:
c.c:10:15: warning: taking address of packed member of struct foo may result in an unaligned pointer value [-Waddress-of-packed-member]
   10 |     int *p0 = &arr[0].x;
      |               ^~~~~~~~~
c.c:11:15: warning: taking address of packed member of struct foo may result in an unaligned pointer value [-Waddress-of-packed-member]
   11 |     int *p1 = &arr[1].x;
      |               ^~~~~~~~~
Keith Thompson
la source
1
est potentiellement désaligné, et générera ... quoi?
Almo
5
les éléments de structure mal alignés sur ARM font des choses bizarres: certains accès provoquent des erreurs, d'autres provoquent une réorganisation contre-intuitive des données récupérées ou incorporent des données inattendues adjacentes.
wallyk
8
Il semble que l'emballage lui-même soit sûr, mais la manière dont les éléments emballés sont utilisés peut être dangereuse. Les anciens processeurs basés sur ARM ne prenaient pas en charge les accès mémoire non alignés non plus, les versions plus récentes le font, mais je sais que Symbian OS interdit toujours les accès non alignés lors de l'exécution sur ces nouvelles versions (la prise en charge est désactivée).
James
14
Une autre façon de résoudre ce problème dans gcc serait d'utiliser le système de types: exiger que les pointeurs vers les membres des structures compactées ne puissent être assignés qu'aux pointeurs qui sont eux-mêmes marqués comme compressés (c'est-à-dire potentiellement non alignés). Mais vraiment: des structures compactées, dites simplement non.
caf
9
@Flavius: Mon objectif principal était de diffuser les informations. Voir aussi meta.stackexchange.com/questions/17463/…
Keith Thompson
62

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-alignoption, elle n'est pas activée par défaut ni avec -Wallou -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:

struct  __attribute__((__packed__)) my_struct {
    char c;
    int i;
};

struct my_struct a = {'a', 123};
struct my_struct *b = &a;
int c = a.i;
int d = b->i;
int *e __attribute__((aligned(1))) = &a.i;
int *f = &a.i;

Ici, le type de aest une structure compressée (comme défini ci-dessus). De même, best un pointeur vers une structure compressée. Le type de l'expression a.iest (fondamentalement) une valeur l int avec un alignement de 1 octet. cet dsont tous deux normaux int. Lors de la lecture a.i, le compilateur génère du code pour un accès non aligné. Quand vous lisez b->i, ble type de s sait toujours qu'il est emballé, donc pas de problème non plus. eest 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'affectation f = &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 -Wallou -Wextra).

Daniel Santos
la source
6
+1 pour expliquer comment utiliser des pointeurs avec des structures non alignées!
Soumya
@Soumya Merci pour les points! :) Gardez à l'esprit que __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.
Daniel Santos
4
@Soumya Malheureusement, __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.
Daniel Santos
@DanielSantos: Un compilateur de qualité que j'utilise (Keil) reconnaît les qualificatifs "emballés" pour les pointeurs; si une structure est déclarée "emballée", prendre l'adresse d'un uint32_tmembre donnera un uint32_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].
supercat
49

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).

ams
la source
Hmm, je me demande ce qui se passe si vous mettez une structure compressée dans une autre structure compressée où l'alignement serait différent? Question intéressante, mais cela ne devrait pas changer la réponse.
ams
GCC n'alignera pas toujours la structure elle-même non plus. Par exemple: struct foo {int x; char c; } __attribute __ ((emballé)); struct bar {char c; struct foo f; }; J'ai trouvé que bar :: f :: x ne sera pas nécessairement aligné, du moins sur certaines saveurs de MIPS.
Anton
3
@antonm: Oui, une structure dans une structure compressée peut très bien être non alignée, mais, encore une fois, le compilateur sait quel est l'alignement de chaque champ, et c'est parfaitement sûr tant que vous n'essayez pas d'utiliser des pointeurs dans la structure. Vous devriez imaginer une structure dans une structure comme une série plate de champs, avec le nom supplémentaire juste pour la lisibilité.
ams
6

L'utilisation de cet attribut est définitivement dangereuse.

Une chose particulière qu'il rompt est la capacité d'un unionqui 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:

6 Une garantie particulière est faite afin de simplifier l'utilisation des unions: si une union contient plusieurs structures qui partagent une séquence initiale commune (voir ci-dessous), et si l'objet d'union contient actuellement l'une de ces structures, il est permis d'inspecter le partie initiale commune de l'un d'entre eux partout où une déclaration du type complété de l'union est visible. Deux structures partagent une séquence initiale commune si les membres correspondants ont des types compatibles (et, pour les champs de bits, les mêmes largeurs) pour une séquence d'un ou plusieurs membres initiaux.

...

9 EXEMPLE 3 Ce qui suit est un fragment valide:

union {
    struct {
        int    alltypes;
    }n;
    struct {
        int    type;
        int    intnode;
    } ni;
    struct {
        int    type;
        double doublenode;
    } nf;
}u;
u.nf.type = 1;
u.nf.doublenode = 3.14;
/*
...
*/
if (u.n.alltypes == 1)
if (sin(u.nf.doublenode) == 0.0)
/*
...
*/

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:

#include <stdio.h>
#include <stdlib.h>

struct s1
{
    short a;
    int b;
} __attribute__((packed));

struct s2
{
    short a;
    int b;
};

union su {
    struct s1 x;
    struct s2 y;
};

int main()
{
    union su s;
    s.x.a = 0x1234;
    s.x.b = 0x56789abc;

    printf("sizeof s1 = %zu, sizeof s2 = %zu\n", sizeof(struct s1), sizeof(struct s2));
    printf("s.y.a=%hx, s.y.b=%x\n", s.y.a, s.y.b);
    return 0;
}

Production:

sizeof s1 = 6, sizeof s2 = 8
s.y.a=1234, s.y.b=5678

Même si struct s1et struct s2ont 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 membre x.bn'est pas la même que la valeur lue depuis le membre y.b, même si la norme dit qu'elles devraient être identiques.

dbush
la source
On pourrait dire que si vous compressez l'une des structures et pas l'autre, vous n'allez pas vous attendre à ce qu'elles aient des dispositions cohérentes. Mais oui, c'est une autre exigence standard qu'elle peut violer.
Keith Thompson
1

(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:

0: message type (1 byte)
1: target address, MSB
2: target address, LSB
3: data (chars)
...
F: checksum (1 byte)

Ensuite, je peux déclarer quelque chose comme

typedef struct {
  uint8_t msgType;
  uint16_t targetAddr; // may have to bswap
  uint8_t data[12];
  uint8_t checksum;
} __attribute__((packed)) myStruct;

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,

John Allsup
la source
À moins que je manque quelque chose, cela ne répond pas à la question. Vous soutenez que l'empaquetage de structure est pratique (ce qui est le cas), mais vous n'abordez pas la question de savoir si c'est sûr. En outre, vous affirmez que les pénalités de performance pour les lectures non alignées; c'est vrai pour x86, mais pas pour tous les systèmes, comme je l'ai démontré dans ma réponse.
Keith Thompson