Czy pakiet __attribute __ gcc ((zapakowany)) / #pragma pack jest niebezpieczny?

164

W C kompilator rozłoży elementy składowe struktury w kolejności, w jakiej są zadeklarowane, z możliwymi bajtami wypełniającymi wstawionymi między składowymi lub po ostatnim składniku, aby upewnić się, że każdy element członkowski jest prawidłowo wyrównany.

gcc udostępnia rozszerzenie języka __attribute__((packed)), które mówi kompilatorowi, aby nie wstawiał dopełnienia, co pozwala na niewłaściwe wyrównanie elementów struktur. Na przykład, jeśli system normalnie wymaga, aby wszystkie intobiekty miały wyrównanie 4-bajtowe, __attribute__((packed))może spowodować intprzydzielenie elementów strukturalnych z nieparzystymi przesunięciami.

Cytując dokumentację gcc:

Atrybut `` spakowany '' określa, że ​​zmienna lub pole struktury powinno mieć najmniejsze możliwe wyrównanie - jeden bajt na zmienną i jeden bit na pole, chyba że określisz większą wartość za pomocą atrybutu wyrównanego.

Oczywiście użycie tego rozszerzenia może skutkować mniejszymi wymaganiami dotyczącymi danych, ale wolniejszym kodem, ponieważ kompilator musi (na niektórych platformach) wygenerować kod, aby uzyskać dostęp do niewyrównanego elementu członkowskiego po bajcie.

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

Keith Thompson
źródło
1
Raport błędu gcc jest teraz oznaczony jako FIXED z dodatkiem ostrzeżenia przy przypisaniu wskaźnika (i opcji wyłączenia ostrzeżenia). Szczegóły w mojej odpowiedzi .
Keith Thompson,

Odpowiedzi:

148

Tak, __attribute__((packed))jest potencjalnie niebezpieczny w niektórych systemach. Symptom prawdopodobnie nie pojawi się na x86, co tylko czyni problem bardziej podstępnym; testowanie na systemach x86 nie ujawni problemu. (Na x86 niedopasowane dostępy są obsługiwane sprzętowo; jeśli wyłuskujesz int*wskaźnik wskazujący na nieparzysty adres, będzie on trochę wolniejszy niż gdyby był odpowiednio wyrównany, ale otrzymasz poprawny wynik).

W niektórych innych systemach, takich jak SPARC, próba uzyskania dostępu do źle wyrównanego intobiektu powoduje błąd magistrali, awarię programu.

Były również systemy, w których niedopasowany dostęp po cichu ignoruje najmniej znaczące bity adresu, powodując dostęp do niewłaściwego fragmentu 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ący wynik:

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 daje to:

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 kompilowany bez dodatkowych opcji gcc packed.c -o packed.

(Program, który używa pojedynczej struktury, a nie tablicy, nie przedstawia problemu w wiarygodny sposób, ponieważ kompilator może przydzielić strukturę pod nieparzystym adresem, aby element xczłonkowski był odpowiednio wyrównany. Z tablicą dwóch struct fooobiektów, co najmniej jednego lub drugiego będzie mieć niewyrównanego xczłonka).

(W tym przypadku p0wskazuje na nieprawidłowo wyrównany adres, ponieważ wskazuje na upakowany intelement członkowski następujący po charelemencie. p1Zdarza się, że jest prawidłowo wyrównany, ponieważ wskazuje na ten sam element w drugim elemencie tablicy, więc charpoprzedzają go dwa obiekty - aw SPARC Solaris tablica arrwydaje się być przydzielona pod adresem parzystym, ale nie wielokrotnym 4.)

Odnosząc się do członka xUrządzony struct foowedług nazwy, kompilator wie, że xjest potencjalnie niewyrównane i wygeneruje dodatkowy kod dostępu do niego prawidłowo.

Gdy adres arr[0].xlub arr[1].xzostał zapisany w obiekcie wskaźnika, ani kompilator, ani uruchomiony program nie wiedzą, że wskazuje na nieprawidłowo wyrównany intobiekt. Zakłada po prostu, że jest prawidłowo wyrównany, co powoduje (w niektórych systemach) błąd magistrali lub podobną inną awarię.

