Didattica

Didattica di Tutanum

Sistemi operativi e sviluppo

Il linguaggio C

Abbiamo chiuso la pagina precedente, quella dedicata al kernel e a MINIX, portandoci dietro una scoperta che merita di essere raccontata per esteso, perché mentre camminavamo dentro il sistema operativo ci è stato rivelato che il cuore dei sistemi come Unix e MINIX, la parte che governa la memoria, i processi e i dispositivi, è scritto quasi interamente in un linguaggio chiamato C, nato negli stessi laboratori che hanno partorito Unix e cresciuto insieme a lui come un fratello minore. In questa pagina scendiamo dal balcone di osservazione e entriamo nella bottega del costruttore, dove vedremo com'è fatto quel linguaggio, perché nacque, come il testo che scriviamo si trasforma in ordini che il processore esegue davvero e con quali gesti si convince una macchina a salutare il mondo, con la promessa che quando arriverai in fondo alla lettura questo mistero sarà diventato una competenza tua, fatta di immagini chiare da tenere in testa e di esperienze vissute davvero sulla tastiera del tuo computer.

Prova a immaginare quei listati come il romanzo di mille pagine che custodisce il comportamento del tuo computer, perché nel nostro giro dentro il kernel abbiamo visto che il gestore dei processi, il sistema di file, la memoria e persino i piccoli operai che parlano con il disco si presentano tutti sotto forma di file di testo scritti in C, ordinati in cartelle come i capitoli di un'enciclopedia, e la buona novella di oggi sta tutta qui, e cioè nel fatto che esiste una lingua abbastanza vicina alla macchina da non tradire i suoi meccanismi e abbastanza umana da permettere a migliaia di programmatori di costruire sistemi operativi leggibili, correggibili e capaci di attraversare decenni senza invecchiare. Nella pagina sull'architettura del set di istruzioni e l'assembler abbiamo imparato il vocabolario minimo che il processore capisce, quella sfilza di gesti spartani che nessun essere umano impiegherebbe per scrivere un'enciclopedia intera, e qui saliamo di un altro gradino, quello in cui le parole tornano ad avere una lunghezza comoda per la mente e la macchina resta ugualmente a portata di mano.

Vale la pena dirlo con franchezza fin da subito, perché tutto il percorso che hai seguito fin qui trova in questa pagina il suo perno, dato che chi sa leggere il C si trova all'improvviso in grado di aprire i sorgenti dei sistemi di cui abbiamo parlato, di seguire una chiamata di funzione dentro il gestore dei file di MINIX, di capire come il kernel risponde a una pressione di tasto e di smontare con gli occhi un meccanismo che per molti resta una scatola chiusa sigillata. Ti avviso però con onestà che qui rinunciamo volentieri alla forma del corso completo, che per il C richiederebbe un intero volume, e preferiamo offrirti la mappa affettuosa di chi ha già camminato quei sentieri, le immagini giuste da ricordare, i paragoni domestici che fanno scattare la lampadina e, alla fine, una manciata di esperimenti da fare davvero seduto al tuo computer, dato che il C si impara con le mani quanto con gli occhi.

La storia del C comincia in un luogo che sembra uscito da un romanzo del Novecento, i Bell Laboratories della compagnia telefonica americana nel New Jersey, dove nei primi anni settanta un gruppo di ricercatori capitanato da Ken Thompson e Dennis Ritchie stava costruendo Unix, il sistema operativo che avrebbe cambiato per sempre il modo di intendere i computer, e il primo Unix era scritto in assembler legato alla macchina specifica su cui nacque, come una pianta legata al vaso che la ospita. Ritchie comprese che per fuggire da quel vaso serviva un linguaggio più comodo dei numeri del processore e più obbediente della macchina stessa, così disegnò il C per riscrivere il kernel di Unix in forma portabile, e nell'estate del 1973 il cuore del sistema fu ricomposto in quel linguaggio nuovo, con un guadagno che si può riassumere così, e cioè che da quel momento in poi portare Unix su un computer diverso richiedeva di adattare una piccola parte scritta a basso livello e di ricompilare tutto il resto, un passaggio che prima richiedeva mesi e ora giorni.

