Ci focalizziamo adesso sulla struttura software di un sistema partendo dall’assembler, ovvero quel programma che traduce codice assembly in rappresentazione binaria.

Anche in questo caso, il risultato sarà un assembler semplice ma che mostra i principi chiave della realizzazione di strumenti più complessi ed evoluti.

A differenza degli articoli precedenti in cui facevamo uso del linguaggio HDL, da questo in poi i progetti potranno essere implementati in qualsiasi linguaggio di programmazione.

Background

Esistono 2 macrotipologie di linguaggi macchina:

  • Simbolici.
  • Binari.

nella sua controparte binaria, che rappresenta quella realmente eseguita dal calcolatore, i bit espressi in ciascuna istruzione vanno a codificare particolari comandi a seconda della loro posizione e del loro valore.

Il risultato è una grande quantità di combinazioni difficili da ricordare e utilizzare per un programmatore.

La soluzione a questo problema è ideare una sintassi che renda più leggibile e usabile un’istruzione, come ad esempio:

LOAD R3, 9 <- anzichè -> 1011 0011 0001 1001

L’assembler avrà dunque l’obiettivo di prendere un programma scritto in assembly, tradurlo e assemblarlo insieme in istruzioni binarie.

Simboli

Esemi dell’uso di simboli nel linguaggio assembly possono essere:

  • LOAD R3, 9
  • LOAD R3, WEIGHT
  • GOTO 250
  • GOTO LOOP

questi sono usati principalmente per:

  • Variabili: il programmatore può usare variabili simboliche che verranno sostituite da indirizzi di memoria.
  • Etichette: il programmatore può marcare varie locazioni nel programma per poi facilitare l’uso di salti condizionali o incondizionali.

Ovviamente questo complica leggermente le cosa e fa sì che un assembler non sia solo un mero traduttore da simbolo a rispettivo codice binario.

Per far sì che i simboli possano essere risolti, si stabilisce una convenzione tale per cui il programma scritto viene memorizzato, ad esempio, a partire dall’indirizzo 0, mentre le variabili dall’indirizzo 1024.

Una volta definita, si costruisce quella che in gergo prende il nome di symbol table. Questa tabella viene poi usata per tradurre il codice nella sua versione symbol-less.

Tieni a mente che nell’architettura di Hack sono state fatte diverse semplificazioni per renderne la realizzazione più semplice possibile, in particolare:

  • in sistemi complessi le variabili vengono salvate in indirizzi più alti di 1024 così da permettere la realizzazione di programmi più lunghi.
  • in sistemi complessi non è detto che ad una istruzione corrisponda sempre una ed una sola word; esistono comandi che necessitano di più word per essere memorizzate e gestite.
  • infine, non è detto che una variabile sia codificabile sempre e solo con una parola. Esistono variabili che occupano più parole a seconda del tipo di dato usato.

Il Progetto

L’assembler che andremo a realizzare è strettamente legato alle specifiche del linguaggio macchina di riferimento (i cui dettagli sono presentati nel paragrafo 6.2 raggiungibile a questo link) e presenta delle caratteristiche comuni a quelli reali.
In particolare esso deve essere composto da 4 componenti fondamentali:

  1. Parser: divide ogni comando nei suo sotto componenti.
  2. Code: traduce il comando mnemonico nella sua controparte binaria.
  3. Symbol table: gestisce i simboli permettendo di recuperare gli indirizzi associati.
  4. Main program: esegue in modo organizzato i 3 componenti precedenti con l’obiettivo di giungere al risultato ultimo.

L’assembler di Hack legge un file denominato {prog}.asm e produce un file del tipo {prog}.hack.

Gli autori propongono inizialmente di sviluppare una prima versione che funzioni solo con programmi senza simboli, ciò può essere fatto usando i primi 2 moduli sopra proposti. Si passa poi ad implementare il modulo 3.

A questo punto si procede ad integrare l’uso dei simboli nell’assembler sviluppato. Per semplicità si fa in modo che il file .asm venga letto 2 volte: la prima per popolare la tabella dei simboli con le etichette presenti, la seconda per sostituirli con l’indirizzo corretto.

Anche in questo caso, ti propongo una possibile soluzione scritta in Python e liberamente scaricabile da questo link.

Testiamo la Soluzione

Per testare l’assembler da noi sviluppato, gli autori consigliano di usare i programmi presenti in nand2tetris/projects/06 presenti in 2 versioni: con (prog.asm) e senza simboli (progl.asm).

Una volta tradotti con l’assembler da noi sviluppato, si fa uso dell’assembler presente nella cartella nand2tetris/tools.

In particolare, una volta caricato il file .asm di uno dei programmi presenti nella cartella 06 cliccando su File -> Load Source File, clicca su File -> Load Comparison File e carica il file .hack generato dal tuo assembler.

Fatto ciò, clicca su Run -> Fast Translation.

Se tutto è andato a buon fine e i 2 compilati coincidono, vedrai comparire un messaggio in basso a sinistra del tipo: File compilation & comparison succeeded.

Conclusioni

In questo capitolo abbiamo discusso i principi fondamentali e come realizzare il primo strato software che ogni architettura che si rispetti deve avere, ovvero l’assembler, il programma che permette di tradurre codice assembly in codice binario.

Come in ogni appuntamento, anche in questo caso ti presento una soluzione possibile scritta in Python e scaricabile da questo link.

Nel prossimo capitolo andremo a discutere la realizzazione di una macchina virtuale così da porre le basi per lo sviluppo di un linguaggio di programmazione simile a Java.