GCC 6 ma nową funkcję optymalizatora : zakłada, że this
zawsze nie jest zerowa i optymalizuje na tej podstawie.
Propagacja zakresu wartości zakłada teraz, że ten wskaźnik funkcji składowych C ++ jest różny od null. Eliminuje to typowe sprawdzanie zerowego wskaźnika, ale także łamie niektóre niezgodne podstawy kodu (takie jak Qt-5, Chromium, KDevelop) . Jako tymczasowe obejście można użyć kontroli -fno-delete-null-pointer-pointer. Błędny kod można zidentyfikować za pomocą -fsanitize = undefined.
Dokument zmian wyraźnie określa to jako niebezpieczne, ponieważ narusza zaskakującą ilość często używanego kodu.
Dlaczego to nowe założenie miałoby złamać praktyczny kod C ++? Czy istnieją określone wzorce, w których niedbali lub niedoinformowani programiści polegają na tym konkretnym niezdefiniowanym zachowaniu? Nie wyobrażam sobie, żeby ktoś pisał, if (this == NULL)
bo to takie nienaturalne.
źródło
this
jest on przekazywany jako niejawny parametr, więc zaczynają go używać tak, jakby był to parametr jawny. To nie jest. Kiedy wyłuskujesz wartość null this, wywołujesz UB tak, jakbyś wyłuskiwał dowolny inny wskaźnik zerowy. To wszystko. Jeśli chcesz przekazać nullptrs, użyj jawnego parametru DUH . Nie będzie wolniejszy, nie będzie trudniejszy, a kod, który ma takie API, i tak jest głęboko zakorzeniony w wewnętrznych elementach, więc ma bardzo ograniczony zakres. Myślę, że koniec historii.Odpowiedzi:
Chyba pytanie, na które należy odpowiedzieć, dlaczego ludzie o dobrych intencjach wypisywali czeki w pierwszej kolejności.
Najczęstszym przypadkiem jest prawdopodobnie sytuacja, w której masz klasę, która jest częścią naturalnie występującego wywołania rekurencyjnego.
Gdybyś miał:
w C możesz napisać:
W C ++ dobrze jest uczynić z tego funkcję składową:
We wczesnych dniach C ++ (przed standaryzacją) podkreślano, że funkcje składowe były cukrem syntaktycznym dla funkcji, w której
this
parametr jest niejawny. Kod został napisany w C ++, przekonwertowany na odpowiednik C i skompilowany. Były nawet wyraźne przykłady, że porównywaniethis
do wartości null było znaczące, a oryginalny kompilator Cfront również to wykorzystał. Więc wychodząc z tła C, oczywistym wyborem do sprawdzenia jest:Uwaga: Bjarne Stroustrup nawet wspomina, że zasady
this
zostały zmienione przez lata tutajI to działało na wielu kompilatorach przez wiele lat. Kiedy nastąpiła standaryzacja, to się zmieniło. Niedawno kompilatory zaczęły wykorzystywać wywoływanie funkcji składowej, w której
this
istnienienullptr
jest niezdefiniowanym zachowaniem, co oznacza, że ten warunek jest zawszefalse
, a kompilator może go pominąć.Oznacza to, że aby przejść przez to drzewo, musisz:
Przed zadzwonieniem wykonaj wszystkie testy
traverse_in_order
Oznacza to również sprawdzanie w KAŻDEJ witrynie wywołania, czy możesz mieć zerowy root.
Nie używaj funkcji członkowskiej
Oznacza to, że piszesz stary kod w stylu C (być może jako metoda statyczna) i wywołujesz go z obiektem jawnie jako parametr. na przykład. wrócisz do pisania,
Node::traverse_in_order(node);
a nienode->traverse_in_order();
do strony telefonicznej.Uważam, że najłatwiejszym / najdelikatniejszym sposobem naprawienia tego konkretnego przykładu w sposób zgodny ze standardami jest faktyczne użycie węzła wartowniczego, a nie
nullptr
.Żadna z dwóch pierwszych opcji nie wydaje się być atrakcyjna i chociaż kod mógł sobie z tym poradzić, napisali zły kod za pomocą
this == nullptr
zamiast używać odpowiedniej poprawki.Zgaduję, że w ten sposób niektóre z tych baz kodu ewoluowały, aby mieć
this == nullptr
w sobie kontrole.źródło
1 == 0
być niezdefiniowane zachowanie? Po prostufalse
.this == nullptr
idiom jest niezdefiniowanym zachowaniem, ponieważ wcześniej wywołałeś funkcję składową na obiekcie nullptr, co jest niezdefiniowane. Kompilator może pominąć sprawdzaniethis
, że kiedykolwiek zostanie dopuszczony do wartości null. Myślę, że może to jest korzyść z nauki C ++ w wieku, w którym istnieje SO, aby zakorzenić niebezpieczeństwa UB w moim mózgu i odwieść mnie od robienia takich dziwacznych hacków.Dzieje się tak, ponieważ „praktyczny” kod został uszkodzony i początkowo obejmował niezdefiniowane zachowanie. Nie ma powodu, aby używać wartości null
this
, poza mikro-optymalizacją, zwykle bardzo przedwczesną.Jest to niebezpieczna praktyka, ponieważ dostosowanie wskaźników z powodu przechodzenia przez hierarchię klas może zmienić wartość zerową
this
w wartość inną niż zerowa. Tak więc przynajmniej klasa, której metody mają działać z wartością null,this
musi być klasą końcową bez klasy bazowej: nie może z niczego pochodzić i nie można jej wyprowadzić. Szybko przechodzimy od praktycznego do brzydkiego hackowania .W praktyce kod nie musi być brzydki:
Jeśli drzewo jest puste, czyli null
Node* root
, nie powinno się na nim wywoływać żadnych metod niestatycznych. Kropka. Całkiem dobrze jest mieć kod drzewa podobny do C, który pobiera wskaźnik instancji przez jawny parametr.Wydaje się, że ten argument sprowadza się do konieczności pisania niestatycznych metod na obiektach, które mogą być wywoływane ze wskaźnika instancji o wartości null. Nie ma takiej potrzeby. Sposób pisania takiego kodu w języku C-with-Object jest nadal o wiele przyjemniejszy w świecie C ++, ponieważ może być co najmniej bezpieczny dla typów. Zasadniczo, null
this
to taka mikro-optymalizacja, z tak wąskim obszarem zastosowania, że odrzucenie jej jest całkowicie w porządku. Żaden publiczny interfejs API nie powinien zależeć od wartości nullthis
.źródło
this
kontrole są zbierane przez różne analizatory kodu statyczne, więc to nie jest tak, jakby ktoś musi ręcznie polować je wszystkie. Łatka zawierałaby prawdopodobnie kilkaset linii trywialnych zmian.this
dereferencja to natychmiastowa awaria. Te problemy zostaną wykryte bardzo szybko, nawet jeśli nikomu nie zależy na uruchomieniu statycznego analizatora kodu. W języku C / C ++ obowiązuje zasada „płać tylko za funkcje, których używasz”. Jeśli chcesz sprawdzić, musisz o nich wyraźnie powiedzieć, a to oznacza, że nie należy ich wykonywaćthis
, gdy jest za późno, ponieważ kompilator zakłada, żethis
nie są puste. W przeciwnym razie musiałby sprawdzićthis
, a dla 99,9999% kodu takie sprawdzenia są stratą czasu.Dokument nie nazywa tego niebezpiecznym. Nie twierdzi też, że łamie zaskakującą ilość kodu . Wskazuje po prostu kilka popularnych baz kodu, które, jak twierdzi, polegają na tym niezdefiniowanym zachowaniu i które mogłyby się zepsuć z powodu zmiany, chyba że zostanie użyta opcja obejścia.
Jeśli praktyczny kod C ++ opiera się na niezdefiniowanym zachowaniu, zmiany tego niezdefiniowanego zachowania mogą je złamać. Dlatego należy unikać UB, nawet jeśli program, na którym się ono opiera, wydaje się działać zgodnie z przeznaczeniem.
Nie wiem, czy jest to szeroko rozpowszechniony anty- wzorzec, ale niedoinformowany programista może pomyśleć, że może naprawić awarię programu, wykonując:
Gdy rzeczywisty błąd usuwa odwołanie do pustego wskaźnika w innym miejscu.
Jestem pewien, że jeśli programista jest niedoinformowany, będzie mógł wymyślić bardziej zaawansowane (anty) wzorce, które opierają się na tym UB.
Mogę.
źródło
if(this == null) PrintSomeHelpfulDebugInformationAboutHowWeGotHere();
Na przykład ładny, łatwy do odczytania dziennik sekwencji zdarzeń, o których debugger nie może łatwo powiedzieć. Baw się dobrze debugując to teraz, bez spędzania godzin na umieszczaniu sprawdzeń wszędzie, gdy w dużym zbiorze danych pojawia się nagła, losowa wartość zerowa, w kodzie, którego nie napisałeś ... A reguła UB dotycząca tego została utworzona później, po utworzeniu C ++. Kiedyś było ważne.-fsanitize=null
jest.-fsanitize=null
rejestrować błędy na karcie SD / MMC na pinach # 5,6,10,11 za pomocą SPI? To nie jest uniwersalne rozwiązanie. Niektórzy argumentowali, że dostęp do obiektu zerowego jest sprzeczny z zasadami zorientowania obiektowego, ale niektóre języki OOP mają obiekt zerowy, na którym można operować, więc nie jest to uniwersalna zasada OOP. 1/2Niektóre z „praktycznych” (zabawnych zapisów „błędny”) kod, który został uszkodzony, wyglądały następująco:
i zapomniał wziąć pod uwagę fakt, że
p->bar()
czasami zwraca pusty wskaźnik, co oznacza, że wyłuskiwanie odwołania do wywołaniabaz()
jest niezdefiniowane.Nie cały uszkodzony kod zawierał jawne
if (this == nullptr)
lubif (!p) return;
kontrole. Niektóre przypadki były po prostu funkcjami, które nie miały dostępu do żadnych zmiennych składowych, więc wydawało się, że działają poprawnie. Na przykład:W tym kodzie, gdy wywołujesz
func<DummyImpl*>(DummyImpl*)
ze wskaźnikiem zerowym, występuje „koncepcyjne” wyłuskiwanie wskaźnika do wywołaniap->DummyImpl::valid()
, ale w rzeczywistości funkcja składowa po prostu zwracafalse
bez dostępu*this
. Toreturn false
może być wbudowane, więc w praktyce nie ma potrzeby uzyskiwania dostępu do wskaźnika. Tak więc z niektórymi kompilatorami wydaje się, że działa OK: nie ma segfaulta dla wyłuskiwania null,p->valid()
jest fałszem, więc kod wywołujedo_something_else(p)
, który sprawdza puste wskaźniki, więc nic nie robi. Nie zaobserwowano awarii ani nieoczekiwanego zachowania.W GCC 6 nadal otrzymujesz wywołanie
p->valid()
, ale teraz kompilator wnioskuje z tego wyrażenia, którep
musi być niezerowe (w przeciwnym raziep->valid()
byłoby niezdefiniowane zachowanie) i odnotowuje te informacje. Wywnioskować, że informacje te są wykorzystywane przez optymalizator tak, że jeśli wywołaniedo_something_else(p)
zostanie inlined Theif (p)
wyboru jest obecnie uważany za zbędne, ponieważ kompilator pamięta, że nie jest zerowa, a więc inlines kod do:To teraz naprawdę wyłuskuje pusty wskaźnik, więc kod, który wcześniej wydawał się działać, przestaje działać.
W tym przykładzie występuje błąd
func
, który powinien był najpierw sprawdzić, czy nie ma null (lub wywołujący nie powinni byli nigdy wywołać go z null):Ważną kwestią do zapamiętania jest to, że większość optymalizacji tego typu nie dotyczy kompilatora mówiącego „ach, programista przetestował ten wskaźnik pod kątem wartości null, usunę go tylko po to, aby był irytujący”. Dzieje się tak, że różne typowe optymalizacje, takie jak inlining i propagacja zakresu wartości, łączą się, aby te sprawdzenia były zbędne, ponieważ pojawiają się po wcześniejszym sprawdzeniu lub dereferencji. Jeśli kompilator wie, że wskaźnik jest różny od null w punkcie A w funkcji, a wskaźnik nie jest zmieniany przed późniejszym punktem B w tej samej funkcji, to wie, że w punkcie B również nie jest zerowy. punkty A i B mogą w rzeczywistości być fragmentami kodu, które pierwotnie znajdowały się w osobnych funkcjach, ale teraz są połączone w jeden fragment kodu, a kompilator może zastosować swoją wiedzę, że wskaźnik nie jest zerowy w wielu miejscach.
źródło
this
?this
do null] " ?this
, po prostu użyj-fsanitize=undefined
Standard C ++ został złamany na wiele ważnych sposobów. Niestety, zamiast chronić użytkowników przed tymi problemami, programiści GCC zdecydowali się użyć niezdefiniowanego zachowania jako pretekstu do wdrożenia marginalnych optymalizacji, nawet jeśli zostało im jasno wyjaśnione, jak szkodliwe jest to.
Tutaj o wiele mądrzejsza osoba, niż wyjaśniam szczegółowo. (Mówi o C, ale sytuacja jest taka sama).
Dlaczego to jest szkodliwe?
Po prostu przekompilowanie wcześniej działającego, bezpiecznego kodu z nowszą wersją kompilatora może wprowadzić luki w zabezpieczeniach . Chociaż nowe zachowanie można wyłączyć flagą, istniejące pliki makefile oczywiście nie mają ustawionej tej flagi. A ponieważ nie jest wyświetlane żadne ostrzeżenie, dla programisty nie jest oczywiste, że zmieniło się wcześniej rozsądne zachowanie.
W tym przykładzie programista uwzględnił sprawdzenie przepełnienia całkowitoliczbowego, używając polecenia
assert
, które zakończy działanie programu, jeśli podano nieprawidłową długość. Zespół GCC usunął sprawdzanie na podstawie tego, że przepełnienie całkowitoliczbowe jest nieokreślone, dlatego sprawdzenie można usunąć. Spowodowało to, że rzeczywiste in-the-wild instancje tej bazy kodów stały się ponownie podatne na ataki po naprawieniu problemu.Przeczytaj całość. To wystarczy, żebyś płakał.
OK, ale co z tym?
Dawno temu istniał dość powszechny idiom, który wyglądał mniej więcej tak:
Więc idiom brzmi: jeśli
pObj
nie jest null, używasz uchwytu, który zawiera, w przeciwnym razie używasz uchwytu domyślnego. Jest to zawarte wGetHandle
funkcji.Sztuczka polega na tym, że wywołanie funkcji niewirtualnej w rzeczywistości nie wykorzystuje
this
wskaźnika, więc nie ma naruszenia zasad dostępu.Nadal nie rozumiem
Istnieje wiele kodu, który jest napisany w ten sposób. Jeśli ktoś po prostu przekompiluje go bez zmiany linii, każde wywołanie
DoThing(NULL)
jest błędem powodującym awarię - jeśli masz szczęście.Jeśli nie masz szczęścia, wywołania błędów powodujących awarie stają się lukami w zdalnym wykonaniu.
Może to nastąpić nawet automatycznie. Masz zautomatyzowany system budowania, prawda? Aktualizacja do najnowszego kompilatora jest nieszkodliwa, prawda? Ale teraz tak nie jest - nie, jeśli twój kompilator to GCC.
OK, więc powiedz im!
Powiedziano im. Robią to z pełną świadomością konsekwencji.
ale dlaczego?
Kto może powiedzieć? Być może:
A może coś innego. Kto może powiedzieć?
źródło