È legale che il codice sorgente contenente un comportamento indefinito blocchi il compilatore?


85

Diciamo che vado a compilare un codice sorgente C ++ scritto male che invoca comportamenti indefiniti, e quindi (come si suol dire) "tutto può succedere".

Dal punto di vista di ciò che la specifica del linguaggio C ++ ritiene accettabile in un compilatore "conforme", fa "qualsiasi cosa" in questo scenario include l'arresto anomalo del compilatore (o il furto delle mie password, o comunque comportamenti anomali o errori in fase di compilazione), o è ambito del comportamento indefinito limitato specificamente a cosa può accadere quando viene eseguito l'eseguibile risultante?


22
"UB è UB. Vivi con esso" ... No aspetta. "Pubblica un MCVE." ... Non aspettare. Amo la domanda per tutti i riflessi che innesca in modo inappropriato. :-)
Yunnosch

14
Non c'è davvero alcuna limitazione, motivo per cui si dice che UB possa evocare demoni nasali .
Un tizio programmatore il

15
UB può far pubblicare all'autore una domanda su SO. : P
Tanveer Badar

46
Indipendentemente da ciò che dice lo standard C ++, se fossi uno scrittore di compilatori lo considererei sicuramente un bug nel mio compilatore. Quindi, se stai vedendo questo, invia una segnalazione di difetto.
giovanni

9
@ LeifWillerts Questo era negli anni '80. Non ricordo il costrutto esatto, ma penso che dipenda dall'utilizzo di un tipo di variabile convoluto. Dopo aver inserito un sostituto ho avuto un momento "a cosa stavo pensando - le cose non funzionano in questo modo". Non ho incolpato il compilatore per aver rifiutato il costrutto, ma solo per aver riavviato la macchina. Dubito che qualcuno possa incontrare quel compilatore oggi. Era il compilatore incrociato HP C per HP 64000 destinato al microprocessore 68000.
Avi Berger

Risposte:


71

La definizione normativa di comportamento indefinito è la seguente:

[defns.undefined]

comportamento per il quale la presente norma internazionale non impone requisiti

[Nota: ci si può aspettare un comportamento indefinito quando la presente norma internazionale omette qualsiasi definizione esplicita di comportamento o quando un programma utilizza un costrutto errato o dati errati. Il comportamento indefinito ammissibile va dall'ignorare completamente la situazione con risultati imprevedibili, al comportamento durante la traduzione o l'esecuzione del programma in modo documentato caratteristico dell'ambiente (con o senza l'emissione di un messaggio diagnostico), al termine di una traduzione o esecuzione (con l'emissione di un messaggio diagnostico). Molti costrutti di programma errati non generano comportamenti indefiniti; devono essere diagnosticati. La valutazione di un'espressione costante non mostra mai un comportamento esplicitamente specificato come non definito. - nota finale]

Sebbene la nota in sé non sia normativa, descrive una gamma di implementazioni di comportamenti che è noto esibire. Quindi il crash del compilatore (che sta terminando bruscamente la traduzione), è legittimo secondo quella nota. Ma in realtà, come dice il testo normativo, lo standard non pone limiti né all'esecuzione né alla traduzione. Se un'implementazione ruba le tue password, non è una violazione di alcun contratto previsto dallo standard.


43
Detto questo, se si può effettivamente fare in modo che un compilatore esegua codice arbitrario in fase di compilazione, senza alcun sandboxing, allora vari addetti alla sicurezza sarebbero molto interessati a conoscerlo. Lo stesso vale per il segfault del compilatore.
Kevin

67
Idem per quello che ha detto Kevin. Come ingegnere compilatore C / C ++ / etc in una precedente carriera, la nostra posizione era che un comportamento indefinito poteva mandare in crash il tuo programma , rovinare i tuoi dati di output, dare fuoco alla tua casa, qualunque cosa. Ma il compilatore non dovrebbe mai bloccarsi, indipendentemente dall'input. (Potrebbe non fornire messaggi di errore utili, ma dovrebbe produrre una sorta di diagnostica e uscita piuttosto che urlare CTHULHU PRENDI LA RUOTA e segfault.)
Ti Strga

8
@TiStrga Scommetto che Cthulhu sarebbe un fantastico pilota di F1.
zeta-band