Il nome stesso del linguaggio racconta la sua genesi con umiltà, perché il C nacque come evoluzione di un linguaggio precedente chiamato B, inventato da Thompson come erede del BCPL, e quella lettera solitaria sulla copertina dei manuali è la traccia di una discendenza che si contende onore con la leggenda, anche se il merito definitivo resta di Ritchie, che ideò il tipo di dati e la gestione della memoria che mancavano al predecessore. Qualche anno dopo, nel 1978, il libro The C Programming Language di Brian Kernighan e Dennis Ritchie fissò le regole del gioco in poche centinaia di pagine elegantissime, diventando di fatto lo standard del linguaggio per un decennio, finché un comitato di standardizzazione americano non pubblicò nel 1989 la versione ufficiale che oggi chiamiamo ANSI C, quella che ancora studiamo con profitto perché le idee di fondo sono rimaste incredibilmente stabili attraverso mezzo secolo di rivoluzioni tecnologiche.

Ci si può legittimamente chiedere perché un linguaggio inventato per risolvere un problema così particolare, riscrivere un sistema operativo, resti ancora oggi il modo migliore per avvicinarsi alla macchina, e la risposta sta nella sua incredibile onestà, perché il C si colloca appena un gradino sopra il linguaggio macchina e ogni costruzione che scrivi corrisponde a un pugnetto di istruzioni che il processore esegue davvero, senza ingranaggi nascosti, senza servizi segreti che girano in sottofondo e senza costi che ti vengono sottratti alla vista. Quando nella pagina sull'assembler abbiamo ammirato il vocabolario spartano del processore, il C è la prima lingua umana che quel vocabolario indossa per farsi capire, e scrivere in C somiglia alla sensazione di guidare un'auto con il cofano trasparente, dove il motore resta visibile, ogni leva obbedisce alla mano e quando qualcosa cigola senti subito da quale parte del vano arriva il rumore.

Per capire che cosa significa che il C è un linguaggio compilato, immagina di avere fra le mani un romanzo straniero e due modi di leggerlo, dove il primo modo è quello di consegnare il libro a un traduttore bravissimo che te lo restituisce intero nella tua lingua, legato con un bel filo, così che potrai rileggerlo cento volte senza pagarlo mai più, mentre il secondo modo è quello di farti accompagnare da un interprete che traduce frase per frase a ogni nuova lettura, sempre con la mano tesa a fine capitolo. Il C funziona come il primo modo, perché un programma chiamato compilatore legge una volta sola tutto il tuo testo, lo traduce nelle istruzioni native del processore destinazione e ti consegna un file binario autosufficiente che la macchina esegue senza più bisogno di traduttori, mentre linguaggi come Java e Python seguono in gran parte il secondo modo, producendo un codice intermedio detto bytecode che una macchina virtuale traduce man mano durante l'esecuzione, un comfort che si paga con la velocità rispetto a chi parla direttamente alla CPU.

Seguiamo con calma il viaggio di un programma C dalla tua scrivania alla memoria del computer, perché le tappe sono poche e meritano di essere conosciute per nome, e il punto di partenza è un file di testo con l'estensione .c che contiene le istruzioni scritte da te o dai tuoi colleghi. La prima tappa è il preprocessore, la cui voce la sentiremo tra un istante, la seconda è il compilatore vero e proprio, che leggendo il testo preparato genera un file oggetto con estensione .o pieno di istruzioni binarie per la macchina destinazione, e se ti stai chiedendo dove finisca l'assembler studiato nelle pagine precedenti, sappi che vive proprio qui dentro, perché il compilatore nei suoi passaggi interni si avvale di lui per produrre il codice macchina finale. L'ultima tappa è il collegamento, dove un programma chiamato linker raccoglie i vari file oggetto, vi annette le funzioni di libreria che hai chiamato, sistema gli indirizzi e consegna il file eseguibile che sulle tradizioni di Unix si chiama a.out, un nome che i vecchi intenditori pronunciano con affetto.

