Czy atrybut gcc ((packed)) / # pragma pack jest niebezpieczny?

W języku C kompilator będzie układał elementy struktury w kolejności, w jakiej zostały zadeklarowane, z ewentualnymi bajtami wypełnienia wstawionymi między elementami lub po ostatnim członie, aby upewnić się, że każdy element jest prawidłowo wyrównany.

Gcc dostarcza rozszerzenie języka, __attribute__((packed)), które mówi kompilatorowi, aby nie wstawiał wypełnienia, co pozwala na niewspółosiowość elementów struktury. Na przykład, jeśli system normalnie wymaga, aby wszystkie obiekty int miały wyrównanie 4-bajtowe, __attribute__((packed)) może spowodować int Struktura członków, które mają być przydzielone przy nieparzystych przesunięciach.

Cytowanie dokumentacji gcc:

Atrybut "spakowany" określa, że pole zmiennej lub struktury powinien mieć jak najmniejsze wyrównanie--jeden bajt dla zmiennej, i jeden bit dla pola, chyba że podasz większą wartość za pomocą atrybut 'aligned'.

Oczywiście użycie tego rozszerzenia może spowodować mniejsze wymagania dotyczące danych, ale wolniejszy Kod, jak musi kompilator (na niektórych platformy) generują kod, aby uzyskać dostęp do nieprawidłowo ustawionego członka bajtu na raz.

Ale czy są jakieś przypadki, w których jest to niebezpieczne? Czy kompilator zawsze generuje poprawny (choć wolniejszy) kod, aby uzyskać dostęp do niewspółosiowych elementów spakowanych struktur? Czy jest to w ogóle możliwe, aby to zrobić we wszystkich przypadkach?

5 answers

Tak, __attribute__((packed)) jest potencjalnie niebezpieczne w niektórych systemach. Objaw prawdopodobnie nie pojawi się na x86, co tylko sprawia, że problem jest bardziej podstępny; testy na systemach x86 nie ujawnią problemu. (Na x86 niepoprawne dostępy są obsługiwane w sprzęcie; jeśli dereferujesz wskaźnik int*, który wskazuje na nieparzysty adres, będzie on nieco wolniejszy niż gdyby był prawidłowo wyrównany, ale otrzymasz poprawny wynik.)

Na niektórych innych systemach, takich jak SPARC, próbujących uzyskać dostęp do misaligned int object powoduje błąd magistrali, awarię programu.

Były również systemy, w których niewłaściwy dostęp ignoruje bity niskiego rzędu adresu, powodując, że dostęp do niewłaściwej części pamięci.

Rozważ następujący program:

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

Na x86 Ubuntu z gcc 4.5.2, generuje następujące wyjście:

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

Na SPARC Solaris 9 z gcc 4.5.1 generuje:

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

W obu przypadkach program jest skompilowany bez dodatkowych opcji, po prostu gcc packed.c -o packed.

(program, który używa pojedynczej struktury zamiast tablicy, nie wykazuje problemu, ponieważ kompilator może przydzielić strukturę pod nieparzystym adresem, aby element x był odpowiednio wyrównany. Jeśli tablica składa się z dwóch obiektów struct foo, co najmniej jeden lub drugi będzie miał nieprawidłowo ustawiony element x.)

(w tym przypadku, p0 wskazuje na niewłaściwy adres, ponieważ wskazuje na spakowany int członek po char członek. p1 zdarza się, że jest poprawnie wyrównana, ponieważ wskazuje na ten sam element w drugim elemencie tablicy, więc istnieją dwa obiekty char poprzedzające ją -- i na SPARC Solaris tablica arr wydaje się być przydzielona pod adresem parzystym, ale nie wielokrotnością 4.)

Gdy odwołuje się do członu x struct foo po nazwie, kompilator wie, że x jest potencjalnie źle dopasowany i wygeneruje dodatkowy kod, aby uzyskać do niego poprawny dostęp.

Raz adres arr[0].x lub arr[1].x został zapisany w obiekcie wskaźnika, ani kompilator, ani uruchomiony program nie wiedzą, że wskazuje na niewłaściwie dopasowany obiekt int. Zakłada tylko, że jest prawidłowo wyrównany, co powoduje (w niektórych systemach) błąd magistrali lub podobną inną awarię.