35
"Se un'implementazione ruba le tue password, non è una violazione di alcun contratto previsto dallo standard." Questo è vero indipendentemente dal fatto che il codice abbia UB, non è vero? Lo standard stabilisce solo cosa dovrebbe fare il programma compilato: un compilatore che compila correttamente il codice ma ruba le tue password nel processo non disobbedirebbe allo standard.
Carmeister

8
@Carmeister, oooh, questo è un buon punto, mi assicurerò di ricordarlo alla gente ogni volta che vengono visualizzati gli argomenti "UB dà il permesso al compilatore di iniziare una guerra nucleare". Ancora.
ilkkachu

8

La maggior parte dei tipi di UB di cui ci preoccupiamo solitamente, come NULL-deref o dividere per zero, sono UB a runtime . La compilazione di una funzione che, se eseguita, provocherebbe UB di runtime, non deve causare il crash del compilatore. A meno che non possa provare che la funzione (e quel percorso attraverso la funzione) verrà sicuramente eseguita dal programma.

(2 ° pensiero: forse non ho considerato la valutazione richiesta da template / constexpr in fase di compilazione. Forse durante la compilazione UB può causare stranezze arbitrarie durante la traduzione anche se la funzione risultante non viene mai chiamata.)

Il comportamento durante la traduzione della parte della citazione ISO C ++ nella risposta di @ StoryTeller è simile al linguaggio utilizzato nello standard ISO C. C non include modelli o constexprvalutazione obbligatoria in fase di compilazione.

Ma curiosità : l'ISO C dice in una nota che se la traduzione viene terminata, deve essere con un messaggio diagnostico. O "comportarsi durante la traduzione ... in modo documentato". Non credo che "ignorare completamente la situazione" possa essere interpretato come un arresto della traduzione.


Vecchia risposta, scritta prima che venissi a conoscenza del tempo di traduzione UB. È vero per runtime-UB, tuttavia, e quindi potenzialmente ancora utile.


Non c'è niente come UB che accade in fase di compilazione. Può essere visibile al compilatore lungo un certo percorso di esecuzione, ma in termini C ++ non è avvenuto fino a quando l'esecuzione non raggiunge quel percorso di esecuzione attraverso una funzione.

I difetti in un programma che rendono impossibile persino la compilazione non sono UB, sono errori di sintassi. Un tale programma "non è ben formato" nella terminologia C ++ (se ho il mio standardese corretto). Un programma può essere ben formato ma contenere UB. Differenza tra comportamento indefinito e malformato, nessun messaggio diagnostico richiesto

A meno che io non stia fraintendendo qualcosa, ISO C ++ richiede che questo programma venga compilato ed eseguito correttamente, perché l'esecuzione non raggiunge mai la divisione per zero. (In pratica ( Godbolt ), i buoni compilatori creano solo eseguibili funzionanti. Gcc / clang avverte di x / 0questo, ma non questo, anche durante l'ottimizzazione. Ma comunque, stiamo cercando di dire quanto basso ISO C ++ permetta di essere la qualità dell'implementazione. Quindi controllare gcc / clang non è certo un test utile se non per confermare che ho scritto correttamente il programma.)

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();
}

Un caso d'uso per questo potrebbe coinvolgere il preprocessore C, o constexprvariabili e ramificazioni su quelle variabili, il che porta a sciocchezze in alcuni percorsi che non vengono mai raggiunti per quelle scelte di costanti.

Si può presumere che i percorsi di esecuzione che causano l'UB visibile in fase di compilazione non vengano mai accettati, ad esempio un compilatore per x86 potrebbe emettere ud2un'eccezione (causa un'eccezione illegale) come definizione di cause_UB(). Oppure all'interno di una funzione, se un lato di un if()porta a UB dimostrabile , il ramo può essere rimosso.

Ma il compilatore deve ancora compilare tutto il resto in modo sano e corretto. Tutti i percorsi che non incontrano (o non è possibile provare che incontrino) UB devono comunque essere compilati in asm che viene eseguito come se la macchina astratta C ++ lo stesse eseguendo.


Si potrebbe sostenere che l'UB visibile in fase di compilazione incondizionato in mainè un'eccezione a questa regola. O altrimenti dimostrabile in fase di compilazione che l'esecuzione a partire da mainraggiunge effettivamente UB garantito.

Continuo a sostenere che i comportamenti legali del compilatore includono la produzione di una granata che esplode se eseguita. O più plausibilmente, una definizione di mainciò consiste in un'unica istruzione illegale. Direi che se non esegui mai il programma, non c'è ancora stato alcun UB. Il compilatore stesso non può esplodere, IMO.


Funzioni contenenti UB possibili o dimostrabili all'interno dei rami

UB lungo un dato percorso di esecuzione arriva indietro nel tempo per "contaminare" tutto il codice precedente. Ma in pratica i compilatori possono trarre vantaggio da questa regola solo quando possono effettivamente dimostrare che i percorsi di esecuzione portano a UB visibili in fase di compilazione. per esempio

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

    return x * 5;
}