Il preprocessore è il factotum che lavora prima del traduttore vero e proprio, e il suo mestiere è di tipo amministrativo, dato che esegue i comandi che cominciano con il cancelletto, va a prendere i file nominati dalle direttive #include sostituendoli alla riga che li chiama, espande i nomi definiti con #define come quando scrivi #define BUFFER_SIZE 4096 per dare un nome parlante alla dimensione di un deposito, e gestisce la compilazione condizionale, per cui un blocco racchiuso fra #ifdef X86 e #endif viene tenuto soltanto quando la macro citata è stata definita, un trucco usatissimo per isolare le parti del codice che dipendono dal processore e far compilare certe righe sulle macchine con CPU di tipo X86 ed altre righe su macchine di tipo SPARC. Le macro possono persino avere parametri, come la celebre #define max(a, b) che consente di scrivere i = max(j, k+1) e di ottenere la selezione del valore più grande senza chiamare alcuna funzione, con la sostituzione che avviene sul testo prima ancora che la compilazione vera cominci, e la buona educazione raccomandata dai libri vuole che quasi ogni costante riceva un nome, fatta eccezione per lo zero, per l'uno e talvolta per il meno uno.

Attorno ai file .c, che custodiscono il codice vero e proprio, ruotano i file di intestazione con estensione .h, che contengono le dichiarazioni e le definizioni condivise fra più sorgenti, un po' come l'indice analitico comune che permette a tanti volumi della stessa enciclopedia di parlare la stessa lingua. Le convenzioni per citarli sono due e vale la pena impararle subito, perché quando il nome è racchiuso fra parentesi angolari, come in #include <stdio.h>, il compilatore cerca il file nella directory di sistema dei sorgenti condivisi, che di solito si chiama /usr/include, mentre quando il nome è racchiuso fra virgolette, come in #include "const.h", la ricerca comincia nella cartella del file che sta compilando e solo dopo si sposta in quella di sistema, così che ogni progetto possa avere intestazioni proprie senza paura di sconfinare. Nei sorgenti di MINIX questa filosofia è portata alle estreme conseguenze, con un'intestazione madre per ogni parte del sistema che obbliga <minix/config.h> a essere la prima riga e <ansi.h> la seconda, seguite dalle altre, come l'apertura rituale di una partita a scacchi che tutti i giocatori conoscono a memoria.

Arriviamo ora al momento solenne, quello in cui una macchina pronuncia il suo primo saluto, e il programma con cui si inaugura ogni viaggio nel C è la versione moderna del classico hello world. La riga #include <stdio.h> chiede al preprocessore di portare dentro il programma l'intestazione della libreria standard di ingresso e uscita, quel deposito di dichiarazioni che rende utilizzabile la funzione di stampa, e il libro su MINIX osserva che proprio stdio.h è il biglietto da visita con cui quasi ogni programmatore ha cominciato il proprio cammino. La riga int main(void) apre la funzione principale, la porta d'ingresso dove il sistema operativo bussa per avviare il programma, la riga printf("Salve, mondo\n"); invoca la stampa formattata facendole recitare la frase racchiusa fra virgolette, con la coppia di caratteri \n che ordina di andare a capo come un carrello di macchina da scrivere, e la riga return 0; consegna allo chiamante il valore zero, che per convenzione significa tutto è andato bene, mentre le parentesi tonde e i punti e virgola fanno le veci della punteggiatura ordinaria della lingua.

Le materie prime con cui il C costruisce i suoi dati sono poche e oneste, e il libro di Tanenbaum le elenca con una frase da incorniciare, parlando di numeri interi che esistono in taglie corte e lunghe, di caratteri e di numeri in virgola mobile, cui si aggiungono i tipi composti costruiti con gli array, le strutture e le unioni. Il punto delicato, e qui conviene fermarsi con attenzione, è che il numero di byte occupato da un tipo dipende dalla macchina per cui compili, scelta che ai principianti sembra sconcertante, e il libro su MINIX lo dimostra con una tabella che confronta i sistemi a sedici e a trentadue bit, dove un identificatore di processo chiamato pid_t occupa sedici bit sulla macchina piccola e trentadue su quella grande, mentre altri tipi come dev_t e gid_t restano identici, con la regola che ogni nome di tipo che finisce con il suffisso _t è riservato dallo standard POSIX, un modo gentile per mantenere l'ordine in un condominio affollato.

