Ricerca…


introduzione

In C, alcune espressioni producono un comportamento indefinito . Lo standard sceglie esplicitamente di non definire come dovrebbe comportarsi un compilatore se incontra una tale espressione. Di conseguenza, un compilatore è libero di fare tutto ciò che ritiene opportuno e può produrre risultati utili, risultati inaspettati o addirittura incidenti.

Il codice che richiama UB può funzionare come previsto su un sistema specifico con uno specifico compilatore, ma probabilmente non funzionerà su un altro sistema, o con un compilatore, una versione del compilatore o delle impostazioni del compilatore diversi.

Osservazioni

Che cos'è Undefined Behavior (UB)?

Il comportamento non definito è un termine usato nello standard C. Lo standard C11 (ISO / IEC 9899: 2011) definisce il termine comportamento indefinito come

comportamento, sull'uso di un costrutto di programma non portatile o errato o di dati errati, per i quali questo Standard internazionale non impone requisiti

Cosa succede se c'è UB nel mio codice?

Questi sono i risultati che possono verificarsi a causa di un comportamento indefinito secondo lo standard:

NOTA Il possibile comportamento indefinito va dall'ignorare completamente la situazione con risultati imprevedibili, a comportarsi durante la traduzione o l'esecuzione del programma in un modo documentato caratteristico dell'ambiente (con o senza emissione di un messaggio diagnostico), a terminare una traduzione o un'esecuzione (con il emissione di un messaggio diagnostico).

La seguente citazione è spesso usata per descrivere (meno formalmente però) risultati derivanti da un comportamento non definito:

