Originariamente pubblicato in data 08/04/1999
A
C
C
O
M
A
Z
Z
I
Dietro a quel PRINT
Dietro a quel PRINT
Una singola riga di istruzione (in questo articolo è un PRINT del linguaggio Basic perché il pezzo venne scritto negli anni Ottanta, ma oggi tutto filerebbe liscio con un DISPLAY DIALOG di AppleScript). Cosa succede davvero?
Di Carlo Bocchetti e Luca AccomazziCon un sospiro vi accasciate davanti alla tastiera del fedele Personal. Siete in
ambiente Basic, con pochi colpi di tasto battete 10 PRINT "CIAO MONDO" e poi
RUN. Prevedibilmente, la scritta CIAO MONDO appare sullo schermo.
Ma come accade? Quali operazioni vengono davvero eseguite, dietro a quel PRINT?
Per capire quello che succede dovremo fare un piccolo passo indietro ...
Un computer in fondo è un insieme di circuiti, interruttori e dispositivi che
segnalano se in un dato circuito passa o non passa corrente. I due stati
fondamentali dei circuiti sono, notoriamente, acceso (ON, o anche 1) e spento
(OFF, o anche 0). E' da qui che dobbiamo partire per capire come sia possibile
trasformare un insieme di componenti elettrici in un eleboratore che risponde
alle nostre domande.
Per cominciare, consideriamo le porte logiche. Esistono 3 tipi di porte logiche,
la porta NOT, la porta OR e infine la porta AND.
Come siano realizzati fisicamente questi dispositivi non ve lo raccontiamo,
eventualmente consultate un buon manuale di elettronica. A noi basta sapere che
fungono da interruttori selettivi rispetto alle tensioni dei fili in ingresso.
Per esempio l'uscita della porta AND avrà lo stato ON se, e solo se, entrambi i
due fili in ingresso sono sotto tensione. In effetti le porte logiche sono state
costruite in questo modo affinché obbediscano alle leggi elementari della logica
simbolica e ai principi matematici dell'algebra booleana. (vedi figura con
tabelle di verità e con qualche formula booleana, tipo legge di De Morgan etc).
Possiamo costruire con più porte logiche dei circuiti che realizzino semplici
operazioni matematiche, ovviamente secondo le regole dell'aritmetica binaria: se
stabiliamo convenzionalmente di attribuire valore 1 allo stato ON e valore 0
allo stato OFF, allora posso pensare di realizzare un sommatore elementare che,
in uscita, dia il valore binario somma rispetto allo stato di due fili in
ingresso. I fili in uscita devono essere 2, uno per la somma e uno per
l'eventuale riporto.
Il compito è abbastanza facile: con due fili in ingresso ho 4 possibili
combinazioni (0+0, 0+1, 1+0 e 1+1 vedi figura con schema). Poiché la
combinazione 0+0 non influenza comunque il circuito, basteranno 3 porte AND
secondo lo schema in fig.. (da notare che la porta OR non funziona per la somma,
in quanto deve essere 1+1=0 con riporto di 1).
Ma i calcolatori non sono stati costruiti certo per sommare i numeri da 0 a 1.
Possiamo effettuare la somma di numeri con più cifre solamente se teniamo conto
ogni volta dell'eventuale riporto nella colonna precedente. Questo complica
leggermente il nostro sommatore elementare, che ora dovrà tenere presente 3 fili
in ingresso, per un totale di 8 possibili combinazioni.
Avremo quindi uno schema come quello rappresentato in figura, dove vediamo che
il risultato è 1 nei casi 0+0+1, 0+1+0, 1+0+0 e 1+1+1, mentre il riporto è 1 nei
casi 1+1+0, 1+0+1, 0+1+1 e 1+1+1.
Con una catena di questi sommatori binari elementari possiamo costruire undentro al singolo sommatore
elementare, ma solamente i suoi effetti esterni. Un bel po' di livelli più in
alto finiremo per ritrovare l'istruzione PRINT.
Grazie ad ulteriori combinazioni di circuiti e porte logiche si possono
realizzare altri dispositivi hardware fondamentali come il registro, lo shifter,
il decodificatore, i bus e così via.
A questo punto disponiamo di una 'macchina' costituita da un insieme di circuiti
in grado di fornire risposte automatiche in base alla configurazione delle varie
porte e al passaggio o meno di corrente nei fili in ingresso.
Se poi i fili in uscita di un dispositivo vengono indirizzati come ingresso in
altri dispositivi, allora è possibile governare questi ultimi partendo dai
primi.
E' qui che si incomincia a parlare di linguaggio macchina. Immaginiamo per
esempio un dispositivo fornito di 4 fili in ingresso e di un numero imprecisato
di uscite, cui sono collegati tutti gli altri circuiti. Con 4 fili si ottengono
16 diverse combinazioni ON/OFF, ed in base alla disposizione delle varie porte
logiche, possiamo stabilire per ognuna di queste combinazioni una certa sequenza
in uscita. Per esempio la sequenza ON ON OFF OFF potrebbe determinare
l'azzeramento di un registro, la sequenza ON OFF ON ON la somma binaria di altri
due registri e così via. Un simile dispositivo può essere realizzato in molti
modi, ma il più semplice ed economico, per ora, consiste nel progettare delle
apposite memorie permanenti, ROM, che costituiranno il punto di riferimento
iniziale per tutte le successive implementazioni software. Grazie ad esse
l'insieme dei nostri circuiti può essere programmato secondo un certo linguaggio
macchina.
Il linguaggio macchina, sebbene sia un insieme di parole-sequenze di ON e OFF
cui corrispondono certe operazioni-macchina, è già una astrazione: siamo noi che
attribuiamo un significato agli stati in ingresso, e stabiliamo che OFF OFF ON
ON si possa rappresentare con 0011, attribuendogli il significato di un numero
binario equivalente al decimale 3. Questa convenzione però ci è utile, in quanto
per programmare un simile dispositivo ci basta avere una tabella di
corrispondenza tra i numeri di codice macchina e le relative operazioni che la
macchina deve eseguire. Per esempio su un Apple//, che utilizza parole 8 bit, la
sequenza 01001100, cui noi attribuiamo il valore dacimale di 76 e quello
esadecimale di 4C equivale all'istruzione di salto incondizionato.
Consultando il manuale del microprocessore su cui è basato il nostro computer
possiamo trovare i codici numerici che indicano le varie operazioni eseguibili.
Naturalmente oggi nessuno programma i microprocessori direttamente in codice
macchina, neppure coloro che progettano nuovi chip, in quanto da questo punto in
poi è lo stesso computer che ci offre gli strumenti necessari per lavorare ad un
livello superiore: un linguaggio di tipo Assembler, un editore, un monitor. E
questo è il secondo salto di astrazione che dobbiamo fare...
Sapere che la sequenza di byte 4C 60 FB fa saltare (GOTO) l'esecuzione del
programma al byte FB60azione di
"jump", cioè "salta"). Eccoci al primo linguaggio, l'Assembler, che sostituisce
agli astrusi codici numerici del linguaggio macchina delle etichette alfabetiche
dette opcode. Dei programmi sofisticati, chiamati "assemblatori", prenderanno il
programma Assembler e lo tradurranno nel corrispondente programma l.m. (Esistono
altri programmi, chiamati "disassemblatori", che fanno l'opposto). Questo è
possibile in quanto la corrispondenza fra parole Assembler e codici macchina è,
come si diceva prima, biunivoca: ogni parola viene tradotta nel suo particolare
codice, parole diverse generano codici diversi e viceversa. Pertanto la
corrispondenza Assembler - l.m. è 1 a 1, ma l'Assembler è più comprensibile e di
più alto livello. Per esempio JMP significa salto incondizionato su molti
microprocessori, mentre il corrispondente codice numerico l.m. cambierà da caso
a caso. Più alto livello significa quindi meno dipendente dalla macchina
rispetto al l.m.. In effetti l'Assembler è ancora troppo legato al particolare
processore su cui è implementato per poter essere considerato un linguaggio di
alto livello. I processori infatti sono così diversi fra loro che in pratica si
preferisce parlare di Assembler del 6502, Assembler dello Z80, Assembler del
68000 e così via.
Riordiniamo le idee: abbiamo costruito fisicamente un aggeggio, chiamato
microprocessore, in grado di eseguire determinate operazioni elementari seguendo
certi codici numerici sequenzialmente. Ora prendiamo tutto questo, mettiamolo in
una scatola nera e prendiamolo per scontato: insomma, saliamo un gradino sulla
scala dell'astrazione, e arriviamo al sistema operativo (credevate che ce ne
fossimo dimenticati?).
Chi "dice" al microprocessore dove comincia il programma da eseguire? Chi lo
interrompe quando ve ne sia bisogno? Chi tiene traccia della memoria disponibile
e la concede ai programmi che ne hanno bisogno? Chi sincronizza il veloce chip
con le lente periferiche? Il sistema operativo, ecco chi.
Il SO è un programma Assembler che gestisce l'uso del microprocessore e della
sua memoria, che concede l'uso del chip ai programmi che ne fanno richiesta, che
controlla tutto l'input/output. Svolge tutto il lavoro sporco, cerca di impedire
i "system hangup", i blocchi anomali delle operazioni, si pone tra il programma
e lo hardware per facilitarne l'uso.
Scrivere un sistema operativo è certo il compito più difficile per un
programmatore, ancora più complesso che un interprete o compilatore di
linguaggio. Richiede una conoscenza assoluta dello hardware della macchina e
l'attenzione più pedissequa ai dettagli ed alle peculiarità. Quasi sempre,
inoltre, richiede la scrittura di routine critiche, cui è richiesto di eseguire
un compito in un tempo preciso al milionesimo di secondo.
Una parte fondamentale del sistema operativo è il DOS, il sistema operativo del
disco, che si limita a gestire il collegamento della macchina con la memoria di
massa su disco, in input ed in output.
Saliamo di un altro scalino ed arriviamo all'ambiente di lavoro ed al
linguaggio. Nella maggo ben
distinte. LOAD <unfile> è un comando passato dal linguaggio Basic al sistema
operativo, mentre LET A = A + 1 è una "vera" istruzione. E' una distinzione che
viene enfatizzata nei linguaggi più rigorosi ("...come il Pascal", sospireranno
tutti prevedendo il seguito; e invece Pascal non è granchè rigido in proposito,
mentre la divisione è portata alle sue estreme conseguenze nel C).
Dunque, siamo rimasti ad una macchina che sa eseguire compiti elementari e che
può comunicare con il mondo esterno grazie al SO; si passa alla comprensione di
istruzioni come X = (A + 4) * 7 2 grazie all'uso di complessi programmi in
l.m. chiamati appunto linguaggi. Nel linguaggio viene fatta corrispondere una
routine in l.m. a ciascun comando, e la routine viene chiamata quando quel
comando viene incontrato nel programma.
Facciamo un esempio, lo stesso da cui eravamo partiti: voi scrivete il programma
10 PRINT "CIAO, MONDO!" e date il RUN.
Il controllo della macchina al momento del RUN sta al Basic. La routine chiamata
"parser" del Basic va a leggere la scritta PRINT, un carattere la volta, e la
confronta con tutti i comandi che conosce. Possiamo immaginarla mentre consulta
frenetica una tabella... non è NEW, non è INPUT, non è LET...
ah, eccola! Nella tabella, a PRINT è associato ad un indirizzo: poniamo sia
$DAD4 come in Applesoft Basic. Il Parser cede il controllo alla routine che
comincia in DAD4; questa cerca di scoprire se al comando segue un argomento, e
legge il carattere " (virgolette). Questo carattere indica un argomento
esplicito, mentre una lettera, come in PRINT STRING$, avrebbe indicato un
argomento implicito, una variabile.
La routine "legge" dal programma la stringa di caratteri "CIAO MONDO". Poi, dato
che va eseguito un output, passa il controllo al sistema operativo
comunicandogli anche la stringa oggetto. SO, infine, passa a visualizzare sullo
schermo (e magari anche sulla stampante) la scritta CIAO MONDO.
Da notare che ogni singolo carattere della frase, compreso lo spazio bianco,
deve essere portato in qualche accumulatore e da lì scaricato nella giusta cella
di memoria della pagina video in posizione corrispondente a quella del cursore
sullo schermo. Eventualmente il sistema operativo (sempre lui!) provvede anche a
mandare a capo il cursore e a fare lo scrolling verticale dello schermo, se
necessario.
Poi, se non ha qualche motivo in contrario, restituisce il controllo del
microprocessore al Basic.
Il tutto ha richiesto circa un migliaio di operazioni della macchina, e forse un
millesimo di secondo per essere eseguito (molto di più in caso di scrolling del
video).
Arivati a questo grado di astrazione possiamo riassumere il tutto in una
piramide ideale: più si sale, più aumenta la complessità di ogni singola parola
e il corrispondente livello di astrazione (vedi figura). Ci siamo molto
allontanati dai singoli circuiti e dal passa/non passa corrente. Eppure una
istruzione in linguaggio di cosiddetto 'alto livello', come Basic o Pascal, è
ancora incredibilmente meno complessa, ricca e potente di una frase in italiano.
E anche se un computer disponesse del sistema necessario per comprendere gli ordini che noi gli impartiamo a voce dovrebbe avere una
macchina in grado di eseguire l'istruzione. Questo ulteriore salto non rientra
naturalmente negli scopi di questo articolo ed è destinato a restare confinato
nell'ambito dei sogni, almeno per un bel po'.