Le variabili sono cassetti etichettati dell'armadio della cucina, dove la dichiarazione attacca un'etichetta con un nome scelto da te e il tipo decide quanto il cassetto è profondo, così che scrivere int anno = 2026; significhi riservare un cassetto adatto ai numeri interi, chiamarlo anno e riporvi dentro il valore duemilaventisei. Gli operatori aritmetici +, -, *, / e il resto della divisione % sono i verbi con cui si combinano i contenuti, gli operatori di confronto come <, > e == fanno da domande il cui esito è un numero che vale zero quando la risposta è negativa e diverso da zero quando è positiva, e una espressione come prezzo * quantita è una piccola ricetta che restituisce un valore nuovo pronto per essere riposto in un cassetto.

Il controllo del flusso è la disciplina che decide quale strada prende l'esecuzione, e il C la esprime con le scelte if e switch e con i cicli for e while, costruzioni che somigliano da vicino alle istruzioni che daresti a un aiutante in cucina, del tipo se l'acqua bolle allora versa il sale, oppure finché ci sono piatti nel lavabo continua a sciacquarli, con la differenza che la macchina capisce solo i numeri, per cui ogni condizione è in realtà una domanda che si risolve in zero oppure in diverso da zero. Se nella pagina sul processore hai incontrato le spie che si alzano dopo i confronti, qui ritrovi quelle stesse spie al lavoro sotto le vesti del linguaggio, e ti invito a notare come il while che ripete un calcolo finché una condizione regge sia la forma adulta del salto condizionato che faceva tornare il program counter all'inizio del ciclo nei programmi in assembler.

Le funzioni sono i laboratori separati in cui si scompone un lavoro grande, ciascuno con la propria porta, il proprio banco e le proprie attrezzature, e l'immagine da tenere a mente è quella di un laboratorio culinario a cui consegni ingredienti attraverso lo spioncello e dal quale ricevi un piatto finito, perché i parametri sono gli ingredienti, il valore restituito con return è il piatto e la funzione main stessa è il laboratorio principale da cui tutto ha inizio. Il C chiede che prima dell'uso mostri al compilatore il prototipo, una riga che promette in anticipo come è fatta la funzione, come int massimo(int a, int b);, che dichiara quali ingredienti entrano e cosa esce, esattamente come il cartello alla porta di un laboratorio, e quando la chiamata avviene il compilatore controlla che gli passi ingredienti del tipo giusto.

Il puntatore è il cuore pulsante del C e merita il paragone più caro di tutta la pagina, immagina dunque il biglietto da visita su cui hai scritto l'indirizzo della tua casa, il biglietto in sé è la variabile puntatore, l'indirizzo scritto sopra è il suo contenuto, e la casa che sta a quell'indirizzo è la variabile puntata, così che il puntatore custodisca il modo per raggiungere il valore anziché il valore vero, come una freccia che attraversa la memoria e indica la porta giusta. L'operatore & copia l'indirizzo di una variabile sul biglietto, l'operatore * suona il campanello della casa il cui indirizzo è scritto sul biglietto e si prende cura del suo contenuto, e in teoria ogni puntatore è specializzato su un tipo, per cui un puntatore a carattere dovrebbe indicare solo case che custodiscono caratteri, anche se nella pratica i compilatori tollerano molte libertà limitandosi a lanciare un avvertimento quando le carte non tornano.

