Passiamo ora a descrivere un altro esempio di macchina convenzionale inventata di sana pianta, questa volta concepita secondo i dettami della "filosofia RISC" (Reduced Instruction Set). Questa macchina ipotetica, che chiameremo con la sigla VM-R (macchina virtuale di tipo RISC), non ha nessuna pretesa di rappresentare un esempio realistico di una architettura commerciale moderna. L'unico scopo di questo secondo esempio é di fornire un altro punto di riferimento preciso e dettagliato in contrapposizione alla macchina VM-2, per arrivare a comprendere meglio come può essere realizzata una macchina convenzionale, quali differenze di impostazione ci possono essere tra macchine RISC e macchine CISC (Complex Instruction Set), e come queste differenze si possono riflettere sulla possibilità di programmare in modo efficiente tali macchine. La scelta di una "vera architettura RISC" (MIPS, Alpha, SPARC, ecc.) comporterebbe di nuovo lo svantaggio di dover tener conto di una quantità di dettagli spaventosamente maggiore, correndo quindi il rischio di non apprezzare le differenze di sostanza.
La struttura del sistema VM-R é simile a quella della VM-2 vista in precedenza. Le differenze si possono riassumere in una maggior quantità di registri all'interno della CPU, in un insieme di istruzioni di dimensione ridotta (solo 16 istruzioni), e nella estensione dell'indirizzamento della RAM a 64K celle, sempre da 16 bit (quest'ultima differenza non sarebbe di per se sostanziale, se non per l'implicazione sulla dimensione dei registri destinati a contenere gli indirizzi, che risultano in questo caso essere da 16 bit, esattamente come i registri destinati a contenere i dati).
In particolare, la microarchitettura si compone di una unità centrale di elaborazione "a 16 bit", una unità di memoria a sola lettura (ROM) organizzata in 16K celle da 16 bit ciascuna (indirizzi da 0 a 16383), destinata a contenere il sistema operativo, un controllore per i dispositivi di ingresso (tastiera), uscita (video alfanumerico) e per l'unità a disco, con registri da 16 bit mappati dall'indirizzo 16384 fino a 32767; una unità di memoria RAM (statica) da 32K parole da 16 bit, che risponde agli indirizzi compresi tra 32768 e 65535.
Il controllore delle unità di ingresso e uscita comunica con la CPU mediante l'uso di due registri "mappati" sugli indirizzi di memoria 16384 e 16385, usando le stesse modalità operative e convenzioni già viste a proposito della macchina VM-2.
Per quanto riguarda la CPU, questa contiene 15 registri "veri" da 16 bit, individuati mediante i nomi R01, R02, ..., R15, più un "registro fittizio" chiamato R00 destinato a contenere esclusivamente la costante zero espressa su 16 bit. Alcuni tra i "registri veri" della CPU hanno un uso particolare, e per questo possono essere individuati con dei nomi alternativi. In particolare, il registro R04 viene usato come contatore di programma e quindi viene anche individuato col nome PC, il registro R05 viene usato come stack pointer (e quindi chiamato anche SP), il registro R06 viene usato come frame pointer (e quindi chiamato anche FP), il registro R07 viene usato per salvare l'indirizzo di ritorno nel caso di chiamata a procedura e viene quindi chiamato anche Retn. In ogni caso, "a livello hardware" i registri sono numerati da 0 a 15, e quindi sono individuabili tutti mediante una codifica binaria espressa su 4 bit. Nel momento in cui si manipolano i contenuti dei registri a seguito dell'esecuzione di istruzioni della macchina convenzionale, non si fa nessuna distinzione tra registri "di uso generale" e registri "specifici" (come per esempio FP). In questa peculiarità la macchina VM-R prende spunto più dall'architettura Digital PDP-11 che dalle odierne architetture RISC. Occorre anche notare che nella macchina VM-R, così come nel PDP-11 e a differenza della VM-2, lo stack pointer (SP) punta all'ultima cella usata nello stack anzichè alla prima libera, quindi l'istruzione "push" viene realizzata prima decrementando e poi scrivendo, mentre l'istruzione "pop" viene realizzata prima leggendo e poi incrementando il registro.
Per consentire la sperimentazione di programmi su questa macchina si suppone di avere la possibilità di introdurre "magicamente" dall'esterno insiemi di valori predefiniti nelle celle di memoria, esattamente come nel caso della macchina VM-2.
Le istruzioni della macchina VM-R vengono tutte codificate su 16 bit, in modo da poter utilizzare una singola cella di memoria per contenere la rappresentazione di una istruzione. Vengono definite solo 16 istruzioni per la VM-R, quindi il codice operativo delle istruzioni viene codificato nei 4 bit più significativi della rappresentazione. I rimanenti 12 bit meno significativi sono usati per codificare gli operandi, usando uno fra i quattro seguenti formati di rappresentazione:
opcode 0XXX |
regA AAAA |
regB BBBB |
regC CCCC |
opcode 10XX |
regA AAAA |
regB BBBB |
const CCCC |
opcode 11XX |
regA AAAA |
const CCCCCCCC |
opcode 1111 |
jcond CCC |
joffs OOOOOOOOO |
L'insieme delle istruzioni con la loro codifica binaria e la loro semantica
é riassunto nelle seguenti quattro tabelle.
Le istruzioni di tipo aritmetico e logico aggiornano un valore di
"condizione" che può poi essere interrogato da istruzioni di
salto condizionale.
L'ultima istruzione aritmetico/logica eseguita prima di un salto
condizionale é quella che determina il valore della
condizione.
0000.AAAA.BBBB.CCCC
vcond <-- rB + rC; rA <-- vcond
0001.AAAA.BBBB.CCCC
vcond <-- rB AND rC; rA <-- vcond
0010.AAAA.BBBB.CCCC
vcond <-- rB OR rC; rA <-- vcond
0011.AAAA.BBBB.CCCC
vcond <-- M[rB+rC]; rA <-- vcond
0100.AAAA.BBBB.CCCC
vcond <-- rB - rC; rA <-- vcond
0101.AAAA.BBBB.CCCC
vcond <-- rB NAND rC; rA <-- vcond
0110.AAAA.BBBB.CCCC
rA <-- rB; rB <-- rC (vcond non cambia)
0111.AAAA.BBBB.CCCC
vcond <-- rA; M[rB+rC] <-- vcond
vcond <-- espr
indica l'aggiornamento
del valore "condizione" col valore denotato da espr
.
Quindi, nel caso in cui rA=R00, come per esempio nell'istruzione ADD3 R00, rB, rC,
vcond viene comunque aggiornato con il valore effettivamente calcolato (rB+rC nell'esempio specifico), mentre rA <-- vcond non ha alcun effetto (essendo R00 un registro fittizio).1000.AAAA.BBBB.CCCC
vcond <-- (rotazione di rB di C posizioni); rA <-- vcondNel linguaggio assembly (simbolico) C è codificata in base 10 e può assumere solo valori negli intervalli [-8..-1] e [1..8];
1001.AAAA.BBBB.CCCC
for i in [1..C]
M[(rB - i) mod 65536] <-- R[(N + C - i) mod 16];
rB <-- (rB - C) mod 65536
(vcond non cambia)
1010.AAAA.BBBB.CCCC
vcond <-- (scorrimento di rB di C posizioni); rA <-- vcond
1011.AAAA.BBBB.CCCC
tmp <-- rB;
rB <-- (rB + C) mod 65536;
for i in [0..C-1]
R[(N + i) mod 16] <-- M[(tmp + i) mod 65536]
(vcond non cambia)
1100.AAAA.CCCCCCCC
vcond <-- rA + C; rA <-- vcond
C
rappresenta un numero con segno in
complemento a 2 su 8 bit;
quindi prima di essere sommato con l'altra rappresentazione
a 16 bit, viene fatta una estensione del bit di segno.
1101.AAAA.CCCCCCCC
vcond <-- rA AND C; rA <-- vcond
C
rappresenta un numero senza segno su 8 bit;
il risultato su 16 bit ha quindi gli 8 bit più significativi
al valore 0.
1110.AAAA.CCCCCCCC
rA <-- c (vcond non cambia)
C
rappresenta un numero con segno in
complemento a 2 su 8 bit;
quindi prima di essere assegnato ad un registro da
16 bit, viene fatta una estensione del bit di segno.
1111.CCC.OOOOOOOOO
if jcond then PC <-- (PC + joffs) mod 65536(il valore vcond rimane invariato; notare anche che mentre si esegue l'istruzione, PC risulta già incrementato e punta quindi all'istruzione successiva)
L'insieme delle istruzioni della macchina VM-R é veramente ridotto ai minimi termini! In particolare, rispetto alla macchina VM-2 sembra che manchino completamente istruzioni fondamentali per una macchina convenzionale moderna come CALL, RETN, ecc. In realtà é possibile ottenere effetti simili a quelli delle istruzioni "mancanti" usando le istruzioni esistenti applicate a "operandi particolari". Al fine di rendere il codice macchina più comprensibile agli esseri umani, useremo quindi un "trucco": introdurremo dei simboli aggiuntivi oltre a quelli che rappresentano le 16 istruzioni macchina vere e proprie, che chiameremo "pseudo-istruzioni". L'idea é che un apposito programma chiamato "assemblatore" potrebbe leggere questi simboli e tradurli automaticamente nei codici operativi delle istruzioni macchina con i valori appropriati di specifica degli operandi. Qui di seguito elenchiamo le pseudo-istruzioni di uso più comune per la programmazione della macchina VM-R:
0000.AAAA.BBBB.0000
rA <-- rB; vcond <-- rA(realizzato con l'istruzione ADD3 rA, rB, R00)
0101.AAAA.BBBB.BBBB
rA <-- (NOT rB); vcond <-- rA(realizzato con l'istruzione NAND rA, rB, rB)
0100.0000.AAAA.BBBB
vcond <-- (rB - rC)(realizzato con l'istruzione SUB3 R00, rB, rC)
1011.AAAA.0100.0000, CCCCCCCCCCCCCCCC
rA <-- const16bit(realizzato con l'istruzione POPR rA, PC, 1 seguita dalla costante memorizzata nella parola successiva; vcond non cambia. Vedasi primo esempio di programma)
1111.000.OOOOOOOOO
PC <-- (PC + joffs) mod 65536
1111.000.111111111
0110.0111.0100.CCCC
Retn <-- PC ; PC <-- rC(realizzato con l'istruzione MOVR Retn, PC, rC; vcond non cambia)
0110.0000.0100.0111
PC <-- Retn(realizzato con l'istruzione MOVR R00, PC, Retn; vcond non cambia)
Cominciamo a considerare un semplice esempio di programma per la lettura dal dispositivo di ingresso. In particolare definiamo una procedura che attenda l'inserimento di una cifra decimale e ne ritorni il valore decodificato (compreso tra 0 e 9) nel registro R03. In caso di errore la procedura ritorna un valore negativo nel registro R03.
32768: 1110000111010000 (LDIB R01, -48 --- 48 e' il carattere "0" in ASCII)Notare che, nonostante la disponibilità di un insieme di istruzioni più limitato, questo programma VM-R risulta essere composto da sole 12 istruzioni contro le 16 istruzioni necessarie alla macchina VM-2 per portare a termine lo stesso compito. Tale maggior compattezza del codice é principalmente dovuto alla disponibilità di un numero più elevato di registri e ad una maggior razionalità nella organizzazione dei modi di indirizzamento.
32769: 1011001001000000 (LDIW R02)
32770: 0100000000000000 (costante 16384 --- indir. registro del disp. di input)
32771: 0011001100100000 (LOAD R03, R02, R00)
32772: 1111111111111110 (CJMP LT -2 --- -2 indica l'istruz. di indirizzo 32771)
32773: 0000001100110001 (ADD3 R03, R03, R01)
32774: 1111011000000001 (CJMP GE +1 --- +1 indica l'istruzione di indirizzo 32776)
32775: 0110000001000111 (RETN)
32776: 1110000100001001 (LDIB R01, 9)
32777: 0100000000110001 (SCMP R03, R01 --- solo confronto)
32778: 1111110111111100 (CJMP LE -4 --- -4 indica l'istruz. di indirizzo 32775)
32779: 1110001111111111 (LDIB R03, -1)
32780: 0110000001000111 (RETN)
Questa procedura, come la successiva, NON usa lo stack. Infatti programmi e procedure che devono usare pochi dati li possono memorizzare direttamente nei registri della CPU. In questi casi prima di chiamare la procedura e prima di eseguire la pseudo istruzione RETN non é necessario manipolare i registri FP e SP. Notare che anche l'indirizzo di ritorno dalla procedura viene salvato temporaneamente nel registro Retn, e non nello stack.
Analogamente possiamo definire una procedura destinata a stampare un carattere numerico sul dispositivo di uscita, dato un valore V compreso tra 0 e 9 nel registro R03. Alla fine della procedura, se il registro R03 contiene un valore negativo allora si é verificato un errore che ha impedito la stampa, altrimenti il carattere é stato stampato. D'ora in avanti scriveremo le istruzioni in forma simbolica, lasciando per esercizio al lettore il gusto di assemblare le rappresentazioni binarie su 16 bit corrispondenti:
32781: SCMP R03, R00 --- confronta V con zero
32782: CJMP GE +1
32783: RETN 32784: LDIB R01, 9 32785: SUB3 R03, R01, R03 --- R03 <-- (9 - V)
32786: CJMP LT -4
32787: LDIB R01, 57 --- 57 = 48+9
32788: SUB3 R03, R01, R03 --- R03 <-- (48 + V)
32789: LDIW R02 32790: 16385 --- indir. registro del disp. di output) 32791: LOAD R01, R02, R00 32792: CJMP GE -2 --- busy waiting
32793: STOR R03, R02, R00 --- stampa il carattere
32794: RETN
Questa volta, il programma VM-R viene realizzato con 13 istruzioni ma il codice occupa 14 parole a causa della presenza della pseudo-istruzione LDIW (che usa una cella addizionale per la memorizzazione della costante), senza vantaggi in termini di compattezza rispetto alla versione VM-2 vista in precedenza.
Consideriamo ora la seguente variante, che scorpora una generica procedura di stampa di un carattere dal resto della funzione di stampa di una cifra decimale (riportiamo solo il codice a partire dalla cella 32789, in quanto quello precedente rimane invariato):
32789: LDIW R01 32790: 32795 --- inizio procedura stampa carattere 32791: PUSH Retn, SP, 1 32792: CALL R01 32793: POPR Retn, SP, 1 32794: RETN 32795: LDIW R02 32796: 16385 --- indir. registro del disp. di output) 32797: LOAD R01, R02, R00 32798: CJMP GE -2Tale codice mostra la necessità di salvare il contenuto del registro Retn sullo stack prima di poter chiamare una procedura dall'interno di un'altra procedura. I registri R01 ed R02 non necessitano di salvataggio in quanto, per convenzione, non li useremo mai per mantenere valori che debbano "sopravvivere" ad una chiamata di procedura (registri temporanei). Il registro R03 NON DEVE essere salvato, in quanto usato per passare un parametro dalla procedura chiamante a quella chiamata.
32799: STOR R03, R02, R00 32800: RETN
Usando questa versione modificata della funzione di scrittura di una cifra decimale e la procedura di scrittura di un generico carattere ASCII, siamo ora in grado di produrre una versione VM-R del programma già sviluppato per la VM-2 di lettura di due cifre numeriche dalla tastiera e stampa sul video del risultato della loro somma, con segnalazione di eventuali condizioni di errore:
32801: LDIW FPNotare la parte di inizializzazione dei valori per i registri SP ed FP e l'uso dell'istruzione POPR R08, PC, 2 per caricare di seguito due indirizzi espressi su 16 bit nei due registri R08 e R09. Notare inoltre che il programma principale memorizza valori che deve riutilizzare dopo la chiamata di procedure nei registri da R08 in poi, mentre i registri R01 ed R02 non vengono usati ed R03 viene usato per passare il parametro sia in ingresso che in uscita dalle funzioni. Notare infine che il valore del registro Retn non é significativo nel contesto del programma principale, e quindi questo può non essere salvato nello stack prima della chiamata delle procedure.
32802: 65535 --- indirizzo ultima cella RAM
32803: SUB3 SP, FP, R00 --- inizializza stack vuoto
32804: POPR R08, PC, 2
32805: 32768 --- indirizzo funzione lettura car.
32806: 32781 --- indirizzo funzione scrittura car.
32807: CALL R08
32808: MOV2 R10, R03 --- salva prima cifra in R10
32809: CJMP GE +4
32810: LDIB R03, 69 --- cod. ASCII di "E"
32811: ADD1 R09, 14 --- ind. proc. stampa car.
32812: CALL R09
32813: HALT 32814: CALL R08 32815: SCMP R03, R00 32816: CJMP LT -7 --- errore lettura 32817: ADD3 R03, R03, R10 32818: CALL R09 32819: SCMP R03, R00 32820: CJMP LT -11 --- errore scrittura 32821: HALT
Proseguendo con la riproposizione degli stessi esempi già visti per la VM-2, passiamo ora alla funzione di lettura di valori numerici in rappresentazione ottale. Come nel caso VM-2 un valore può essere rappresentato dall'utente digitando una qualunque sequenza di caratteri di tipo numerico (ciascuno compreso tra "0" e "7"); considereremo qualsiasi carattere diverso dalle cifre ottali come terminatore di una stringa di caratteri che identifica un numero. Il valore letto sarà passato al programma chiamante nel registro R03, e non verrà mai segnalata nessuna condizione di errore:
32768: LDIB R01, -48 --- 48 e' il carattere "0" in ASCIINotare che in questa versione occorre salvare e ripristinare i valori dei registri R08 e R09 per evitare interferenze coi dati eventualmente in essi memorizzati dal programma chiamante. Notare anche che la ricezione di un singolo carattere non ottale viene semplicemente interpretato come la terminazione di un numero (che per default assume il valore zero). Notare anche che non viene trattata esplicitamente nessuna condizione di overflow: se l'utente digita troppe cifre ottali, vengono semplicemente caricati gli ultimi 16 bit (quelli meno significativi) nel registro R03.
32769: LDIW R02
32770: 16384 --- indir. registro del disp. di input
32771: LDIB R03, 0
32772: PUSH R08, SP, 2
32773: LDIB R08, 8
32774: LOAD R09, R02, R00
32775: CJMP LT -2 --- -2 indica l'istruzione di indirizzo 32774
32776: ADD3 R09, R09, R01
32777: CJMP GE +2 --- +2 indica l'istruzione di indirizzo 32780
32778: POPR R08, SP, 2
32779: RETN
32780: SCMP R09, R08
32781: CJMP GE -4 --- fine lettura
32782: SHFT R03, R03, +3 32783: ADD3 R03, R03, R09 32784: JUMP -11 --- lettura prossimo carattere
Vediamo ora una versione ricorsiva per la realizzazione della procedura di stampa in codice ottale, analoga a quella già vista per l'architettura VM-2:
32785: PUSH Retn, SP, 1Anche in questo caso non sono previste segnalazioni di errore: la stampa va sempre a buon fine, utilizzando un numero di caratteri variabile a seconda del valore da stampare.
32786: SCMP R03, R00
32787: CJMP NE +6
32788: LDIB R03, 48 --- carattere "0" in ASCII
32789: LDIW R01
32790: 32804 --- inizio procedura stampa carattere
32791: CALL R01
32792: POPR Retn, SP, 1
32793: RETN
32794: PUSH R03, SP, 1
32795: SHFT R03, R03, -3
32796: CJMP EQ +3
32797: LDIW R01
32798: 32785 --- inizio procedura ricorsiva
32799: CALL R01
32800: POPR R03, SP, 1
32801: AND1 R03, 7 32802: ADD1 R03, 48 32803: JUMP -15 32804: LDIW R02 --- stampa carattere ASCII 32805: 16385 --- indir. registro del disp. di output 32806: LOAD R01, R02, R00 32807: CJMP GE -2 32808: STOR R03, R02, R00 32809: RETN
La versione modificata del programma principale VM-2 per leggere due valori, farne l'eco sul dispositivo di uscita e stampare il risultato della somma in ottale sarà:
32810: LDIW FP
32811: 65535 --- indirizzo ultima cella RAM
32812: SUB3 SP, FP, R00 --- inizializza stack vuoto
32813: POPR R08, PC, 3
32814: 32768 --- indirizzo funzione lettura val.
32815: 32785 --- indirizzo proc. scrittura val.
32816: 32804 --- indirizzo proc. scrittura car. ASCII
32817: CALL R08
32818: MOV2 R11, R03 --- salva primo val. in R11
32819: CALL R09
32820: LDIB R03, 43 --- cod. ASCII di "+"
32821: CALL R10
32822: CALL R08
32823: MOV2 R12, R03 --- salva secondo val. in R12
32824: CALL R09
32825: LDIB R03, 61 --- cod. ASCII di "="
32826: CALL R10
32827: ADD3 R03, R11, R12
32828: CALL R09 --- stampa risultato
32829: HALT
Notare nuovamente l'uso dei registri da R08 in poi per la memorizzazione di valori destinati a "sopravvivere" alle chiamate di procedura e di funzione, e l'uso del registro R03 per il passaggio di valori in ingresso ed in uscita alla procedura o funzione chiamata.
Analogamente a quanto fatto in precedenza per la macchina VM-2 vediamo ora una funzione che realizza la moltiplicazione tra due numeri positivi mediante l'algoritmo di somma e scorrimento. I due operandi vengono passati alla procedura inserendoli nei registri R01 e R02 prima della chiamata. Il risultato viene restituito dalla procedura al programma principale nel registro R03. Le condizioni di errore vengono segnalate producendo un risultato negativo nel registro R03. La procedura non garantisce di mantenere invariati i valori dei due registri R01 ed R02:
33000: LDIB R03, 0
33001: SCMP R01, R00
33002: CJMP GE +2
33003: LDIB R03, -1
33004: RETN
33005: CJMP EQ -2
33006: SCMP R02, R00
33007: CJMP LT -5
33008: CJMP EQ -5
33009: SCMP R01, R02
33010: CJMP LT +1
33011: MOVR R01, R02, R01 --- scambia R01 con R02
33012: PUSH R08, SP, 1
33013: LDIB R08, 1
33014: AND3 R00, R02, R08 33015: CJMP EQ +1 33016: ADD3 R03, R03, R01 33017: SHFT R02, R02, -1 33018: CJMP EQ +4 33019: SHFT R01, R01, +1 33020: CJMP LT +1 33021: JUMP -8 33022: LDIB R03, -1 33023: POPR R08, SP, 1 33024: RETN