"Quando il compilatore incontra [un dato costrutto indefinito] è legale che faccia volare fuori i demoni dal naso" (l'implicazione è che il compilatore può scegliere qualsiasi modo arbitrariamente bizzarro per interpretare il codice senza violare lo standard ANSI C)

Perché esiste UB?

Se è così brutto, perché non lo hanno appena definito o reso definitivo?

Il comportamento indefinito consente maggiori opportunità di ottimizzazione; Il compilatore può legittimamente presumere che qualsiasi codice non contenga un comportamento indefinito, che può consentirgli di evitare i controlli di run-time ed eseguire ottimizzazioni la cui validità sarebbe costosa o impossibile da dimostrare in altro modo.

Perché UB è difficile da rintracciare?

Ci sono almeno due ragioni per cui un comportamento indefinito crea bug difficili da rilevare:

  • Il compilatore non è tenuto a - e generalmente non può in modo affidabile - avvisarti riguardo al comportamento non definito. In effetti, la sua richiesta di farlo andrebbe direttamente contro la ragione dell'esistenza di un comportamento non definito.
  • I risultati imprevedibili potrebbero non iniziare a svolgersi nel punto esatto dell'operazione in cui si verifica il costrutto il cui comportamento non è definito; Il comportamento indefinito rende l'intera esecuzione e i suoi effetti possono verificarsi in qualsiasi momento: durante, dopo, o anche prima del costrutto indefinito.

Considera la dereferenziazione del puntatore nullo: il compilatore non è obbligato a diagnosticare il dereferenziamento del puntatore nullo, e nemmeno potrebbe farlo, poiché in fase di runtime qualsiasi puntatore passato in una funzione o in una variabile globale potrebbe essere nullo. E quando si verifica la dereferenza del puntatore nullo, lo standard non impone il crash del programma. Piuttosto, il programma potrebbe bloccarsi prima, dopo o non arrestarsi affatto; potrebbe anche comportarsi come se il puntatore nullo indicasse un oggetto valido, e si comportasse in modo completamente normale, solo per bloccarsi in altre circostanze.

Nel caso della dereferenziazione del puntatore nullo, il linguaggio C differisce dai linguaggi gestiti come Java o C #, in cui viene definito il comportamento di dereferenziazione del puntatore nullo: viene generata un'eccezione ( NullPointerException in Java, NullReferenceException in C #) , quindi quelli provenienti da Java o C # potrebbero erroneamente credere che in tal caso, un programma C deve arrestarsi in modo anomalo, con o senza l'emissione di un messaggio diagnostico .

Informazioni aggiuntive

Ci sono diverse situazioni simili che dovrebbero essere chiaramente distinte:

  • Comportamento esplicitamente indefinito, cioè dove lo standard C ti dice esplicitamente che sei fuori dai limiti.
  • Comportamento implicitamente indefinito, in cui non esiste semplicemente un testo nello standard che preveda un comportamento per la situazione in cui hai inserito il tuo programma.

Inoltre, tenere presente che in molti luoghi il comportamento di determinati costrutti è deliberatamente indefinito dallo standard C per lasciare spazio al compilatore e agli implementatori di librerie per elaborare le proprie definizioni. Un buon esempio sono i segnali e i gestori di segnale, in cui le estensioni a C, come lo standard del sistema operativo POSIX, definiscono regole molto più elaborate. In questi casi devi solo controllare la documentazione della tua piattaforma; lo standard C non può dirti nulla.

Si noti inoltre che se si verifica un comportamento non definito nel programma, ciò non significa che solo il punto in cui si è verificato il comportamento non definito è problematico, piuttosto l'intero programma diventa privo di significato.

A causa di tali preoccupazioni è importante (soprattutto dal momento che i compilatori non ci avvisano sempre di UB) perché la programmazione in C di una persona abbia almeno familiarità con il tipo di cose che scatenano un comportamento indefinito.

Va notato che esistono alcuni strumenti (ad esempio strumenti di analisi statica come PC-Lint) che aiutano a rilevare comportamenti non definiti, ma, ancora una volta, non sono in grado di rilevare tutte le occorrenze di comportamenti non definiti.

Dereferenziamento di un puntatore nullo

Questo è un esempio di dereferenziazione di un puntatore NULL, che causa un comportamento indefinito.

int * pointer = NULL;
int value = *pointer; /* Dereferencing happens here */

Un puntatore NULL è garantito dallo standard C per confrontare non uguali a qualsiasi puntatore a un oggetto valido e il dereferenziazione richiama un comportamento non definito.

Modifica di qualsiasi oggetto più di una volta tra due punti di sequenza

int i = 42;
i = i++; /* Assignment changes variable, post-increment as well */
int a = i++ + i--;

Codice come questo spesso porta a speculazioni sul "valore risultante" di i . Piuttosto che specificare un risultato, tuttavia, gli standard C specificano che la valutazione di tale espressione produce un comportamento indefinito . Prima del C2011, lo standard ha formalizzato queste regole in termini di cosiddetti punti di sequenza :

Tra il punto di sequenza precedente e quello successivo, un oggetto scalare deve avere il suo valore memorizzato modificato al massimo una volta dalla valutazione di un'espressione. Inoltre, il valore precedente deve essere letto solo per determinare il valore da memorizzare.

(Standard C99, sezione 6.5, paragrafo 2)

Questo schema si è dimostrato un po 'troppo rozzo, con il risultato che alcune espressioni esibivano un comportamento indefinito rispetto al C99 che plausibilmente non dovrebbe fare. C2011 conserva punti di sequenza, ma introduce un approccio più sfumato a quest'area basato sul sequenziamento e una relazione che chiama "sequenziata prima":

Se un effetto collaterale su un oggetto scalare è ingiustificato rispetto a un effetto collaterale diverso sullo stesso oggetto scalare o un calcolo del valore che utilizza il valore dello stesso oggetto scalare, il comportamento non è definito. Se esistono più ordinamenti consentiti delle sottoespressioni di un'espressione, il comportamento non è definito se si verifica un effetto collaterale non eseguito in uno degli ordini.

(Standard C2011, sezione 6.5, paragrafo 2)

I dettagli completi della relazione "sequenziata prima" sono troppo lunghi per essere descritti qui, ma integrano i punti di sequenza anziché sostituirli, quindi hanno l'effetto di definire il comportamento per alcune valutazioni il cui comportamento precedentemente non era definito. In particolare, se c'è un punto di sequenza tra due valutazioni, allora quella prima del punto di sequenza è "sequenziata prima" di quella successiva.

L'esempio seguente ha un comportamento ben definito:

int i = 42;
i = (i++, i+42); /* The comma-operator creates a sequence point */

L'esempio seguente ha un comportamento non definito:

int i = 42;
printf("%d %d\n", i++, i++); /* commas as separator of function arguments are not comma-operators */

Come in ogni forma di comportamento non definito, l'osservazione del comportamento effettivo della valutazione di espressioni che violano le regole di sequenziamento non è informativa, se non in senso retrospettivo. Lo standard linguistico non fornisce alcuna base per prevedere che tali osservazioni siano predittive anche del comportamento futuro dello stesso programma.

Istruzione di ritorno mancante nella funzione di ritorno del valore

int foo(void) {
  /* do stuff */
  /* no return here */
}

int main(void) {
  /* Trying to use the (not) returned value causes UB */
  int value = foo();
  return 0;
}

Quando una funzione viene dichiarata per restituire un valore, deve farlo su ogni possibile percorso di codice attraverso di essa. Il comportamento non definito si verifica non appena il chiamante (che si aspetta un valore di ritorno) tenta di utilizzare il valore restituito 1 .

Si noti che il comportamento non definito si verifica solo se il chiamante tenta di utilizzare / accedere al valore dalla funzione. Per esempio,

int foo(void) {
  /* do stuff */
  /* no return here */
}

int main(void) {
  /* The value (not) returned from foo() is unused. So, this program
   * doesn't cause *undefined behaviour*. */
  foo();
  return 0;
}
C99

La funzione main() è un'eccezione a questa regola in quanto è possibile che venga terminata senza un'istruzione return poiché in questo caso verrà automaticamente utilizzato un valore di ritorno ipotizzato pari a 0 2 .


1 ( ISO / IEC 9899: 201x , 6.9.1 / 12)

Se viene raggiunto il} che termina una funzione e il chiamante chiama il valore della chiamata della funzione, il comportamento non è definito.