Guardiamo all'opera il celebre esempio che il libro di Tanenbaum propone per i puntatori, dove le quattro righe char c1, c2, *p;, c1 = 'c';, p = &c1; e c2 = *p; raccontano una storia completa, perché la prima dichiara due cassetti per caratteri e un biglietto per indicarli, la seconda ripone nel primo cassetto il codice della lettera c, la terza copia sul biglietto l'indirizzo del cassetto e la quarta visita la casa e ne trasporta il contenuto nel secondo cassetto, così alla fine anche c2 custodisce la lettera c. In queste quattro righe sta racchiusa tutta la potenza del C, dato che visitare la memoria attraverso indirizzi è il gesto con cui i sistemi operativi scorrono tabelle, collegano strutture e passano messaggi, e sta anche racchiuso il suo pericolo, perché lo stesso libro avverte che i puntatori sono potentissimi e insieme una grande fonte di errori quando vengono usati con distrazione.

Gli array, chiamati anche vettori, sono i file di cassetti contigui dell'officina, dove la fila intera viene riservata in un colpo solo e ogni cassetto riceve un numero progressive che parte da zero e non da uno, così che dichiarare int voti[10]; significhi allestire dieci cassetti in fila e indicarli con numeri da zero a nove, con il nome dell'array che si comporta in molte situazioni come il biglietto segnato sulla cassetta iniziale. Le stringhe di testo nascono come array di caratteri chiusi da un carattere speciale di valore zero che fa da sentinella alla fine della fila, il che significa che il C non possiede stringhe vere e proprie incorporate nel linguaggio come fanno Java e Python, e la prova di questo stile artigianale la trovi nei sorgenti di MINIX, dove il nome di un file può contenere fino a sessanta caratteri perché ogni voce di directory è un array di quella misura fissata, con la libreria che offre funzioni di manipolazione raccolte nell'intestazione string.h.

Le strutture sono lo schedario che raccoglie in un'unica cartella dati di natura diversa, e la cartella intera riceve un nome solo, mentre dentro restano riposti in ordine campi di tipi diversi ciascuno con la propria etichetta, esattamente come una scheda anagrafica raccoglie accanto a un nome una data, un indirizzo e un numero di telefono senza che nessuno confonda le parti. Il libro su MINIX cita la struttura termios, che raccoglie sotto un unico nome i segnalatori di modo di funzionamento, le velocità di trasmissione e un array dei caratteri speciali come quello che interrompe il programma e quello che cancella la riga, e cita anche la struttura restituita da stat e fstat con tutte le proprietà di un documento. A fianco delle strutture esistono le unioni, dove i campi condividono lo stesso spazio fisico come cassetti sovrapposti che non possono mai essere pieni insieme, e con questi mattoni il kernel descrive in poche righe cose complesse come una voce della tabella dei processi o le informazioni di un disco.

È ora di dire con altrettanta chiarezza ciò che il C non possiede, e l'elenco stupisce chi arriva da linguaggi moderni, perché qui non esistono classi, oggetti, pacchetti né un addetto ai lavori che raccolga la spazzatura della memoria, dato che ogni spazio è dichiarato una volta sola e resta sotto la responsabilità diretta di chi scrive, il quale lo ottiene con la funzione di libreria malloc quando serve e lo restituisce con free quando ha finito, con la possibilità di rovinare tutto dimenticando una consegna e con il controllo totale che nessun altro ti concede. Il libro di Tanenbaum spiega perché questa mancanza è in realtà una scelta geniale per chi costruisce sistemi operativi, e cioè che un sistema operativo è in buona misura un sistema di tempo reale, visto che quando arriva un'interruzione può avere pochi millesimi di milionesimo di secondo per reagire prima che l'informazione si perda, e un netturbino automatico della memoria che si svegliasse in un momento imprevedibile per mettere ordine tra i cassetti renderebbe quel tempo di reazione impossibile da garantire, così il controllo esplicito della memoria diventa una necessità operativa e non un vezzo di nostalgici.

