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”)
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 programujeszOdpowiedzi:
Wartość typu
void**
jest wskaźnikiem do obiektu typuvoid*
. Obiekt typuFoo*
nie jest obiektem typuvoid*
.Istnieje domyślna konwersja między wartościami typu
Foo*
ivoid*
. 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 ustawieniemx
wartości3.0
, aledouble *p = (double*)&n;
ma niezdefiniowane zachowanie (iw praktyce nie będzie ustawionep
na „wskaźnik do3.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 ivoid*
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ć
freefunc
makro:Jest to związane ze zwykłymi ograniczeniami makr: brak bezpieczeństwa typu,
p
jest oceniany dwukrotnie. Zauważ, że daje to bezpieczeństwo, nie pozostawiając wiszących wskaźników, jeślip
byłby pojedynczym wskaźnikiem do uwolnionego obiektu.źródło
Foo*
ivoid*
mają taką samą reprezentację w swojej architekturze, to jeszcze niezdefiniowane do typu pun nich.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, konkretnievoid *
.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:
Które możesz nazwać tak:
Ma to jednak ograniczenie, ponieważ powyższe makro będzie oceniać
obj
dwukrotnie. Jeśli używasz GCC, można tego uniknąć dzięki niektórym rozszerzeniom, w szczególnościtypeof
słowom kluczowym i wyrażeniom:źródło
#define
polega na tym, że będzie on oceniałobj
dwukrotnie. 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ćobj
po użyciu jego wartości.typeof
, aby uniknąć ocenyobj
dwukrotnie:#define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
.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
free
akceptuje wskaźniki zerowe.źródło
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.float*
nie zmodyfikujeint32_t
obiektu, więc np. kompilatorint32_t*
nie musiint32_t *restrict ptr
zakładać, że nie wskazuje tej samej pamięci. To samo dotyczy sklepów,void**
ponieważ zakłada się, że nie modyfikujeFoo*
obiektu.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:
W twojej
*obj = NULL;
instrukcji obiekt ma typ skuteczny,Foo*
ale dostęp do niego ma wyrażenie lvalue*obj
z typemvoid*
.W 6.7.5.1 pkt 2 mamy
Tak
void*
iFoo*
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:
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.
źródło
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:
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
cudaMalloc
iposix_memalign
przykł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.źródło
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 typuvoid *
i przekazanie adresu tej funkcji do funkcji zwalniającej, a następnie ...(void **)
rzutowanie, powodując niezdefiniowane zachowanie.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.źródło