2 ( ISO / IEC 9899: 201x, 5.1.2.2.3 / 1)

raggiungendo il} che termina la funzione principale restituisce un valore di 0.

Overflow intero con segno

Per il paragrafo 6.5 / 5 di C99 e C11, la valutazione di un'espressione produce un comportamento non definito se il risultato non è un valore rappresentabile del tipo dell'espressione. Per i tipi aritmetici, si parla di overflow . L'aritmetica dei numeri interi senza segno non si sovrappone perché si applica il paragrafo 6.2.5 / 9, facendo sì che qualsiasi risultato senza segno che altrimenti sarebbe fuori intervallo venga ridotto a un valore di intervallo. Non v'è alcuna disposizione analoga per i tipi interi firmati, tuttavia; questi possono e superano, producendo un comportamento indefinito. Per esempio,

#include <limits.h>      /* to get INT_MAX */

int main(void) {
    int i = INT_MAX + 1; /* Overflow happens here */
    return 0;
}

La maggior parte delle istanze di questo tipo di comportamento non definito sono più difficili da riconoscere o prevedere. L'overflow può in linea di massima derivare da qualsiasi operazione di addizione, sottrazione o moltiplicazione su interi con segno (soggetti alle consuete conversioni aritmetiche) in cui non esistono limiti effettivi o una relazione tra gli operandi per prevenirla. Ad esempio, questa funzione:

int square(int x) {
    return x * x;  /* overflows for some values of x */
}

è ragionevole, e fa la cosa giusta per valori di argomenti sufficientemente piccoli, ma il suo comportamento non è definito per valori di argomenti più grandi. Non si può giudicare dalla sola funzione se i programmi che lo chiamano mostrano un comportamento indefinito come risultato. Dipende da quali argomenti gli passano.

D'altra parte, si consideri questo banale esempio di aritmetica di interi con segno sicuro overflow:

int zero(int x) {
    return x - x;  /* Cannot overflow */
}

La relazione tra gli operandi dell'operatore di sottrazione assicura che la sottrazione non trabocchi mai. Oppure considera questo esempio un po 'più pratico:

int sizeDelta(FILE *f1, FILE *f2) {
    int count1 = 0;
    int count2 = 0;
    while (fgetc(f1) != EOF) count1++;  /* might overflow */
    while (fgetc(f2) != EOF) count2++;  /* might overflow */

    return count1 - count2; /* provided no UB to this point, will not overflow */
}

Fintanto che i contatori non hanno un overflow individuale, gli operandi della sottrazione finale saranno entrambi non negativi. Tutte le differenze tra due valori di questo tipo sono rappresentabili come int .

Uso di una variabile non inizializzata

int a; 
printf("%d", a);

La variabile a è un int con durata di archiviazione automatica. Il codice di esempio di cui sopra sta tentando di stampare il valore di una variabile non inizializzata ( a non è mai stato inizializzato). Le variabili automatiche non inizializzate hanno valori indeterminati; l'accesso a questi può portare a comportamenti non definiti.

Nota: le variabili con memoria locale statica o thread, comprese le variabili globali senza la parola chiave static , vengono inizializzate su zero o il loro valore inizializzato. Quindi il seguente è legale.

static int b;
printf("%d", b);

Un errore molto comune è quello di non inizializzare le variabili che fungono da contatori a 0. Si aggiungono valori a loro, ma poiché il valore iniziale è spazzatura, si invocherà il comportamento indefinito , come nella domanda La compilazione sul terminale emette un avviso del puntatore e simboli strani .

Esempio:

#include <stdio.h>

int main(void) {
    int i, counter;
    for(i = 0; i < 10; ++i)
        counter += i;
    printf("%d\n", counter);
    return 0;
}

Produzione:

C02QT2UBFVH6-lm:~ gsamaras$ gcc main.c -Wall -o main
main.c:6:9: warning: variable 'counter' is uninitialized when used here [-Wuninitialized]
        counter += i;
        ^~~~~~~
main.c:4:19: note: initialize the variable 'counter' to silence this warning
    int i, counter;
                  ^
                   = 0
1 warning generated.
C02QT2UBFVH6-lm:~ gsamaras$ ./main
32812