Uważam, że naprawienie tego w gcc byłoby niepraktyczne. Ogólne rozwiązanie wymagałoby, aby dla każdej próby wyłuskiwania wskaźnika do dowolnego typu z nietrywialnymi wymaganiami dotyczącymi wyrównania albo (a) udowodnić w czasie kompilacji, że wskaźnik nie wskazuje na nieprawidłowo wyrównany element składowy spakowanej struktury lub (b) generowanie obszerniejszego i wolniejszego kodu, który może obsługiwać wyrównane lub źle wyrównane obiekty.

Mam przedstawiła raport gcc błędzie . Jak powiedziałem, nie uważam, aby naprawianie tego było praktyczne, ale dokumentacja powinna o tym wspominać (obecnie tak nie jest).

AKTUALIZACJA : od 20.12.2018 ten błąd jest oznaczony jako NAPRAWIONY. Łatka pojawi się w gcc 9 z dodatkiem nowej -Waddress-of-packed-memberopcji, domyślnie włączonej.

Kiedy brany jest adres spakowanego członka struktury lub unii, może to skutkować niewyrównaną wartością wskaźnika. Ta poprawka dodaje -Waddress-of-spakowany-element członkowski, aby sprawdzić wyrównanie przy przypisaniu wskaźnika i ostrzec nie wyrównany adres, jak również niewyrównany wskaźnik

Właśnie zbudowałem tę wersję gcc ze źródeł. Dla powyższego programu generuje następujące informacje diagnostyczne:

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
źródło
1
jest potencjalnie niewspółosiowy i wygeneruje ... co?
Almo
5
niewspółosiowe elementy strukturalne w ARM robią dziwne rzeczy: niektóre dostępy powodują błędy, inne powodują, że pobrane dane są przestawiane wbrew intuicji lub włączają sąsiednie nieoczekiwane dane.
wallyk
8
Wydaje się, że samo pakowanie jest bezpieczne, ale sposób wykorzystania zapakowanych elementów może być niebezpieczny. Starsze procesory oparte na architekturze ARM również nie obsługiwały niewyrównanego dostępu do pamięci, nowsze wersje obsługują, ale wiem, że system operacyjny Symbian nadal nie zezwala na dostęp bez wyrównania podczas pracy na nowszych wersjach (obsługa jest wyłączona).
James,
14
Innym sposobem naprawienia tego w gcc byłoby użycie systemu typów: wymagaj, aby wskaźniki do elementów składowych spakowanych struktur mogły być przypisane tylko do wskaźników, które same są oznaczone jako spakowane (tj. Potencjalnie niewyrównane). Ale tak naprawdę: zapakowane struktury, po prostu powiedz nie.
kawiarnia
9
@Flavius: Moim głównym celem było zdobycie informacji. Zobacz także meta.stackexchange.com/questions/17463/…
Keith Thompson,
62

Jak wspomniano powyżej, nie bierz wskaźnika do elementu składowego struktury, która jest spakowana. To po prostu igranie z ogniem. Kiedy mówisz __attribute__((__packed__))lub #pragma pack(1), tak naprawdę mówisz: „Hej gcc, naprawdę wiem, co robię”. Kiedy okaże się, że tego nie robisz, nie możesz słusznie winić kompilatora.

Być może jednak możemy winić kompilator za jego samozadowolenie. Chociaż gcc ma -Wcast-alignopcję, nie jest ona domyślnie włączona ani za pomocą -Walllub -Wextra. Wynika to najwyraźniej z tego, że programiści gcc uważają ten typ kodu za martwą " obrzydliwość ", której nie warto się zajmować - zrozumiała pogarda, ale nie pomaga to, gdy wpada na niego niedoświadczony programista.

Rozważ następujące:

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 ajest strukturą spakowaną (jak zdefiniowano powyżej). Podobnie bjest wskaźnikiem do zapakowanej struktury. Typ wyrażenia a.ito (w zasadzie) int l-wartość z wyrównaniem 1-bajtowym. ci doba są normalne int. Podczas czytania a.ikompilator generuje kod dla niewyrównanego dostępu. Kiedy czytasz b->i, ten btyp nadal wie, że jest zapakowany, więc nie ma problemu z ich też. ejest wskaźnikiem do jednobajtowego int wyrównanego do jednego bajtu, więc kompilator wie, jak to poprawnie wyłuskać. Ale kiedy wykonujesz przypisanie f = &a.i, przechowujesz wartość niewyrównanego wskaźnika int w wyrównanej zmiennej wskaźnika int - to jest miejsce, w którym poszło źle. I zgadzam się, gcc powinno mieć włączone to ostrzeżenie przezdomyślnie (nawet nie w -Walllub -Wextra).

