Archivi delle etichette: nasal demons

Comportamenti indefiniti

ResearchBlogging.org

Il quiz dello scorso post era un pochino a trabocchetto, lo ammetto, ma Alessandro ha fatto un buon lavoro ed è quasi arrivato alla soluzione. Il quiz era il seguente:

Qual’è il comportamento del seguente programmino C?

#include <stdio.h>

int d = 5;

int set(int x)
{
    return d = x;
}

int main()
{
    int r = set(0) + 7 / d;
    printf("risultato = %d\n", r);
    return 1; 
}

E la risposta è

(rullo di tamburi)

il programma ha un comportamento indefinito!

Come, non sapete cos’è un comportamento indefinito? Beh, se programmate in C, è il momento di fare un corso di aggiornamento.

Benvenuti nel fantasmagorico mondo degli Undefined Behaviours!

Se volete una introduzione veloce, al solito c’è wikipedia. Se non ci avete capito molto, vi consiglio questa serie di tre post del bravissimo John Regher (tra l’altro, John ha anche un corso su Udacity, sul testing: se avete tempo di seguirvi un intero corso e volete studiare software testing, il corso di John è quello che fa per voi!).

Se invece volete continuare a leggere, vi spiego in quattro e quattr’otto di cosa stiamo parlando. La semantica del C/C++ è un po lasca, e in certi punti lascia libertà al compilatore. L’esempio classico è quello dell’accesso a un array al di là della sua dimensione. Per esempio, se avete un array di 10 interi, e accedete all’11-esima posizione, che succede? Lo standard C vi dice che, in tal caso, il comportamento del programma è “undefined”.

Che deve fare un compilatore quando si trova di fronte a un comportamento indefinito? Beh, l’interpretazione più diffusa è che il compilatore è libero di fare quello che gli pare. Qualunque cosa, e per “qualunque cosa” intendo proprio qualsiasi. Infatti, un programma che ha un “undefined behaviour” è semanticamente incorretto, e quindi da una cosa scorretta si può tirar fuori qualunque altra cosa, no? Persino dei diavoletti dal naso (nasal demons!).

Torniamo al quiz e vediamo perché il programma ha un comportamento indefinito. In C, l’ordine con cui vengono valutati gli operandi di una espressione non è sempre ben definito. Ci sono regole un pochino complicate, che hanno a che fare con i cosidetti sequence points. In pratica, un sequence point è un punto del programma in cui, prima di proseguire bisogna acertarsi di aver risolto tutti i “side effects” delle istruzioni precedenti. Il problema del nostro programma è che nella fattispecie l’operatore di somma non è un sequence point. Quando scriverte (x + y), il compilatore è libero di calcolare prima x e poi y, oppure viceversa. Di solito, l’ordine di valutazione degli argomenti non è importante: dopo tutto, in matematica l’addizione gode della proprietà commutativa, quindi ci si aspetta che cambiando l’ordine degli addendi il risultato non cambi! Per cui, chi ha scritto lo standard ha lasciato libero il compilatore di eseguire la valutazione degli addendi nell’ordine che preferiva. Per esempio, se per ragioni di ottimizzazione, è meglio valutare prima y e poi x, il compilatore lo farà, perché è libero di farlo, secondo lo standard.

Purtroppo, nel nostro programma, valutare prima la chiamata set(0) e poi 7/d, oppure prima 7/d e poi set(0) cambia il risultato: nel primo caso si ha una bella divisione per 0; nel secondo caso invece il risultato dovrebbe essere 1.
Poiché il risultato non è univoco, ma dipende dall’ordine in cui il compilatore decide di valutare i due operandi, allora il nostro programma ha un “Undefined Behaviour”. In pratica, è scorretto e non aderente allo standard. Tenete anche presente che una cosa è la precedenza fra operatori, e una cosa completamente diversa è l’ordine di valutazione degli operandi: la differenza è spiegata piuttosto bene qui.

Nella pratica, in questo caso il compilatore gcc segue sempre l’ordine di valutazione da sinistra verso destra, per cui se compilate ed eseguite il programma con gcc avrete come risultato un bell’errore di divisione per zero.

Più interessante è il comportamento di un altro compilatore che ultimamente sembra andare per la maggiore: si tratta di clang. Se compilate con il comando:

$ clang prova.c
$ ./a.out
Floating point exception (core dumped)

che è quello che ci si aspetta. Mentre se compilate con le ottimizzazioni:

$ clang -O2 prova.c
$ ./a.out
risultato = 894604808

Il numero che tira fuori è assolutamente casuale! Come mai? Beh, semplice: come detto prima, in presenza di “Undefined Behaviour” tutto può succedere, anche che vi escano i demoni dal naso! Più seriamente: Se il programma è non corretto (ha dei comportamenti indefiniti), il compilatore può tirare fuori eseguibili non corretti, non necessariamente nel modo che ci si aspetta.

Soluzioni?

Adesso la parte seria. Recentemente due ricercatori dell’University of Illinois at Urbana-Champaign (UIUC), Ellison e Rosu, hanno scritto un articolo molto interessante in cui si sono presi la briga di specificare in maniera formale la semantica del linguaggio C, utilizzando un altro linguaggio formale chiamato K. A differenza di altre specifiche formali, quella proposta da Ellison e Rosu è eseguibile, e questo significa che possono prendere un programma C e analizzarlo formalmente, ad esempio alla ricerca di undefined behaviours. Il loro codice è disponibile su googlecode, e teoricamente avrei potuto provarlo sul nostro programmino di esempio. (Ci ho provato per un po’ ma purtroppo il processo di compilazione mi si ferma a metà, devo aver sbagliato qualcosa. Se ci riesco ve lo faccio sapere.) Comunque, il programma dei nostri due ricercatori dovrebbe identificare la presenza di un undefined behaviour nel nostro programmino di quiz e metterlo bene in evidenza.

Il lavoro di Ellison e Rosu è molto importante: dato che molto codice per sistemi embedded e sistemi critici è ancora scritto in C, spesso in maniera euristica o artigianale, avere a disposizione strumenti automatici di verifica è essenziale per l’eliminazione di errori che potrebbero addirittura mettere a rischio la nostra sicurezza. La mia previsione è che nel futuro sentiremo parlare sempre di più di strumenti di verifica formale come assistenza alla programmazione.

Chucky Ellison, Grigore Rosu (2012). An executable formal semantics of C with applications ACM SIGPLAN Notices, 47 (1), 533-544 : 10.1145/2103621.2103719