L'attribut gcc ((emballé)) / #pragma pack est-il dangereux?

en C, le compilateur va disposer les membres d'une structure dans l'ordre dans lequel ils sont déclarés, avec des octets de remplissage possibles insérés entre les membres, ou après le dernier membre, pour s'assurer que chaque membre est aligné correctement.

gcc fournit une extension de langue, __attribute__((packed)) , qui dit au compilateur de ne pas insérer de capitonnage, permettant aux membres de struct d'être désalignés. Par exemple, si le système exige normalement que tous les objets int aient 4 octets l'alignement, __attribute__((packed)) peut faire que les membres de la structure int soient affectés à des décalages impairs.

citant la documentation du gcc:

l'attribut "packed" spécifie qu'un champ de variable ou de structure devrait avoir le plus petit alignement possible--un byte pour une variable, et un peu pour un champ, sauf si vous spécifiez une valeur supérieure à la `alignés' attribut.

évidemment l'utilisation de cette extension peut entraîner des besoins de données plus petits mais un code plus lent, car le compilateur doit (sur certaines plateformes) 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 est dangereux? 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 emballées? Est-il même possible de le faire dans tous les cas?

133
demandé sur Mohammadreza Panahi 2011-12-20 02:28:00

4 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 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 traités en matériel; si vous déréférentez un pointeur int* qui pointe vers une adresse impaire, il sera un peu plus lent que s'il était correctement aligné, mais vous obtiendrez le bon résultat.)

Sur certains autres systèmes, comme SPARC, tenter d'accéder à un objet mal aligné int provoque une erreur de bus, écrasant le programme.

Il ya aussi eu des systèmes où un accès mal aligné ignore tranquillement les bits d'ordre bas de l'adresse, ce qui lui permet d'accéder au mauvais morceau de mémoire.

envisager 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 x86 Ubuntu 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 ce qui suit:

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 seule structure plutôt qu'un tableau ne présente pas le problème de manière fiable, puisque le compilateur peut affecter la structure sur une adresse impaire de sorte que le membre x est correctement aligné. Avec un tableau de deux struct foo objets, au moins un ou le d'autres auront un membre désaligné x .)

(dans ce cas, p0 indique une adresse mal alignée, parce qu'elle indique un membre int emballé suivant un membre char . p1 se trouve être correctement aligné, puisqu'il pointe vers le même membre dans le deuxième élément du tableau, donc il y a deux char objets le précédant -- et sur SPARC Solaris le tableau arr semble être alloué à une adresse qui est même, mais pas un multiple de 4.)

en se référant au membre x d'un struct foo par son nom, le compilateur sait que x 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 ou arr[1].x a été stockée dans un objet pointeur, ni le compilateur ni le programme courant ne savent qu'il pointe vers un objet mal aligné int . Juste suppose qu'il est correctement aligné, ce qui entraîne (sur certains systèmes) une erreur de bus ou une autre défaillance similaire.

réparer cela dans gcc serait, je crois, impraticable. Une solution générale exigerait, pour chaque tentative de déréférence d'un pointeur à un type avec des exigences d'alignement non trivial soit (a) prouver au moment de la compilation que le pointeur ne pointe pas vers un membre mal aligné d'une structure emballée, ou (b) générer un code plus volumineux et plus lent qui peut gérer soit aligné, soit mal alignées objets.

j'ai soumis un rapport de bogue gcc . Comme je l'ai dit, Je ne crois pas qu'il soit pratique de le corriger, mais la documentation devrait le mentionner (ce n'est pas le cas actuellement).

120
répondu Keith Thompson 2016-01-21 23:36:03

c'est parfaitement sûr tant que vous accédez toujours aux valeurs via la notation . (point) ou -> .

What's not safe est de prendre le pointeur de données non alignées et d'y accéder sans en tenir compte.

aussi, 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 , donc la structure comme un tout doit être aligné comme le souhaite le compilateur, sinon il y aura des problèmes (sur certaines plateformes, ou à l'avenir si une nouvelle méthode est inventée pour optimiser les accès non alignés).

47
répondu ams 2011-12-20 10:53:12

comme dit ams ci-dessus, ne prenez pas un pointeur vers un membre d'une structure qui est emballé. C'est tout simplement jouer avec le feu. Quand vous dites __attribute__((__packed__)) ou #pragma pack(1) , ce que vous dites vraiment, c'est "Hey 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.

peut-être que nous pouvons blâmer le compilateur pour sa complaisance cependant. Alors que gcc a une option -Wcast-align , 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 un cerveau-mort " abomination " indigne d'adresser -- dédain compréhensible, mais il n'aide pas quand un programmeur inexpérimenté bourdonne dedans.

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 a est une structure emballée (telle que définie ci-dessus). De même, b est un pointeur vers une paniers struct. Le type de de l'expression a.i est (fondamentalement) un int l-value avec 1 byte alignement. c et d sont tous deux normaux int s. En lisant a.i , le compilateur génère du code pour l'accès non aligné. Quand vous lisez b->i , le type de b sait toujours qu'il est emballé, donc aucun problème leur soit. e est un pointeur vers un int aligné d'un octet, donc le compilateur sait déréférencer que correctement ainsi. Mais quand vous faites la tâche f = &a.i , vous stockez la valeur d'un pointeur int non aligné dans une variable alignée pointeur int -- c'est là que vous vous êtes trompé. Et je suis d'accord, gcc devrait avoir cet avertissement activé par par défaut (pas même dans -Wall ou -Wextra ).

44
répondu Daniel Santos 2013-05-05 03:21:26

(ce qui suit est un exemple très artificiel préparé pour illustrer.) Une utilisation majeure des structures empaquetées est où vous avez un flux de données (disons 256 octets) auquel vous souhaitez donner un sens. Si je prends un exemple plus petit, supposons que j'ai un programme tournant sur mon Arduino qui envoie via serial 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)

alors 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;

et ensuite je peux me référer aux octets targetAddr via aStruct.targettaddr plutôt que de jouer avec l'arithmétique pointer.

maintenant avec des choses d'alignement qui se produisent, prendre un pointeur vide* en mémoire aux données reçues et le lancer à un myStruct* ne fonctionnera pas à moins que le compilateur traite la structure comme emballé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 en utilisant des structures emballées pour les données avec lesquelles votre programme travaille activement, ce n'est pas nécessairement une bonne idée. Mais lorsque votre programme est fourni avec une liste d'octets, paniers structures facilitent l'écriture des programmes d'accès aux contenus.

sinon vous finissez par utiliser C++ et écrire une classe avec des méthodes accessor et des trucs qui font de l'arithmétique pointer dans les coulisses. En bref, les structures emballées sont pour traiter efficacement des données emballées, et des données emballées peuvent être ce que votre programme est donné à travailler avec. Pour la plupart, vous codez devrait lire les valeurs hors de la structure, travailler avec eux, et les écrire en arrière quand fait. Tout le reste doit être fait en dehors de la structure emballée. Une partie du problème est la chose de bas niveau que C essaie de cacher au programmeur, et le saut de cerceau qui est nécessaire si de telles choses ont vraiment de l'importance pour le programmeur. (Vous avez presque besoin d'une construction de 'data layout' différente dans la langue de sorte que vous pouvez dire 'cette chose est de 48 bytes de long, foo se réfère aux données 13 bytes dans, et devrait être interprété et donc'; et séparée des données structurées de construire, où vous dites " je veux une structure contenant deux entiers, appelée alice et bob, et d'un flotteur appelle carol, et je n'aime pas comment la mettre en œuvre-C ces deux cas d'utilisation sont entassé dans la structure de la construction.)

-1
répondu John Allsup 2015-08-16 14:45:51