Le regole di cui sopra sono applicabili anche per i puntatori. Ad esempio, i seguenti risultati nel comportamento non definito

int main(void)
{
    int *p;
    p++; // Trying to increment an uninitialized pointer.
}

Si noti che il codice precedente potrebbe non causare un errore o un errore di segmentazione, ma provare a dereferenziare questo puntatore in un secondo momento causerebbe un comportamento indefinito.

Dereferenziare un puntatore alla variabile oltre la sua durata

int* foo(int bar)
{
    int baz = 6;
    baz += bar;
    return &baz; /* (&baz) copied to new memory location outside of foo. */
} /* (1) The lifetime of baz and bar end here as they have automatic storage   
   * duration (local variables), thus the returned pointer is not valid! */

int main (void)
{
    int* p;

    p = foo(5);  /* (2) this expression's behavior is undefined */
    *p = *p - 6; /* (3) Undefined behaviour here */

    return 0;
}

Alcuni compilatori lo segnalano utilmente. Ad esempio, gcc avvisa con:

warning: function returns address of local variable [-Wreturn-local-addr]

e clang avverte con:

warning: address of stack memory associated with local variable 'baz' returned 
[-Wreturn-stack-address]

per il codice sopra. Ma i compilatori potrebbero non essere in grado di aiutare nel codice complesso.

(1) Il ritorno del riferimento alla variabile dichiarata static è un comportamento definito, in quanto la variabile non viene distrutta dopo aver abbandonato l'ambito corrente.

(2) Secondo ISO / IEC 9899: 2011 6.2.4 §2, "Il valore di un puntatore diventa indeterminato quando l'oggetto a cui punta raggiunge la fine della sua vita."

(3) Il dereferenziamento del puntatore restituito dalla funzione foo è un comportamento indefinito in quanto la memoria a cui fa riferimento detiene un valore indeterminato.

Divisione per zero

int x = 0;
int y = 5 / x;  /* integer division */

o

double x = 0.0;
double y = 5.0 / x;  /* floating point division */

o

int x = 0;
int y = 5 % x;  /* modulo operation */

Per la seconda riga di ogni esempio, in cui il valore del secondo operando (x) è zero, il comportamento non è definito.

Si noti che la maggior parte delle implementazioni della matematica in virgola mobile seguiranno uno standard (ad es. IEEE 754), nel qual caso le operazioni come lo split-per-zero avranno risultati coerenti (ad esempio INFINITY ) anche se lo standard C dice che l'operazione non è definita.

Accesso alla memoria oltre il blocco assegnato

Un puntatore a un pezzo di memoria contenente n elementi può essere dereferenziato solo se si trova nella memory dell'intervallo e nella memory + (n - 1) . Il dereferenziamento di un puntatore al di fuori di tale intervallo comporta un comportamento indefinito. Ad esempio, considera il seguente codice:

int array[3];
int *beyond_array = array + 3;
*beyond_array = 0; /* Accesses memory that has not been allocated. */

La terza riga accede al quarto elemento in un array che ha una lunghezza di soli 3 elementi, il che porta a un comportamento non definito. Analogamente, anche il comportamento della seconda riga nel seguente frammento di codice non è ben definito:

int array[3];
array[3] = 0;

Si noti che il puntamento oltre l'ultimo elemento di un array non è un comportamento non definito ( beyond_array = array + 3 è ben definito qui), ma il dereferenziamento è ( *beyond_array è un comportamento non definito). Questa regola vale anche per la memoria allocata dinamicamente (come i buffer creati tramite malloc ).

Copia della memoria sovrapposta

Un'ampia varietà di funzioni di libreria standard hanno tra i loro effetti la copia di sequenze di byte da una regione di memoria a un'altra. La maggior parte di queste funzioni ha un comportamento non definito quando le regioni di origine e di destinazione si sovrappongono.

Ad esempio, questo ...

#include <string.h> /* for memcpy() */

char str[19] = "This is an example";
memcpy(str + 7, str, 10);

... tenta di copiare 10 byte in cui le aree di memoria di origine e destinazione si sovrappongono di tre byte. Per visualizzare:

               overlapping area
               |
               _ _
              |   |
              v   v
T h i s   i s   a n   e x a m p l e \0
^             ^
|             |
|             destination
|
source

A causa della sovrapposizione, il comportamento risultante non è definito.

Tra le funzioni di libreria standard con una limitazione di questo tipo sono memcpy() , strcpy() , strcat() , sprintf() e sscanf() . Lo standard dice di queste e di molte altre funzioni:

Se la copia avviene tra oggetti che si sovrappongono, il comportamento non è definito.

