Compilatore

Transcript

Compilatore
COMPILAZIONE
Tarsformazione di linguaggi
Le fasi della compilazione
File sorgente
Compilazione
File oggetto
File sorgente
File sorgente
Compilazione
File oggetto
Compilazione
File oggetto
Link
File eseguibile
File sorgente
Compilazione
Libreria di
run-time
File oggetto
Il set di istruzioni del C è molto limitato: molte
operazioni vengono delegate alla libreria di
runtime, che contiene programmi di supporto
Le funzioni sono divise in gruppi, quali I/O,
gestione della memoria, operazioni matematiche e
manipolazione di stringhe, …
Per ogni gruppo di funzioni esiste un file
sorgente, chiamato file header, contenente le
informazioni necessarie per utilizzare le funzioni
I codici sorgente ed oggetto possono essere suddivisi in più file, il codice
eseguibile di un programma risiede in un unico file
Le fasi della compilazione
File sorgente
Compilazione
File oggetto
Link
File eseguibile
La compilazione del file sorgente
Il preprocessore legge in input un sorgente C e produce in output un sorgente C,
dopo aver effettuato trasformazioni e sostituzioni sul file sorgente iniziale.
Alcune operazioni gestite dal preprocessore C sono
• Inclusione di file esterni (tramite la direttiva #include)
• Rimozione dei commenti
• Espansione delle macro
• Esclusione di parti di codice al verificarsi di determinate condizioni
(compilazione condizionale)
Il compilatore ha il compito di tradurre il codice sorgente in codice oggetto ed è
esso stesso un programma (o un gruppo di programmi) che deve essere eseguito
Il compilatore riceve in ingresso un programma scritto in un determinato
linguaggio di programmazione di alto livello e produce in uscita il codice oggetto,
cioè istruzioni in un linguaggio tipicamente di basso livello, come l’assembly
(mappabile 1 a 1 con il codice macchina ma ancora comprensibile all’uomo), che
rappresenta una fase intermedia tra il codice sorgente ed il codice eseguibile
La compilazione del file sorgente
Il codice assembly deve essere ora tradotto in codice macchina binario. La
traduzione è eseguita dall’Assembler. Nel linguaggio macchina le istruzioni sono
rappresentate da una sequenza di cifre binarie che codificano le istruzioni e i dati su
cui lavora la CPU:
codice operativo dell’istruzione
00000010
operando
000000000011100
Un programma (istruzioni + dati) è quindi una sequenza di numeri binary:
La compilazione del file sorgente
Nel linguaggio assembly le singole istruzioni binarie sono rappresentate con un
codice mnemonico:
LOAD
SUM
MEM
220
252
220
Il passaggio da assembly a linguaggio macchina è quindi un’operazione più semplice
di quanto non lo sia quella di passare da un lingiaggio ad alto livello ad assembly.
Il linguaggio assembly rispecchia fedelmente le istruzioni del linguaggio macchina,
quindi linguaggi assembly differenti per CPU caratterizzate da diversi insiemi di
istruzioni
I linguaggi di alto livello sono più simili al linguaggio naturale:
somma = somma+nuovo_dato;
Il link ed il caricamento
I file oggetto creati dal compilatore vengono trasformati in un unico file eseguibile
mediante il programma di link. Il linker, nel caso in cui la costruzione del programma
oggetto richieda l’unione di più moduli (compilati separatamente), provvede a collegarli
formando un unico programma eseguibile
Il linker provvede anche alla risoluzione dei riferimenti a funzioni e variabili definite
altrove (ad es., in librerie standard o definite dall’utente)
La necessità di collegare tra loro procedure diverse deriva dal fatto che di solito i
programmi vengono scritti suddividendoli in più procedure, le quali sono tipicamente
tradotte in modo separato. La scelta di compilare e assemblare separatamente ogni
procedura sorgente risponde a criteri di modularità, in quanto in tal modo risulta più
semplice la modifica di una delle sue parti, a vantaggio della velocità complessiva del
processo di traduzione e collegamento. Ciò risulta tanto più importante al crescere del
numero di procedure presenti (che in programmi particolarmente complessi possono
raggiungere le migliaia di unità). Inoltre, i programmi fanno spesso appello a funzioni
standard il cui codice è già stato scritto, compilato e testato, tanto che il relativo file
oggetto viene opportunamente conservato in un’apposita parte del software di sistema,
detta libreria di codice, pronto per essere utilizzato da chiunque ne abbia bisogno.
Il link ed il caricamento
A un livello architetturale più basso, il ruolo fondamentale del linker consiste nella
risoluzione del problema della rilocazione e del binding dei nomi, operazioni legate alle
variazioni di indirizzo dei segmenti di codice in seguito alla compilazione.
Nonostante l’operazione di link sia gestita automaticamente in alcuni sistemi operativi
(per es., UNIX), il linker è un programma distinto dal compilatore: in alcuni ambienti il
programma di link deve essere lanciato separatamente
Infine, durante la fase di caricamento (o loading), il programma eseguibile viene
prelevato dalla memoria di massa e caricato nella memoria principale; la maggior
parte dei sistemi operativi carica automaticamente un programma quando viene digitato il
nome (o “cliccata” l’icona) di un file eseguibile
Per ciascuna istruzione e ciascun dato del programma deve essere definito l’indirizzo di
memoria principale in cui devono essere registrati. Il caricatore, con l’ausilio del S. O.,
individua una area della memoria centrale che può contenere la sequenza di istruzioni e
dati del programma da caricare
Riassumendo
1. Traduzione in linguaggio target (tipicamente assembly)
• Analisi: lessicale (token), grammaticale (albero di sintassi), contestuale (albero
di sintassi astratto)
• Trasformazione del programma sorgente in programma oggetto (forma
più vicina al linguaggio macchina):
• Creazione della tabella dei simboli
• Ottimizzazioni (rimozione ripetizioni, eliminazione cicli, gestione
registri, etc.) per aumentare l’efficienza di calcolo
2. Collegamento
• Il codice oggetto così formato…
• …può ancora contenere simboli irrisolti e riferimenti esterni a
programmi di servizio (librerie di runtime)
• …contiene indirizzi relativi
• Il linker collega i diversi moduli oggetto
Albero della sintassi
Ad esempio, due alberi sintattici distinti per
l’istruzione
if B1 then if B2 then S1 else S2
potrebbero essere quellli rappresentati in
figura:
Riassumendo
3. Caricamento in memoria
• Il loader serve per caricare in memoria un programma rilocabile (in un
programma rilocabile gli indirizzi di memoria generati dal compilatore sono
relativi: gli indirizzi sono espressi a meno di una costante di rilocazione (o
spiazzamento) Il valore della costante di rilocazione è definito all’atto del
caricamento del programma per la sua esecuzione sulla base dell’effettiva
occupazione, stato e pianificazione della memoria in quel momento. Agli
indirizzi relativi è aggiunto il valore della costante di rilocazione per ottenere
quello dell’effettivo indirizzo assoluto in cui caricare ciascuna istruzione)
• Nel caricamento vengono fissati tutti gli indirizzi relativi
• variabili, salti, etc.
• Vengono caricati anche i programmi di supporto, se necessari
Riassumendo
I file che vengono creati
sono i seguenti:
.c - file sorgente prodotto
da un editor di testo e
preso in ingresso dal
compilatore
.obj - file oggetto
prodotto dal compilatore
dopo aver processato i file
sorgente e elaborato poi
dal linker
.exe - file eseguibile
prodotto dal linker.
Editor
Preprocessor
Compiler
Linker
Disk
Il programma è creato con
l’editor e salvato su disco
Disk
Il preprocessore analizza il
codice
Il compilatore crea il codice
oggetto e lo salva su disco
Disk
Il linker collega l’oggetto
con le librerie, crea un file
eseguibile e lo salva su disco
Disk
Primary
Memory
Loader
Disk
Il loader carica il
programma in memoria
..
..
..
Primary
Memory
CPU
..
..
..
La CPU preleva le singole
istruzioni e le elabora,
salvando i nuovi valori
ottenuti durante l’esecuzione
Ambiente di sviluppo
È l’insieme dei programmi che, complessivamente, consentono la scrittura, la
verifica e l’esecuzione di nuovi programmi (fasi di sviluppo)
Oltre a editor (per la scrittura dei file sorgente), compilatore, linker e loader, può
includere un programma di rilevamento e correzione degli errori
La compilazione, per quanto consista delle diverse fasi appena descritte, può
essere eseguita con un unico comando che traduce il file sorgente in un file
binario eseguibile
Il compilatore interrompe la compilazione su file sorgenti che contengono
errori lessicali e sintattici, oltre che evidenti errori semantici. Inoltre, il
compilatore produce messaggi di warning indicando parti di codice che
potrebbero presentare problemi durante l’esecuzione.
Debugger: consente di eseguire passo passo un programma, controllandone la
correttezza, al fine di scoprire ed eliminare errori non rilevati in fase di
compilazione
Compilatori e interpreti
Compilatore: PRIMA si traduce tutto il programma POI si esegue la versione
tradotta
Compilatori e interpreti
Interprete: PRIMA si traduce una singola istruzione del programma sorgente
nell’istruzione in linguaggio macchina equivalente, la quale viene subito eseguita,
POI si passa all’interpretazione dell’istruzione successiva.
Traduzione ed esecuzione sono intercalate.
(L’interpretazione può essere vista come la riproposizione ad alto livello del ciclo
di fetch-execute della macchina di von Neumann)
Compilatori e interpreti
Esempio di Compilatore:
Dobbiamo sottoporre un curriculum, in inglese, ad una azienda, ma non
conosciamo l’inglese
Abbiamo bisogno di un traduttore che traduca quanto scritto da noi
dall’italiano all’inglese
• contattiamo il traduttore
• il traduttore riceve il testo da tradurre
• il traduttore fornisce il testo tradotto
• possiamo sottoporre il nostro curriculum all’azienda
Compilatori e interpreti
Esempio di interprete:
Dobbiamo incontrare un manager cinese per motivi di lavoro ma non
conosciamo il cinese
Abbiamo bisogno di un interprete che traduca il nostro dialogo
• contattiamo l’interprete
• parliamo in italiano, in presenza dell’interprete
• contemporaneamente l’interprete comunica al manager cinese quanto
detto da noi e viceversa
Il compito dell’interprete si svolge contestualmente all’incontro col
manager cinese
Compilatori e interpreti
Riassumendo…
I compilatori traducono un intero programma dal linguaggio sorgente al
linguaggio macchina della macchina prescelta:
• traduzione e esecuzione procedono separatamente
• al termine della compilazione è disponibile la versione tradotta del
programma
• la versione tradotta è però specifica per quella macchina
• per eseguire il programma basta avere disponibile la versione tradotta
(non è necessario ricompilare)
Gli interpreti invece traducono e immediatamente eseguono il programma
istruzione per istruzione, infatti:
• traduzione ed esecuzione procedono insieme
• al termine non vi è alcuna versione tradotta del programma originale
• se si vuole rieseguire il programma occorre anche ritradurlo
Compilatori e interpreti
L’esecuzione di un programma compilato è più veloce dell’esecuzione di un
programma interpretato.
Per distribuire un programma interpretato si deve necessariamente distribuire il
codice sorgente, rendendo possibili operazioni di plagio.
Nei programmi interpretati, è facilitato il rilevamento di errori di runtime.
Sebbene in linea di principio un qualsiasi linguaggio possa essere tradotto sia
mediante compilatori sia mediante interpreti, nella pratica si tende verso una
differenziazione già a livello di linguaggio:
• Tipici linguaggi interpretati: Basic, Javascript, Perl, ...
• Tipici linguaggi compilati: C, Fortran, Pascal, ADA, …
(NOTA: Java costituisce un caso particolare, anche se si tende a considerarlo
interpretato; applica un ibrido fra le due soluzioni, utilizzando un compilatore per
produrre del codice intermedio che viene successivamente interpretato)
Compilatore, sistema operativo, hardware
Interprete, sistema operativo, hardware