Il pacchetto __attribute __ ((imballato)) / #pragma di gcc non è sicuro?


164

In C, il compilatore disporrà i membri di una struttura nell'ordine in cui sono dichiarati, con possibili byte di riempimento inseriti tra i membri o dopo l'ultimo membro, per garantire che ciascun membro sia allineato correttamente.

gcc fornisce un'estensione del linguaggio __attribute__((packed)), che dice al compilatore di non inserire il riempimento, consentendo ai membri di struct di essere disallineati. Ad esempio, se il sistema richiede normalmente che tutti gli intoggetti abbiano un allineamento a 4 byte, è __attribute__((packed))possibile che i intmembri di una struttura vengano allocati a offset dispari.

Citando la documentazione di gcc:

L'attributo "pacchetto" specifica che un campo variabile o struttura dovrebbe avere il minimo allineamento possibile - un byte per una variabile e un bit per un campo, a meno che non si specifichi un valore maggiore con l'attributo "allineato".

Ovviamente l'uso di questa estensione può comportare requisiti di dati più piccoli ma un codice più lento, poiché il compilatore deve (su alcune piattaforme) generare codice per accedere a un membro disallineato un byte alla volta.

Ma ci sono casi in cui questo non è sicuro? Il compilatore genera sempre codice corretto (anche se più lento) per accedere a membri disallineati di strutture impacchettate? È anche possibile farlo in tutti i casi?


1
Il rapporto sui bug di gcc è ora contrassegnato come FISSO con l'aggiunta di un avviso sull'assegnazione del puntatore (e un'opzione per disabilitare l'avviso). Dettagli nella mia risposta .
Keith Thompson,

Risposte:


148

Sì, __attribute__((packed))potenzialmente non è sicuro su alcuni sistemi. Il sintomo probabilmente non si presenterà su un x86, il che rende il problema più insidioso; i test sui sistemi x86 non rivelano il problema. (Sulla x86, gli accessi disallineati sono gestiti nell'hardware; se si fa riferimento a un int*puntatore che punta a un indirizzo dispari, sarà un po 'più lento rispetto a se fosse correttamente allineato, ma si otterrà il risultato corretto.)

Su alcuni altri sistemi, come SPARC, il tentativo di accedere a un intoggetto non allineato provoca un errore del bus, causando l'arresto anomalo del programma.

Ci sono stati anche sistemi in cui un accesso disallineato ignora silenziosamente i bit di ordine inferiore dell'indirizzo, causando l'accesso a un blocco di memoria errato.

Considera il seguente programma:

#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;
}

Su x86 Ubuntu con gcc 4.5.2, produce il seguente output:

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

Su SPARC Solaris 9 con gcc 4.5.1, produce quanto segue:

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

In entrambi i casi, il programma è compilato senza opzioni extra, solo gcc packed.c -o packed.

