Dlaczego to twierdzenie, że ostrzeżenie o typie dereferencji jest specyficzne dla kompilatora?


38

Czytałem różne posty na temat Przepełnienia stosu RE: dereferujący błąd wskaźnika pisanego czcionką typu. Rozumiem, że błąd jest zasadniczo ostrzeżeniem kompilatora o niebezpieczeństwie dostępu do obiektu za pomocą wskaźnika innego typu (choć wydaje się, że istnieje wyjątek char*), co jest zrozumiałym i rozsądnym ostrzeżeniem.

Moje pytanie jest specyficzne dla poniższego kodu: dlaczego przesłanie adresu wskaźnika do void**kwalifikującego się do tego ostrzeżenia (awansowane przez błąd -Werror)?

Co więcej, ten kod jest kompilowany dla wielu architektur docelowych, z których tylko jedna generuje ostrzeżenie / błąd - czy może to sugerować, że jest to uzasadniony brak specyficzny dla wersji kompilatora?

// main.c
#include <stdlib.h>

typedef struct Foo
{
  int i;
} Foo;

void freeFunc( void** obj )
{
  if ( obj && * obj )
  {
    free( *obj );
    *obj = NULL;
  }
}

int main( int argc, char* argv[] )
{
  Foo* f = calloc( 1, sizeof( Foo ) );
  freeFunc( (void**)(&f) );

  return 0;
}

Jeśli moje rozumienie, o którym mowa powyżej, jest poprawne, a void**będąc nadal tylko wskaźnikiem, powinno to być bezpieczne rzucanie.

Czy istnieje obejście, w którym nie stosuje się wartości lv, które uspokoją to ostrzeżenie / błąd specyficzne dla kompilatora? Tzn. Rozumiem to i dlaczego to rozwiązuje problem, ale chciałbym uniknąć tego podejścia, ponieważ chcę skorzystać z freeFunc() NULL w zamierzonym out-arg:

void* tmp = f;
freeFunc( &tmp );
f = NULL;

Kompilator problemów (jeden z jednego):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc --version && /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-fc3-linux-gnu-gcc (GCC) 3.4.5
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

