C / C ++: Wymuś kolejność i wyrównanie pola bitowego


87

Czytałem, że kolejność pól bitowych w strukturze zależy od platformy. A jeśli użyję różnych opcji pakowania specyficznych dla kompilatora, czy ta gwarancja będzie przechowywana we właściwej kolejności, w jakiej są zapisywane? Na przykład:

struct Message
{
  unsigned int version : 3;
  unsigned int type : 1;
  unsigned int id : 5;
  unsigned int data : 6;
} __attribute__ ((__packed__));

Na procesorze Intela z kompilatorem GCC pola zostały ułożone w pamięci tak, jak są pokazane. Message.versionbył pierwszymi 3 bitami w buforze, a Message.typenastępnie. Jeśli znajdę równoważne opcje pakowania struktury dla różnych kompilatorów, czy będzie to wieloplatformowe?


17
Ponieważ bufor to zestaw bajtów, a nie bitów, „pierwsze 3 bity w buforze” nie są precyzyjnym pojęciem. Czy potraktowałbyś 3 bity najniższego rzędu pierwszego bajtu jako pierwsze 3 bity, czy 3 bity najwyższego rzędu?
kawiarnia

2
Podczas tranzytu w sieci „Pierwsze 3 bity w buforze” okazują się być bardzo dobrze zdefiniowane.
Joshua

2
@Joshua IIRC, Ethernet przesyła najpierw najmniej znaczący bit z każdego bajtu (dlatego bit transmisji jest tam, gdzie się znajduje).
tc.

Kiedy mówisz „przenośny” i „wieloplatformowy”, co masz na myśli? Plik wykonywalny będzie poprawnie uzyskiwał dostęp do zamówienia niezależnie od docelowego systemu operacyjnego - lub - kod będzie się kompilował niezależnie od toolchaina?
Garet Claborn

Odpowiedzi:


103

Nie, nie będzie w pełni przenośny. Opcje pakowania struktur są rozszerzeniami i same w sobie nie są w pełni przenośne. Oprócz tego, C99 §6.7.2.1, paragraf 10 mówi: „Kolejność przydzielania pól bitowych w jednostce (od wysokiego do niskiego lub od niskiego do wysokiego) jest określona przez implementację”.

Nawet pojedynczy kompilator może inaczej rozłożyć pole bitowe, na przykład w zależności od endianness platformy docelowej.


Tak, na przykład GCC wyraźnie zauważa, że ​​pola bitowe są ułożone zgodnie z ABI, a nie implementacją. Tak więc samo pozostanie na jednym kompilatorze nie wystarczy do zagwarantowania zamówienia. Trzeba też sprawdzić architekturę. Trochę koszmaru przenośności, naprawdę.
underscore_d

10
Dlaczego standard C nie gwarantował zamówienia pól bitowych?
Aaron Campbell

7
Trudno jest konsekwentnie i przenośnie zdefiniować „kolejność” bitów w bajtach, a tym bardziej kolejność bitów, które mogą przekraczać granice bajtów. Żadna definicja, na którą się zdecydujesz, nie będzie pasować do znacznej ilości istniejących praktyk.
Stephen Canon,

2
Zdefiniowane w implementacji pozwala na optymalizację specyficzną dla platformy. Na niektórych platformach wypełnienie między polami bitów może poprawić dostęp, wyobraź sobie cztery siedmiobitowe pola w 32-bitowej liczbie int: wyrównanie ich co ósmy bit jest znaczącym ulepszeniem dla platform, które mają odczyty bajtów.
peterchen


45

Pola bitowe różnią się znacznie od kompilatora do kompilatora, przepraszam.

W przypadku GCC maszyny big endian układają bity jako pierwszy, a maszyny little endian układają bity jako pierwszy.

K&R mówi: „Sąsiadujące [bit-] elementy składowe pól struktur są pakowane do jednostek pamięci zależnych od implementacji w kierunku zależnym od implementacji. Kiedy pole następujące po innym polu nie będzie pasować ... może zostać podzielone na jednostki lub jednostka może być wypełnione. Nienazwane pole o szerokości 0 wymusza to wypełnienie ... ”

Dlatego jeśli potrzebujesz niezależnego od komputera układu binarnego, musisz to zrobić samodzielnie.

To ostatnie stwierdzenie odnosi się również do pól innych niż bitowe ze względu na wypełnienie - jednak wydaje się, że wszystkie kompilatory mają jakiś sposób na wymuszenie pakowania bajtów w strukturze, jak widzę już odkryłeś dla GCC.


Czy K&R rzeczywiście uważa się za przydatne odniesienie, biorąc pod uwagę, że było to przed standaryzacją i (jak zakładam?) Prawdopodobnie zostało zastąpione w wielu obszarach?
underscore_d

