Nello sviluppo del nostro progetto Simpletron, abbiamo raggiunto una tappa fondamentale. Dopo aver progettato e simulato la CPU, è arrivato il momento di colmare il divario tra il codice leggibile dall’uomo e le istruzioni macchina. Oggi iniziamo lo sviluppo di un assemblatore per il linguaggio SML, scritto interamente in C.
Scrivere un assemblatore è uno dei modi migliori per comprendere la “magia” che accade dietro le quinte della compilazione. In questo articolo (e nel video allegato) ci concentriamo su una tecnica classica e robusta: l’assemblatore a due passate (Two-Pass Assembler).
Il problema dei riferimenti in avanti (Forward References)
Perché abbiamo bisogno di due passate? Perché non possiamo semplicemente leggere il codice SML riga per riga e tradurlo subito in numeri?
Immaginate di avere questa istruzione all’inizio del vostro codice:
20 BRANCHZERO FINE
...
...
50 FINE: HALT
Quando l’assemblatore legge la riga 20, incontra l’etichetta FINE . Tuttavia, non ha ancora letto la riga 50, quindi non sa a quale indirizzo di memoria corrisponda FINE . Questo è un riferimento in avanti. Se provassimo a tradurre tutto in un colpo solo, ci bloccheremmo.
La soluzione: Divide et Impera
L’approccio a due passate risolve elegantemente questo problema dividendo il lavoro:
- Passata 1 (First Pass): Scansioniamo il codice sorgente solo per trovare le etichette e assegnare loro un indirizzo. Non generiamo alcun codice macchina qui. Costruiamo solo una mappa.
- Passata 2 (Second Pass): Rileggiamo il codice dall’inizio. Ora che conosciamo gli indirizzi di tutte le etichette (grazie alla prima passata), possiamo tradurre le istruzioni e generare il file eseguibile finale.
Implementare la Passata 1 in C
L’obiettivo di questa fase è costruire la Symbol Table (Tabella dei Simboli). In C, possiamo rappresentare questa tabella come un array di strutture, dove ogni struttura contiene il nome dell’etichetta e il suo indirizzo corrispondente (il Location Counter).
Ecco una possibile struttura dati in C:
#define MAX_SYMBOLS 100
#define MAX_LABEL 20
typedef struct {
char label[MAX_LABEL];
int address;
} symbol_t;
symbol_t symbolTable[MAX_SYMBOLS];
int symbolCount = 0;
L’algoritmo della prima passata funziona così:
- Inizializziamo un contatore di locazione ( locationCounter ) a 00.
- Leggiamo il file sorgente SML riga per riga.
- Per ogni riga, verifichiamo se c’è un’etichetta (stringa seguita dai due punti).
- Se troviamo un’etichetta, la salviamo nella symbolTable insieme al valore attuale del locationCounter .
- Incrementiamo il locationCounter (poiché ogni istruzione SML occupa una parola di memoria).
- Ripetiamo fino alla fine del file.
Perché il C?
Il linguaggio C è la scelta naturale per questo tipo di attività. La gestione diretta della memoria, la facilità di manipolazione delle stringhe (con strtok o puntatori) e l’uso delle struct ci permettono di creare un assemblatore efficiente e molto simile a quelli reali utilizzati nei primi sistemi Unix.
