TRADUZIONE DEL PROGRAMMA Una volta che un programma sia stato scritto in C, esso non può essere eseguito senza unulteriore traduzione. Ciò perché qualsiasi computer è in grado di eseguire solo le istruzioni scritte nel linguaggio proprio della sua architettura, detto linguaggio macchina, costituito da una serie di 0 e 1. Dato che la programmazione in linguaggio macchina è difficile e soggetta a errori, sono stati sviluppati dei programmi, detti genericamente traduttori, che accettano in ingresso un programma scritto in un linguaggio di alto livello (per esempio in C), detto programma sorgente, e producono in uscita un programma scritto in linguaggio macchina, detto programma (o codice) oggetto.
Un programma può essere tradotto in linguaggio macchina secondo due strategie diverse, ossia usando un programma interprete oppure un programma compilatore: un programma interprete traduce individualmente ogni istruzione del programma sorgente e la esegue immediatamente; quindi non produce un codice oggetto. un programma compilatore traduce tutte le istruzioni del programma sorgente in un programma equivalente, prima che ciascuna di esse sia eseguita; quindi produce un codice oggetto. Il C è un linguaggio compilato, quindi il programma sorgente è tradotto come un tuttuno nel linguaggio macchina. In realtà, la traduzione di un programma sorgente in un programma eseguibile avviene in due passaggi consecutivi: il primo eseguito da un programma detto compilatore il secondo eseguito da un programma detto loader/linker.
Il compilatore dunque accetta in ingresso il programma sorgente e produce in uscita un programma oggetto, scritto nelle istruzioni proprie del microprocessore usato (ossia in linguaggio assembly). In genere il programma oggetto non consiste in un codice adatto per lesecuzione, codice che sarà prodotto successivamente da un gruppo di tre programmi datti assemblatore, legatore, caricatore.
Intel 4004 Instruction Set
Programma compilatore. Un compilatore è costituito in realtà da diversi programmi, che eseguono sul sorgente un certo numero di operazioni. Esse si possono dividere in due parti: analisi e sintesi. Lanalisi suddivide il programma sorgente nelle sue parti costituenti e ne crea una rappresentazione intermedia. La sintesi genera il programma target dalla rappresentazione intermedia. La parte di analisi si può suddividere nelle seguenti fasi: 1. analisi lessicale (o lexing); 2. analisi sintattica (o parsing); 3. analisi semantica; La parte di sintesi si può suddividere nelle seguenti fasi: 1. generazione del codice intermedio; 2. ottimizzazione del codice; 3. generazione del codice finale.
Alla fine viene prodotto il programma target.
Lanalisi lessicale può rilevare errori relativi alla grafia delle parole chiave o di identificatori creati dallutente, al formato delle costanti,... Ad es., il seguente diagramma lessicale permette di stabilire se un identificatore abbia il formato corretto (cioè inizi con una lettera, seguita da lettere e/o cifre). Nellanalisi lessicale (o lexing) il programma è considerato come ununica sequenza di caratteri; essa viene scansionata per individuare gli elementi base del linguaggio che costituiscono unistruzione, quali parole chiave, identificatori, costanti, operatori, delimitatori,... Un analizzatore lessicale legge il programma da sinistra a destra e raggruppa le sequenze di caratteri in token, che sono unità lessicali di significato compiuto. La sequenza di caratteri che dà luogo a un token è detta lessema.
Una volta eseguita lanalisi lessicale, le fasi successive della compilazione non faranno più riferimento ai singoli caratteri, ma alle parole individuate come elementi di base.
Consideriamo, come esempio, la seguente istruzione di assegnazione: totale = base + increm * 60 Lanalizzatore lessicale raggruppa i caratteri nei seguenti token: A questo punto il compilatore costruisce una Tabella dei simboli, dove registra gli identificatori usati nel programma insieme ai loro attributi. Essa ha laspetto indicato in figura
Come si vede, gli attributi di un token ID si possono riferire a: memoria allocata, tipo dati, portata o ambito, numero e tipo di argomenti, ecc. Quando viene rilevato un identificatore e generato un token ID, il lessema corrispondente è inserito nella Tabella dei simboli, e al token ID viene associato un puntatore alla posizione nella Tabella. Si ottengono anche una o più tabelle dei simboli contenenti tutti gli identificatori dichiarati nellambito del programma come tipi, variabili, procedure, funzioni,... che saranno utilizzate nelle successive fasi di analisi semantica, generazione del codice e gestione della memoria.
Nellanalisi sintattica, o parsing, viene riconosciuta la struttura del programma e viene rappresentato il suo significato in una forma intermedia, dalla quale verrà poi prodotto con semplici trasformazioni il codice oggetto. Lanalisi sintattica è guidata dalla definizione formale del linguaggio di programmazione - data per esempio mediante la forma normale di Backus o i diagrammi sintattici - e consente di caratterizzare tutti e soli i programmi validi, cioè sintatticamente corretti, e di comprenderne il significato. Ad es., il seguente diagramma sintattico permettere di stabilire se sia stato impiegato un identificatore valido in C
Il diagramma seguente permette invece di controllare la validità di una istruzione if.
I token vengono raggruppati in frasi grammaticali rappresentate da un albero parse, che fornisce una struttura gerarchica al programma sorgente. La struttura gerarchica è espressa da regole ricorsive dette produzioni. Gli errori che si rilevano in questa fase riguardano lerronea strutturazione del programma a partire dalle proposizioni, ad es. la mancata corrispondenza di parametri in unespressione, lassenza di parole chiave attese in determinate posizioni (come il while dopo il do), la mancanza della parola chiave case in corrispondenza di una switch e così via. Come risultato di questa fase si ottiene una forma intermedia (albero, matrice,...) corrispondente alla struttura sintattica del programma analizzato.
Ecco un esempio di albero parse ID = ID | NUM | | ( ) + | - | *| / Ad es., produzioni per istruzioni di asegnazione sono:
Nellanalisi semantica vengono compiute diverse verifiche di consistenza semantica dei concetti utilizzati dal programma. Ad esempio, la frase il libro legge lo studente è sintatticamente corretta, mentre risulta errata dal punto di vista del significato. Lanalisi semantica di un programma verifica: se gli identificatori usati in una procedura siano stati dichiarati nella procedura stessa, se rispettino le regole di visibilità previste dal linguaggio per variabili e procedure, e se il loro tipo sia coerente con luso che ne viene fatto (ad es., i numeri reali non possono essere usati come indici dei vettori). In caso di errori viene fornita la diagnostica relativa.
Fase di sintesi A partire dalla forma intermedia acquisita durante lanalisi sintattica avviene la generazione del codice intermedio, nella forma di un programma per una macchina astratta. Il codice deve essere facilmente traducibile nel programma target. Tale generazione avviene in base a regole molto semplici, che fanno corrispondere unistruzione macchina a ciascuna struttura elementare rappresentata nella forma intermedia.
Questi interventi possono essere: dipendenti dalla macchina, se tengono conto delle caratteristiche hardware e del repertorio di istruzioni del computer su cui dovrà operare il codice oggetto; indipendenti dalla macchina, se non ne tengono conto. I diversi compilatori adottano diverse tecniche di ottimizzazione. Il codice prodotto nella fase precedente può non essere efficiente. Al fine di ottenere un programma oggetto corto ed efficiente si possono effettuare vari interventi di ottimizzazione del codice. Per esempio spostare allesterno di un ciclo le istruzioni che non dipendono dalle variabili del ciclo stesso, eliminare il calcolo di espressioni ripetute più volte o semplificare espressioni aritmetiche o logiche.
Nella generazione del codice viene prodotto il codice target in linguaggio assembly, una versione mnemonica del codice macchina che usa: codici operativi o mnemoniche per le operazioni (Tabella dei codici); nomi simbolici per gli operandi (al posto delle locazioni di memoria) (Tabelle dei simboli); Vengono scelte le locazioni di memoria per ciascuna variabile, le istruzioni sono tradotte in una sequenza di istruzioni assembly e le variabili e i risultati intermedi sono assegnati ai registri di memoria.
Programmi assemblatore, legatore, caricatore. Il programma assemblatore (assembler) sincarica di tradurre ciascuna istruzione del codice target in una in linguaggio macchina, producendo un codice in linguaggio macchina secondo lo schema seguente Tuttavia, mentre la traduzione del codice operativo in linguaggio macchina è in ogni caso univoca, non è sempre possibile sostituire immediatamente a ogni riferimento simbolico di locazione di memoria lindirizzo effettivo della relativa locazione.
Infatti gli operandi che indicano una locazione di memoria possono indicare: indirizzi assoluti, quali quelli dei dispositivi di ingresso/uscita, che non dipendono ovviamente dalla particolare collocazione del programma in memoria. Essi possono essere tradotti direttamente in indirizzi binari; indirizzi relativi ai dati e alle istruzioni del codice macchina. Ovviamente tali indirizzi cominciano da 0, e affinché il codice possa esere eseguito correttamente, essi dovrebbero rimanere gli stessi anche quando il codice viene caricato in memoria centrale. Ciò richiederebbe che il codice venisse allocato in memoria a partire dallindirizzo 0, cosa non sempre possibile; daltra parte, la grande maggioranza dei codici possono essere caricati in qualsiasi zona di memoria disponibile (per cui sono detti codici rilocabili). Gli indirizzi relativi, che a differenza di quelli assoluti devono essere tradotti in indirizzi di memoria centrale tenendo conto dellindirizzo di memoria a partire dal quale il programma viene caricato, sono detti anche rilocabili.
Per gli operandi rilocabili lassemblatore crea, in una prima fase, una tabella dei simboli, nella quale pone i nomi che individua come riferimenti simbolici e la loro posizione relativa nel programma. Solo in una seconda fase gli operandi simbolici saranno sostituiti con gli indirizzi corrispondenti, consultando la tabella dei simboli. Perciò il programma assemblatore viene detto a due passi o fasi.
Programma legatore. Prima della trasformazione definitiva di tutti gli indirizzi in assoluti, è però necessaria unaltra operazione, eseguita dal programma legatore (linker). Esso unisce insieme i differenti file e moduli che possono costituire un sigolo programma (file oggetto), ai quali aggiunge eventualmente dei file di biblioteca (library). Spesso i termini linker e loader sono usati in modo interscambiabile.
Programma caricatore. A questo punto interviene il programma caricatore (loader), per trasformare tutti gli indirizzi in assoluti. Esso memorizza in uno speciale registro base o di riferimento lindirizzo di memoria a partire dal quale viene caricato il programma (detto indirizzo base), quindi somma tale indirizzo base a ogni indirizzo relativo, ottenendo un indirizzo assoluto.
In tal modo, se sarà necessario spostare il programma in unaltra zona di memoria (processo detto rilocazione), sarà sufficiente modificare il valore dellindirizzo base. Riassumendo, la trasformazione degli operandi di un programma in linguaggio assembly avviene, nel caso più generale, nelle tre fasi seguenti:
lassemblatore trasforma gli operandi simbolici in indirizzi assoluti, rilocabili o globali il legatore trasforma gli indirizzi globali in rilocabili il caricatore trasforma gli indirizzi rilocabili in assoluti.
Compilatore DMC. In Internet si trovano ottimi compilatori assolutamente freeware. Noi utilizzeremo il compilatore DMC della Digital Mars, che si può scaricare gratuitamente allindirizzo cliccando sul link:
Si tratta di un programma che esegue la complazione e il link di file C, C++ e ASM in un solo passaggio. Il download crea nella cartella Documenti una cartella dm con alcune sottocartelle:
Dato che il compilatore ( dmc.exe ) si trova nella sottocartella bin, il modo più veloce (anche se non il più elegante) per provare il funzionamento dei propri programmi in C consiste nel: 1.salvare il proprio programma in formato solo testo e con estensione.c nella sottocartella bin della cartella dm ; 2.eseguire il programma Prompt dei comandi da Start Tutti i programmi Accessori 3. dal prompt C:\Documents … \Documenti> digitare: cd dm\bin dmc hello.c 4. se loperazione ha successo, vengono creati i tre file hello.exe, hello.map, hello.obj, il primo dei quali si può ora eseguire con il comando hello