1
Mój K&R jest post-ANSI.
Joshua

1
Teraz to jest żenujące: nie zdawałem sobie sprawy, że wydali wersję post-ANSI. Mój błąd!
underscore_d

35

Należy unikać pól bitowych - nie są one zbyt przenośne między kompilatorami, nawet dla tej samej platformy. ze standardu C99 6.7.2.1/10 - „Specyfikatory struktury i unii” (podobne sformułowanie występuje w standardzie C90):

Implementacja może przydzielić dowolną adresowalną jednostkę pamięci wystarczająco dużą, aby pomieścić pole bitowe. Jeśli pozostanie wystarczająca ilość miejsca, pole bitowe, które następuje bezpośrednio po innym polu bitowym w strukturze, będzie upakowane w sąsiednie bity tej samej jednostki. Jeśli pozostanie niewystarczająca ilość miejsca, to czy pole bitowe, które nie pasuje, zostanie wstawione do następnej jednostki, czy też zachodzi na sąsiednie jednostki, jest definiowane implementacyjnie. Kolejność alokacji pól bitowych w jednostce (od wysokiego do niskiego lub od niskiego do wysokiego) jest określona przez implementację. Wyrównanie adresowalnej jednostki pamięci nie jest określone.

Nie możesz zagwarantować, czy pole bitowe `` obejmie '' granicę int, czy nie, i nie możesz określić, czy pole bitowe zaczyna się od dolnego końca int czy górnego końca int (jest to niezależne od tego, czy procesor jest big-endian lub little-endian).

Preferuj maski bitowe. Użyj wbudowanych (lub nawet makr), aby ustawić, wyczyścić i przetestować bity.


2
Kolejność pól bitowych można określić w czasie kompilacji.
Greg A. Woods

9
Ponadto pola bitowe są bardzo preferowane w przypadku flag bitowych, które nie mają zewnętrznej reprezentacji poza programem (tj. Na dysku lub w rejestrach lub w pamięci, do której mają dostęp inne programy itp.).
Greg A. Woods

1
@ GregA.Woods: Jeśli tak jest naprawdę, podaj odpowiedź opisującą, jak to zrobić. Nie mogłem znaleźć nic oprócz twojego komentarza, kiedy google
szukałem

1
@ GregA.Woods: Przepraszam, powinienem napisać, do którego komentarza się odniosłem. Miałem na myśli: Mówisz, że „Kolejność pól bitowych można określić w czasie kompilacji”. Nie mogę nic na ten temat i jak to zrobić.
mozzbozz,

2
@mozzbozz Zajrzyj na planix.com/~woods/projects/wsg2000.c i poszukaj definicji i zastosowania _BIT_FIELDS_LTOHoraz_BIT_FIELDS_HTOL
Greg A. Woods

11

endianness mówi o porządkach bajtów, a nie o porządkach bitowych. Obecnie istnieje 99% pewności, że zamówienia bitów są stałe. Jednak w przypadku korzystania z pól bitowych endianness należy traktować jako liczbę. Zobacz poniższy przykład.

#include <stdio.h>

typedef struct tagT{

    int a:4;
    int b:4;
    int c:8;
    int d:16;
}T;


int main()
{
    char data[]={0x12,0x34,0x56,0x78};
    T *t = (T*)data;
    printf("a =0x%x\n" ,t->a);
    printf("b =0x%x\n" ,t->b);
    printf("c =0x%x\n" ,t->c);
    printf("d =0x%x\n" ,t->d);

    return 0;
}

//- big endian :  mips24k-linux-gcc (GCC) 4.2.3 - big endian
a =0x1
b =0x2
c =0x34
d =0x5678
 1   2   3   4   5   6   7   8
\_/ \_/ \_____/ \_____________/
 a   b     c           d

// - little endian : gcc (Ubuntu 4.3.2-1ubuntu11) 4.3.2
a =0x2
b =0x1
c =0x34
d =0x7856
 7   8   5   6   3   4   1   2
\_____________/ \_____/ \_/ \_/
       d           c     b   a

6
Dane wyjściowe a i b wskazują, że endianness wciąż mówi o kolejności bitów ORAZ kolejności bajtów.
Programista Windows

wspaniały przykład z problematyką porządkowania bitów i porządkowania bajtów
Jonathan

1
Czy faktycznie skompilowałeś i uruchomiłeś kod? Wartości "a" i "b" nie wydają mi się logiczne: w zasadzie mówisz, że kompilator zamieni nibble w bajcie z powodu endianizmu. W przypadku „d” endiany nie powinny wpływać na kolejność bajtów w tablicach znaków (zakładając, że znak ma długość 1 bajtu); gdyby kompilator to zrobił, nie bylibyśmy w stanie iterować po tablicy przy użyciu wskaźników. Jeśli, z drugiej strony, użyłeś tablicy dwóch 16-bitowych liczb całkowitych, np .: uint16 data [] = {0x1234,0x5678}; wtedy d na pewno będzie 0x7856 w systemach little endian.
Krauss

