Pytam o dobrze znaną sztuczkę „ostatni element struktury ma zmienną długość”. To wygląda mniej więcej tak:
struct T {
int len;
char s[1];
};
struct T *p = malloc(sizeof(struct T) + 100);
p->len = 100;
strcpy(p->s, "hello world");
Ze względu na sposób, w jaki struktura jest umieszczona w pamięci, możemy nałożyć strukturę na większy niż to konieczne blok i traktować ostatni element tak, jakby był większy niż 1 char
określony.
Zatem pytanie brzmi: czy ta technika jest technicznie niezdefiniowanym zachowaniem? . Spodziewałbym się, że tak, ale byłem ciekawy, co mówi o tym norma.
PS: Zdaję sobie sprawę z podejścia C99 do tego, chciałbym, aby odpowiedzi dotyczyły konkretnie wersji sztuczki wymienionej powyżej.
c
undefined-behavior
c89
Evan Teran
źródło
źródło
Odpowiedzi:
Jak mówi C FAQ :
i:
Uzasadnienie bitu `` ściśle zgodnego '' znajduje się w specyfikacji, sekcja J.2 Niezdefiniowane zachowanie , która obejmuje listę niezdefiniowanych zachowań:
Paragraf 8 w sekcji 6.5.6 Operatory addytywne wspominają jeszcze o tym, że dostęp poza zdefiniowanymi granicami tablicy jest niezdefiniowany:
źródło
p->s
nigdy nie jest używany jako tablica. Jest przekazywany dostrcpy
, w którym to przypadku rozpada się na zwykłąchar *
, co zdarza się wskazywać obiekt, który można zgodnie z prawem zinterpretować jakochar [100];
wewnątrz przydzielonego obiektu.malloc
, gdy po prostu przekonwertowałeś zwracanevoid *
do wskaźnika do [struktury zawierającej] tablicę. Nadal ważne jest, aby uzyskać dostęp do dowolnej części przydzielonego obiektu za pomocą wskaźnika dochar
(lub najlepiejunsigned char
).malloc
. Wyszukaj "obiekt" w standardzie, zanim wyrzucisz bs.Uważam, że technicznie jest to niezdefiniowane zachowanie. Norma (prawdopodobnie) nie odnosi się do tego bezpośrednio, więc wchodzi w zakres „lub przez pominięcie jakiejkolwiek wyraźnej definicji zachowania”. klauzula (§4/2 z C99, §3.16 / 2 z C89), która mówi, że jest to niezdefiniowane zachowanie.
„Prawdopodobnie” powyżej zależy od definicji operatora indeksowania tablicy. W szczególności mówi: „Wyrażenie porostkowe, po którym następuje wyrażenie w nawiasach kwadratowych [], to indeksowane oznaczenie obiektu tablicy”. (C89, §6.3.2.1 / 2).
Możesz argumentować, że naruszane jest tutaj "obiektu tablicy" (ponieważ indeksujesz poza zdefiniowanym zakresem obiektu tablicy), w którym to przypadku zachowanie jest (odrobinę bardziej) jawnie niezdefiniowane, a nie tylko niezdefiniowane dzięki uprzejmości niczego, co ją definiuje.
Teoretycznie mogę sobie wyobrazić kompilator, który sprawdza granice tablicy i (na przykład) przerwałby program, jeśli / jeśli spróbujesz użyć indeksu spoza zakresu. W rzeczywistości nie wiem, czy coś takiego istnieje, a biorąc pod uwagę popularność tego stylu kodu, nawet jeśli kompilator próbował w pewnych okolicznościach wymusić indeksy dolne, trudno sobie wyobrazić, aby ktokolwiek mógł to znieść w ta sytuacja.
źródło
arr[x] = y;
mogłaby zostać przepisana jakoarr[0] = y;
; dla tablicy o rozmiarze 2,arr[i] = 4;
może zostać przepisane jakoi ? arr[1] = 4 : arr[0] = 4;
Chociaż nigdy nie widziałem kompilatora wykonującego takie optymalizacje, w niektórych systemach wbudowanych mogą one być bardzo produktywne. Na PIC18x, używającym 8-bitowych typów danych, kod pierwszej instrukcji miałby szesnaście bajtów, drugi - dwa lub cztery, a trzeci - osiem lub dwanaście. Niezła optymalizacja, jeśli jest legalna.a[2] == a + 2
), to tak nie jest. Jeśli mam rację, wszystkie standardy C definiują dostęp do tablicy jako arytmatykę wskaźnika.Tak, jest to niezdefiniowane zachowanie.
Raport dotyczący defektów języka C nr 051 zawiera ostateczną odpowiedź na to pytanie:
http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html
W dokumencie uzasadnienia C99 komisja C dodaje:
źródło
malloc
) jest ważny w dodatku, więc jak można identyczny wskaźnik, uzyskane inną drogą, być nieważne w dodatku? Nawet jeśli chcą twierdzić, że jest to UB, to jest to bez znaczenia, ponieważ nie ma obliczeniowego sposobu, aby implementacja rozróżniła dobrze zdefiniowane użycie od rzekomo niezdefiniowanego użycia.*foo
zawierały tablicy jednoelementowejboz
, wyrażeniefoo->boz[biz()*391]=9;
można uprościć jakobiz(),foo->boz[0]=9;
). Niestety, odrzucanie przez kompilatory tablic zerowych oznacza, że wiele kodu używa zamiast tego tablic jednoelementowych i zostanie zepsuty przez tę optymalizację.Ten konkretny sposób robienia tego nie jest wyraźnie zdefiniowany w żadnym standardzie C, ale C99 zawiera „struct hack” jako część języka. W C99, ostatni element struktury może być „elastycznym składnikiem tablicy”, zadeklarowanym jako
char foo[]
(z dowolnym typem, który chcesz zastąpićchar
).źródło
Nie jest to zachowanie nieokreślone , niezależnie od tego, co mówi ktoś, urzędnik lub w inny sposób , ponieważ jest zdefiniowane przez standard.
p->s
, z wyjątkiem sytuacji, gdy jest używany jako lwartość, jest obliczany na wskaźnik identyczny z(char *)p + offsetof(struct T, s)
. W szczególności jest to prawidłowychar
wskaźnik wewnątrz obiektu malloc, i istnieje 100 (lub więcej, zależnie od kwestii wyrównania) kolejnych adresów bezpośrednio po nim, które są również prawidłowe jakochar
obiekty wewnątrz przydzielonego obiektu. Fakt, że wskaźnik został wyprowadzony przy użyciu->
zamiast jawnego dodawania przesunięcia do wskaźnika zwróconego przezmalloc
, rzutowany dochar *
, jest nieistotny.Technicznie rzecz biorąc,
p->s[0]
jest to pojedynczy elementchar
tablicy wewnątrz struktury, kilka następnych elementów (np.p->s[1]
Throughp->s[3]
) prawdopodobnie wypełnia bajty wewnątrz struktury, które mogą zostać uszkodzone, jeśli wykonasz przypisanie do struktury jako całości, ale nie jeśli uzyskasz dostęp tylko do poszczególnych członków, a pozostałe elementy to dodatkowe miejsce w przydzielonym obiekcie, z którego możesz dowolnie korzystać, o ile spełniasz wymagania dotyczące wyrównania (ichar
nie masz żadnych wymagań dotyczących wyrównania).Jeśli obawiasz się, że możliwość nakładania się na bajty wypełniające w strukturze może w jakiś sposób wywołać demony nosowe, możesz tego uniknąć, zastępując
1
in[1]
wartością, która zapewnia, że na końcu struktury nie ma wypełnienia. Prostym, ale marnotrawnym sposobem byłoby utworzenie struktury z identycznymi składowymi, z wyjątkiem braku tablicy na końcu, i użycies[sizeof struct that_other_struct];
jej jako tablicy. Następniep->s[i]
jest jasno zdefiniowany jako element tablicy w strukturze fori<sizeof struct that_other_struct
i jako obiekt char pod adresem następującym po końcu struktury fori>=sizeof struct that_other_struct
.Edycja: W rzeczywistości, w powyższej sztuczce, aby uzyskać odpowiedni rozmiar, może być również konieczne umieszczenie unii zawierającej każdy prosty typ przed tablicą, aby upewnić się, że sama tablica zaczyna się od maksymalnego wyrównania, a nie w środku wypełnienia innego elementu . Ponownie, nie uważam, aby to wszystko było konieczne, ale oferuję to dla najbardziej paranoicznych prawników językowych.
Edycja 2: Nakładanie się bajtów wypełniających zdecydowanie nie stanowi problemu, ze względu na inną część standardu. C wymaga, aby jeśli dwie struktury zgadzały się w początkowym podciągu ich elementów, dostęp do wspólnych elementów początkowych można uzyskać za pomocą wskaźnika do dowolnego typu. W konsekwencji, gdyby
struct T
zadeklarowano strukturę identyczną z, ale z większą tablicą końcową, elements[0]
musiałby pokrywać się z elementems[0]
instruct T
, a obecność tych dodatkowych elementów nie mogłaby wpływać ani na nią nie wpływać dostęp do wspólnych elementów większej struktury używając wskaźnika dostruct T
.źródło
malloc
który jest uzyskiwany dostęp jako tablica, lub jeśli jest to większa struktura, do której można uzyskać dostęp za pośrednictwem wskaźnika do mniejszej struktury, której elementy są między innymi początkowym podzbiorem elementów większej struktury przypadkach.malloc
nie przydzieli zakresu pamięci, do którego można uzyskać dostęp za pomocą arytmetyki wskaźników, jaki by to był pożytek? A jeślip->s[1]
jest zdefiniowany przez standard jako cukier syntaktyczny dla arytmetyki wskaźnikowej, to ta odpowiedź po prostu potwierdza, żemalloc
jest użyteczna. Co zostało do omówienia? :)1
. To jest właśnie takie proste.int m[1]; int n[1]; if(m+1 == n) m[1] = 0;
założenie, żeif
wpisano gałąź. To jest UB (i nie gwarantuje się zainicjowanian
) zgodnie z 6.5.6 p8 (ostatnie zdanie), tak jak to czytałem. Związane z: 6.5.9 p6 z przypisem 109. (Odniesienia do C11 n1570.) [...]Tak, jest to technicznie niezdefiniowane zachowanie.
Zwróć uwagę, że istnieją co najmniej trzy sposoby implementacji „struct hack”:
(1) Zadeklarowanie końcowej tablicy o rozmiarze 0 (najbardziej „popularny” sposób w starszym kodzie). Jest to oczywiście UB, ponieważ deklaracje tablic o rozmiarze zerowym są zawsze nielegalne w C. Nawet jeśli się kompiluje, język nie gwarantuje zachowania jakiegokolwiek kodu naruszającego ograniczenia.
(2) Deklarowanie tablicy o minimalnym dozwolonym rozmiarze - 1 (Twój przypadek). W tym przypadku każda próba wzięcia wskaźnika
p->s[0]
i użycia go do arytmetyki wskaźników, która wykracza pozap->s[1]
to, jest zachowaniem niezdefiniowanym. Na przykład implementacja debugowania może tworzyć specjalny wskaźnik z osadzonymi informacjami o zakresie, który będzie przechwytywał za każdym razem, gdy spróbujesz utworzyć wskaźnik pozap->s[1]
.(3) Zadeklarowanie tablicy o „bardzo dużym” rozmiarze , na przykład 10000. Chodzi o to, że deklarowany rozmiar powinien być większy niż cokolwiek, czego możesz potrzebować w praktyce. Ta metoda jest wolna od UB w odniesieniu do zakresu dostępu do tablicy. Jednak w praktyce oczywiście zawsze będziemy alokować mniejszą ilość pamięci (tylko tyle, ile naprawdę potrzeba). Nie jestem pewien co do legalności tego, tj. Zastanawiam się, jak legalne jest przydzielanie mniejszej ilości pamięci dla obiektu niż zadeklarowany rozmiar obiektu (zakładając, że nigdy nie uzyskamy dostępu do „nieprzydzielonych” elementów członkowskich).
źródło
s[1]
nie jest niezdefiniowanym zachowaniem. To to samo*(s+1)
, co, czyli to samo co*((char *)p + offsetof(struct T, s) + 1)
, które jest prawidłowym wskaźnikiem do achar
w przydzielonym obiekcie.foo[]
cukier syntaktyczny*foo
), wówczas każdy dostęp poza mniejszym z zadeklarowanego rozmiaru i przydzielony rozmiar to UB, niezależnie od tego, jak wykonano arytmetykę wskaźnika.foo[]
w strukturze nie jest cukrem syntaktycznym*foo
; jest to elastyczny element tablicy C99. Co do reszty, zobacz moją odpowiedź i komentarze do innych odpowiedzi.unsigned char [sizeof object]
tablicy . Podtrzymuję moje twierdzenie, że „hack” elementu elastycznej tablicy dla wersji sprzed C99 ma dobrze zdefiniowane zachowanie.Standard jest całkiem jasny, że nie możesz uzyskać dostępu do rzeczy poza końcem tablicy. (i przechodzenie przez wskaźniki nie pomaga, ponieważ nie możesz nawet zwiększać wskaźników poza jeden po końcu tablicy).
I za „pracę w praktyce”. Widziałem, jak optymalizator gcc / g ++ używa tej części standardu, generując w ten sposób niewłaściwy kod, gdy spełnia ten nieprawidłowy C.
źródło
Jeśli kompilator akceptuje coś takiego jak
Myślę, że jest całkiem jasne, że musi być gotowy do zaakceptowania indeksu dolnego „dat” poza jego długością. Z drugiej strony, jeśli ktoś koduje coś takiego:
a później uzyskuje dostęp do somestruct-> dat [x]; Nie sądzę, aby kompilator był zobowiązany do używania kodu obliczającego adresy, który będzie działał z dużymi wartościami x. Myślę, że gdyby ktoś chciał być naprawdę bezpieczny, właściwy paradygmat byłby bardziej następujący:
a następnie wykonaj malloc (sizeof (MYSTRUCT) -LARGEST_DAT_SIZE + pożądana_długość_tablicy) bajtów (pamiętając, że jeśli pożądana_długość_tablicy jest większa niż LARGEST_DAT_SIZE, wyniki mogą być nieokreślone).
Nawiasem mówiąc, myślę, że decyzja o zakazaniu tablic o zerowej długości była niefortunna (niektóre starsze dialekty, takie jak Turbo C, obsługują ją), ponieważ tablicę o zerowej długości można uznać za znak, że kompilator musi wygenerować kod, który będzie działał z większymi indeksami .
źródło