La funzione memmove() è l'eccezione principale a questa regola. La sua definizione specifica che la funzione si comporta come se i dati di origine fossero stati prima copiati in un buffer temporaneo e quindi scritti nell'indirizzo di destinazione. Non ci sono eccezioni per la sovrapposizione di regioni di origine e destinazione, né alcuna necessità per una, quindi memmove() ha un comportamento ben definito in questi casi.

La distinzione riflette un'efficienza vs. compromesso di generalità. La copia di tali funzioni si verifica in genere tra regioni disgiunte della memoria e spesso è possibile sapere al momento dello sviluppo se una determinata istanza di copia della memoria si troverà in quella categoria. Supponendo che la non sovrapposizione offra implementazioni relativamente più efficienti che non producono in modo affidabile risultati corretti quando l'ipotesi non regge. La maggior parte delle funzioni della libreria C è permessa le implementazioni più efficienti e memmove() riempie gli spazi vuoti, servendo i casi in cui l'origine e la destinazione possono o si sovrappongono. Tuttavia, per produrre l'effetto corretto in tutti i casi, è necessario eseguire ulteriori test e / o impiegare un'implementazione relativamente meno efficiente.

Leggere un oggetto non inizializzato che non è supportato dalla memoria

C11

La lettura di un oggetto causerà un comportamento indefinito, se l'oggetto è 1 :

  • inizializzata
  • definito con durata di archiviazione automatica
  • il suo indirizzo non è mai stato preso

La variabile a nell'esempio seguente soddisfa tutte queste condizioni:

void Function( void )
{
    int a;
    int b = a;
} 