La libreria standard è la cassetta degli attrezzi condivisa che accompagna ogni compilatore, e il libro su MINIX la presenta stanza per stanza, raccomandando stdio.h per la stampa formattata con printf e per la lettura formattata da tastiera con scanf, stdlib.h per le conversioni e per la gestione della memoria dinamica, unistd.h per i prototipi che coprono le chiamate di sistema, string.h per le operazioni sulle stringhe, signal.h per i nomi dei segnali e fcntl.h per i parametri simbolici delle operazioni sui file, come il nome O_RDONLY che sostituisce il numero zero quando chiedi di aprire un documento in sola lettura, un'abitudine che rende il codice leggibile come una lettera. Per i file offre da un lato le funzioni di flusso e dall'altro le chiamate nude e crude come read e write con i loro descrittori numerici, mentre lseek sposta il puntatore di posizione dentro il documento, proprio come muovi il segnalibro dentro un libro per riprendere la lettura da un altro capitolo.

Quando il progetto cresce e diventa un sistema operativo vero, con i suoi milioni di righe, entrano in scena due alleati da salutare per nome, e il primo è make, che legge un file di istruzioni chiamato Makefile e ricompila soltanto le parti diventate vecchie dopo l'ultima modifica, così che cambiare una riga non costringa a rifare da capo un lavoro di ore. Il secondo alleato è il linker visto poco fa, che al termine raccoglie i file oggetto, risolve i riferimenti incrociati fra le funzioni, sposta gli indirizzi dove serve e incorpora dal magazzino chiamato libc.a le funzioni di libreria invocate, consegnando alla fine un eseguibile unico che sulle tradizioni di Unix risponde al nome a.out, e il bello è che questo percorso dagli altipiani dei progetti grandi scende senza sforzo fino ai tuoi piccoli programmi di domani, perché le tappe restano le stesse cambiano soltanto le dimensioni del carico.

Anche quando il sistema è finalmente avviato e gira, il C continua a raccontarsi nella forma della memoria, perché il programma in esecuzione occupa tre zone ben distinte, con il segmento di testo che custodisce il codice immutabile e di solito sta in basso, il segmento dei dati che parte da una certa dimensione e cresce verso l'alto man mano che il programma ne ha bisogno, e il segmento di pila che viene collocato a un indirizzo alto e si abbassa come un cassetto che scende dal soffitto ogni volta che una funzione viene chiamata e risale quando la funzione restituisce il controllo, un meccanismo che hai già incontrato come lo stack quando abbiamo studiato le chiamate di procedura. Il sistema può persino caricare pezzi al volo che non erano stati inclusi nell'eseguibile, come certi driver e certi sistemi di file, e soprattutto il codice viene eseguito direttamente dall'hardware, senza interprete né compilazione just-in-time, chiudendo il cerchio aperto all'inizio, dato che il romanzo tradotto una volta sola viaggia nelle vene della macchina senza traduttori di fiducia.

Tiriamo ora le fila di tutta la giornata, perché il senso d'insieme è questo, e cioè che con il C in mano le pagine precedenti del percorso smettono di essere vetrine dietro vetro e diventano libri aperti sul tuo tavolo, dato che i sorgenti di MINIX che abbiamo ammirato dal corridoio ora sono comprensibili riga per riga, con le loro cartelle ordinate, le intestazioni madri, le strutture e i contratti dei prototipi, e perfino le storie dei sistemi che usi ogni giorno acquistano un fondo nuovo, perché sapere che qualcuno ha scritto in C la funzione che legge i tuoi file ti fa sentire dentro una tradizione artigiana che si tramanda da cinquant'anni. C'è di più, perché questo linguaggio è il passaporto per il futuro che ti stiamo preparando, e chi si sente pronto può un giorno scrivere un piccolo sistema tutto suo, partendo da un programma che saluta il mondo e arrivando a un puledrino capace di caricare programmi, e nessuna delle cose che hai letto fin qui è troppo lontana per chi ha pazienza, immaginazione e un compilatore amico.

