Czy legalne jest, aby kod źródłowy zawierający niezdefiniowane zachowanie powodował awarię kompilatora?

85

Powiedzmy, że zamierzam skompilować słabo napisany kod źródłowy w C ++, który wywołuje niezdefiniowane zachowanie, a zatem (jak mówią) „wszystko może się zdarzyć”.

Z punktu widzenia tego, co specyfikacja języka C ++ uważa za dopuszczalne w kompilatorze zgodnym z wymaganiami, robi „cokolwiek” w tym scenariuszu, w tym awarię kompilatora (lub kradzież moich haseł, lub inne niewłaściwe działanie lub błędy w czasie kompilacji) lub zakres niezdefiniowanego zachowania ograniczony konkretnie do tego, co może się stać, gdy wynikowy plik wykonywalny zostanie uruchomiony?

Jeremy Friesner
źródło
22
„UB to UB. Żyj z tym” ... Nie czekaj. „Proszę opublikować MCVE”. ... Nie, czekaj. Uwielbiam to pytanie za wszystkie odruchy, które wyzwala w niewłaściwy sposób. :-)
Yunnosch
14
Naprawdę nie ma ograniczeń, dlatego mówi się, że UB może przywoływać demony nosowe .
Jakiś programista,
15
UB może zmusić autora do wysłania pytania na SO. : P
Tanveer Badar
45
Niezależnie od tego, co mówi standard C ++, gdybym był autorem kompilatora, z pewnością uznałbym to za błąd w moim kompilatorze. Jeśli więc to widzisz, prześlij raport o usterce.
john
9
@LeifWillerts To było w latach 80-tych. Nie pamiętam dokładnej konstrukcji, ale myślę, że opierała się na użyciu zawiłego typu zmiennej. Po założeniu zamiennika miałem moment „o czym myślałem - rzeczy nie działają w ten sposób”. Nie winiłem kompilatora za odrzucenie konstrukcji, tylko za ponowne uruchomienie maszyny. Wątpię, czy ktokolwiek spotkałby się dzisiaj z tym kompilatorem. Był to kompilator krzyżowy HP C dla HP 64000 ukierunkowany na mikroprocesor 68000.
Avi Berger

Odpowiedzi:

71

Normatywna definicja niezdefiniowanego zachowania jest następująca:

[defns.undefined]

zachowanie, dla którego niniejsza Norma Międzynarodowa nie nakłada żadnych wymagań

[Uwaga: można oczekiwać niezdefiniowanego zachowania, gdy w niniejszej Normie Międzynarodowej pomija się jakąkolwiek wyraźną definicję zachowania lub gdy program używa błędnej konstrukcji lub błędnych danych. Dopuszczalne niezdefiniowane zachowanie sięga od całkowitego zignorowania sytuacji z nieprzewidywalnymi skutkami, poprzez zachowanie podczas tłumaczenia lub wykonywania programu w udokumentowany sposób charakterystyczny dla środowiska (z lub bez wydania komunikatu diagnostycznego), aż po zakończenie tłumaczenia lub wykonania (z wydaniem komunikatu diagnostycznego). Wiele błędnych konstrukcji programów nie powoduje nieokreślonego zachowania; wymagana jest diagnoza. Ocena wyrażenia stałego nigdy nie wykazuje zachowania jawnie określonego jako niezdefiniowane. - notatka końcowa]

Chociaż sama notatka nie jest normatywna, opisuje szereg implementacji zachowań, które są znane. Zatem awaria kompilatora (która jest nagłym zakończeniem tłumaczenia) jest zgodna z tą uwagą. Ale tak naprawdę, jak mówi tekst normatywny, standard nie nakłada żadnych ograniczeń ani dla wykonania, ani dla tłumaczenia. Jeśli implementacja kradnie Twoje hasła, nie jest to naruszenie żadnej umowy zawartej w standardzie.