1 (Citato da: ISO: IEC 9899: 201X 6.3.2.1 Lvalues, matrici e designatori di funzioni 2)
Se il lvalue designa un oggetto di durata di archiviazione automatica che avrebbe potuto essere dichiarato con la classe di archiviazione del registro (non ha mai avuto l'indirizzo preso), e quell'oggetto non è inizializzato (non dichiarato con un inizializzatore e non è stato eseguito alcun incarico prima dell'uso ), il comportamento non è definito.

Gara di dati

C11

C11 ha introdotto il supporto per più thread di esecuzione, che offre la possibilità di gare di dati. Un programma contiene una corsa di dati se si accede a un oggetto in esso 1 da due thread diversi, in cui almeno uno degli accessi è non atomico, almeno uno modifica l'oggetto e la semantica del programma non riesce a garantire che i due accessi non si sovrappongano temporalmente. 2 Si noti bene che la concomitanza effettiva degli accessi coinvolti non è una condizione per una corsa di dati; le gare di dati coprono una più ampia classe di problemi derivanti da (ammesse) incoerenze nelle diverse visualizzazioni di memoria dei thread.

Considera questo esempio:

#include <threads.h>

int a = 0;

int Function( void* ignore )
{
    a = 1;

    return 0;
}

int main( void )
{
    thrd_t id;
    thrd_create( &id , Function , NULL );

    int b = a;

    thrd_join( id , NULL );
}

I principali thread chiama thrd_create per iniziare una nuova funzione di thread in esecuzione Function . Il secondo thread modifica a , e il thread principale legge a . Nessuno di questi accessi è atomico, ei due thread non fanno nulla né individualmente né congiuntamente per garantire che non si sovrappongano, quindi c'è una corsa ai dati.

Tra i modi in cui questo programma potrebbe evitare la corsa dei dati sono

  • il thread principale potrebbe eseguire la lettura di a prima di iniziare l'altro thread;
  • il thread principale potrebbe eseguire la sua lettura di a dopo assicurandosi tramite thrd_join che l'altro è terminato;
  • i thread potevano sincronizzare i loro accessi tramite un mutex, ognuno bloccando quel mutex prima di accedere a e sbloccarlo in seguito.

Come dimostra l'opzione mutex, evitare una corsa di dati non richiede di garantire uno specifico ordine di operazioni, come ad esempio il thread figlio che modifica a prima che il thread principale lo legga; è sufficiente (per evitare una corsa di dati) assicurarsi che per una determinata esecuzione, un accesso avverrà prima dell'altro.


1 Modifica o lettura di un oggetto.

2 (Citato da ISO: IEC 9889: 201x, sezione 5.1.2.4 "Esecuzioni multi-threaded e corse di dati")
L'esecuzione di un programma contiene una corsa di dati se contiene due azioni in conflitto in thread diversi, almeno uno dei quali non è atomico, e nessuno dei due avviene prima dell'altro. Qualsiasi razza di dati di questo genere ha un comportamento indefinito.

Leggi il valore del puntatore che è stato liberato

Anche solo la lettura del valore di un puntatore che è stato liberato (cioè senza cercare di dereferenziare il puntatore) è un comportamento indefinito (UB), ad es.

char *p = malloc(5);
free(p);
if (p == NULL) /* NOTE: even without dereferencing, this may have UB */
{

}

Citando ISO / IEC 9899: 2011 , sezione 6.2.4 §2:

[...] Il valore di un puntatore diventa indeterminato quando l'oggetto a cui punta (o appena passato) raggiunge la fine della sua vita.

L'uso della memoria indeterminata per qualsiasi cosa, incluso il confronto apparentemente innocuo o l'aritmetica, può avere un comportamento indefinito se il valore può essere una rappresentazione trap per il tipo.

Modifica la stringa letterale

In questo esempio di codice, il puntatore char p viene inizializzato all'indirizzo di una stringa letterale. Il tentativo di modificare la stringa letterale ha un comportamento indefinito.

char *p = "hello world";
p[0] = 'H'; // Undefined behavior

Tuttavia, modificare un array mutevole di char direttamente o tramite un puntatore non è naturalmente un comportamento indefinito, anche se il suo inizializzatore è una stringa letterale. Quanto segue va bene:

char a[] = "hello, world";
char *p = a;

a[0] = 'H';
p[7] = 'W';

Questo perché la stringa letterale viene effettivamente copiata nell'array ogni volta che l'array viene inizializzato (una volta per le variabili con durata statica, ogni volta che la matrice viene creata per le variabili con durata automatica o di thread - le variabili con durata allocata non vengono inizializzate) e va bene per modificare i contenuti dell'array.

Liberare memoria due volte

Liberare la memoria due volte è un comportamento indefinito, ad es

int * x = malloc(sizeof(int));
*x = 9;
free(x);
free(x);

Citazione dallo standard (7.20.3.2. La funzione libera di C99):

Altrimenti, se l'argomento non corrisponde a un puntatore precedentemente restituito dalla funzione calloc, malloc o realloc, o se lo spazio è stato deallocato da una chiamata a free o realloc, il comportamento non è definito.

Utilizzo di un identificatore di formato errato in printf

L'utilizzo di un identificatore di formato non corretto nel primo argomento di printf richiama il comportamento non definito. Ad esempio, il codice seguente richiama il comportamento non definito:

long z = 'B';
printf("%c\n", z);

Ecco un altro esempio

printf("%f\n",0);

La linea superiore del codice è un comportamento non definito. %f aspetta il doppio. Tuttavia 0 è di tipo int .

Nota che il tuo compilatore di solito può aiutarti a evitare casi come questi, se attivi i flag appropriati durante la compilazione ( -Wformat in clang e gcc ). Dall'ultimo esempio:

warning: format specifies type 'double' but the argument has type
      'int' [-Wformat]
    printf("%f\n",0);
            ~~    ^
            %d

La conversione tra i tipi di puntatore produce risultati allineati in modo errato

Quanto segue potrebbe avere un comportamento indefinito a causa di un allineamento errato del puntatore:

 char *memory_block = calloc(sizeof(uint32_t) + 1, 1);
 uint32_t *intptr = (uint32_t*)(memory_block + 1);  /* possible undefined behavior */
 uint32_t mvalue = *intptr;

Il comportamento non definito si verifica quando il puntatore viene convertito. Secondo C11, se una conversione tra due tipi di puntatore produce un risultato allineato in modo errato (6.3.2.3), il comportamento non è definito . Qui un uint32_t potrebbe richiedere l'allineamento di 2 o 4, ad esempio.

calloc d'altra parte è necessario per restituire un puntatore che sia adeguatamente allineato per qualsiasi tipo di oggetto; quindi memory_block è correttamente allineato per contenere un uint32_t nella sua parte iniziale. Quindi, su un sistema in cui uint32_t ha richiesto l'allineamento di 2 o 4, memory_block + 1 sarà un indirizzo dispari e quindi non correttamente allineato.

Osservare che lo standard C richiede che l'operazione di cast sia già definita. Questo è imposto perché su piattaforme in cui gli indirizzi sono segmentati, l'indirizzo di byte memory_block + 1 potrebbe non avere nemmeno una rappresentazione corretta come puntatore intero.

Lanciare char * a puntatori ad altri tipi senza alcuna preoccupazione per i requisiti di allineamento viene talvolta erroneamente utilizzato per la decodifica di strutture compresse come intestazioni di file o pacchetti di rete.

È possibile evitare il comportamento non definito derivante dalla conversione del puntatore disallineata utilizzando memcpy :

memcpy(&mvalue, memory_block + 1, sizeof mvalue);

Qui non avviene alcuna conversione del puntatore a uint32_t* e i byte vengono copiati uno per uno.

Questa operazione di copia per il nostro esempio porta solo al valore valido del valore mvalue perché:

  • Abbiamo usato calloc , quindi i byte sono inizializzati correttamente. Nel nostro caso tutti i byte hanno valore 0 , ma qualsiasi altra inizializzazione corretta dovrebbe fare.
  • uint32_t è un tipo di larghezza esatta e non ha bit di riempimento
  • Qualsiasi modello di bit arbitrario è una rappresentazione valida per qualsiasi tipo senza segno.

Aggiunta o sottrazione del puntatore non propriamente limitato

Il seguente codice ha un comportamento non definito:

char buffer[6] = "hello";
char *ptr1 = buffer - 1;  /* undefined behavior */
char *ptr2 = buffer + 5;  /* OK, pointing to the '\0' inside the array */
char *ptr3 = buffer + 6;  /* OK, pointing to just beyond */
char *ptr4 = buffer + 7;  /* undefined behavior */

Secondo C11, se l'addizione o la sottrazione di un puntatore in, o appena oltre, un oggetto array e un tipo intero produce un risultato che non punta, o appena oltre, lo stesso oggetto array, il comportamento non è definito (6.5.6 ).

Inoltre è naturalmente un comportamento indefinito a dereferenziare un puntatore che punta appena oltre l'array:

char buffer[6] = "hello";
char *ptr3 = buffer + 6;  /* OK, pointing to just beyond */
char value = *ptr3;       /* undefined behavior */

Modifica di una variabile const mediante un puntatore

int main (void)
{
    const int foo_readonly = 10;
    int *foo_ptr;

    foo_ptr = (int *)&foo_readonly; /* (1) This casts away the const qualifier */
    *foo_ptr = 20; /* This is undefined behavior */

    return 0;
}

Citando ISO / IEC 9899: 201x , sezione 6.7.3 §2:

Se viene effettuato un tentativo di modificare un oggetto definito con un tipo qualificato const tramite l'utilizzo di un valore lvalue con tipo non const-qualificato, il comportamento non è definito. [...]


(1) In GCC questo può lanciare il seguente avvertimento: warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]

Passare un puntatore nullo alla conversione di printf% s

La conversione %s di printf indica che l'argomento corrispondente è un puntatore all'elemento iniziale di una matrice di tipo carattere . Un puntatore nullo non punta all'elemento iniziale di qualsiasi matrice di tipo di carattere e pertanto il comportamento di quanto segue non è definito:

char *foo = NULL;
printf("%s", foo); /* undefined behavior */

Tuttavia, il comportamento non definito non sempre significa che il programma si arresta in modo anomalo: alcuni sistemi adottano misure per evitare l'arresto anomalo che normalmente si verifica quando un puntatore nullo viene dereferenziato. Ad esempio, Glibc è noto per la stampa

(null)

per il codice sopra. Tuttavia, aggiungi (solo) una nuova riga alla stringa di formato e otterrai un arresto anomalo:

char *foo = 0;
printf("%s\n", foo); /* undefined behavior */

In questo caso, succede perché GCC ha un'ottimizzazione che trasforma printf("%s\n", argument); in una chiamata a puts con puts(argument) e puts in Glibc non gestisce i puntatori nulli. Tutto questo comportamento è conforme allo standard.

Si noti che il puntatore nullo è diverso da una stringa vuota . Quindi, il seguente è valido e non ha un comportamento indefinito. Stamperà solo una nuova riga :

char *foo = "";
printf("%s\n", foo);

Collegamento incoerente di identificatori

extern int var;
static int var; /* Undefined behaviour */

C11, §6.2.2, 7 dice:

Se, all'interno di un'unità di traduzione, lo stesso identificatore appare con il collegamento sia interno che esterno, il comportamento è indefinito.

Si noti che se una dichiarazione preventiva di un identificatore è visibile, avrà il collegamento della dichiarazione precedente. C11, §6.2.2, 4 consente:

Per un identificatore dichiarato con la classe di memoria speci fi catore esterno in uno scope in cui è visibile una dichiarazione preliminare di tale identificatore, 31) se la dichiarazione precedente specifica il collegamento interno o esterno, il collegamento dell'identi fi catore alla dichiarazione successiva è lo stesso di il collegamento speci fi cato alla dichiarazione precedente. Se nessuna dichiarazione precedente è visibile, o se la dichiarazione precedente non specifica alcun collegamento, allora l'identificatore ha un collegamento esterno.

