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 runtime, 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 runtime) • …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 runtime. 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