Daniel Santos
źródło
6
+1 za wyjaśnienie, jak używać wskaźników z niewyrównanymi strukturami!
Soumya
@Soumya Dzięki za punkty! :) Pamiętaj jednak, że __attribute__((aligned(1)))jest to rozszerzenie gcc i nie jest przenośne. O ile mi wiadomo, jedynym naprawdę przenośnym sposobem wykonania niewyrównanego dostępu w C (z dowolną kombinacją kompilatora / sprzętu) jest bajtowa kopia pamięci (memcpy lub podobna). Niektóre urządzenia nie mają nawet instrukcji dotyczących niewyrównanego dostępu. Moje doświadczenie dotyczy arm i x86, które mogą robić jedno i drugie, chociaż dostęp bez wyrównania jest wolniejszy. Więc jeśli kiedykolwiek będziesz musiał to zrobić z wysoką wydajnością, będziesz musiał węszyć sprzęt i użyć sztuczek specyficznych dla łuków.
Daniel Santos
4
@Soumya Niestety, __attribute__((aligned(x)))teraz wydaje się być ignorowany, gdy jest używany jako wskaźniki. :( Nie mam jeszcze pełnych szczegółów na ten temat, ale __builtin_assume_aligned(ptr, align)wydaje się , że użycie gcc do wygenerowania poprawnego kodu. Kiedy otrzymam bardziej zwięzłą odpowiedź (i mam nadzieję, że raport o błędzie), zaktualizuję swoją odpowiedź.
Daniel Santos
@DanielSantos: Wysokiej jakości kompilator, którego używam (Keil) rozpoznaje „spakowane” kwalifikatory dla wskaźników; jeśli struktura jest zadeklarowana jako „zapakowana”, pobranie adresu uint32_tczłonka da uint32_t packed*; próba odczytania z takiego wskaźnika np. na Cortex-M0 spowoduje wywołanie przez IIRC podprogramu, który zajmie ~ 7x tak długo, jak normalny odczyt, jeśli wskaźnik jest niewyrównany lub ~ 3x dłuższy, jeśli jest wyrównany, ale będzie zachowywał się przewidywalnie w każdym przypadku [kod wbudowany zająłby 5 razy więcej czasu, niezależnie od tego, czy jest wyrównany, czy nie].
supercat
49

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

To, co nie jest bezpieczne, to branie wskaźnika na niewyrównane dane, a następnie uzyskiwanie do nich dostępu bez uwzględnienia tego.

Ponadto, nawet jeśli wiadomo, że każdy element w strukturze jest niewyrównany, wiadomo, że jest niewyrównany w określony sposób , więc struktura jako całość musi być wyrównana zgodnie z oczekiwaniami kompilatora lub wystąpią problemy (na niektórych platformach lub w przyszłości, jeśli zostanie wynaleziony nowy sposób optymalizacji niewyrównanych dostępów).

ams
źródło
Hmm, zastanawiam się, co się stanie, jeśli umieścisz jedną spakowaną strukturę wewnątrz innej spakowanej struktury, w której wyrównanie będzie inne? Ciekawe pytanie, ale nie powinno to zmieniać odpowiedzi.
rano
GCC nie zawsze też dopasuje samą strukturę. Na przykład: struct foo {int x; char c; } __attribute __ ((zapakowane)); struct bar {char c; struct foo f; }; Odkryłem, że bar :: f :: x niekoniecznie będzie wyrównany, przynajmniej w niektórych smakach MIPS.
Anton
3
@antonm: Tak, struktura w spakowanej strukturze może być niewyrównana, ale ponownie kompilator wie, jakie jest wyrównanie każdego pola i jest całkowicie bezpieczny, o ile nie próbujesz używać wskaźników do struktury. Powinieneś wyobrazić sobie strukturę w strukturze jako jedną płaską serię pól, z dodatkową nazwą tylko dla czytelności.
AMS
6

Używanie tego atrybutu jest zdecydowanie niebezpieczne.

Jedną z rzeczy, które niszczy, jest zdolność elementu unionzawierającego dwie lub więcej struktur do zapisania jednego elementu członkowskiego i odczytania innego, jeśli struktury mają wspólną początkową sekwencję elementów członkowskich. Sekcja 6.5.2.3 normy C11 mówi:

6 Jedna specjalna gwarancja ma na celu uproszczenie korzystania ze związków: jeśli związek zawiera kilka struktur, które mają wspólną sekwencję początkową (patrz poniżej) i jeśli obiekt unii zawiera obecnie jedną z tych struktur, dozwolone jest sprawdzenie wspólna początkowa część któregokolwiek z nich wszędzie tam, gdzie jest widoczna deklaracja ukończonego typu połączenia. Dwie struktury mają wspólną sekwencję początkową, jeśli odpowiadające jej elementy członkowskie mają zgodne typy (i, w przypadku pól bitowych, te same szerokości) dla sekwencji jednego lub więcej początkowych elementów członkowskich.

...

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 wprowadzany, to zrywa to. Poniższy przykład został uruchomiony w systemie 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;
}