Naprawienie tego w gcc byłoby, moim zdaniem, niepraktyczne. Ogólne rozwiązanie wymagałoby, dla każdej próby dereferencji wskaźnik do dowolnego typu z nietrywialnych wymagań wyrównania albo (a) udowodnienie w czasie kompilacji, że wskaźnik nie wskazuje na nieprawidłowy element spakowanej struktury ani (b) generuje większy i wolniejszy kod, który może obsługiwać obiekty wyrównane lub wyrównane.

Wysłałem zgłoszenie błędu gcc . Jak powiedziałem, nie sądzę, aby to było praktyczne, aby to naprawić, ale dokumentacja powinna o tym wspomnieć(obecnie nie ma).

Aktualizacja: od 2018-12-20 ten błąd jest oznaczony jako naprawiony. Patch pojawi się w gcc 9 z dodaniem nowej opcji -Waddress-of-packed-member, włączonej domyślnie.

Gdy adres spakowanego członka struct lub union jest brany, może wynikiem jest niepodpisana wartość wskaźnika. Ten patch dodaje - Waddress-of-packed-member to check alignment at pointer assignment and warn unaligned address as well as unaligned pointer

Właśnie zbudowałem tę wersję gcc ze źródła. Dla powyższego programu, produkuje te diagnostyki:

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;
      |               ^~~~~~~~~
 154
Author: Keith Thompson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-12-21 06:42:46

Jak wspomniałem powyżej, nie bierz wskaźnika do członka struktury, która jest spakowana. To po prostu igraszki z ogniem. Kiedy mówisz __attribute__((__packed__)) lub #pragma pack(1), tak naprawdę mówisz: "Hej gcc, naprawdę wiem, co robię."Kiedy okazuje się, że nie, nie można słusznie winić kompilatora.

Być może możemy obwinić kompilator za jego samozadowolenie. Chociaż gcc ma opcję -Wcast-align, nie jest ona domyślnie włączona ani przez -Wall ani -Wextra. Wynika to najwyraźniej z gcc Programiści uważający ten typ kodu za martwy mózg "obrzydliwość " niegodny adresowania-zrozumiała pogarda, ale to nie pomaga, gdy niedoświadczony programista wpada w niego.

Rozważ co następuje:

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;

Tutaj typ a jest strukturą spakowaną (jak zdefiniowano powyżej). Podobnie, b jest wskaźnikiem do spakowanej struktury. Typ wyrażenia a.i jest (zasadniczo) wartością int l-value z wyrównaniem 1 bajtów. c i d oba są normalne ints. podczas odczytu a.i kompilator generuje kod dla dostępu bez przypisania. Kiedy czytasz b->i, b'typ s nadal wie, że jest spakowany, więc nie ma problemu. e jest wskaźnikiem do jednobajtowej int wyrównanej, więc kompilator wie, jak również poprawnie dereferować. Ale kiedy wykonujesz przypisanie f = &a.i, przechowujesz wartość niepalowanego wskaźnika int w wyrównanej zmiennej wskaźnika int -- to miejsce, w którym się pomyliłeś. I zgadzam się, gcc powinno mieć to warning enabled by default (not even in -Wall or -Wextra).

 63
Author: Daniel Santos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-05-05 03:21:26

Jest to całkowicie bezpieczne, o ile zawsze uzyskujesz dostęp do wartości za pośrednictwem struktury za pomocą notacji . (kropka) lub ->.

To, co Nie jest bezpieczne, to wzięcie wskaźnika niepodpisanych danych, a następnie uzyskanie do nich dostępu bez brania tego pod uwagę.

Również, nawet jeśli każdy element struktury jest znany jako niepodpisany, jest znany jako niepodpisany w określony sposób , więc struktura jako całość musi być wyrównana zgodnie z oczekiwaniami kompilatora, inaczej będą problemy (na niektórych przykładach platformy, lub w przyszłości, jeśli zostanie wymyślony nowy sposób optymalizacji niezaleganych dostępów).

 49
Author: ams,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-20 10:53:12

Używanie tego atrybutu jest zdecydowanie niebezpieczne.

