Compilatori, Assemblatori e Linker - Dal sorgente all'eseguibile | Sistemi e Reti Anno 3 Modulo 2 Lezione 02 | Prof. Giagnotti
Sistemi e Reti · Anno 3 · Modulo 2 · Lezione 02
✍ Autore: profgiagnotti · Materia: Sistemi e Reti

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.

⏱ 60 min 📚 Anno 3 🧠 Compilazione 💻 8086 Assembly
// OBIETTIVI DI APPRENDIMENTO
✓Descrivere il ciclo di vita di un programma: da codice sorgente a codice macchina eseguibile dalla CPU.
✓Distinguere con sicurezza i ruoli di compilatore, assemblatore, linker e loader, e i file .o, .obj, .exe.
✓Confrontare i tre modelli di traduzione: compilato (C/C++), interpretato (Python/JS) e ibrido (Java/JVM).
✓Spiegare le 4 fasi interne di un compilatore e perché l'assemblatore ha mapping quasi 1:1.
✓Collegare il processo al laboratorio 8086: da .asm a .obj a .exe caricato in memoria reale.

1. Il ciclo di vita di un programma

📌 Definizione da ricordare

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.

💬 Tip del docente

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.

SORGENTE main.c / prog.asm testo leggibile COMPILATORE / ASSEMBLATORE analisi lessicale → sintattica → ott. → gen. codice .o / .obj .o file oggetto parziale LINKER unisce .o + librerie risolve simboli → .exe / a.out CPU 8086 → ESECUZIONE fetch-decode-execute codice macchina reale caricato in RAM

2. I tre modelli di traduzione

Esistono tre modalità di traduzione, ognuna adatta a contesti diversi:

01 · PRE-ESECUZIONE

Compilazione

Traduzione completa prima dell'esecuzione — produce un eseguibile autonomo ad alte prestazioni. Tipico C/C++.

02 · RUNTIME

Interpretazione

Traduzione durante l'esecuzione — l'interprete legge ed esegue riga per riga. Nessun .exe, più lento ma flessibile.

03 · IBRIDO

Modello ibrido

Compilazione in bytecode intermedio, eseguito da una macchina virtuale portabile: Java JVM, C# CLR.

🎯 Esempio in classe

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

01 · LESSICALE

Analisi lessicale

Il compilatore legge il sorgente e identifica i token: parole chiave (if, while), operatori (+, =), identificatori, numeri.

02 · SINTATTICA + SEMANTICA

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).

03 · OTTIMIZZAZIONE

Ottimizzazione intermedia

Elimina codice morto, srotola loop, propaga costanti. Il codice diventa più veloce senza cambiare risultato. -O2 in GCC fa questo.

04 · GENERAZIONE

Generazione codice oggetto

Produce file oggetto .o / .obj: codice macchina parziale, con simboli non ancora risolti, non eseguibile da solo.

main.c int a = 5+3; preprocessore → #include, #define COMPILATORE (gcc -c) LEXERtoken PARSERAST + types OPTIMIZER-O2 CODEGEN.o .obj genera codice oggetto rilocabile — indirizzi ancora simbolici main.o codice macchina + tabella simboli NON eseguibile
C · TERMINALE GCC
// Solo compilazione, senza link → produce .o gcc -c main.c -o main.o gcc -c utils.c -o utils.o // Vedi simboli non risolti: nm main.o // U printf → undefined, sarà risolto dal linker

4. Il linker e i file oggetto: come nasce l'eseguibile

📌 Definizione da ricordare

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.

COMPITO 1

Unione moduli

Raccoglie tutti i .o e le librerie statiche (.a, .lib) e li concatena in un unico file.

COMPITO 2

Risoluzione simboli

Collega ogni chiamata a funzione/variabile esterna al suo indirizzo reale. Es: printf → libc.

COMPITO 3

Rilocazione

Assegna indirizzi finali (code, data, bss) e crea header eseguibile per il loader OS.

⚠️ Errore comune in verifica

"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!

BASH · LINKING
// Fase 2: linking → eseguibile gcc main.o utils.o -o programma.exe -lm // Cosa succede: # main.o (usa add() e printf) # utils.o (definisce add()) # -lm = libm.a + libc → printf, ecc. # → programma.exe pronto per loader // Modello statico vs dinamico: # statico: libreria copiata dentro .exe (file grande, autonomo) # dinamico: .exe contiene riferimento a .dll/.so (piccolo, richiede lib a runtime)
main.o utils.o libc.a / .soprintf, ecc. LINKER (ld)risolve simboliriloca .text .datacrea header ELF/PEprogramma.exe ESEGUIBILE CARICABILE Header: entry point 0x00401000 .text (codice macchina) .data .bss .rodata tabella import .dll/.so → OS loader lo mette in RAM

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.