Wynik:

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

Chociaż struct s1i struct s2mają „wspólną początkową sekwencję”, opakowanie zastosowane do dawnych oznacza, że odpowiadające członkowie nie mieszkają w tym samym bajcie offsetu. Wynik jest taki, że wartość zapisana elementowi x.bnie jest taka sama, jak wartość odczytana z elementu członkowskiego y.b, mimo że norma mówi, że powinny być takie same.

dbush
źródło
Ktoś mógłby argumentować, że jeśli zapakujesz jedną ze struktur, a nie drugą, nie będziesz oczekiwać, że będą miały spójne układy. Ale tak, to kolejny standardowy wymóg, który może naruszać.
Keith Thompson
1

(Poniższy przykład jest bardzo sztucznym przykładem przygotowanym do zilustrowania). Jednym z głównych zastosowań upakowanych struktur jest to, że masz strumień danych (powiedzmy 256 bajtów), którym chcesz nadać znaczenie. Jeśli wezmę mniejszy przykład, przypuść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ś takiego

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

a następnie mogę odwołać się do bajtów targetAddr za pośrednictwem aStruct.targetAddr zamiast bawić się arytmetyką wskaźnika.

Teraz, gdy dzieje się wyrównanie, pobranie wskaźnika void * w pamięci do odebranych danych i przesłanie go do myStruct * nie zadziała, chyba że kompilator potraktuje strukturę jako spakowaną (to znaczy przechowuje dane w określonej kolejności i używa dokładnie 16 bajtów w tym przykładzie). Istnieją ograniczenia wydajności w przypadku niewyrównanych odczytów, więc używanie spakowanych struktur dla danych, z którymi aktywnie pracuje program, niekoniecznie jest dobrym pomysłem. Ale kiedy 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 użyjesz C ++ i napiszesz klasę z metodami dostępowymi i rzeczami, które wykonują arytmetykę wskaźników za kulisami. Krótko mówiąc, spakowane struktury służą do wydajnego radzenia sobie z spakowanymi danymi, a spakowane dane mogą być tym, z czym Twój program ma pracować. W większości przypadków kod powinien odczytywać wartości ze struktury, pracować z nimi i zapisywać je z powrotem po zakończeniu. Cała reszta powinna być wykonana poza spakowaną strukturą. Częścią problemu są rzeczy niskiego poziomu, które C próbuje ukryć przed programistą, oraz skoki do kółek, które są potrzebne, jeśli takie rzeczy naprawdę mają znaczenie dla programisty. (Prawie potrzebujesz innej konstrukcji `` układu danych '' w języku, abyś mógł powiedzieć `` ta rzecz ma 48 bajtów, foo odnosi się do danych w 13 bajtach i powinno być tak zinterpretowane ''; i oddzielna konstrukcja danych strukturalnych,

John Allsup
źródło
O ile czegoś nie brakuje, to nie odpowiada na pytanie. Twierdzisz, że pakowanie strukturalne jest wygodne (a tak jest), ale nie odpowiadasz na pytanie, czy jest bezpieczne. Twierdzisz również, że obniżają wydajność za niewyrównane odczyty; to prawda dla x86, ale nie dla wszystkich systemów, jak pokazałem w mojej odpowiedzi.
Keith Thompson,