./main.c: In function `main':
./main.c:21: warning: dereferencing type-punned pointer will break strict-aliasing rules

user@8d63f499ed92:/build$

Nie narzekający kompilator (jeden z wielu):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc --version && /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-rh73-linux-gnu-gcc (GCC) 3.2.3
Copyright (C) 2002 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

user@8d63f499ed92:/build$

Aktualizacja: Odkryłem, że ostrzeżenie wydaje się być generowane specjalnie po kompilacji -O2(nadal tylko z zaznaczonym „kompilatorem problemów”)



1
„a void**, będąc wciąż tylko wskaźnikiem, powinien to być bezpieczny rzut”. Wow, pomijam! Wygląda na to, że masz pewne podstawowe założenia. Staraj się mniej myśleć w kategoriach bajtów i dźwigni, a więcej w kategoriach abstrakcji, ponieważ tak właśnie programujesz
Lightness Races in Orbit

7
Stycznie, kompilatory, których używasz, mają 15 i 17 lat! Nie polegałbym na żadnym z nich.
Tavian Barnes,

4
@TavianBarnes Ponadto, jeśli z jakiegoś powodu musisz polegać na GCC 3, najlepiej użyć końcowej wersji, która, jak sądzę, miała wersję 3.4.6. Dlaczego nie skorzystać z wszystkich dostępnych poprawek do tej serii, zanim zostanie ona wstrzymana?
Kaz

Jaki standard kodowania C ++ określa te wszystkie spacje?
Peter Mortensen,

Odpowiedzi:


33

Wartość typu void**jest wskaźnikiem do obiektu typu void*. Obiekt typu Foo*nie jest obiektem typu void*.

Istnieje domyślna konwersja między wartościami typu Foo*i void*. Ta konwersja może zmienić reprezentację wartości. Podobnie możesz pisać, int n = 3; double x = n;a to ma dobrze zdefiniowane zachowanie związane z ustawieniem xwartości 3.0, ale double *p = (double*)&n;ma niezdefiniowane zachowanie (iw praktyce nie będzie ustawione pna „wskaźnik do 3.0” na żadnej wspólnej architekturze).

Architektury, w których różne typy wskaźników do obiektów mają różne reprezentacje, są obecnie rzadkie, ale są dozwolone przez standard C. Istnieją (rzadkie) stare maszyny ze wskaźnikami słów, które są adresami słowa w pamięci i wskaźnikami bajtów, które są adresami słowa wraz z przesunięciem bajtów w tym słowie; Foo*byłby wskaźnikiem słownym i void*byłby wskaźnikiem bajtowym na takich architekturach. Istnieją (rzadkie) maszyny ze wskaźnikami tłuszczu, które zawierają informacje nie tylko o adresie obiektu, ale także o jego typie, rozmiarze i listach kontroli dostępu; wskaźnik do określonego typu może mieć inną reprezentację niż ta, void*która wymaga dodatkowych informacji o typie w czasie wykonywania.

Takie maszyny są rzadkie, ale dozwolone przez standard C. Niektóre kompilatory C wykorzystują pozwolenie, aby traktować wskaźniki ze znacznikami typu jako odrębne elementy w celu optymalizacji kodu. Ryzyko aliasingu wskaźników jest głównym ograniczeniem zdolności kompilatora do optymalizacji kodu, więc kompilatory zwykle korzystają z takich uprawnień.

Kompilator może ci powiedzieć, że robisz coś złego lub po cichu zrobić to, czego nie chciałeś, lub po cichu zrobić to, co chciałeś. Niezdefiniowane zachowanie pozwala na dowolne z nich.

Możesz zrobić freefuncmakro:

#define FREE_SINGLE_REFERENCE(p) (free(p), (p) = NULL)

Jest to związane ze zwykłymi ograniczeniami makr: brak bezpieczeństwa typu, pjest oceniany dwukrotnie. Zauważ, że daje to bezpieczeństwo, nie pozostawiając wiszących wskaźników, jeśli pbyłby pojedynczym wskaźnikiem do uwolnionego obiektu.


1
I to dobrze wiedzieć, że nawet jeśli Foo*i void*mają taką samą reprezentację w swojej architekturze, to jeszcze niezdefiniowane do typu pun nich.
Tavian Barnes,

12

Część A void *jest traktowana specjalnie przez standard C, ponieważ odnosi się do niekompletnego typu. Zabieg ten ma nie rozciąga się void **, jak to robi punkt do kompletnego typu, konkretnie void *.

Surowe reguły aliasingu mówią, że nie można przekonwertować wskaźnika jednego typu na wskaźnik innego typu, a następnie odrzucić ten wskaźnik, ponieważ oznacza to ponowną interpretację bajtów jednego typu jako innego. Jedynym wyjątkiem jest konwersja na typ znaku, który pozwala odczytać reprezentację obiektu.

Można obejść to ograniczenie, używając makra podobnego do funkcji zamiast funkcji:

#define freeFunc(obj) (free(obj), (obj) = NULL)

Które możesz nazwać tak:

freeFunc(f);

Ma to jednak ograniczenie, ponieważ powyższe makro będzie oceniać objdwukrotnie. Jeśli używasz GCC, można tego uniknąć dzięki niektórym rozszerzeniom, w szczególności typeofsłowom kluczowym i wyrażeniom:

#define freeFunc(obj) ({ typeof (&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })

3
+1 za lepszą implementację zamierzonego zachowania. Jedyny problem, jaki widzę, #definepolega na tym, że będzie on oceniał objdwukrotnie. Nie znam jednak dobrego sposobu na uniknięcie tej drugiej oceny. Nawet wyrażenie instrukcji (rozszerzenie GNU) nie załatwi sprawy, ponieważ musisz je przypisać objpo użyciu jego wartości.
cmaster

2
@cmaster: Jeśli jesteś gotów użyć rozszerzenia GNU wyrażeń takich jak sprawozdania, a następnie można użyć typeof, aby uniknąć oceny objdwukrotnie: #define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; }).
ruakh

@ruakh Bardzo fajnie :-) Byłoby wspaniale, gdyby dbush edytował to w odpowiedzi, więc nie będzie masowo usuwany wraz z komentarzami.
cmaster

9

Dereferencje wskaźnika pisanego czcionką to UB i nie możesz liczyć na to, co się stanie.

Różne kompilatory generują różne ostrzeżenia, w tym celu różne wersje tego samego kompilatora można uznać za różne kompilatory. To wydaje się lepsze wytłumaczenie wariancji, którą widzisz, niż zależność od architektury.

Przypadkiem, który może pomóc zrozumieć, dlaczego pisanie na klawiaturze w tym przypadku może być złe, jest to, że twoja funkcja nie będzie działać na architekturze, dla której sizeof(Foo*) != sizeof(void*). Jest to dozwolone przez standard, chociaż nie znam żadnego z obecnych, dla którego jest to prawdą.

Obejściem byłoby użycie makra zamiast funkcji.

Zauważ, że freeakceptuje wskaźniki zerowe.


2
Fascynujące, że to możliwe sizeof Foo* != sizeof void*. Nigdy nie spotkałem „na wolności” rozmiarów wskaźników zależnych od typu, dlatego z biegiem lat uważałem za aksjomatyczne, że rozmiary wskaźników są takie same w danej architekturze.
StoneThrow

1
@Stonethrow standardowym przykładem są wskaźniki tłuszczu używane do adresowania bajtów w architekturze adresowalnej przez słowa. Ale myślę, że obecne adresowalne słowa używają alternatywnego sizeof char == sizeof word .
AProgrammer

2
Należy pamiętać, że typ należy podać w nawiasach dla rozmiaru ...
Antti Haapala,

@StoneThrow: Niezależnie od wielkości wskaźnika analiza aliasów na podstawie typu czyni ją niebezpieczną; pomaga to kompilatorom w optymalizacji poprzez założenie, że sklep float*nie zmodyfikuje int32_tobiektu, więc np. kompilator int32_t*nie musi int32_t *restrict ptrzakładać, że nie wskazuje tej samej pamięci. To samo dotyczy sklepów, void**ponieważ zakłada się, że nie modyfikuje Foo*obiektu.
Peter Cordes,

4

Ten kod jest nieprawidłowy zgodnie ze standardem C, więc może działać w niektórych przypadkach, ale niekoniecznie jest przenośny.

„Ścisła reguła aliasingu” w celu uzyskania dostępu do wartości za pomocą wskaźnika, który został rzutowany na inny typ wskaźnika, znajduje się w 6.5 akapit 7:

Dostęp do przechowywanej wartości obiektu może mieć tylko wyrażenie wartości, które ma jeden z następujących typów:

  • typ zgodny z efektywnym typem obiektu,

  • kwalifikowana wersja typu zgodna z efektywnym typem obiektu,

  • typ, który jest typem podpisanym lub niepodpisanym odpowiadającym efektywnemu typowi obiektu,

  • typ, który jest typem podpisanym lub niepodpisanym odpowiadającym kwalifikowanej wersji efektywnego typu obiektu,

  • typ agregatu lub związku, który obejmuje jeden z wyżej wymienionych typów wśród jego członków (w tym, rekurencyjnie, członka podagregatu lub zawartego związku), lub

  • typ postaci.

W twojej *obj = NULL;instrukcji obiekt ma typ skuteczny, Foo*ale dostęp do niego ma wyrażenie lvalue *objz typem void*.

W 6.7.5.1 pkt 2 mamy

Aby dwa typy wskaźników były kompatybilne, oba powinny być identyczne i oba powinny wskazywać na kompatybilne typy.

Tak void*i Foo*nie są kompatybilne rodzaje i typy kompatybilne z kwalifikatorów dodanych, a na pewno nie pasują do żadnej z pozostałych opcji w ścisłej reguły aliasing.

Chociaż nie jest to techniczną przyczyną, że kod jest nieprawidłowy, należy również zwrócić uwagę na sekcję 6.2.5 pkt 26:

Wskaźnik do voidpowinien mieć takie same wymagania dotyczące reprezentacji i wyrównania, jak wskaźnik do typu znaku. Podobnie wskaźniki do kwalifikowanych lub niekwalifikowanych wersji kompatybilnych typów powinny mieć takie same wymagania dotyczące reprezentacji i dostosowania. Wszystkie wskaźniki do typów konstrukcji powinny mieć takie same wymagania dotyczące reprezentacji i wyrównania. Wszystkie wskaźniki do typów unii mają takie same wymagania dotyczące reprezentacji i wyrównania. Wskaźniki do innych typów nie muszą mieć takich samych wymagań dotyczących reprezentacji lub wyrównania.

Jeśli chodzi o różnice w ostrzeżeniach, nie jest to przypadek, w którym Standard wymaga komunikatu diagnostycznego, więc chodzi tylko o to, jak dobry jest kompilator lub jego wersja w wykrywaniu potencjalnych problemów i wskazywaniu ich w pomocny sposób. Zauważyłeś, że ustawienia optymalizacji mogą mieć znaczenie. Wynika to często z tego, że więcej informacji jest generowanych wewnętrznie na temat tego, jak różne części programu faktycznie pasują do siebie w praktyce, a zatem dodatkowe informacje są również dostępne do kontroli ostrzegawczych.


2

Oprócz tego, co powiedzieli inne odpowiedzi, jest to klasyczny anty-wzór w C i taki, który należy spalić ogniem. Pojawia się w:

  1. Bezpłatne i zerowe funkcje, takie jak ta, w której znalazłeś ostrzeżenie.
  2. Funkcje alokacji, które unikają standardowego idiomu C zwracania void *(który nie cierpi z powodu tego problemu, ponieważ obejmuje konwersję wartości zamiast typowania ), zamiast tego zwraca flagę błędu i zapisuje wynik za pomocą wskaźnika od wskaźnika do wskaźnika.

W innym przykładzie (1) od dawna niesławny przypadek dotyczy funkcji ffmpeg / libavcodec av_free. Wierzę, że ostatecznie zostało to naprawione za pomocą makra lub innej sztuczki, ale nie jestem pewien.

W przypadku (2), jak cudaMalloci posix_memalignprzykłady.

W żadnym przypadku interfejs z natury nie wymaga niewłaściwego użycia, ale zdecydowanie go zachęca i dopuszcza prawidłowe użycie tylko z dodatkowym tymczasowym obiektem typu, void *który nie spełnia celu funkcji „zeruj i zeruj” i sprawia, że ​​alokacja jest niewygodna.


Czy masz link wyjaśniający, dlaczego (1) jest anty-wzorem? Nie sądzę, że znam tę sytuację / argument i chciałbym dowiedzieć się więcej.
StoneThrow,

1
@StoneThrow: To jest naprawdę proste - celem jest zapobieganie niewłaściwemu użyciu poprzez wyzerowanie obiektu przechowującego wskaźnik do uwolnionej pamięci, ale jedynym sposobem, w jaki można to zrobić, jest to, że osoba wywołująca faktycznie przechowuje wskaźnik w obiekcie pisz void *i konwertuj / konwertuj za każdym razem, gdy chcesz go wyrejestrować. To bardzo mało prawdopodobne. Jeśli program wywołujący przechowuje jakiś inny typ wskaźnika, jedynym sposobem wywołania funkcji bez wywoływania UB jest skopiowanie wskaźnika do obiektu tymczasowego typu void *i przekazanie adresu tej funkcji do funkcji zwalniającej, a następnie ...
R .. GitHub ZATRZYMAJ LÓD

1
... zeruje obiekt tymczasowy, a nie rzeczywistą pamięć, w której program wywołujący miał wskaźnik. Oczywiście tak naprawdę dzieje się tak, że użytkownicy funkcji wykonują (void **)rzutowanie, powodując niezdefiniowane zachowanie.
R .. GitHub ZATRZYMAJ LÓD

2

Chociaż C został zaprojektowany dla maszyn, które używają tej samej reprezentacji dla wszystkich wskaźników, autorzy Standardu chcieli, aby język był użyteczny na maszynach, które używają różnych reprezentacji dla wskaźników dla różnych typów obiektów. Dlatego też nie wymagały, aby maszyny, które używają różnych reprezentacji wskaźników dla różnych rodzajów wskaźników, obsługiwały typ „wskaźnik do dowolnego rodzaju wskaźnika”, nawet jeśli wiele komputerów mogłoby to zrobić za zerową cenę.

Przed napisaniem standardu implementacje dla platform, które używały tej samej reprezentacji dla wszystkich typów wskaźników, jednogłośnie pozwoliłyby void**na użycie, przynajmniej z odpowiednim rzutowaniem, jako „wskaźnika do dowolnego wskaźnika”. Autorzy Standardu prawie na pewno uznali, że przydałoby się to na platformach, które go wspierały, ale ponieważ nie można go było powszechnie wspierać, odmówili jego upoważnienia. Zamiast tego spodziewali się, że wdrożenie jakości przetworzy takie konstrukty, jak to, co uzasadnienie określiłoby jako „popularne rozszerzenie”, w przypadkach, w których byłoby to uzasadnione.

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.