Il compilatore deve creare un asm che funzioni per tutti gli xaltri 3, fino ai punti in cui x * 5causa l'overflow del segno UB a INT_MIN e INT_MAX. Se questa funzione non viene mai chiamata con x==3, il programma ovviamente non contiene UB e deve funzionare come scritto.

Potremmo anche aver scritto if(x == 3) __builtin_unreachable();in GNU C per dire al compilatore che xsicuramente non è 3.

In pratica c'è codice "minato" ovunque nei normali programmi. ad esempio, qualsiasi divisione per un numero intero promette al compilatore che è diverso da zero. Qualsiasi puntatore deref promette al compilatore che non è NULL.


3

Cosa significa "legale" qui? Tutto ciò che non contraddice lo standard C o lo standard C ++ è legale, secondo questi standard. Se esegui una dichiarazione i = i++;e di conseguenza i dinosauri conquistano il mondo, ciò non contraddice gli standard. Tuttavia contraddice le leggi della fisica, quindi non succederà :-)

Se un comportamento indefinito blocca il compilatore, ciò non viola lo standard C o C ++. Significa comunque che la qualità del compilatore potrebbe (e probabilmente dovrebbe) essere migliorata.

Nelle versioni precedenti dello standard C, c'erano dichiarazioni che erano errori o non dipendevano da un comportamento indefinito:

char* p = 1 / 0;

È consentito assegnare uno 0 costante a un carattere *. Consentire una costante diversa da zero non lo è. Poiché il valore 1/0 è un comportamento indefinito, è un comportamento indefinito se il compilatore debba o meno accettare questa affermazione. (Al giorno d'oggi, 1/0 non soddisfa più la definizione di "espressione costante intera").


4
Per essere precisi: i dinosauri che conquistano il mondo non contraddicono alcuna legge della fisica (es. Variazione di Jurassic Park). È solo altamente improbabile. :)
bizzarro

-1

Lo standard non imporrebbe alcun requisito sul comportamento di un'implementazione se incontra #include "'foo'". Se l'autore del compilatore ritiene che sarebbe utile elaborare direttive di inclusione di quella forma (contenenti gli apostrofi all'interno del nome del file) eseguendo il programma indicato con il suo output diretto a un file temporaneo e quindi comportandosi come un #includefile di quel file, allora un tentativo per elaborare un programma contenente la riga precedente potrebbe eseguire il programma foo, con qualsiasi conseguenza ne derivi.

Quindi, in generale, non ci sono limiti a ciò che potrebbe accadere come conseguenza del tentativo di tradurre un programma in C, anche se non si fa alcuno sforzo per eseguirlo.


Si potrebbe dire la stessa cosa di qualsiasi traduttore o compilatore in qualsiasi linguaggio di programmazione. O, per quella materia, qualsiasi programma qualunque.
Robert Harvey

@RobertHarvey: Molte specifiche del linguaggio di programmazione sono molto più specifiche su queste cose. Se una specifica della lingua dice che una certa direttiva leggerà l'input da un flusso il cui percorso del sistema operativo è come specificato e il sistema operativo fa qualcosa di strano quando legge un certo percorso, ciò sarebbe fuori dal controllo della specifica della lingua, ma non penso che la maggior parte delle specifiche del linguaggio darebbe carta bianca alle implementazioni per elaborare tali direttive in modo arbitrario a loro piacimento, senza doverlo documentare, anche su piattaforme che altrimenti definirebbero il comportamento.
supercat
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.