6

Prawdopodobnie przez większość czasu, ale nie stawiaj na to farmy, bo jeśli się pomylisz, dużo stracisz.

Jeśli naprawdę potrzebujesz identycznych informacji binarnych, będziesz musiał utworzyć pola bitowe z maskami bitowymi - np. Użyjesz skrótu bez znaku (16 bitów) dla Message, a następnie utwórz rzeczy takie jak versionMask = 0xE000, aby reprezentowały trzy najwyższe bity.

Podobny problem występuje z wyrównaniem wewnątrz struktur. Na przykład procesory Sparc, PowerPC i 680x0 to wszystkie procesory typu big-endian, a typową wartością domyślną kompilatorów Sparc i PowerPC jest wyrównywanie elementów struktur na granicach 4-bajtowych. Jednak jeden kompilator, którego użyłem dla 680x0, wyrównał tylko do granic 2-bajtowych - i nie było opcji zmiany wyrównania!

Tak więc dla niektórych struktur rozmiary Sparc i PowerPC są identyczne, ale mniejsze na 680x0, a niektóre elementy są w różnych przesunięciach pamięci w strukturze.

Był to problem z jednym projektem, nad którym pracowałem, ponieważ proces serwera działający na Sparc odpytywał klienta i dowiadywał się, że jest to big-endian i zakładał, że może po prostu wypuścić struktury binarne w sieci, a klient sobie z tym poradzi. I to działało dobrze na klientach PowerPC i powodowało duże awarie na klientach 680x0. Nie napisałem kodu, a znalezienie problemu zajęło trochę czasu. Ale kiedy już to zrobiłem, łatwo było to naprawić.


1

Dzięki @BenVoigt za bardzo przydatny komentarz na początku

Nie, zostały stworzone, aby oszczędzać pamięć.

Źródło Linux robi użyć pola bitowego, aby dopasować się do konstrukcji zewnętrznej: /usr/include/linux/ip.h ma ten kod na pierwszy bajt datagramu IP

struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
        __u8    ihl:4,
                version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
        __u8    version:4,
                ihl:4;
#else
#error  "Please fix <asm/byteorder.h>"
#endif

Jednak w świetle twojego komentarza rezygnuję z prób, aby to działało dla wielobajtowego pola bitowego frag_off .


-9

Oczywiście najlepszą odpowiedzią jest użycie klasy, która odczytuje / zapisuje pola bitowe jako strumień. Użycie struktury pola bitowego C po prostu nie jest gwarantowane. Nie wspominając o tym, że używanie tego w prawdziwym kodowaniu jest uważane za nieprofesjonalne / leniwe / głupie.


5
Myślę, że błędem jest stwierdzenie, że używanie pól bitowych jest głupie, ponieważ zapewnia bardzo czysty sposób reprezentowania rejestrów sprzętowych, które zostały stworzone do modelowania, w C.
trondd

13
@trondd: Nie, zostały stworzone, aby oszczędzać pamięć. Pola bitowe nie są przeznaczone do mapowania na zewnętrzne struktury danych, takie jak rejestry sprzętowe mapowane w pamięci, protokoły sieciowe lub formaty plików. Gdyby miały one mapować do zewnętrznych struktur danych, kolejność pakowania zostałaby ustandaryzowana.
Ben Voigt

2
Używanie bitów oszczędza pamięć. Korzystanie z pól bitowych zwiększa czytelność. Korzystanie z mniejszej ilości pamięci jest szybsze. Używanie bitów pozwala na bardziej złożone operacje atomowe. W naszych rzeczywistych aplikacjach potrzebne są wydajne i złożone operacje atomowe. Ta odpowiedź nie zadziała dla nas.
johnnycrash

@BenVoigt prawdopodobnie prawda, ale jeśli programista jest skłonny potwierdzić, że kolejność ich kompilatora / ABI odpowiada temu, czego potrzebuje, i odpowiednio poświęcić szybką przenośność - to z pewnością może spełnić tę rolę. A jeśli chodzi o 9 *, to która autorytatywna masa „prawdziwych koderów świata” uważa, że ​​każde użycie pól bitowych jest „nieprofesjonalne / leniwe / głupie” i gdzie to stwierdzili?
underscore_d

2
Korzystanie z mniejszej ilości pamięci nie zawsze jest szybsze; często bardziej wydajne jest użycie większej ilości pamięci i ograniczenie operacji po odczycie, a tryb procesor / procesor może to jeszcze bardziej uczynić.
Dave Newton,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.