(Un programma che utilizza una singola struttura anziché una matrice non presenta in modo affidabile il problema, poiché il compilatore può allocare la struttura su un indirizzo dispari in modo che il xmembro sia correttamente allineato. Con una matrice di due struct foooggetti, almeno uno o l'altro avrà un xmembro disallineato .)

(In questo caso, p0punta a un indirizzo disallineato, perché punta a un intmembro impacchettato che segue un charmembro. p1Sembra essere allineato correttamente, poiché punta allo stesso membro nel secondo elemento dell'array, quindi ci sono due charoggetti che lo precedono - e su SPARC Solaris l'array arrsembra essere assegnato a un indirizzo che è pari, ma non un multiplo di 4.)

Quando si riferisce al membro xdi un struct foonome, il compilatore sa che xè potenzialmente disallineato e genererà un codice aggiuntivo per accedervi correttamente.

Una volta che l'indirizzo di arr[0].xo arr[1].xè stato memorizzato in un oggetto puntatore, né il compilatore né il programma in esecuzione sanno che punta a un intoggetto non allineato . Presuppone solo che sia correttamente allineato, risultando (su alcuni sistemi) in un errore del bus o altro errore simile.

Risolvere questo problema in CCG sarebbe, credo, poco pratico. Una soluzione generale richiederebbe, per ogni tentativo di dereferenziare un puntatore a qualsiasi tipo con requisiti di allineamento non banali (a) dimostrando in fase di compilazione che il puntatore non punta a un membro disallineato di una struttura impacchettata, o (b) generare codice più voluminoso e più lento in grado di gestire oggetti allineati o disallineati.

Ho inviato una segnalazione di bug gcc . Come ho detto, non credo sia pratico risolverlo, ma la documentazione dovrebbe menzionarlo (al momento non lo fa).

AGGIORNAMENTO : A partire dal 20-12-2018, questo errore è contrassegnato come FISSO. La patch apparirà in gcc 9 con l'aggiunta di una nuova -Waddress-of-packed-memberopzione, abilitata di default.

Quando viene preso l'indirizzo del membro impacchettato di struct o union, può risultare in un valore puntatore non allineato. Questa patch aggiunge -Waddress-of-pacchetto-member per verificare l'allineamento durante l'assegnazione del puntatore e avvisare l'indirizzo non allineato e il puntatore non allineato

Ho appena creato quella versione di gcc dal sorgente. Per il programma sopra, produce questi diagnostici:

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;
      |               ^~~~~~~~~

1
è potenzialmente disallineato e genererà ... cosa?
Almo,

5
gli elementi struct disallineati su ARM fanno cose strane: alcuni accessi causano errori, altri fanno sì che i dati recuperati vengano riorganizzati in modo controintuitivo o incorporino dati imprevisti adiacenti.
Wallyk,

8
Sembra che l'imballaggio stesso sia sicuro, ma il modo in cui vengono utilizzati i membri impacchettati può non essere sicuro. Le precedenti CPU basate su ARM non supportavano accessi di memoria non allineati, le versioni più recenti lo fanno, ma so che Symbian OS non consente ancora accessi non allineati quando è in esecuzione su queste versioni più recenti (il supporto è disattivato).
James,

14
Un altro modo per risolverlo all'interno di gcc sarebbe usare il sistema dei tipi: richiedere che i puntatori ai membri delle strutture impacchettate possano essere assegnati solo ai puntatori che sono essi stessi contrassegnati come impacchettati (cioè potenzialmente non allineati). Ma davvero: strutture piene, basta dire di no.
caf

9
@ Flavio: il mio scopo principale era quello di ottenere le informazioni là fuori. Vedi anche meta.stackexchange.com/questions/17463/…
Keith Thompson,

62

Come ho detto sopra, non puntare su un membro di una struttura che è piena. Questo è semplicemente giocare con il fuoco. Quando dici __attribute__((__packed__))o #pragma pack(1), quello che stai veramente dicendo è "Ehi gcc, so davvero cosa sto facendo." Quando si scopre di no, non si può incolpare giustamente il compilatore.

Forse possiamo incolpare il compilatore per la sua compiacenza però. Mentre gcc ha a -Wcast-align un'opzione, non è abilitato di default né con -Wallo -Wextra. Ciò è apparentemente dovuto agli sviluppatori di gcc che considerano questo tipo di codice come un " abominio " cerebrale indegno di indirizzo - comprensibile disprezzo, ma non aiuta quando un programmatore inesperto vi si imbatte.

Considera quanto segue:

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;

Qui, il tipo di aè una struttura impacchettata (come definito sopra). Allo stesso modo, bè un puntatore a una struttura affollata. Il tipo di espressione a.iè (sostanzialmente) un valore int l con allineamento a 1 byte. ce dsono entrambi normali int. Durante la lettura a.i, il compilatore genera codice per l'accesso non allineato. Quando leggi l'b->i , bil tipo sa ancora che è pieno, quindi nessun problema neanche loro. eè un puntatore a un int allineato a un byte, quindi anche il compilatore sa come dedurlo correttamente. Ma quando esegui l'assegnazione f = &a.i, stai memorizzando il valore di un puntatore int non allineato in una variabile puntatore int allineato - è lì che hai sbagliato. E sono d'accordo, gcc dovrebbe avere questo avviso abilitato daimpostazione predefinita (nemmeno in -Wallo -Wextra).


6
+1 per spiegare come usare i puntatori con strutture non allineate!
Soumya,

@Soumya Grazie per i punti! :) Tieni presente tuttavia che si __attribute__((aligned(1)))tratta di un'estensione gcc e non è portatile. Per quanto ne sappia, l'unico modo davvero portatile per eseguire l'accesso non allineato in C (con qualsiasi combinazione compilatore / hardware) è con una copia di memoria byte-byte (memcpy o simile). Alcuni hardware non hanno nemmeno istruzioni per l'accesso non allineato. La mia esperienza è con arm e x86 che possono fare entrambe le cose, anche se l'accesso non allineato è più lento. Quindi, se mai dovessi farlo con prestazioni elevate, dovrai annusare l'hardware e usare trucchi specifici per l'arch.
Daniel Santos,