🎯 Perché si usa?

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.

PYTHON vs C · CONFRONTO
# Python - interpretato # python3 main.py → interprete legge ed esegue subito x = 5 print(x) # tradotto qui e ora /* C - compilato */ // gcc main.c -o main → tradotto PRIMA, poi esegui ./main int x = 5; printf("%d", x);

Modello ibrido — Java / C#

Il modello ibrido unisce i due mondi: compilazione in bytecode intermedio portabile, poi esecuzione tramite macchina virtuale.

JAVA

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.

C# / ANDROID

Stessa logica

C# → IL → CLR. Kotlin/Java Android → dex bytecode → ART. L'idea è: scrivi una volta, la VM adatta al processore reale.

Main.javasorgente javac Main.classbytecode portabile java JVM - Java Virtual Machineinterprete + JIT compilerJIT → x86/ARMGC, stack CPU nativaWindows/LinuxmacOS/ARM

6. L'assemblatore: Assembly → macchina 1:1

📌 Definizione da ricordare

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.

ASM 8086 · NASM / EMU8086
; prog.asm - somma semplice 8086 .MODEL SMALL .DATA a DB 5 b DB 3 .CODE START: MOV AL, a ; B0 05 → codice macchina reale ADD AL, b ; 02 06 xx xx MOV AH, 4Ch INT 21h END START ; Assemblaggio: ; nasm prog.asm -f obj -o prog.obj ; oppure emu8086 → assemble → emula o crea .exe ; tlink / tasm prog.obj → prog.exe
🎯 Perché Assembly in questo corso?

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.

Assembly 8086MOV AX, 5ADD AX, BXINT 21hleggibile umano ASM ASSEMBLATOREMASM / TASM / NASMEMU8086mapping 1:1 → veloce Codice macchina 8086B8 05 0001 D8CD 21eseguito direttamente dalla CPU
💬 Tip del docente — verifica orale

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:

// ASSEMBLAGGIO

Traduzione 1:1 reale

Ogni istruzione Assembly che scriviamo viene tradotta in codice macchina reale dall'assemblatore. Vedi opcode in hex.

// LINKING

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.

// ESECUZIONE

Codice reale

Il codice prodotto è codice macchina reale — eseguito direttamente dalla CPU, ciclo per ciclo, senza VM.

🎯 Laboratorio 8086 — toolchain completa
  • Passo 1: scrivi prog.asm in 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.
EMU8086 · WORKFLOW COMPLETO
; --- prog.asm --- .MODEL SMALL .STACK 100h .DATA msg DB 'Ciao 8086 $' .CODE MAIN PROC MOV AX, @DATA MOV DS, AX MOV DX, OFFSET msg MOV AH, 09h INT 21h MOV AH, 4Ch INT 21h MAIN ENDP END MAIN ;; EMU8086: ;; 1. File → Compile → prog.obj creato (codice macchina parziale) ;; 2. Emulate → vedi registri, memoria, opcode tradotti ;; 3. Create .exe → linker interno produce prog.exe ;; 4. OS loader DOS → carica in memoria, CS:IP punta a entry
// perché studiare questo

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.

✓Il codice sorgente deve essere tradotto in codice macchina prima dell'esecuzione — CPU capisce solo 0/1
✓Nei linguaggi compilati (C/C++) traduzione prima dell'esecuzione — eseguibile autonomo ad alte prestazioni, 4 fasi: lexer, parser, optimizer, codegen
✓Il compilatore genera file oggetto .o / .obj — codice macchina parziale rilocabile, simboli non risolti, non eseguibile
✓Il linker unisce file oggetto e librerie, risolve simboli, riloca, produce file eseguibile finale ELF/PE
✓Nei linguaggi interpretati (Python/JS) traduzione a runtime — nessun .exe, serve interprete, errore solo quando riga eseguita
✓Modello ibrido (Java) compila in bytecode .class eseguito da JVM → portabilità + JIT veloce
✓Assemblatore traduce Assembly in macchina con corrispondenza quasi 1:1 — ideale per capire 8086 ciclo per ciclo
✓Nel corso 8086 usiamo Assembly — codice prodotto è codice macchina reale eseguito direttamente dalla CPU, caricato dal loader in RAM

📚 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.