Arriva il momento di lasciare la poltrona e sporcarti le dita, e i prossimi sette esperimenti sono pensati per essere fatti davvero, tutti al tuo computer, senza rischi e senza spese, ordinati dal più gentile al più intrigante, e ti serviranno un editor di testo qualsiasi, purché salvi file semplici, e un compilatore, che sui sistemi derivati da Unix è già installato e si chiama cc oppure gcc, mentre sulle altre piattaforme si scarica gratuitamente in pochi minuti e nei casi più urgenti si trova perfino nel browser. Il metodo che ti consiglio è quello di battere ogni esempio con le dita tue anziché copiarlo e incollarlo, perché le dita imparano i ritmi del linguaggio e gli errori di battitura commessi e corretti diventano amici che riconoscerai al volo, e ogni esperimento termina con un invito identico, e cioè a modificare qualcosa e a ricompilare, dato che la curiosità è il vero motore di questo mestiere.

Per il primo esperimento crea una cartella tutta tua, aprila nel terminale, apri l'editor e scrivi un file chiamato hello.c contenente le quattro righe che abbiamo letto poco fa, con l'intestazione che invoca la libreria di ingresso e uscita, la porta principale, la stampa del saluto e la restituzione del valore zero. Salva, torna nel terminale e compila con cc hello.c -o hello, dove cc è il nome del compilatore, hello.c è il sorgente e -o hello dichiara il nome che avrà l'eseguibile, poi eseguilo scrivendo ./hello e osserva il saluto apparire come un piccolo miracolo domestico, dopo di che cambia la frase, ricompila e riesegui, così che il giro sorgente, compilatore, eseguibile ti diventi familiare come un gesto antico.

Per il secondo esperimento riscrivi la compilazione aggiungendo un'opzione e serviti di cc -Wall hello.c -o hello, dove la scritta -Wall chiede al compilatore di lamentarsi per ogni piccola sbadataggine, compresa quella variabile che hai dichiarato e poi dimenticato, e per metterlo alla prova aggiungi al programma una riga come int anno = 2026; senza mai usarla, poi ricompila e goditi il messaggio di avvertimento che ti spiega con precisione da quale parte del testo proviene la dimenticanza. Questo compilatore pignolo è il tuo alleato più prezioso, perché legge il compito prima della maestra, e la soddisfazione di vedere sparire l'avviso dopo aver stampato la variabile con printf("%d\n", anno); ti insegna il ritmo del mestiere, fatto di cicli scrivi, compila, ascolta, correggi.

Per il terzo esperimento verifica con le tue mani la lezione dei cassetti di misura variabile, scrivendo un programmino che ripeta per cinque volte la stampa della dimensione dei tipi fondamentali con righe come printf("%zu\n", sizeof(int)); al posto di int mettendo di volta in volta char, int, long, float e double, dato che la scritta sizeof chiede al compilatore quanto è profondo il cassetto di quel tipo e il codice %zu è il vestito giusto per presentare una dimensione. Compila, esegui e annota i numeri che escono, poi mandali a un amico che ha un computer diverso dal tuo e confrontate le risposte, perché scoprire che il long misura quattro byte sulla tua macchina e otto sulla sua è la dimostrazione sperimentale della tabella dei tipi di MINIX e della regola che il C lascia alla macchina l'ultima parola sulle misure.

Per il quarto esperimento metti alla prova i puntatori ricreando nel tuo editor le quattro righe famose, dichiarando due caratteri e un puntatore con char c1, c2, *p;, riponendo la lettera c con c1 = 'c';, copiando l'indirizzo con p = &c1; e visitando la casa con c2 = *p;, poi fai stampare entrambi i caratteri con printf("%c %c\n", c1, c2); e conferma che sullo schermo appaiono due lettere c gemelle. A questo punto spingiti oltre e chiedi al programma di mostrarti l'indirizzo custodito sul biglietto con printf("%p\n", (void *)p);, quindi cambia il contenuto del primo cassetto assegnando c1 = 'z'; prima della visita e osserva che la seconda lettera si adegua, gioca infine a smarrire il biglietto assegnandogli un valore inventato e leggi con attenzione gli avvisi del compilatore, che in questo gioco diventano cartelli stradali luminosi.