StoryTeller - Unslander Monica
źródło
43
To powiedziawszy, jeśli rzeczywiście możesz sprawić, by kompilator wykonywał dowolny kod w czasie kompilacji, bez żadnego piaskownicy, różni ludzie od bezpieczeństwa byliby bardzo zainteresowani wiedzą o tym. To samo dotyczy segfaultowania kompilatora.
Kevin
67
Tak samo za to, co powiedział Kevin. Jako inżynier kompilatorów C / C ++ / etc w poprzedniej karierze uważaliśmy, że niezdefiniowane zachowanie może spowodować awarię programu , zepsuć dane wyjściowe, podpalić dom, cokolwiek. Jednak kompilator nigdy nie powinien ulec awarii bez względu na dane wejściowe. (Może nie wyświetlać pomocnych komunikatów o błędach, ale powinien wywołać jakiś rodzaj diagnostyki i wyjść, a nie tylko krzyczeć CTHULHU WEŹ KOŁO i segfaulting).
Ti Strga
8
@TiStrga Założę się, że Cthulhu byłby niesamowitym kierowcą F1.
zeta-band
35
„Jeśli implementacja kradnie Twoje hasła, nie jest to naruszenie żadnej umowy zawartej w standardzie”. To prawda niezależnie od tego, czy kod ma UB, prawda? Standard tylko dyktuje, co powinien zrobić skompilowany program - kompilator, który poprawnie kompiluje kod, ale kradnie twoje hasła w procesie, nie byłby niezgodny ze standardem.
Carmeister
8
@Carmeister, oooh, to dobra uwaga, przypomnę o tym wszystkim, gdy pojawią się argumenty „UB zezwala kompilatorowi na rozpoczęcie wojny nuklearnej”. Jeszcze raz.
ilkkachu
8

Większość rodzajów UB, o które zwykle się martwimy, takich jak NULL-deref lub dzielenie przez zero, to UB środowiska uruchomieniowego . Kompilowanie funkcji, która spowodowałaby UB w czasie wykonywania, gdyby została wykonana, nie może powodować awarii kompilatora. Chyba że może udowodnić, że funkcja (i ta ścieżka przez funkcję) z pewnością zostanie wykonana przez program.

(Druga myśl: może nie uważałem, że szablon / constexpr wymaga oceny w czasie kompilacji. Możliwe, że UB podczas tego może powodować dowolną dziwność podczas tłumaczenia, nawet jeśli wynikowa funkcja nigdy nie jest wywoływana.)

Zachowanie podczas tłumaczenia części cytatu ISO C ++ w odpowiedzi @ StoryTeller jest podobne do języka używanego w standardzie ISO C. C nie zawiera szablonów ani constexprobowiązkowej ewaluacji w czasie kompilacji.

Ale zabawny fakt : ISO C mówi w notatce, że jeśli tłumaczenie zostanie zakończone, musi zawierać komunikat diagnostyczny. Lub „zachowując się podczas tłumaczenia ... w udokumentowany sposób”. Nie sądzę, aby „całkowite zignorowanie sytuacji” można było odczytać jako zatrzymanie tłumaczenia.


Stara odpowiedź, napisana zanim dowiedziałem się o UB czasu tłumaczenia. Jest to jednak prawdziwe dla runtime-UB, a zatem potencjalnie nadal użyteczne.


Nie ma czegoś takiego jak UB, co dzieje się w czasie kompilacji. Może to być widoczne dla kompilatora na określonej ścieżce wykonania, ale w terminologii C ++ nie miało to miejsca, dopóki wykonanie nie osiągnie tej ścieżki wykonania za pośrednictwem funkcji.

Błędy w programie, które uniemożliwiają nawet kompilację, to nie UB, lecz błędy składniowe. Taki program jest „źle sformułowany” w terminologii C ++ (jeśli mam poprawny standard). Program może być dobrze sformułowany, ale zawiera UB. Różnica między niezdefiniowanym zachowaniem a źle sformułowanym, nie jest wymagana żadna wiadomość diagnostyczna

O ile czegoś nie rozumiem, ISO C ++ wymaga, aby ten program poprawnie się skompilował i wykonał, ponieważ wykonanie nigdy nie osiąga dzielenia przez zero. (W praktyce ( Godbolt ), dobre kompilatory po prostu tworzą działające pliki wykonywalne. Gcc / clang ostrzegają, x / 0ale nie przed tym, nawet podczas optymalizacji. Ale i tak próbujemy określić, jak niski ISO C ++ pozwala na jakość implementacji. Więc sprawdzanie gcc / clang nie jest użytecznym testem poza potwierdzeniem, że napisałem program poprawnie.)

int cause_UB() {
    int x=0;
    return 1 / x;      // UB if ever reached.
 // Note I'm avoiding  x/0  in case that counts as translation time UB.
 // UB still obvious when optimizing across statements, though.
}

int main(){
    if (0)
        cause_UB();
}

Przykład użycia może obejmować preprocesor C lub constexprzmienne i rozgałęzianie się na tych zmiennych, co prowadzi do bzdur na niektórych ścieżkach, które nigdy nie są osiągane dla tych wyborów stałych.

