Dlaczego jest niebezpieczny?
goto
sam w sobie nie powoduje niestabilności. Mimo około 100 000 goto
s jądro Linuksa wciąż jest modelem stabilności.
goto
sam w sobie nie powinien powodować luk w zabezpieczeniach. W językach, jednak niektóre zmieszanie go z try
/ catch
wyjątku bloki zarządzania może prowadzić do luk w sposób opisany w niniejszym zaleceniu CERT . Kompilatory głównego nurtu C ++ sygnalizują i zapobiegają takim błędom, ale niestety starsze lub bardziej egzotyczne kompilatory tego nie robią.
goto
powoduje nieczytelny i niemożliwy do utrzymania kod. Nazywa się to również kodem spaghetti , ponieważ podobnie jak na talerzu do spaghetti bardzo trudno jest podążać za kontrolą, gdy jest zbyt wiele goto.
Nawet jeśli uda ci się uniknąć kodu spaghetti i jeśli użyjesz tylko kilku gotów, nadal ułatwiają one takie błędy, jak i wyciekanie zasobów:
- Kod wykorzystujący programowanie struktur z wyraźnymi zagnieżdżonymi blokami i pętlami lub przełącznikami jest łatwy do naśladowania; jego przepływ kontroli jest bardzo przewidywalny. Dlatego łatwiej jest zapewnić przestrzeganie niezmienników.
- Za pomocą
goto
oświadczenia przerywasz ten prosty przepływ i przełamujesz oczekiwania. Na przykład możesz nie zauważyć, że nadal musisz zwolnić zasoby.
- Wiele
goto
w różnych miejscach może wysłać cię do jednego celu goto. Więc nie jest oczywiste, aby wiedzieć na pewno, w jakim jesteś stanie, gdy dotrzesz do tego miejsca. Ryzyko dokonywania błędnych / bezpodstawnych założeń jest zatem dość duże.
Dodatkowe informacje i cytaty:
C zapewnia nieskończenie nadużywające goto
oświadczenie i etykiety, do których można się rozgałęzić. Formalnie goto
nigdy nie jest to konieczne, a w praktyce prawie zawsze łatwo jest napisać kod bez niego. (...)
Niemniej jednak zasugerujemy kilka sytuacji, w których goto może znaleźć miejsce. Najczęstszym zastosowaniem jest porzucenie przetwarzania w niektórych głęboko zagnieżdżonych strukturach, takich jak zerwanie dwóch pętli jednocześnie. (...)
Chociaż nie jesteśmy dogmatami w tej sprawie, wydaje się, że oświadczenia goto powinny być stosowane oszczędnie, jeśli w ogóle .
Kiedy można go użyć?
Podobnie jak K&R, nie jestem dogmatyczny w kwestii gotos. Przyznaję, że są sytuacje, w których goto może ułatwić sobie życie.
Zazwyczaj w C goto pozwala na wielopoziomowe wyjście z pętli lub obsługę błędów wymagającą dotarcia do odpowiedniego punktu wyjścia, który uwalnia / odblokowuje wszystkie zasoby, które zostały przydzielone do tej pory (wielokrotna alokacja w sekwencji oznacza wiele etykiet). W tym artykule opisano różne zastosowania goto w jądrze systemu Linux.
Osobiście wolę tego unikać i za 10 lat C użyłem maksymalnie 10 gotów. Wolę używać zagnieżdżonych if
, które moim zdaniem są bardziej czytelne. Gdyby to prowadziło do zbyt głębokiego zagnieżdżenia, wolałbym albo rozłożyć moją funkcję na mniejsze części, albo użyć wskaźnika logicznego w kaskadzie. Dzisiejsze kompilatory optymalizujące są wystarczająco sprytne, aby wygenerować prawie ten sam kod niż ten sam kod goto
.
Zastosowanie goto w dużej mierze zależy od języka:
W C ++ prawidłowe użycie RAII powoduje, że kompilator automatycznie niszczy obiekty, które wykraczają poza zakres, tak że zasoby / blokada i tak zostaną wyczyszczone, i nie ma już potrzeby używania goto.
W Javie nie ma potrzeby goto (patrz autora cytat Javy powyżej i to doskonałe przepełnieniem stosu odpowiedź ): garbage collector że czyści bałagan break
, continue
i try
/ catch
obsługa wyjątków obejmują wszystkie sprawy, gdzie goto
mogłyby być pomocne, ale w bezpieczniejszym i lepiej sposób. Popularność Javy dowodzi, że oświadczenia goto można uniknąć w nowoczesnym języku.
Powiększ słynną lukę w zabezpieczeniach SSL goto fail
Ważna informacja: z uwagi na zaciętą dyskusję w komentarzach chcę wyjaśnić, że nie udaję, że jedyną przyczyną tego błędu jest instrukcja goto. Nie udaję, że bez goto nie byłoby błędu. Chcę tylko pokazać, że goto może być związane z poważnym błędem.
Nie wiem, z iloma poważnymi błędami wiąże się goto
historia programowania: szczegóły często nie są przekazywane. Był jednak znany błąd Apple SSL, który osłabił bezpieczeństwo iOS. Oświadczenie, które doprowadziło do tego błędu, było błędne goto
.
Niektórzy twierdzą, że główną przyczyną błędu nie była sama instrukcja goto, ale niewłaściwe kopiowanie / wklejanie, wprowadzające w błąd wcięcie, brak nawiasów klamrowych wokół bloku warunkowego lub może nawyki pracy programisty. Nie mogę potwierdzić żadnego z nich: wszystkie te argumenty są prawdopodobnymi hipotezami i interpretacją. Nikt tak naprawdę nie wie. ( tymczasem hipoteza scalenia, która poszła nie tak, jak ktoś sugerował w komentarzach, wydaje się być bardzo dobrym kandydatem, biorąc pod uwagę inne niespójności wcięcia w tej samej funkcji ).
Jedynym obiektywnym faktem jest to, że duplikat goto
doprowadził do przedwczesnego wyjścia z funkcji. Patrząc na kod, jedyną inną instrukcją, która mogła wywołać ten sam efekt, byłby zwrot.
Błąd działa SSLEncodeSignedServerKeyExchange()
w tym pliku :
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
goto fail;
if ((err =...) !=0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail; // <====OUCH: INDENTATION MISLEADS: THIS IS UNCONDITIONDAL!!
if (...)
goto fail;
... // Do some cryptographic operations here
fail:
... // Free resources to process error
Rzeczywiście, nawiasy klamrowe wokół bloku warunkowego mogłyby zapobiec
błędowi : doprowadziłoby to albo do błędu składniowego przy kompilacji (a zatem do korekty), albo do zbędnego nieszkodliwego goto. Nawiasem mówiąc, GCC 6 byłby w stanie wykryć te błędy dzięki opcjonalnemu ostrzeżeniu, aby wykryć niespójne wcięcie.
Ale przede wszystkim tych wszystkich gotów można było uniknąć dzięki bardziej uporządkowanemu kodowi. Więc goto jest przynajmniej pośrednią przyczyną tego błędu. Istnieją co najmniej dwa różne sposoby, aby tego uniknąć:
Podejście 1: jeśli klauzula lub zagnieżdżone if
s
Zamiast sekwencyjnego testowania wielu warunków pod kątem błędu i za każdym razem, gdy wysyłany jest do fail
etykiety w przypadku problemu, można było zdecydować się na wykonanie operacji kryptograficznych w stanie if
, który zrobiłby to tylko, gdyby nie było złego warunku wstępnego:
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
(err = ...) == 0 ) &&
(err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
...
(err = ...) == 0 ) )
{
... // Do some cryptographic operations here
}
... // Free resources
Podejście 2: użyj akumulatora błędów
Podejście to opiera się na fakcie, że prawie wszystkie instrukcje tutaj wywołują jakąś funkcję w celu ustawienia err
kodu błędu i wykonują resztę kodu tylko wtedy, gdy err
wynosi 0 (tj. Funkcja wykonana bez błędu). Dobrą bezpieczną i czytelną alternatywą jest:
bool ok = true;
ok = ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok = ok && (err = NextFunction(...)) == 0;
...
ok = ok && (err = ...) == 0;
... // Free resources
Tutaj nie ma jednego goto: nie ma ryzyka, aby szybko przejść do punktu wyjścia z awarii. A wizualnie łatwo byłoby zauważyć niedopasowaną linię lub zapomnianą ok &&
.
Ta konstrukcja jest bardziej zwarta. Opiera się na fakcie, że w C druga część logicznego i ( &&
) jest oceniana tylko wtedy, gdy pierwsza część jest prawdziwa. W rzeczywistości asembler wygenerowany przez kompilator optymalizacyjny jest prawie równoważny oryginalnemu kodowi z gotos: Optymalizator bardzo dobrze wykrywa łańcuch warunków i generuje kod, który przy pierwszej niezerowej wartości zwrotnej przeskakuje na koniec ( dowód online ).
Można nawet przewidzieć kontrolę spójności na końcu funkcji, która może podczas fazy testowania zidentyfikować niedopasowania między flagą ok a kodem błędu.
assert( (ok==false && err!=0) || (ok==true && err==0) );
Błędy takie jak ==0
przypadkowo zastąpione !=0
błędami lub logicznymi błędami złącza można łatwo wykryć podczas fazy debugowania.
Jak powiedziano: nie udaję, że alternatywne konstrukcje uniknęłyby jakiegokolwiek błędu. Chcę tylko powiedzieć, że mogły sprawić, że błąd będzie trudniejszy do wystąpienia.