Per il quinto esperimento costruisci il tuo primo laboratorio con la porta e il cartello, dichiarando prima della funzione principale il prototipo int massimo(int a, int b); e scrivendo dopo di essa il corpo della funzione che restituisce il primo ingrediente quando supera il secondo e il secondo in caso contrario, poi chiamala dalla funzione principale e stampa il risultato, così che la promessa fatta dal cartello e l'adempimento del laboratorio ti rimangano chiari come il sole. Nello stesso programma allestisci poi il tuo primo ciclo con for (i = 1; i <= 10; i++) facendogli stampare i quadrati dei numeri da uno a dieci con printf("%d\n", i * i);, e osserva come il ciclo somigli all'aiutante della cucina che ripete un gesto finché la condizione regge, perché vedere le due costruzioni cooperare nello stesso file è il momento in cui il linguaggio smette di essere un elenco e diventa una conversazione.

Per il sesto esperimento vai a curiosare nella stanza degli armadi comuni del tuo sistema scrivendo ls /usr/include nel terminale, così vedrai sfilare decine di file di intestazione con l'estensione che ormai conosci, poi aprine uno, per esempio proprio stdio.h, dentro l'editor e scorri il testo cercando le righe che cominciano con #define, i prototipi di printf e gli accordi #ifdef che proteggono le varie parti, riconoscendo in un documento vero gli stessi gesti che questa pagina ti ha raccontato. Se poi ti va di proseguire la visita, collegati a il sito del progetto MINIX e apri qualcuno dei file di intestazione del sistema che abbiamo esplorato nella pagina precedente, così potrai toccare con mano l'intestazione madre con le sue due righe obbligatorie e capire che gli scaffali della cucina di un sistema operativo vero sono sorprendentemente alla portata di chi ha appena cominciato.

Per il settimo esperimento fai il percorso a ritroso rispetto alla tua prima compilazione, spostando la funzione massimo in un secondo file chiamato aiuto.c e collocando il suo prototipo in un intestazione chiamata aiuto.h che entrambi i sorgenti includono, poi compila tutto insieme con cc principale.c aiuto.c -o saluti e osserva che il linker lavora in silenzio, unendo i pezzi senza farsi notare come un falegname preciso. Per apprezzare il suo valore prova poi a rinominare la funzione in uno solo dei due file e ricompila, così leggerai il suo lamento per un simbolo promesso e mai trovato, e capirai perché nei grandi progetti si dice che il linker sia il notaio che mette l'ultima firma sul contratto fra migliaia di file, un mestiere che in MINIX viene svolto in forma orchestrata dal comando che ricompila l'intero sistema con un gesto solo.

Il modulo attinge alla sezione 1.8 del capitolo 1 del libro Modern Operating Systems di Andrew S. Tanenbaum e Herbert Bos, intitolata The world according to C, per la storia del linguaggio, il confronto fra linguaggi compilati e interpretati, i tipi di dati, i puntatori, la gestione esplicita della memoria, le macro, la compilazione condizionale, il ruolo del preprocessore, del compilatore e del linker, l'uso di make nei progetti grandi e il modello della memoria a tempo di esecuzione, e al libro Operating Systems, Design and Implementation di Andrew S. Tanenbaum e Albert S. Woodhull, in particolare al capitolo 1 sulle chiamate di sistema e al capitolo 2 sull'organizzazione dei sorgenti di MINIX, per le convenzioni sui file di intestazione e le parentesi angolari, i tipi di dato con il suffisso _t, gli esempi di strutture come quella del terminale, la divisione della memoria nei tre segmenti e la descrizione del modo in cui un progetto di milioni di righe viene ricompilato e ricomposto.

Con il C nel taschino e le dita ancora tiepide per le compilazioni appena fatte, il percorso ora ti chiama verso la stanza dei mestieri più grandi, dove scopriremo come il software cresce da un file solitario a un progetto costruito da tante persone, come si organizzano le prove, le versioni e gli strumenti del mestiere, e come le buone abitudini trasformano un mucchio di programmi in qualcosa di cui fidarsi, quindi prosegui pure verso la pagina successiva, perché il laboratorio è già allestito, la luce è accesa e i progetti della tua vita da costruttore di software aspettano soltanto te.