4
@Soumya Purtroppo, __attribute__((aligned(x)))ora sembra essere ignorato quando utilizzato per i puntatori. :( Non ho ancora i dettagli completi di questo, ma usare __builtin_assume_aligned(ptr, align)sembra ottenere gcc per generare il codice corretto. Quando avrò una risposta più concisa (e spero una segnalazione di bug) aggiornerò la mia risposta.
Daniel Santos

@DanielSantos: un compilatore di qualità che uso (Keil) riconosce qualificatori "compressi" per i puntatori; se una struttura viene dichiarata "imballata", prendendo l'indirizzo di un uint32_tmembro si otterrà un uint32_t packed*; tentando di leggere da un tale puntatore su un Cortex-M0, ad esempio, IIRC chiamerà una subroutine che impiegherà ~ 7x tanto quanto una lettura normale se il puntatore non è allineato o ~ 3x se è allineato, ma si comporterà in modo prevedibile in entrambi i casi [il codice in linea richiederebbe 5 volte il tempo se allineato o non allineato].
supercat


49

È perfettamente sicuro purché si acceda sempre ai valori tramite la struttura tramite il .(punto) o-> notazione.

Cosa no è sicuro è prendere il puntatore di dati non allineati e quindi accedervi senza tenerne conto.

Inoltre, anche se è noto che ogni elemento nella struttura non è allineato, è noto che non è allineato in un modo particolare , quindi la struttura nel suo insieme deve essere allineata come previsto dal compilatore o ci saranno problemi (su alcune piattaforme, o in futuro se verrà inventato un nuovo modo per ottimizzare gli accessi non allineati).


Hmm, mi chiedo cosa succede se si inserisce una struttura imballata all'interno di un'altra struttura imballata in cui l'allineamento sarebbe diverso? Domanda interessante, ma non dovrebbe cambiare la risposta.
AMS

Anche GCC non allineerà sempre la struttura stessa. Ad esempio: struct foo {int x; char c; } __attribute __ ((imballato)); struct bar {carattere c; struct foo f; }; Ho scoperto che bar :: f :: x non sarà necessariamente allineato, almeno su alcuni gusti di MIPS.
Anton,

3
@antonm: Sì, una struttura all'interno di una struttura impaccata potrebbe non essere allineata, ma, di nuovo, il compilatore sa quale sia l'allineamento di ciascun campo ed è perfettamente sicuro finché non si tenta di utilizzare i puntatori nella struttura. Dovresti immaginare una struttura all'interno di una struttura come una serie piatta di campi, con il nome extra solo per leggibilità.
ems

6

L'uso di questo attributo non è sicuro.

Una cosa particolare che rompe è la capacità di un unionche contiene due o più strutture di scrivere un membro e di leggerne un altro se le strutture hanno una sequenza iniziale comune di membri. La sezione 6.5.2.3 della norma C11 afferma:

6 Viene fornita una garanzia speciale per semplificare l'uso dei sindacati: se un'unione contiene più strutture che condividono una sequenza iniziale comune (vedi sotto) e se l'oggetto unione contiene attualmente una di queste strutture, è consentito ispezionare parte iniziale comune di una qualsiasi di esse ovunque sia visibile una dichiarazione del tipo completo di unione. Due strutture condividono una sequenza iniziale comune se i membri corrispondenti hanno tipi compatibili (e, per i campi di bit, le stesse larghezze) per una sequenza di uno o più membri iniziali.

...

9 ESEMPIO 3 Di seguito è riportato un frammento valido:

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

Quando __attribute__((packed))viene introdotto si rompe questo. L'esempio seguente è stato eseguito su Ubuntu 16.04 x64 usando gcc 5.4.0 con ottimizzazioni disabilitate:

#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;
}