Jedną z szczególnych rzeczy, które łamie, jest zdolność union, która zawiera dwie lub więcej struktur do zapisu jednego członu i odczytu drugiego, jeśli struktury mają wspólną początkową sekwencję członów. Sekcja 6.5.2.3 normy C11 stwierdza:

6 w celu uproszczenia korzystania ze związków: jeśli związek zawiera kilka struktur, które dzielą wspólną Sekwencja początkowa (patrz niżej), a jeśli celem Unii obecnie zawiera jedną z tych struktur, dozwolone jest aby sprawdzić wspólną początkową część dowolnego z nich w dowolnym miejscu, które a widoczna jest deklaracja wypełnionego typu Unii. Tw o struktury mają wspólną sekwencję początkową, jeśli odpowiada Członkowie mają kompatybilne typy (i, dla pól bitowych, te same szerokości) dla sekwencji jednego lub więcej członów początkowych.

...

9 Przykład 3 Poniżej znajduje się poprawny fragment:

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

Kiedy __attribute__((packed)) jest wprowadzony, łamie to. Poniższy przykład został uruchomiony na Ubuntu 16.04 x64 przy użyciu gcc 5.4.0 z wyłączonymi optymalizacjami:

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

Wyjście:

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

Mimo że struct s1 i struct s2 mają "wspólną sekwencję początkową", opakowanie zastosowane do pierwszego oznacza, że odpowiednie elementy nie żyją w tym samym offsecie bajtowym. Wynik jest taki, że wartość zapisana do elementu x.b nie jest taka sama jak wartość odczytywana z member y.b, mimo że standard mówi, że powinny być takie same.

 6
Author: dbush,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-04-02 18:43:46

(poniższy przykład jest bardzo sztucznym przykładem do zilustrowania.) Jednym z głównych zastosowań spakowanych struktur jest tam, gdzie masz strumień danych (powiedzmy 256 bajtów), do którego chcesz podać znaczenie. Jeśli wezmę mniejszy przykład, załóżmy, że mam program uruchomiony na moim Arduino, który wysyła szeregowo pakiet 16 bajtów, które mają następujące znaczenie:

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

Wtedy mogę zadeklarować coś w rodzaju

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

I wtedy mogę odwołać się do bajtów targetAddr poprzez aStruct.targetAddr niż majstrować przy arytmetyce wskaźników.

Teraz, gdy dzieje się wyrównanie, pobranie wskaźnika void* w pamięci do odebranych danych i wysłanie go do myStruct* nie zadziała chyba że kompilator traktuje strukturę jako spakowaną (tzn. przechowuje dane w podanej kolejności i używa dokładnie 16 bajtów dla tego przykładu). Istnieją kary wydajnościowe za niezaliczone odczyty, więc używanie spakowanych struktur danych, z którymi aktywnie współpracuje Twój program, niekoniecznie jest dobrym pomysłem. Ale gdy program jest dostarczany z listą bajtów, spakowane struktury ułatwiają pisanie programów, które mają dostęp do zawartości.

W przeciwnym razie skończysz używając C++ i pisząc klasę z metodami accessor i takimi tam, które robią arytmetykę wskaźników za kulisami. Krótko mówiąc, spakowane struktury służą do efektywnego radzenia sobie z spakowanymi danymi, a spakowane dane mogą być tym, z czym dany jest program do pracy. W większości przypadków kod powinien odczytywać wartości ze struktury, pracować z nimi i pisać wróciły, kiedy skończyły. Wszystko inne powinno być wykonane poza spakowaną strukturą. Częścią problemu są rzeczy niskiego poziomu, które C próbuje ukryć przed programistą, i skoki obręczy, które są potrzebne, jeśli takie rzeczy naprawdę mają dla programisty znaczenie. (Prawie potrzebujesz innego "układu danych" w języku, aby można było powiedzieć "to coś ma 48 bajtów, foo odnosi się do danych 13 bajtów i powinno być interpretowane w ten sposób"; i oddzielny strukturalny konstrukt danych, gdzie mówisz " chcę struktura zawierająca dwa inty, o nazwie alice i bob, oraz float o nazwie carol, i nie obchodzi mnie jak to zaimplementujesz ' -- w C oba te przypadki użycia są przypisane do struktury struct.)

 1
Author: John Allsup,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-08-16 14:45:51