Można założyć, że ścieżki wykonania, które powodują, że UB jest widoczny w czasie kompilacji, nigdy nie zostaną podjęte, np. Kompilator dla x86 może wyemitować ud2(powodując wyjątek niedozwolonej instrukcji) jako definicję dla cause_UB(). Lub w ramach funkcji, jeśli jedna strona if()prowadzi do możliwego do udowodnienia UB, gałąź można usunąć.

Ale kompilator nadal musi skompilować wszystko inne w rozsądny i poprawny sposób. Wszystkie ścieżki, które nie napotykają (lub nie można udowodnić, że napotykają) UB muszą być nadal skompilowane do asm, który wykonuje się tak, jakby działał na abstrakcyjnej maszynie C ++.


Można argumentować, że bezwarunkowy UB widoczny w czasie kompilacji w programie mainjest wyjątkiem od tej reguły. Lub w inny sposób możliwe do udowodnienia w czasie kompilacji, że wykonanie zaczynające się od mainfaktycznie osiąga gwarantowany UB.

Nadal twierdzę, że legalne zachowania kompilatora obejmują wytwarzanie granatu, który eksploduje po uruchomieniu. Lub, co bardziej prawdopodobne, definicja maintego składa się z jednej nielegalnej instrukcji. Twierdzę, że jeśli nigdy nie uruchomisz programu, nie było jeszcze żadnego UB. Sam kompilator nie może eksplodować, IMO.


Funkcje zawierające możliwe lub możliwe do udowodnienia UB wewnątrz gałęzi

UB wzdłuż dowolnej ścieżki wykonania sięga wstecz w czasie, aby „zanieczyścić” cały poprzedni kod. Jednak w praktyce kompilatory mogą skorzystać z tej reguły tylko wtedy, gdy mogą faktycznie udowodnić, że ścieżki wykonania prowadzą do UB widocznego w czasie kompilacji. na przykład

int minefield(int x) {
    if (x == 3) {
        *(char*)nullptr = x/0;
    }

    return x * 5;
}

Kompilator musi zrobić asm, który działa dla wszystkich xinnych niż 3, aż do punktów, w których x * 5powoduje przepełnienie podpisu UB na INT_MIN i INT_MAX. Jeśli ta funkcja nie jest nigdy wywoływana z x==3, program oczywiście nie zawiera UB i musi działać tak, jak napisano.

Równie dobrze moglibyśmy napisać if(x == 3) __builtin_unreachable();w GNU C, aby powiedzieć kompilatorowi, że xzdecydowanie nie jest to 3.

W praktyce kod „pola minowego” jest wszędzie w normalnych programach. np. każde dzielenie przez liczbę całkowitą obiecuje kompilatorowi, że jest ona niezerowa. Każde deref wskaźnika obiecuje kompilatorowi, że nie ma wartości NULL.

Peter Cordes
źródło
3

Co oznacza tutaj „legalny”? Wszystko, co nie jest sprzeczne ze standardem C lub C ++, jest legalne zgodnie z tymi standardami. Jeśli wykonasz polecenie i = i++;i w rezultacie dinozaury przejmą władzę nad światem, nie jest to sprzeczne ze standardami. Jest to jednak sprzeczne z prawami fizyki, więc tak się nie stanie :-)

Jeśli niezdefiniowane zachowanie powoduje awarię kompilatora, nie narusza to standardu C lub C ++. Oznacza to jednak, że jakość kompilatora mogłaby (i prawdopodobnie powinna) ulec poprawie.

W poprzednich wersjach standardu C występowały stwierdzenia, które były błędami lub nie były zależne od nieokreślonego zachowania:

char* p = 1 / 0;

Przypisywanie stałej 0 do znaku * jest dozwolone. Zezwolenie na niezerową stałą nie jest. Ponieważ wartość 1/0 jest niezdefiniowanym zachowaniem, niezdefiniowanym zachowaniem jest to, czy kompilator powinien, czy nie powinien zaakceptować tej instrukcji. (W dzisiejszych czasach 1/0 nie spełnia już definicji „wyrażenia liczb całkowitych stałych”).

gnasher729
źródło
3
A dokładniej: dinozaury przejmujące władzę nad światem nie zaprzeczają żadnym prawom fizyki (np. Wariacji Parku Jurajskiego). Jest to po prostu mało prawdopodobne. :)
dziwaczny