Produzione:

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

Anche se struct s1e struct s2hanno una "sequenza iniziale comune", l'imballaggio applicato al primo significa che i membri corrispondenti non vivono allo stesso offset di byte. Il risultato è che il valore scritto sul membro x.bnon è lo stesso del valore letto dal membro y.b, anche se lo standard dice che dovrebbero essere uguali.


Si potrebbe sostenere che se si impacchettano una delle strutture e non l'altra, non ci si aspetta che abbiano layout coerenti. Sì, questo è un altro requisito standard che può violare.
Keith Thompson,

1

(Di seguito è riportato un esempio molto artificiale elaborato per illustrare.) Un uso importante di strutture impaccate è dove si dispone di un flusso di dati (diciamo 256 byte) a cui si desidera fornire un significato. Se faccio un esempio più piccolo, supponiamo di avere un programma in esecuzione sul mio Arduino che invia via seriale un pacchetto di 16 byte che ha il seguente significato:

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

Quindi posso dichiarare qualcosa del genere

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

e quindi posso fare riferimento ai byte targetAddr tramite aStruct.targetAddr anziché armeggiare con l'aritmetica del puntatore.

Ora con le cose di allineamento in corso, prendere un puntatore void * in memoria per i dati ricevuti e trasmetterlo a un myStruct * non funzionerà a meno che il compilatore non tratti la struttura come compresso (ovvero, memorizza i dati nell'ordine specificato e usa esattamente 16 byte per questo esempio). Ci sono delle penalità prestazionali per letture non allineate, quindi l'uso di strutture impacchettate per i dati con cui il tuo programma sta lavorando attivamente non è necessariamente una buona idea. Ma quando al tuo programma viene fornito un elenco di byte, le strutture impacchettate semplificano la scrittura di programmi che accedono ai contenuti.

Altrimenti si finisce per usare C ++ e scrivere una classe con metodi di accesso e cose che fanno l'aritmetica del puntatore dietro le quinte. In breve, le strutture impacchettate servono per gestire in modo efficiente i dati impaccati e i dati impaccati potrebbero essere ciò con cui il tuo programma è stato progettato per funzionare. Per la maggior parte, il codice dovrebbe leggere i valori fuori dalla struttura, lavorare con loro e riscriverli al termine. Tutto il resto dovrebbe essere fatto al di fuori della struttura imballata. Parte del problema è la roba di basso livello che C cerca di nascondere al programmatore e il salto del cerchio che è necessario se tali cose contano davvero per il programmatore. (Hai quasi bisogno di un diverso costrutto "layout di dati" nella lingua in modo da poter dire "questa cosa è lunga 48 byte, foo si riferisce ai dati 13 byte in, e dovrebbe essere interpretato così"; e un costrutto separato di dati strutturati,


A meno che non mi manchi qualcosa, questo non risponde alla domanda. Sostieni che l'imballaggio della struttura sia conveniente (quale è), ma non affronti la questione se sia sicuro. Inoltre, affermi che le penalità di prestazione per letture non allineate; questo è vero per x86, ma non per tutti i sistemi, come ho dimostrato nella mia risposta.
Keith Thompson,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.