/* 1. This is NOT undefined */
static int var;
extern int var; 


/* 2. This is NOT undefined */
static int var;
static int var; 

/* 3. This is NOT undefined */
extern int var;
extern int var; 

Usando fflush su un flusso di input

Gli standard POSIX e C affermano esplicitamente che l'uso di fflush su un flusso di input è un comportamento indefinito. Il fflush è definito solo per i flussi di output.

#include <stdio.h>

int main()
{
    int i;
    char input[4096];

    scanf("%i", &i);
    fflush(stdin); // <-- undefined behavior
    gets(input);

    return 0;
}

Non esiste un modo standard per scartare i caratteri non letti da un flusso di input. D'altra parte, alcune implementazioni usano fflush per cancellare il buffer di stdin . Microsoft definisce il comportamento di fflush su un flusso di input: se il flusso è aperto per l'input, fflush cancella il contenuto del buffer. Secondo POSIX.1-2008, il comportamento di fflush non è definito a meno che il file di input non sia ricercabile.

Vedi Usare fflush(stdin) per molti più dettagli.

Spostamento di bit usando conteggi negativi o oltre la larghezza del tipo

Se il valore del conteggio dei turni è un valore negativo, entrambe le operazioni di spostamento a sinistra e di spostamento a destra non sono definite 1 :

