Compilatori, Assemblatori e Linker: dal sorgente all'eseguibile
Il viaggio di un programma dal testo che scrivi al bit che la CPU esegue davvero. Capiremo perché serve tradurre, chi fa cosa tra compilatore, assemblatore e linker e dove si incastra il nostro 8086.
1. Il ciclo di vita di un programma
Codice sorgente = testo scritto da te in linguaggio ad alto livello. Codice macchina = sequenza binaria di opcode che la CPU sa eseguire. Senza traduzione, la CPU non capisce nulla.
Un programma scritto in un linguaggio ad alto livello prende il nome di codice sorgente. Il microprocessore può eseguire esclusivamente codice macchina — sequenze binarie di istruzioni. Il codice sorgente deve quindi essere tradotto prima di poter essere eseguito.
All'interrogazione parti sempre così: "La CPU esegue solo 0 e 1. Il mio file .c è testo ASCII, devo tradurlo". Mostri che hai capito la differenza hardware/software. È la frase che fa punteggio.
2. I tre modelli di traduzione
Esistono tre modalità di traduzione, ognuna adatta a contesti diversi:
Compilazione
Traduzione completa prima dell'esecuzione — produce un eseguibile autonomo ad alte prestazioni. Tipico C/C++.
Interpretazione
Traduzione durante l'esecuzione — l'interprete legge ed esegue riga per riga. Nessun .exe, più lento ma flessibile.
Modello ibrido
Compilazione in bytecode intermedio, eseguito da una macchina virtuale portabile: Java JVM, C# CLR.
Stesso algoritmo "somma di due numeri" in 3 modi: C → compila una volta, .exe gira veloce ovunque. Python → nessuna compilazione, l'interprete Python deve essere installato, traduce while gira. Java → compili in .class bytecode, la JVM traduce al volo in macchina specifica.
3. Linguaggi compilati — C / C++: le 4 fasi del compilatore
Nei linguaggi compilati la traduzione avviene interamente prima dell'esecuzione. Il processo si articola in due macro-fasi, ma il compilatore al suo interno fa 4 passi.
Fase 1 — Compilazione vera e propria
Analisi lessicale
Il compilatore legge il sorgente e identifica i token: parole chiave (if, while), operatori (+, =), identificatori, numeri.
Analisi sintattica e semantica
Verifica che la grammatica sia corretta (albero sintattico) e che le regole di tipo abbiano senso (non sommi una stringa a un int).
Ottimizzazione intermedia
Elimina codice morto, srotola loop, propaga costanti. Il codice diventa più veloce senza cambiare risultato. -O2 in GCC fa questo.
Generazione codice oggetto
Produce file oggetto .o / .obj: codice macchina parziale, con simboli non ancora risolti, non eseguibile da solo.
4. Il linker e i file oggetto: come nasce l'eseguibile
File oggetto .o = codice macchina parziale rilocabile. Linker = programma che fonde .o + librerie, risolve indirizzi e simboli, crea l'eseguibile finale.
Il compiler lavora file per file. Se il progetto ha 10 file .c, ottieni 10 file .o. Nessuno di questi sa dove è in memoria l'altro. Qui entra il linker.
Unione moduli
Raccoglie tutti i .o e le librerie statiche (.a, .lib) e li concatena in un unico file.
Risoluzione simboli
Collega ogni chiamata a funzione/variabile esterna al suo indirizzo reale. Es: printf → libc.
Rilocazione
Assegna indirizzi finali (code, data, bss) e crea header eseguibile per il loader OS.
"Il compilatore produce l'eseguibile". NO. Il compilatore produce file oggetto. È il linker che produce l'eseguibile. Se dimentichi una libreria ottieni "undefined reference", errore di linking, non di compilazione!
5. Linguaggi interpretati e modello ibrido
Interpretazione pura — Python, JavaScript
Nei linguaggi interpretati la traduzione avviene a runtime: l'interprete legge il sorgente riga per riga, lo traduce al volo in codice macchina (o lo esegue via albero) e continua. Nessun eseguibile autonomo: serve l'interprete installato.
Prototipazione rapida, scripting, web. Python sul Raspberry: modifichi file .py e lo rilanci senza compilare. Svantaggio: più lento, errore scoperto solo quando quella riga viene eseguita.
Modello ibrido — Java / C#
Il modello ibrido unisce i due mondi: compilazione in bytecode intermedio portabile, poi esecuzione tramite macchina virtuale.
Sorgente → bytecode → JVM
javac compila .java in .class (bytecode). La JVM interpreta o JIT-compila il bytecode in macchina reale al momento dell'uso. Portabile ovunque c'è JVM.
Stessa logica
C# → IL → CLR. Kotlin/Java Android → dex bytecode → ART. L'idea è: scrivi una volta, la VM adatta al processore reale.
6. L'assemblatore: Assembly → macchina 1:1
Assemblatore: traduce linguaggio Assembly (mnemonico leggibile) in codice macchina con corrispondenza quasi 1:1. A differenza del compilatore, non ottimizza molto: una riga ASM = una istruzione macchina.
L'Assembly 8086 è il linguaggio più vicino alla CPU che useremo. Ogni mnemonico come MOV AX, 5 corrisponde a un opcode reale: B8 05 00.
Perché vedi davvero cosa fa la CPU. In C scrivi a = b + c e il compilatore genera 10 istruzioni ottimizzate che non capisci. In ASM scrivi tu MOV, ADD, INT e ogni ciclo di clock è tuo. È didattico: impari Von Neumann sul serio.
Se ti chiedo "differenza tra compilatore e assemblatore?" rispondi: "Il compilatore traduce da alto livello e deve ottimizzare, una riga C genera molte istruzioni. L'assemblatore traduce quasi 1:1, una riga ASM = un opcode, non ottimizza, è quasi un traduttore meccanico". Così prendi punteggio pieno.
7. Tabella di confronto definitiva
| Modello | Prodotto compilazione | Serve linker? | Eseguibile autonomo? | File tipici |
|---|---|---|---|---|
| Compilato C/C++ | File oggetto (codice macchina parziale) | ✔ Sì, unisce .o + librerie | ✔ Sì, .exe / a.out | .c → .o → .exe |
| Assembly | File oggetto .obj / .o con opcode 1:1 | ✔ Sì (LINK/TLINK) | ✔ Sì, .exe / .com reale 8086 | .asm → .obj → .exe |
| Interpretato puro Python, JS |
✖ Nessuno (traduzione a runtime) | ✖ No, solo loader dinamico moduli | ✖ No, serve interprete | .py / .js diretti |
| Ibrido Java | ✔ Bytecode portabile .class |
✖ No (JVM carica classi dinamicamente) | Pseudo: .class + JVM = gira ovunque | .java → .class → JVM |
8. Collegamento al corso — microprocessore 8086
Nel contesto dello studio del microprocessore 8086 lavoreremo direttamente con il linguaggio Assembly. Questo significa che:
Traduzione 1:1 reale
Ogni istruzione Assembly che scriviamo viene tradotta in codice macchina reale dall'assemblatore. Vedi opcode in hex.
Unisce moduli
Il linker unisce i moduli e produce l'eseguibile che viene caricato in memoria dalla CPU. Anche con un solo file, Emu8086 fa link implicito.
Codice reale
Il codice prodotto è codice macchina reale — eseguito direttamente dalla CPU, ciclo per ciclo, senza VM.
- Passo 1: scrivi
prog.asmin EMU8086. - Passo 2: Assembla →
prog.obj(controlla con HEX editor: vedi B8, CD...). - Passo 3: Linka →
prog.exe(DOS MZ header). - Passo 4: Esegui/Debug: step-by-step vedi IP, AX, FLAGS cambiare. È esattamente ciclo fetch-decode-execute di Von Neumann.
La traduzione di un programma non è un passaggio secondario — è il momento in cui il codice diventa eseguibile dall'hardware. Comprendere compilatori, assemblatori e linker è essenziale per capire cosa accade concretamente quando un programma viene caricato in memoria ed eseguito dal microprocessore 8086.
9. Riepilogo e concetti chiave
📚 Prossimo passo — Algoritmi, programmi e classificazione dei linguaggi
Ora che sai come il codice diventa eseguibile, la prossima lezione mette ordine nei linguaggi: cosa è un algoritmo, differenza tra programma e algoritmo, linguaggi naturali vs formali, paradigma imperativo vs dichiarativo. Porta appunti su compilatore vs interprete, ti chiederò tabella confronto.