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 int
obiekty miały wyrównanie 4-bajtowe, __attribute__((packed))
może spowodować int
przydzielenie 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?
źródło
Odpowiedzi:
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łuskujeszint*
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
int
obiektu 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:
Na x86 Ubuntu z gcc 4.5.2 generuje następujący wynik:
Na SPARC Solaris 9 z gcc 4.5.1 daje to:
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
x
członkowski był odpowiednio wyrównany. Z tablicą dwóchstruct foo
obiektów, co najmniej jednego lub drugiego będzie mieć niewyrównanegox
członka).(W tym przypadku
p0
wskazuje na nieprawidłowo wyrównany adres, ponieważ wskazuje na upakowanyint
element członkowski następujący pochar
elemencie.p1
Zdarza się, że jest prawidłowo wyrównany, ponieważ wskazuje na ten sam element w drugim elemencie tablicy, więcchar
poprzedzają go dwa obiekty - aw SPARC Solaris tablicaarr
wydaje się być przydzielona pod adresem parzystym, ale nie wielokrotnym 4.)Odnosząc się do członka
x
Urządzonystruct foo
według nazwy, kompilator wie, żex
jest potencjalnie niewyrównane i wygeneruje dodatkowy kod dostępu do niego prawidłowo.Gdy adres
arr[0].x
lubarr[1].x
został zapisany w obiekcie wskaźnika, ani kompilator, ani uruchomiony program nie wiedzą, że wskazuje na nieprawidłowo wyrównanyint
obiekt. 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-member
opcji, domyślnie włączonej.Właśnie zbudowałem tę wersję gcc ze źródeł. Dla powyższego programu generuje następujące informacje diagnostyczne:
źródło
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-align
opcję, nie jest ona domyślnie włączona ani za pomocą-Wall
lub-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:
Tutaj typ
a
jest strukturą spakowaną (jak zdefiniowano powyżej). Podobnieb
jest wskaźnikiem do zapakowanej struktury. Typ wyrażeniaa.i
to (w zasadzie) int l-wartość z wyrównaniem 1-bajtowym.c
id
oba są normalneint
. Podczas czytaniaa.i
kompilator generuje kod dla niewyrównanego dostępu. Kiedy czytaszb->i
, tenb
typ nadal wie, że jest zapakowany, więc nie ma problemu z ich też.e
jest wskaźnikiem do jednobajtowego int wyrównanego do jednego bajtu, więc kompilator wie, jak to poprawnie wyłuskać. Ale kiedy wykonujesz przypisanief = &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-Wall
lub-Wextra
).źródło
__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.__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ź.uint32_t
członka dauint32_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].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).
źródło
Używanie tego atrybutu jest zdecydowanie niebezpieczne.
Jedną z rzeczy, które niszczy, jest zdolność elementu
union
zawierają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: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:Wynik:
Chociaż
struct s1
istruct s2
mają „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 elementowix.b
nie jest taka sama, jak wartość odczytana z elementu członkowskiegoy.b
, mimo że norma mówi, że powinny być takie same.źródło
(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:
Wtedy mogę zadeklarować coś takiego
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,
źródło