int x = 5 << -3; /* undefined */
int x = 5 >> -3; /* undefined */

Se lo spostamento a sinistra viene eseguito su un valore negativo , non è definito:

int x = -5 << 3; /* undefined */

Se lo spostamento a sinistra viene eseguito su un valore positivo e il risultato del valore matematico non è rappresentabile nel tipo, non è definito 1 :

/* Assuming an int is 32-bits wide, the value '5 * 2^72' doesn't fit 
 * in an int. So, this is undefined. */
       
int x = 5 << 72;

Si noti che il passaggio a destra su un valore negativo (.eg -5 >> 3 ) non è indefinito ma definito dall'implementazione .


1 Citando ISO / IEC 9899: 201x , sezione 6.5.7:

Se il valore dell'operando di destra è negativo o è maggiore o uguale alla larghezza dell'operando di sinistra promosso, il comportamento non è definito.

Modifica della stringa restituita dalle funzioni getenv, strerror e setlocale

La modifica delle stringhe restituite dalle funzioni standard getenv() , strerror() e setlocale() non è definita. Pertanto, le implementazioni potrebbero utilizzare l'archiviazione statica per queste stringhe.

La funzione getenv (), C11, §7.22.4.7, 4 , dice:

La funzione getenv restituisce un puntatore a una stringa associata al membro della lista corrispondente. La stringa puntata su non deve essere modificata dal programma, ma può essere sovrascritta da una chiamata successiva alla funzione getenv.

La funzione strerror (), C11, §7.23.6.3, 4 dice:

La funzione strerror restituisce un puntatore alla stringa, il cui contenuto è localespeci fi c. L'array puntato su non deve essere modificato dal programma, ma può essere sovrascritto da una chiamata successiva alla funzione strerror.

La funzione setlocale (), C11, §7.11.1.1, 8 dice:

Il puntatore alla stringa restituito dalla funzione setlocale è tale che una chiamata successiva con quel valore stringa e la relativa categoria ripristinerà quella parte delle impostazioni locali del programma. La stringa puntata su non deve essere modificata dal programma, ma può essere sovrascritta da una chiamata successiva alla funzione setlocale.

Allo stesso modo la funzione localeconv() restituisce un puntatore a struct lconv che non deve essere modificato.

La funzione localeconv (), C11, §7.11.2.1, 8 dice:

La funzione localeconv restituisce un puntatore all'oggetto compilato. La struttura indicata dal valore di ritorno non deve essere modificata dal programma, ma può essere sovrascritta da una chiamata successiva alla funzione localeconv.

Di ritorno da una funzione dichiarata con l'identificatore di funzione `_Noreturn` o` noreturn`

C11

Lo specificatore della funzione _Noreturn stato introdotto in C11. L'intestazione <stdnoreturn.h> fornisce una macro noreturn che si espande in _Noreturn . Quindi usare _Noreturn o noreturn da <stdnoreturn.h> va bene ed è equivalente.

Una funzione dichiarata con _Noreturn (o noreturn ) non può tornare al suo chiamante. Se tale funzione non tornare al chiamante, il comportamento è indefinito.

Nell'esempio seguente, func() è dichiarato con specificatore noreturn ma ritorna al suo chiamante.

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void func(void);

void func(void)
{
    printf("In func()...\n");
} /* Undefined behavior as func() returns */

int main(void)
{
    func();
    return 0;
}

gcc e clang producono avvisi per il programma di cui sopra:

$ gcc test.c
test.c: In function ‘func’:
test.c:9:1: warning: ‘noreturn’ function does return
 }
 ^
$ clang test.c
test.c:9:1: warning: function declared 'noreturn' should not return [-Winvalid-noreturn]
}
^

Un esempio di noreturn che ha un comportamento ben definito:

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void my_exit(void);

/* calls exit() and doesn't return to its caller. */
void my_exit(void)
{
    printf("Exiting...\n");
    exit(0);
}

int main(void)
{
    my_exit();
    return 0;
}


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow