web-dev-qa-db-fra.com

Est-ce que l'attribut __attribute __ ((emballé))/#pragma de gcc est dangereux?

En C, le compilateur disposera les membres d'une structure dans l'ordre dans lequel ils ont été déclarés, avec des octets de remplissage éventuellement insérés entre les membres ou après le dernier membre, afin de garantir l'alignement correct de chaque membre.

gcc fournit une extension de langue, __attribute__((packed)), qui indique au compilateur de ne pas insérer de remplissage, ce qui permet aux membres de la structure d'être mal alignés. Par exemple, si le système requiert normalement un alignement de 4 octets sur tous les objets int, __attribute__((packed)) peut entraîner l'allocation de __ membres de structure int à des décalages impairs.

Citant la documentation gcc:

L'attribut `emballé 'spécifie qu'il s'agit d'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 le Attribut "aligné".

Évidemment, l'utilisation de cette extension peut entraîner des exigences de données moins importantes, 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é, octet à la fois.

Mais y a-t-il des cas où cela est dangereux? Le compilateur génère-t-il toujours le code correct (bien que plus lent) pour accéder aux membres mal alignés des structures condensées? Est-il même possible qu'il le fasse dans tous les cas?

136
Keith Thompson

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. tester sur des systèmes x86 ne révélera pas le problème. (Sur le x86, les accès mal alignés sont gérés matériellement; si vous déréférenciez un pointeur int* qui pointe vers une adresse impaire, ce sera un peu plus lent que s'il était correctement aligné, mais vous obtiendrez le résultat correct.)

Sur d'autres systèmes, tels que SPARC, une tentative d'accès à un objet int mal aligné provoque une erreur de bus qui bloque le programme.

Il existe également des systèmes dans lesquels un accès mal aligné ignore discrètement les bits de poids faible de l'adresse, ce qui lui permet d'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 x86 Ubuntu avec gcc 4.5.2, il produit le résultat suivant:

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

Sous SPARC Solaris 9 avec gcc 4.5.1, les éléments suivants sont générés:

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 pose pas le problème de manière fiable, car le compilateur peut allouer la structure à une adresse impaire afin que le membre x soit correctement aligné. Avec un tableau de deux objets struct foo, au moins un ou l'autre aura un membre x mal aligné.)

(Dans ce cas, p0 pointe sur une adresse mal alignée, car il pointe sur un membre int emballé suivant un membre char. p1 se trouve être correctement aligné, car il pointe sur le même membre dans le deuxième élément du tableau; deux objets char le précédant - et sous SPARC Solaris, le tableau arr semble être alloué à une adresse paire, mais pas un multiple de 4.)

Lorsqu'il fait référence 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 arr[0].x ou arr[1].x a été stockée dans un objet pointeur, ni le compilateur ni le programme en cours ne savent qu'il pointe vers un objet int mal aligné. Cela suppose simplement que l'alignement est correct, ce qui entraîne (sur certains systèmes) une erreur de bus ou une défaillance similaire.

Je pense que corriger cela dans gcc serait peu pratique. Une solution générale nécessiterait, pour chaque tentative de déréférence d'un pointeur sur un type avec des exigences d'alignement non triviales, 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 pouvant 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 résoudre ce problème, mais la documentation devrait le mentionner (ce n'est pas le cas actuellement).

UPDATE: Depuis 2018-12-20, ce bogue est marqué comme étant FIXE. Le correctif apparaîtra dans gcc 9 avec l’ajout d’une nouvelle option -Waddress-of-packed-member, activée par défaut.

Lorsque l'adresse du membre emballé de la structure ou de l'union est prise, elle peut résulte en une valeur de pointeur non alignée. Ce patch ajoute -Adresse-de-membre-emballé pour vérifier l'alignement lors de l'attribution du pointeur et avertir l'adresse non alignée ainsi que le pointeur non aligné

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

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;
      |               ^~~~~~~~~
125
Keith Thompson

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

Ce qui est not safe prend le pointeur de données non alignées et y accède sans prendre cela en 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 prévu par le compilateur. certaines plates-formes, ou à l'avenir si une nouvelle méthode est inventée pour optimiser les accès non alignés).

47
ams

Comme je l'ai dit plus haut, ne vous dirigez pas vers un membre d'une structure emballée. C'est simplement jouer avec le feu. Lorsque vous dites __attribute__((__packed__)) ou #pragma pack(1), ce que vous dites réellement 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 correctement le compilateur.

Peut-être pouvons-nous blâmer le compilateur pour sa complaisance cependant. Bien que gcc ait une option -Wcast-align, elle n’est pas activée par défaut, ni avec -Wall ni -Wextra. Ceci est apparemment dû au fait que les développeurs de gcc considèrent ce type de code comme un cerveau mort " une abomination " indigne d'adresser - un dédain compréhensible, mais cela n'aide pas lorsqu'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 a est une structure compactée (telle que définie ci-dessus). De même, b est un pointeur sur une structure compactée. Le type de l'expression a.i est (fondamentalement) un entier int l-value avec un alignement sur 1 octet. c et d sont tous deux normaux ints. Lors de la lecture de a.i, le compilateur génère du code pour un accès non aligné. Lorsque vous lisez b->i, le type de b sait toujours qu'il est emballé, alors aucun problème pour eux non plus. e est un pointeur sur un int aligné sur un octet. Le compilateur sait également comment déréférencer correctement cette référence. Mais lorsque vous faites l'affectation f = &a.i, vous stockez la valeur d'un pointeur int non aligné dans une variable de pointeur int alignée - c'est là que vous vous êtes trompé. Et je suis d'accord, cet avertissement devrait être activé pour gcc par default (même pas dans -Wall ou -Wextra).

46
Daniel Santos

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

Une chose particulière qu’elle casse est la capacité d’une union qui contient deux ou plusieurs structures d’écrire un membre et d’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 spéciale est fournie afin de simplifier l'utilisation des unions: si une union contient plusieurs structures partageant une séquence initiale commune (voir ci-dessous) et si l'objet union contient actuellement l'une de ces structures, il est permis d'inspecter la partie initiale commune de l'un d'entre eux où qu'une déclaration du type complété de l'union soit 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)
/*
...
*/

Lorsque __attribute__((packed)) est introduit, cela rompt ceci. 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;
}

Sortie:

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

Même si struct s1 et struct s2 ont une "séquence initiale commune", l'empilement appliqué à l'ancien signifie que les membres correspondants n'habitent pas au même décalage d'octet. Le résultat est que la valeur écrite pour le membre x.b n'est pas identique à la valeur lue du membre y.b, même si la norme dit qu'elles devraient être identiques.

2
dbush

(Ce qui suit est un exemple très artificiel cuit pour illustrer.) Une des utilisations majeures des structures condensées est la suivante: vous avez un flux de données (disons 256 octets) auquel vous souhaitez donner du sens. Si je prends un exemple plus petit, supposons qu'un programme fonctionnant sur mon Arduino envoie par le biais d'un port série un paquet de 16 octets ayant 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;

et puis je peux faire référence aux octets targetAddr via aStruct.targetAddr plutôt que de jouer avec l'arithmétique de pointeur.

Maintenant, avec les problèmes d'alignement, prendre un pointeur vide * en mémoire sur les données reçues et le convertir en un myStruct * ne fonctionnera pas à moins que le compilateur traite la structure comme compactée (c'est-à-dire qu'elle 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 condensé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 condensées facilitent l’écriture de programmes qui accèdent au contenu.

Sinon, vous finissez par utiliser le langage C++ et écrire une classe avec des méthodes d'accès et des fonctions permettant d'effectuer l'arithmétique de pointeur en arrière-plan. En bref, les structures empaquetées permettent de traiter efficacement des données empaquetées, et ces données peuvent être celles avec lesquelles votre programme est conçu. Dans la plupart des cas, votre code doit lire les valeurs dans la structure, les utiliser et les réécrire à la fin. Tout le reste devrait être fait en dehors de la structure compactée. Une partie du problème réside dans les éléments de bas niveau que C essaie de dissimuler au programmeur, ainsi que dans les sauts de cerceau nécessaires si cela est vraiment important pour le programmeur. (Vous avez presque besoin d'une construction différente de 'disposition des données' dans le langage pour pouvoir dire 'cette chose est longue de 48 octets, foo fait référence aux données de 13 octets, et doit être interprété de cette manière'; et une construction de données structurée séparée, où vous dites "je veux une structure contenant deux ints, alice et bob, et un float appelé carol, et je me fiche de la façon dont vous l'implémentez" - en C, ces deux cas d'utilisation sont traités dans la structure.)

0
John Allsup