Costruire un assemblatore in C: la prima passata e la Symbol Table

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:

  1. 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.
  2. 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ì:

    1. Inizializziamo un contatore di locazione ( locationCounter ) a 00.
    2. Leggiamo il file sorgente SML riga per riga.
    3. Per ogni riga, verifichiamo se c’è un’etichetta (stringa seguita dai due punti).
    4. Se troviamo un’etichetta, la salviamo nella  symbolTable  insieme al valore attuale del  locationCounter .
    5. Incrementiamo il  locationCounter  (poiché ogni istruzione SML occupa una parola di memoria).
    6. 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.

    Avatar Paolo Godino