Sei sulla pagina 1di 730

CALCOLATORI

Introduzione
Giovanni Iacca
giovanni.iacca@unitn.it

Luigi Palopoli
luigi.palopoli@unitn.it
Descrizione del corso
• Il corso (che si chiama anche «Architettura
degli elaboratori») si compone essenzialmente
di lezioni teoriche
• In aggiunta, avremo qualche esercitazione
sugli aspetti più pratici del corso (ad es.
aritmetica dei calcolatori e Assembly)
• In totale il corso è coperto da 48 ore di
didattica frontale (12 settimane x 2 lezioni x
2h)
Struttura del corso
Lez. Argomento

• Prima parte: introduzione, 1 Introduzione


2-4 Aritmetica dei calcolatori
aritmetica dei calcolatori, 5 Reti logiche

cenni su reti logiche (5 lez.) 6 Assembly


7-10 Assembly RISC-V
• Seconda parte: linguaggio 11 Assembly INTEL

Assembly (10 lez.) 12 Assembly ARM


13-14 Esercizi Assembly

• Terza parte: elementi di 15 Toolchain


16-17 CPU
architettura dei calcolatori 18-19 Pipeline

(8 lez.) + esempio d’esame 20-21 Gerarchia di memoria


22 Input/Output
(1 lez.) 23 Esempi di esame
24 Esercizi
Modalità di esame
• E’ prevista una prova scritta intermedia
• L’esame si compone di una prova scritta (svolto in forma
elettronica attraverso la piattaforma Moodle) e consiste in
larga parte di quesiti a risposta multipla (diversi per
ciascuno studente!)
• E’ previsto inoltre un orale di verifica dello scritto e
approfondimento della teoria
• Durante il corso vedremo periodicamente alcuni esercizi e
quesiti di test che costituiscono un esempio di quello che
incontrerete all’esame
Altre info
• Pagina Moodle del corso (per annunci, slide e altro materiale)

• Libro di riferimento:
David A. Patterson, John L. Hennessy,
«Struttura e progetto dei calcolatori
Progettare con RISC-V»,
Zanichelli 2019
• Ricevimento (via Zoom/Meet) su appuntamento
• Contatti
§ Giovanni Iacca
giovanni.iacca@unitn.it
§ Leonardo Custode, Andrea Ferigo (assistenti)
leonardo.custode@unitn.it
andrea.ferigo@unitn.it
Dove studiare cosa
Indicativamente:

• Lez. 1: Sez. 1.1-1.5, 1.7, 1.8, cenni 1.6 + Slide/materiale su Moodle


• Lez. 2-4: Sez. 2.4, 3.1-3.3, 3.5 (no HW e RISC-V) + Slide /materiale su Moodle
• Lez. 5: Sez. 2.6 + Slide/materiale su Moodle
• Lez. 6-10: Sez. 2.1-2.11 + Slide/materiale su Moodle
• Lez. 11: Sez. 2.17 + Slide/materiale su Moodle
• Lez. 12: Slide/materiale su Moodle
• Lez. 13-15: Slide/materiale su Moodle
• Lez. 16: Sez. 2.12-2.15 + Slide/materiale su Moodle
• Lez. 17-20: Cap. 4 (in parte) + Slide/materiale su Moodle
• Lez. 20-22: Cap. 5 (in parte) + Slide/materiale su Moodle
• Lez. 23-24: Slide/materiale su Moodle
CALCOLATORI
Aritmetica dei Calcolatori

Giovanni Iacca
giovanni.iacca@unitn.it

Lezione basata su materiale preparato


dal Prof. Luca Abeni
Calcolatori
• I calcolatori elettronici sono il prodotto di una
tecnologia estremamente vitale
• Produce il 10% del PIL degli Stati Uniti e
pervade le nostre vite
• E tutto cominciò da…
ENIAC
• Nel 1943 il Dipartimento della Difesa degli Stati
Uniti commissionò una macchina per il calcolo
delle traiettorie dei proiettili di artiglieria
• Nel Febbraio 1946 l’Università della Pennsylvania
mise in funzione ENIAC (Electronic Numerical
Integrator and Computer)
§ Occupava una stanza di 9 x 30 metri
§ Consumava così tanta energia che alla prima
accensione generò un blackout
ENIAC

… x 18000!

https://it.wikipedia.org/wiki/ENIAC
Apollo Guidance Computer (IBM, 1969)
• 61 x 32 x 17 cm, 32 kg
• 2800 circuiti integrati (nota: i primi risalivano al '59!)
• Processore @0.043-2 MHz (frequenze
diverse nei vari sottosistemi)
• 152 kByte complessivi ROM/RAM
• Interfaccia DSKY (display&keyboard)
§ tastiera numerica
(istruzioni: verbo + nome)
§ piccolo display
§ vari indicatori luminosi
Margaret Hamilton, 1969
Per fare un confronto: iPhone X
• 143.6 x 70.9 x 7.7 mm, 174 g
• 4.3 miliardi di transistor
• Processore @2.39 GHz (A11 Bionic 6-core ARMv8-A)
• 3GB RAM, fino a 64GB di memoria storage
• Interfaccia: touch-screen + video/voice
Una profezia pessimistica

“Mentre l’ENIAC è dotato di 18000 valvole


e pesa 30 tonnellate, i calcolatori del
futuro potranno avere solo 1000 valvole e
pesare solo una tonnellata e mezzo”,
Popular Mechanics 1949
…e poi: la rivoluzione dei PC

?
…e poi: la rivoluzione dei PC

Bendix G15 (1956)

Olivetti Programma 101 (1964)


https://www.youtube.com/watch?v=UWFZLgEiP0M

LGP-30 (1956)
E oggi?
La rivoluzione dei calcolatori
• I calcolatori hanno creato la terza rivoluzione della
nostra società portandoci nel mondo post industriale
• Solo pochi anni fa le seguenti applicazioni erano
considerate fantascienza
§ Calcolatori negli automobili
§ Telefoni cellulari
§ Mappatura del genoma umano
§ World Wide Web
§ Motori di ricerca
§ Robot di servizio
§ Auto a guida automatica
Calcolatore o calcolatori?
• I calcolatori che operano nelle applicazioni che
abbiamo introdotto condividono la stessa idea
di base…
• Ma le soluzioni usate per ciascuna tipologia di
applicazione possono essere piuttosto diverse
• Per questo parliamo di vari tipi di calcolatori
Vari tipi di calcolatori
• Calcolatori personali (desktop o laptop)
§ buone prestazione a costo ridotto
§ eseguono software di terze parti (architetture aperte)
• Server
§ Pensati per eseguire grandi carichi di lavoro
ü poche applicazioni molto complesse (calcolatori scientifici)
ü tantissime applicazioni molto semplici (web-server)
• Embedded
§ Coprono un vasto spettro di applicazioni (mobile, automotive, avionica, gaming)
§ Le applicazioni sono spesso “dedicate” e operano a stretto contatto con l’hardware
§ Requisiti non funzionali essenziali
ü consumi
ü rispetto di vincoli temporali
ü costo
Perché è importante studiarli?
• Le prestazioni del software sono importanti nel
decretarne il successo commerciale
• Un programma che viene eseguito più velocemente
o che ha minori requisiti hardware ha maggiori
probabilità di soddisfare le aspettative del cliente
• Fino a qualche tempo fa le prestazioni erano
dominate dalla disponibilità di memoria
• Oggi, questo è un problema solo per alcune
applicazioni embedded piuttosto specifiche
Perché è importante studiarli?
• Tuttavia, per scrivere un programma con buone
prestazioni un programmatore moderno deve
§ comprendere la gerarchia di memoria
§ fare un uso efficiente del parallelismo (multi-
threading, GPU, calcolo distribuito)
• In altre parole, deve conoscere (e comprendere)
l’organizzazione del calcolatore
Obiettivi del corso
• Alla fine del corso, sapremo:
§ Quali sono i componenti di base che permettono ad
un calcolatore di operare?
§ Come vengono tradotti i programmi in modo che il
calcolatore possa eseguirli?
§ Qual è l’interfaccia HW/SW tramite la quale il
programmatore può far fare all’HW ciò che richiede?
§ Cosa influenza le prestazioni di un programma e come
può un programmatore migliorarle?
§ Cosa può fare il progettista HW per migliorare le
prestazioni, e perché oggi si ricorre sempre di più alle
architetture multi-core?
Capire le presentazioni
Componente HW/SW Cosa influenza Dove è trattato?

Determina il numero di
Algoritmi istruzioni di alto livello e di Altri corsi
operazioni di IO
Determina il numero di
Linguaggi di programmazione,
istruzioni macchina per ogni Cap. 2, 3
compilatori e architetture
istruzione di basso livello
Determinano quanto
Processore e sistema di
velocemente è possibile Cap. 4, 5, 6
memoria
eseguire ciascuna istruzione

Determina quanto
Sistema operativo,
velocemente possono essere Cap. 4, 5, 6
gestione HW e I/O
eseguite le istruzioni

Cfr. tabella p. 7 Patterson-Hennessy


Software di sistema
• Sistema Operativo (SO)
§ gestisce le operazioni di I/O
§ alloca la memoria
§ consente il multitasking
• Compilatore
§ traduce da linguaggio ad alto livello a linguaggio macchina
Linguaggio macchina
• Il componente base di un calcolatore sono le porte
logiche, che corrispondono a “interruttori” elettrici
• L'unità base di informazione è il bit: un interruttore
vale 1 se acceso, 0 se spento
• Anche un’istruzione di linguaggio macchina deve
dunque essere codificata come una stringa (una
sequenza) di bit. Ad esempio:
1000110010100000
Programmazione Assembly
• Programmare tramite sequenze di bit è
estremamente difficile (per usare un eufemismo!)
• Per questo motivo, è stato introdotto un linguaggio
mnemonico (chiamato Assembly) che viene tradotto
in stringhe di bit da un traduttore (assembler)
• Ad esempio:

add A, B 00000000000010001000100001000000
Il programmatore L’assemblatore traduce l’istruzione
scrive questa in una sequenza di bit
istruzione
(somma A a B)
Il flusso completo

Molti compilatori
saltano questa fase
Componenti del Calcolatore
Le elaborazioni dei
dati sono effettuate
dal Processore (diviso in
una parte operativa e
una di controllo)

I dati vengono memorizza-


ti nelle unità di memoria

I dispositivi di input
(tastiera, mouse, ecc.)
e quelli di output
(video, stampanti, ecc.)
permettono di scambiare
informazioni con l’esterno
Esempi di IO: il mouse
• Il mouse è stato inventato nel 1967 da Doug
Engelbart nei famosi laboratori della Xerox
• La versione più moderna usa una tecnologia ottica
§ Alcuni led illuminano il piano ed elaborano l’immagine
§ Facendo la differenza tra immagini successive si scopre
in che direzione si è spostato il mouse

«The mother of all demos»


https://vimeo.com/32381658
Esempi di IO: lo schermo LCD

• Gli schermi (Liquid Crystal Display) LCD sono


attualmente diffusissimi sia nei PC sia nei telefoni
cellulari e in altri dispositivi embedded
§ Alcune molecole di cristalli liquidi ‘‘galleggiano’’ in un fluido (poco in realtà)
§ Uno strato di molecole di cristallo è associato a un punto (pixel)
§ Tramite un campo elettrico (applicato a ciascun pixel) si riesce a far ruotare
di 90 gradi il cristallo, che lascia passare o blocca la luce
Esempi di IO: lo schermo LCD
• L’immagine si compone di una matrice di pixel (es. 640 x 480,
1440 x 900 ecc.)
• Ciascun pixel è associato tipicamente a tre byte, ciascuno associato a
una delle tre componenti fondamentali Red (R) Green (G) e Blue (B)
• L’immagine viene memorizzata in una matrice (frame buffer), che è
essenzialmente una velocissima RAM che viene aggiornata molte
volte al secondo (da 50 a 100)
Dentro il PC

La scheda madre è una piastra


su cui sono montati i vari circuiti
integrati (chip)
La memoria volatile
è costituita da vari
banchi di (tipicamente) 8
chip di RAM dinamica

La memoria permanente
è costituita da Hard Disk/SSD
e CD/DVD ROM
Il processore
• Il processore è la parte attiva di ogni calcolatore.
Si compone di:
§ Datapath: esegue le operazioni aritmetiche sui dati
§ Parte di controllo: indica al datapath, alla memoria, e
alle componenti di IO cosa fare sulla base di quanto
stabilito nel programma Una memoria RAM
aggiuntiva (cache) all’interno
della CPU migliora
sostanzialmente
le prestazioni

Questa unità si compone di


quattro CPU (core)
Astrazioni
• Nel mondo dell’informatica si lavora molto con le
astrazioni che permettono di gestire un progetto di
grande complessità nascondendo i dettagli
• Il processore viene “nascosto” nei suoi dettagli
esportando come interfaccia l’insieme delle istruzioni
macchina che il processore offre (Instruction Set
Architecture)
• Insieme all’interfaccia del sistema operativo, l’ISA
costituisce l’interfaccia binaria delle applicazioni
(Application Binary Interface)
• Una volta definita una ABI, lo sviluppatore è svincolato
da come i dettagli HW sottostanti l’applicazione sono
implementati
La memoria
• La memoria si distingue in due tipi:
§ volatile (dominata dalle DRAM)
§ non volatile (dischi rigidi e memorie a stato solido)
• La memoria volatile viene usata per memorizzare dati
e programmi mentre questi vengono eseguiti. Per
questo motivo viene detta memoria principale
§ Allo spegnimento i dati vengono persi
• La memoria non volatile (o persistente) viene usata
per memorizzare dati e programmi tra esecuzioni
diverse.
§ Vista la quantità enorme di dati memorizzati si parla di
memoria di massa
Le DRAM
Memorie di massa
• Attualmente abbiamo
essenzialmente tre tipi di
memorie di massa
§ Memorie Flash (es. SSD)
§ Dischi rigidi
§ CD/DVD
• Le memorie Flash sono molto
simili a memorie RAM. L’idea è
di memorizzare il dato
intrappolando una carica
elettrica. Il fenomeno fisico
usato dalle Flash consente alla
carica di rimanere intrappolata
in maniera ‘‘permanente’’
Memorie di massa
• Hard Disk: il principio di funzionamento è di
magnetizzare delle particelle metalliche
distribuite su un substrato
§ I dischi sono organizzati in strutture
sovrapposte (cilindri)
§ Le particelle vengono lette da
dispositivo meccanico (testina) che si
sposta radialmente su un braccetto (in
grado di fare movimenti angolari).
§ Questa componente rallenta i tempi di
accesso ma aumenta la densità di
memorizzazione (è possibile arrivare
facilmente ai Terabyte)

https://www.youtube.com/watch?v=4BYrKhejb88
Dischi ottici

• I dischi ottici funzionano sulla base di


un semplice principio: la riflessione
della luce
• Viene emesso un raggio laser che
viene riflesso dai ‘‘rilievi’’ (bit 1) e
assorbito dalle ‘‘buche’’
• Nei dischi riscrivibili, un particolare
substrato consente (tramite
riscaldamento) di ritornare alla
situazione originale “spianando le
buche”
Interconnessione

• Gli sviluppi più significativi degli ultimi anni hanno


coinciso con lo sviluppo impetuoso della rete
• I calcolatori esprimono ormai la maggior parte del
loro potenziale quando sono interconnessi
§ Local Area Network: condivisione di risorse (dischi,
stampanti)
§ Wide Area Network: condivisione di contenuti
Definizione di prestazioni

• In genere, in un calcolatore si possono identificare


due tipi di prestazioni:
§ Per il singolo utente interessa sapere quanto è il tempo
medio di risposta (tempo che intercorre tra avvio e
terminazione di un task)
§ Per il gestore di un centro di calcolo, interessa di più il
throughput, cioè quanti task sono in grado di
completare nell’unità di tempo
Tempo di esecuzione
• In una macchina multitask il tempo di risposta dipende anche dagli altri
task attivi e dalle loro priorità
• Il tempo di esecuzione della CPU tiene conto solo del tempo
effettivamente speso per il task
• Tale tempo è in parte dedicato al programma utente e in parte al sistema
operativo

Interferenza
dovuta agli altri
task Tempo di CPU
Tempo di Risposta Utente
Tempo di
esecuzione
della CPU Tempo di CPU
Sistema
Capire le prestazioni

• I moderni processori, come vedremo, sono costruiti


usando un segnale periodico che ne sincronizza le
operazioni
• Il ciclo di clock è l’intervallo di tempo che intercorre tra
due colpi di clock (la frequenza ne è l’inverso)
• Il ciclo di clock è misurato in secondi (o in frazioni di
secondo), la frequenza in Hertz (o equivalentemente in
cicli al secondo)
• Ad esempio un clock che va a un Giga Hertz (10^9
Hertz) equivale a un periodo di clock pari a 10^-9
secondi (un miliardesimo di secondo)
Misurare le prestazioni
• La maniera migliore di valutare le prestazioni di
un computer è di misurare il tempo necessario
per l’esecuzione di un programma
• Tale tempo è il risultato di tre fattori:
§ Numero istruzioni
§ Cicli di clock per Istruzione (CPI)
§ Frequenza di clock (1/Ciclo di clock)
Equazione classica delle prestazioni
Componenti delle prestazioni

• Nessuna delle tre componenti può essere trascurata


§ Non ha nessun senso dire che un computer è più veloce di
un altro perché ha una frequenza di clock più alta
• Ha invece senso cercare di capire come i diversi
componenti e gli strumenti usati nello sviluppo di un
determinato sistema (HW+SW) abbiano impatto sulle
prestazioni (in particolare su ciascuno dei tre fattori)
Capire le presentazioni
Componente HW/SW Cosa influenza Dove è trattato?

Determina il numero di
Algoritmi istruzioni di alto livello e di Altri corsi
operazioni di IO
Determina il numero di
Linguaggi di programmazione,
istruzioni macchina per ogni Cap. 2, 3
compilatori e architetture
istruzione di basso livello
Determinano quanto
Processore e sistema di
velocemente è possibile Cap. 4, 5, 6
memoria
eseguire ciascuna istruzione

Determina quanto
Sistema operativo,
velocemente possono essere Cap. 4, 5, 6
gestione HW e I/O
eseguite le istruzioni

Cfr. tabella p. 7 Patterson-Hennessy


Algoritmo

• L’algoritmo adottato certamente influenza il numero


di istruzioni ed eventualmente il CPI
• Perché?
§ Un algoritmo più efficiente può essere strutturato in modo
da risparmiare istruzioni
§ Un algoritmo ben pensato (per una particolare
architettura) utilizza istruzioni più efficienti (quelle con un
basso CPI)
Linguaggio di programmazione

• Il linguaggio di programmazione influenza il


numero di istruzioni e il CPI
• Perché?
§ I costrutti ad alto livello vengono tradotti in
sequenze di istruzioni macchina
§ Un linguaggio con molte chiamate indirette (es.
Java) ha in generale un valore di CPI più alto
Compilatore

• Il compilatore sicuramente influenza sia il numero


di istruzioni che il CPI
• Perché?
§ Un compilatore più o meno efficiente genera un
numero di istruzioni macchina diverso per ogni
costrutto ad alto livello
§ Un compilatore ottimizzato (e ottimizzante) può
tenere conto di una serie di effetti piuttosto
complessi per ridurre il CPI
ISA
• L’architettura del set di istruzioni (ISA) consiste
nell’interfaccia che la macchina offre al software
• Essa ha impatto sul numero di istruzioni, sul CPI, e
sulla frequenza di clock
• Perché?
§ Numero di istruzioni: l’ISA può fornire istruzioni di alto
o basso livello (quindi più o meno istruzioni per
eseguire un’operazione)
§ CPI: il modo in cui un’ISA è progettata influenza il
numero di cicli per eseguire ciascuna istruzione
§ Un’ISA ben progettata permette di avere frequenze di
clock più spinte
ISA
• Durante questo corso vedremo 3 architetture:
§ RISC-V (esempio di architettura RISC)
Usata ad es. per cloud computing e sistemi embedded
§ Intel (esempio di architettura CISC)
Usata soprattutto su PC
§ ARM (esempio di architettura Advanced RISC)
Usata soprattutto su sistemi embedded/mobile (+70%
dispositivi mobili -cellulari e tablet- nel mondo)
La scalata delle prestazioni
• Negli scorsi anni le prestazioni dei calcolatori sono
aumentati costantemente
• Di recente si è assistito a una diminuzione
dell’incremento tra una generazione e l’altra
La scalata delle prestazioni

https://www.technologyreview.com/s/601441/moores-law-is-dead-now-what/
https://www.technologyreview.com/s/601962/chip-makers-admit-transistors-are-about-to-stop-shrinking/
https://gizmodo.com/how-chip-makers-are-circumventing-moores-law-to-build-s-1831268322
La barriera dell’energia
• Gli attuali processori sono costituiti di moltissimi
interruttori che dissipano energia quando sono in fase
di conduzione (commutazioni tra zero e uno)
• C’è un limite alla capacità di estrarre la potenza
prodotta dai processori (e il conseguente calore)
tramite ventole o radiatori (diciamo intorno ai 100W)
• Superato questo limite la refrigerazione diventa molto
costosa e non è attuabile in un normale desktop (per
non parlare dei laptop)
• La potenza è data da:
La barriera dell’energia
• La frequenza di commutazione è legata alla
frequenza di clock…
• Eppure dagli anni ’80 a oggi si è riusciti in un
‘‘miracolo”: aumentare la frequenza di +1000 volte
a spese di un aumento di consumi di un fattore 30
Come è stato possibile?

• Il ‘‘trucco” è stato quello di abbassare la tensione di


alimentazione che agisce in maniera quadratica
• Si è passati in venti anni da tensioni di alimentazioni
di 5V ai circa 1.2V attuali
• Questo ha permesso di incrementare la frequenza
con impatti limitati sui consumi
• Sfortunatamente non ci si può spingere oltre lungo
questa direzione perché se si abbassa la tensione
sotto il Volt ci sono dei fenomeni di scarica in
condizioni statiche che aumentano la dissipazione
anche lontani dalle fasi di commutazione
Una nuova strada
• Nell’impossibilità di riuscire a drenare una grossa
quantità di calore si è giunti a una sostanziale
saturazione delle prestazioni del processore
• La nuova strada che allora viene esplorata è quella
di aumentare il parallelismo (architetture multi-
core)
• Questo crea molte difficoltà al programmatore per
i seguenti motivi:
§ correttezza: è molto più difficile progettare (e
debuggare) un programma che opera in maniera
“parallela” rispetto a uno che opera in maniera
sequenziale
§ efficienza: occorre che il carico di lavoro sulle CPU si
mantenga bilanciato
Ci sono solo 10 tipi di persone: quelle che
conoscono la numerazione binaria e quelle
che non la capiscono.
[classica battuta “nerdissima”]

2 / 45
Informazione nei computer

• Un computer è un insieme di circuiti elettronici...


• ... In ogni circuito, la corrente può passare o non passare
• Passa corrente: 1
• Non passa corrente: 0
• ⇒ un computer memorizza (e manipola) solo sequenze di 0 e 1
• Sequenze di 0 e 1 devono poter rappresentare tutti i tipi di
informazione che un computer gestisce: valori numerici, caratteri o
simboli, immagini, suoni, programmi, ...
• Una singola cifra (0 o 1) è detta bit; sequenze di 8 bit sono chiamate
byte

3 / 45
Codifica delle informazioni - 1

• Per far sı́ che una sequenza di bit (cifre 0 o 1) possa essere
interpretata come informazione utile, deve essere stabilita una codifica
(rappresentazione digitale)
• Tutto deve essere codificato opportunamente per rappresentarlo in
questo modo
• Numeri (interi con e senza segno, razionali, reali, ...)
• Caratteri / testo
• Programmi (sequenze di istruzioni macchina)
• Immagini e suoni
• Sı́, ma come codifichiamo tutto ció?

4 / 45
Codifica delle informazioni - 2

• Codifica: funzione che associa ad un oggetto / simbolo una sequenza


di bit
• Codifica su n bit: associa una sequenza di n bit ad ogni entità da
codificare
• Permette di codificare 2n entità / simboli distinti
• Esempio: per codificare i semi delle carte bastano 2 bit
• Picche → 00
• Quadri → 01
• Fiori → 10
• Cuori → 11

5 / 45
Codifica dei numeri

• Rappresentiamo un numero come sequenza di 0 e 1...


• ... cominciamo con cose semplici: numeri naturali (interi positivi)
n∈N
• Per capire come fare, consideriamo i numeri “che conosciamo” (base
10)
• Se scrivo 501, intendo 5 ∗ 100 + 0 ∗ 10 + 1(∗100 )
• Sistema posizionale: il valore di ogni cifra dipende dalla sua
posizione
• Sistema decimale (base 10): ho 10 cifre (0 . . . 9)

6 / 45
Basi di numerazione

• In generale, un numero naturale è una sequenza di cifre


• B possibili cifre (da 0 a B − 1)
• B è detto base di numerazione
• Siamo abituati a 10 cifre (sistema decimale, o in base 10), ma nulla
vieta di usarne di più o di meno...
• Il valore di una sequenza di cifre ci ci−1 . . . c0 si calcola moltiplicando
ogni cifra per una potenza della base B, che dipende dalla posizione
della cifra:
i
!
ci ci−1 ci−2 . . . c0 = ci ∗ B i + ci−1 ∗ B i−1 + . . . + c0 ∗ B 0 = ck B k
k=0
• Una sequenza di cifre da interpretarsi come numero in base B è
spesso indicata con pedice B (50110 , 7538 , 1101102 , 1AF16 ...)

7 / 45
Esempi con basi di numerazione diverse da 10

• Base 2 (cifre: 0 e 1): sistema binario


• Esempio: 11012 = 1 ∗ 23 + 1 ∗ 22 + 0 ∗ 21 + 1 ∗ 20 = 1310
• Base 8: sistema ottale
• Esempio: 1708 = 1 ∗ 82 + 7 ∗ 81 + 0 ∗ 80 = 12010
• Base 16: sistema esadecimale
• Esempio: 1AF16 = 1 ∗ 162 + 10 ∗ 161 + 15 ∗ 160 = 43110
• Le cifre da 10 a 15 sono rappresentate dalle lettere da A ad F
(A16 = 1010 , B16 = 1110 , ... F16 = 1510 )

8 / 45
Hai detto... binario?

• Sistema binario: 2 sole cifre (0 e 1)


• Sembra fatto apposta per i computer!
• Un numero naturale è rappresentato in binario su k cifre binarie
• Cifra binaria: binary digit — bit
• Numero binario su k cifre: valori da 0 a 2k − 1
• Valori comuni per k: 8, 16, 32, 64
• Le potenze di 2 rivestono un ruolo molto importante!
• Ricordate? Bit raggruppati in sequenze di 8, dette byte
• Ogni byte può assumere 28 valori, da 0 (= 000000002 ) a 255
(= 111111112 )

9 / 45
Altre basi di numerazioni utili

• La base 2 è la base “nativa” dei computer...


• ... ma i numeri in base 2 possono richiedere un gran numero di cifre!
• Esempio: 100010 = 11111010002
• Spesso si usa la base 16 (numerazione esadecimale) per ridurre il
numero di cifre
• Ogni cifra esadecimale corrisponde a 4 cifre binarie
• La conversione da binario a esadecimale (e viceversa) è molto
semplice
• Esempio: (0011 1110 1000)2 = 3E816

10 / 45
Conversioni fra basi

• Base 2 ↔ 16: come visto (si convertono 4 bit in una cifra esadecimale
e viceversa)
• Base B → 10: già visto anche questo (si moltiplica ogni cifra ci per B i ,
dipendentemente dalla sua posizione)
• Base 10 → B: si divide iterativamente il numero per B
1. Per convertire x10 in base B:
2. Divisione intera fra x e B
3. Il resto e’ la cifra da inserire a sinistra nel numero convertito
4. Il quoziente viene assegnato ad x
5. Ritorna al punto 2

11 / 45
Esempio di conversione

100010 =?2 ! 100010 =?16


X X/2 X%2  
1000 500 0 

500 250 0 

250 125 0  !
 X X/16 X%16 

125 62 1  
 1000 62 8 
62 31 0  
 62 3 14 (E) 
31 15 1  
 3 0 3 
15 7 1 

7 3 1 

3 1 1 

1 0 1 
100010 = 11111010002 100010 = 3E816

12 / 45
Somme e sottrazioni in base 2

• Un’introduzione, prima:

A B Carry C Carry
0 0 0 0 0
1 0 0 1 0
0 1 0 1 0
1 1 0 0 1
0 0 1 1 0
1 0 1 0 1
0 1 1 0 1
1 1 1 1 1

A + B + Carry = C + Carry

13 / 45
Somma di naturali

• Esempio
11710 + 9710 = 11101012 + 11000012 = ?

1 1 1 0 1 0 1 +
1 1 0 0 0 0 1 =
1 1 0 1 0 1 1 0

110101102 = 21410

14 / 45
Sottrazione di naturali

• Sottrazione di naturali: operazione di base con “prestito”.


Come nella somma si va da destra a sinistra seguendo delle regole
banali:

A 0 1 1 0
B 0 0 1 1
A-B 0 1 0 1

Nell’ultimo caso bisogna scorrere a sinistra finché non si trova un 1,


invertendo il valore di tutte le cifre incontrate, 1 compreso.

10..0 → 01..1

15 / 45
Sottrazione di naturali

• Esempio
21410 − 11710 = 110101102 − 11101012 = ?

1 1 0 1 0 1 1 0 -
1 1 1 0 1 0 1 =
1 1 0 0 0 0 1

11000012 = 9710

16 / 45
Moltiplicazione per potenze di 2

• Passiamo ora a moltiplicazione (e divisione) per potenze di due. Se


dobbiamo moltiplicare un numero base 10 per 10 dobbiamo...

.... aggiungere uno 0 a destra.

• Nel caso dei numeri base 2, aggiungere uno 0 a destra significa


moltiplicare il numero per 2, mentre eliminando la prima cifra a destra
significa dividere per 2.

Questa operazione é chiamata shifting, e viene utilizzata spesso dai


compilatori per ottimizzare la moltiplicazione.

17 / 45
Moltiplicazione di due naturali

• Moltiplicare due numeri base 2 naturali altro non é che l’applicazione


di shifting multipli e poi della somma sui risultati degli shifting.
Alla fine questa operazione non é diversa dalla moltiplicazione in base
10, solo che é piú semplice perché o bisogna sommare uno dei due
moltiplicatori shiftati, o nulla.
• Esempio
1310 × 1710 = 11012 × 100012 = ?

1 1 0 1 x
1 0 0 0 1 =
1 1 0 1 +
- +
- +
- +
1 1 0 1
1 1 0 1 1 1 0 1

110111012 = 22110
18 / 45
Codifica dei numeri interi

• k bit → codificano 2k simboli/valori/numeri...


• Si usa la base 2 per codificare i numeri
• Numeri naturali n ∈ N : valori da 0 a 2k − 1
• Come codificare numeri interi z ∈ Z?
• Problema: numeri negativi z < 0 ⇒ z ∈
/N
• Si codificano sempre 2k valori...
• ... ma quali?
• Varie possibilità:
• modulo e segno
• complemento a 1
• complemento a 2

19 / 45
Codifica con modulo e segno

• Idea semplice: si usano k − 1 bit per rappresentare il valore assoluto


(modulo) del numero...
• ... ed un bit per codificare il segno!
• bit più significativo a 0: numero positivo
• bit più significativo a 1: numero negativo
• Valori codificati: da −2k−1 + 1 a 2k−1 − 1
• Perché? Due diverse sequenze di bit per codificare lo 0?
(+0 e −0?)
k−1 k−1
! "# $ ! "# $
• 1 000 . . . 0 vs 0 000 . . . 0

20 / 45
Codifica in complemento a 1

• Altra idea semplice: i numeri negativi si rappresentano facendo il


complemento a 1 del valore assoluto
• Numero positivo: rappresento il valore assoluto
• Numero negativo: rappresento il complemento a 1 del valore
assoluto
Operazione di complemento a 1 di un numero binario x:
• Metodo 1: cambio tutti i bit di x, da 0 a 1, e da 1 a 0
k
! "# $
• Metodo 2: calcolo: (2k − 1)2 − x = 1 . . . 1 −x
• Anche in questo caso:
• bit più significativo a 0: numero positivo
• bit più significativo a 1: numero negativo
k k
! "# $ ! "# $
• Ancora, due rappresentazioni dello 0 (+0 e −0)? 111 . . . 1 vs 000 . . . 0
• Somme e sottrazioni in modulo e segno → non facile!
• Somme e sottrazioni in complemento a 1 → vedi prossima slide.

21 / 45
Somma e sottrazione di numeri in complemento a 1

• Somma di numeri interi codificati in complemento a 1:


1. Sommare le rappresentazioni dei due numeri
2. Riporto sul bit più significativo → sommarlo al risultato
3. Se i segni dei due operandi sono uguali ma diversi da quello del
risultato di quest’ultima somma, il risultato non è attendibile,
ovvero non è rappresentabile su k bit (overflow)
4. Altrimenti, il risultato è corretto
• Esempio: 6 + −3 (codifica su 5 bit)
(1)
0 0 1 1 0
• 6 = 00110; −3 = 00011 = 11100
1 1 1 0 0
0 0 0 1 0
• 00010 + 1 = 00011 (= 3)

22 / 45
Codifica in complemento a 2

Operazione di complemento a 2 di un numero binario x:


• Metodo 1:
• Scorro i bit di x a partire dalla destra (bit meno significativo)
• Fino al primo bit che vale 1 (compreso), lascio invariato
• A partire dal bit successivo, faccio il complemento a 1
• Metodo 2: faccio il complemento a 1 di x, e poi sommo 1
k
! "# $
• k
Metodo 3: calcolo (2 )2 − x = 1 0 . . . 0 −x
(utile nelle somme, vedi prossima slide)
Esempio: complemento in base 2 di 87 (= 1010111)
• Metodo 1: 1 più a destra invariato, poi inverto : 0101001
• Metodo 2: complemento a 1, e sommo +1: 0101000 + 1 = 101001
• Metodo 3: (27 )2 − 1010111 = 10000000 − 1010111 = 101001

23 / 45
Codifica in complemento a 2

• Numeri positivi: rappresento il valore assoluto


• Numeri negativi: rappresento il complemento a 2 del valore assoluto
• Anche in questo caso:
• bit più significativo a 0: numero positivo
• bit più significativo a 1: numero negativo
• Tuttavia, la codifica dello 0 è unica
• Codifica i numeri da −2k−1 a 2k−1 − 1
• Inoltre, anche la somma è piú semplice...
• x + (−y) = x + (2k − y) = 2k + x − y...
• ... su k bit, questo è equivalente a x − y
• Operazione inversa (dato un numero binario in complemento a 2, per
ottenere il numero decimale corrispondente):
• Se il primo bit a sinistra (bit piú significativo) é 0, converto
semplicemente il binario in numero decimale
• Se il primo bit a sinistra é 1, faccio il complemento a 2 e sommo
+1, e converto in numero decimale 24 / 45
Esempi di codifica

decimale modulo complemento complemento


e segno a1 a2
-4 100
-3 111 100 101
-2 110 101 110
-1 101 110 111
0 100 / 000 111 / 000 000
1 001 001 001
2 010 010 010
3 011 011 011

Nota: Usando il complemento a 2 su k bit, il minimo valore


rappresentabile é sempre −2k−1 . Perché?
Es. complemento a 2 di −4: 100 + 1 = 011 + 1 = 100

25 / 45
Somma e sottrazione di numeri in complemento a 2

• Se si usa il complemento a 2, somme e sottrazioni si applicano


facilmente anche con numeri negativi...
• Ma occhio agli overflow!
• Overflow? Numeri su k bit, risultato non rappresentabile su k bit
• Si usa la matematica “dell’orologio” (vedi prossime slide)

Facciamo un primo esempio (senza overflow):


• k = 6, 5 + 12
• 5 = 0000101; 12 = 001100
0 0 0 1 0 1
0 0 1 1 0 0
0 1 0 0 0 1
• 010001 = 17

26 / 45
Esempi senza overflow

• k = 7, 5 − 8
• 5 = 0000101; −8 = 0001000 + 1 = 1110111 + 1 = 1111000
0 0 0 0 1 0 1
1 1 1 1 0 0 0
1 1 1 1 1 0 1
• 1111101 =... −(1111101 + 1) = −0000011 = −3
• k = 7, −5 + 8
• −5 = 0000101 + 1 = 1111010 + 1 = 1111011; 8 = 0001000
(1)
1 1 1 1 0 1 1
0 0 0 1 0 0 0
0 0 0 0 0 1 1
• 0000011 = 3

27 / 45
Esempi con overflow

• k = 7, −64 − 8
• 64 = 1000000; −64 = 1000000 + 1 = 0111111 + 1 = 1000000
• 8 = 0001000; −8 = 0001000 + 1 = 1110111 + 1 = 1111000
(1)
1 0 0 0 0 0 0
1 1 1 1 0 0 0
0 1 1 1 0 0 0
• 0111000 = 56...
Sommando 2 numeri negativi si ottiene un numero positivo?
• Numeri rappresentabili (k = 7): da −26 = −64 a 26 − 1 = 63
• −64 − 8 = −72 non è rappresentabile!

28 / 45
Esempi con overflow

• k = 7, 63 + 1
• 63 = 0111111
• 1 = 00000001
0 1 1 1 1 1 1
0 0 0 0 0 0 1
1 0 0 0 0 0 0
• 1000000 = −(1000000 + 1) = −(0111111 + 1) = −1000000 = −64...
Sommando 2 numeri positivi si ottiene un numero negativo?
• Ancora, il risultato corretto (63 + 1 = 64) non è rappresentabile!

29 / 45
Esempi ed esercizi

• Numeri su k = 7 bit, codificati in complemento a 2


• Verificare se c’è overflow
• 0110110 + 0000011
• Somma di due numeri positivi: mi aspetto risultato positivo...
• 1001010 + 1111101
• Somma di due numeri negativi: mi aspetto risultato negativo...
• 1001010 + 1100000
• Somma di due numeri negativi: mi aspetto risultato negativo...
• 0100000 + 1111010
• Somma di un numero positivo e uno negativo: verifichiamo...
• 1100000 + 0000110
• Somma di un numero positivo e uno negativo: verifichiamo...

30 / 45
Ancora su overflow

• Abbiamo visto overflow in somme e sottrazione di interi


• Se a + b > 2k−1 − 1, overflow!
• Se a + b < −2k−1 , overflow!
• Nota: per definizione, −2k−1 ≤ a ≤ 2k−1 − 1 e −2k−1 ≤ b ≤ 2k−1 − 1
• Overflow solo se a e b hanno lo stesso segno!

31 / 45
La matematica dell’orologio

• Aritmetica modulare: considero solo n numeri interi


• Numeri da 0 a n − 1...
• ... oppure numeri da −n/2 a n/2 − 1...
• Arrivato al numero più grande, riparto dal più piccolo
• Se i numeri vanno da 0 a n − 1, (n − 1) + 1 = 0 (contando dopo
n − 1 riparto da 0)
• Se i numeri vanno da −n/2 a n/2 − 1, n/2 − 1 + 1 = −n/2
• Numeri sul quadrante di un orologio!
• Definizione più formale: classi di equivalenza modulo n
• Relazione di equivalenza fra due numeri: a ≡ b ⇔ a%n = b%n

32 / 45
Cosa succede in caso di overflow?

• Operazioni su numeri di k bit → aritmetica modulo 2k


• Calcolando a + b si ottiene in realtà (a + b)%2k
• Se a + b < 2k , allora (a + b)%2k = a + b → no overflow!
• Altrimenti, risultato “strano”... cosa significa (a + b)%2k ?
• Usando la rappresentazione in complemento a 2 dei numeri interi, il
discorso è analogo:
• Se a + b è compreso fra −2k−1 e 2k−1 − 1, allora no overflow
(risultato corretto)!
• Se a + b < −2k−1 , ottengo a + b + 2k . Positivo! Overflow!
• Se a + b > 2k−1 − 1, ottengo a + b − 2k . Negativo! Overflow!

33 / 45
Rappresentazione dei numeri reali

• Rappresentare esattamente un numero reale x ∈ R non è sempre


possibile
• E’ il caso dei numeri irrazionali (es. π), ovvero numeri reali con un
numero infinito di cifre “dopo la virgola” non periodiche...
• ... non rappresentabili con un numero finito di bit!
• In alcuni casi possiamo quindi rappresentare solo un’approssimazione
dei numeri reali
• Alcuni numeri reali sono rappresentabili correttamente...
• ... altri no
• Rappresentazioni in “virgola fissa” vs “virgola mobile” (rispettivamente
dette “fixed point” e “floating point”)

34 / 45
Virgola fissa

• Numero reale su k cifre: si possono dedicare k − f cifre alla parte


k
! "# $
intera e f cifre alla parte frazionaria: ck−1 ck−2 · · · cf . cf −1 · · · c0
# $! " # $! "
k−f f
%k−1 %k−1 &% '
k−1
• x10 = i=0 ci B i−f = i=0 ci B i · B −f = i=0 c i B i · B −f

• In altre parole, convertiamo il numero in base 10 come se non


avesse virgola e poi moltiplichiamo per B −f .
• Un grosso vantaggio é che le operazioni si fanno in maniera
semplice. E’ sufficiente effettuare somme, sottrazioni,
moltiplicazioni come se fossero numeri interi e riscalare il risultato.
• Questo semplifica i requisiti per realizzare una CPU.
• Inoltre se i risultati sono rappresentabili, le operazioni non
introducono errori di approssimazione.
• Gli svantaggi emergono quando vogliamo trattare nella stessa
applicazione sia numeri grandi che numeri piccoli (cioé quando la
nostra applicazione abbraccia piú ordini di grandezza).

35 / 45
Virgola fissa: esempio di conversione

• Prendiamo in considerazione un paio di esempi di codifica a virgola


fissa a 6 bit, 3 per la parte intera e 3 per la parte decimale
• Limitiamoci per ora a due casi di numeri reali rappresentabili con
questa codifica
• Conversione ! da2 binario1a decimale " di
! 101.100 "
0
101.100 = 1 · 2 + 0 · 2 + 1 · 2 + 1 · 2 + 0 · 2 + 0 · 2
−1 −2 −3 = 5.5
• Conversione da decimale a binario di 6.125
• Parte intera → converto direttamente: 6 = 110
• Parte decimale → moltiplico la parte decimale ricorsivamente per
2, tante volte quanti sono i bit della parte decimale, e considero
nell’ordine le parti intere della moltiplicazione:
1) 0.125 ∗ 2 = 0.25 - parte intera: 0
2) 0.25 ∗ 2 = 0.5 - parte intera: 0
3) 0.5 ∗ 2 = 1 - parte intera: 1
• Mettendo insieme → 6.125 = 110.001

36 / 45
Virgola mobile

• Un numero reale x può essere riscritto come:


• x = M · BE
• M : mantissa
• E: esponente
• x puó essere quindi rappresentato in virgola mobile su k bit:
• m bit per la mantissa M
• e = k − m bit per l’esponente E
• L’esponente E è quello che fa “muovere la virgola”
• Nei computer, B = 2 (rappresentazione binaria)
• Mantissa ed esponente: positivi o negativi
• Segno mantissa: segno di x
• Esponente negativo: x “piccolo”
• Esponente positivo: x “grande”
• Rappresentati in complemento a 2, o con altre tecniche di codifica...
... vedi prossima slide.
37 / 45
Numeri reali in C (Standard IEEE 754)

• Esponente −2e−1 + 1 < E ≤ 2e−1 − 1 rappresentato come:


E " = E + 2e−1 − 1
Nota: per definizione, E " ≥ 0 (vedi limiti di E)
• Se E " > 0 → numeri “normalizzati”:
• Mantissa 1.M (M ≥ 0 e bit di segno s)
• Esponente E = E " − (2e−1 − 1)
• Se E " = 0 → numeri “denormalizzati”:
• Mantissa 0.M (M > 0 e bit di segno s)
• Esponente E = −(2e−1 − 2)
! ! −(2e−1 −1)
1.M ·2 E se s = 0
E" > 0 : x = !
−1.M · 2E −(2 −1)
e−1
se s = 1
! e−1 −2)
0.M · 2 −(2 se s = 0
E" = 0 : x = e−1
−0.M · 2−(2 −2) se s = 1

38 / 45
Tipi di floating point in C

• float: precisione singola


• k = 32
• e = 8, m = 23
• double: precisione doppia
• k = 64
• e = 11, m = 52
• long double: precisione estesa o quadrupla
• k = 80 o k = 128
• e = 15, m = 64 o m = 112

39 / 45
Precisione singola

• Approssimazioni di numeri reali rappresentati su k = 32 bit


• Esponente rappresentato su e = 8 bit
• Mantissa rappresentata su m = 23 bit (e bit di segno s)
• Se E ! > 0, esponente E = E ! − (27 − 1) = E ! − 127
• Se E ! = 0, esponente E = −(27 − 2) = −126

! s E ! −127
E > 0 ⇒ x = (−1) · 1.M · 2
E ! = 0 ⇒ x = (−1)s · 0.M · 2−126

40 / 45
Precisione singola: valori massimo e minimo

• Massimo valore normalizzato rappresentabile:


23
! "# $
• Massima mantissa: M = 1 · · · 1
• Massimo esponente: E = 254 − 127 = 127
(nota: il massimo valore di E ! é 254 e non 255, perché 255 é
riservato per casi particolari, vedi dopo)
23
! "# $
• x = 1. 1 · · · 1 ·2127 = (2 − 2−23 ) · 2127 " 3.4 · 1038
(1.1 · · · 11 + 0.0 · · · 01 = 2 → 1.1 · · · 11 = 2 − 0.0 · · · 01 = 2 − 2−23 )
• Minimo valore normalizzato rappresentabile: " −3.4 · 1038

41 / 45
Precisione singola: valori massimo e minimo

• Minimo valore normalizzato (positivo) rappresentabile:


• Minima mantissa: M = 0
• Minimo esponente: E = 1 − 127 = −126
(nota: il minimo valore di E ! é 1 e non 0, perché 0 é riservato ai
numeri denormalizzati, vedi dopo)
23
! "# $
• x = 1. 0 · · · 0 ·2−126 = 2−126 " 1.755 · 10−38
• Minimo valore denormalizzato (positivo) rappresentabile:
23
! "# $
• Minima mantissa: M = 0 · · · 01
• Esponente: E ! = 0 ⇒ E = −126
23
! "# $
• x = 0. 0 · · · 01 ·2−126 = 2−23 · 2−126 " 1.4 · 10−45

42 / 45
Precisione singola: riepilogo - 1

Lo standard comprende quindi due categorie di numeri:


• Numeri normalizzati → 1.M , M ≥ 0, E ! > 0.
• Numeri denormalizzati (o subnormalizzati) → 0.M , M > 0, E ! = 0.
Questi sono numeri compresi nell’intervallo tra zero ed il più piccolo
numero normalizzato rappresentabile. Si verificano in caso di
“underflow” (con una perdita graduale di precisione man mano che ci
si allontana dallo zero). Rappresentati con esponente pari a 0 e
mantissa pari ad una qualunque sequenza di bit diversa da 0.
Oltre a questi, lo standard prevede i seguenti casi particolari:
• Due zeri (±0), uno positivo e l’altro negativo, determinati dal bit di
segno. Entrambi rappresentati con esponente 0 e mantissa 0.
• Due valori di ”infinito” (±∞), uno positivo e l’altro negativo, determinati
dal bit di segno. Entrambi rappresentati con esponente pari 255 (non
permesso per i numeri normalizzati) e mantissa 0.
• Un formato speciale, chiamato “NaN” (Not a Number). Rappresentato
con esponente pari a 255 (non permesso per i numeri normalizzati) e
mantissa pari ad una qualunque sequenza di bit diversa da 0.
43 / 45
Precisione singola: riepilogo - 2

Categoria E! M s
Numeri normalizzati 1-254 qualunque 0/1
Numeri denormalizzati 0 non zero 0/1
± Zero 0 0 0/1
± Infinito 255 0 0/1
NaN (Not a Number) 255 non zero 0/1

44 / 45
Precisione singola: esempio di conversione

Convertire in virgola mobile il numero 132.125


1. Converto parte intera e parte decimale in binario:
132 = 10000100; 0.125 = 001 ⇒ 10000100.001
2. Sposto la virgola a sinistra, lasciando solo un 1 a sinistra
(rappresentazione normalizzata, equivale a dividere per 2k dove k é
la lunghezza in bit della parte intera)
!
10000100.001 = 1.0000100001 · 27 = (−1)s · 1.M · 2E −127
3. Mantissa M = 0000100001[0000000000000] (sottintendo l’1 a sinistra
della virgola, e aggiungo tanti 0 a destra quanti sono i bit della parte
decimale, 23 nel caso di precisione singola)
4. Esponente: E = E " − 127 = 7 → E " = 7 + 127 = 134 = 10000110
(notazione “eccesso 127”)
5. Segno: s = 0 (132.125 > 0)
6. Mettendo insieme: !"#$0 10000110
! "# $ 00001000010000000000000
! "# $
segno esponente mantissa

45 / 45
CALCOLATORI
Codifica del testo

Giovanni Iacca
giovanni.iacca@unitn.it

Lezione basata su materiale preparato


dal Prof. Luca Abeni
Codifica del testo

• Come rappresentare il testo tramite sequenze di 0 e 1?


• Testo: sequenza di caratteri
• Quindi, il problema è rappresentare caratteri come sequenze di 0 e
1...
• Ricorda: n bit possono codificare 2n simboli diversi
• Quanti possibili caratteri si devono rappresentare?
• Alfabeto “anglosassone”: 7 bit (27 = 128 diversi caratteri)
• Lettere maiuscole e minuscole, numeri, punteggiatura, ecc.
• ASCII (American Standard Code for Information Interchange)

2/8
Lo standard ASCII

• Specifica come codificare lettere, numeri e punteggiatura su 7 bit


• Ma un byte è composto da 8 bit...
• Bit più significativo sempre a 0
• Cosa fare per caratteri accentati o “strani”?
• Ci sono altre 128 combinazioni di bit disponibili...
• Extended ASCII: usa 8 bit per codificare caratteri addizionali
• Non esiste un unico standard “esteso”...
• Varie estensioni per supportare vari alfabeti (europa dell’est, ovest,
ecc.)

3/8
Lo standard ASCII

Decimal Hex Char Decimal Hex Char Decimal Hex Char Decimal Hex Char

4/8
Esempio

• Codifichiamo la parola “Ciao”


• C è codificata come 67 = 0x43 = 01000011
• i è codificata come 105 = 0x69 = 01101001
• a è codificata come 97 = 0x61 = 01100001
• o è codificata come 111 = 0x6F = 01101111

01000011 01101001 01100001 01101111


C i a o

5/8
ASCII Esteso

• Un byte con valore < 128 (bit più significativo a 0) si interpreta in modo
univoco come carattere
• Esempio: 01000001 è sempre “A”
• L’interpretazione di byte col bit più significativo ad 1 non è univoca
• ISO 8859-1 (Latin1): caratteri dell’Europa Occidentale (lettere
accentate, ecc.)
• ISO 8859-2: caratteri dell’Europa Orientale
• ISO 8859-5: per i caratteri cirillici
• ...
• Esempio: il valore 224 è “à” per ISO 8859-1, “ŕ” per ISO 8859-2, ecc..

6/8
Problemi con ASCII Esteso

• ASCII esteso: codifica non univoca di caratteri “non standard”


• Problemi nella condivisione di documenti
• Se utilizzo una “è” in un documento testo e lo trasmetto ad altre
persone...
• ... devo assicurarmi che i computer delle altre persone utilizzino
ISO 8859-1 come il mio computer...
• ... altrimenti strani simboli possono essere visualizzati al posto
della mia “è”
• E cosa dire degli alfabeti che non usano l’alfabeto anglosassone
(Cirillico, Cinese, Giapponese, Arabo, ecc.)?

7/8
Altri standard di codifica dei caratteri

• Codifica univoca di tutti i possibili caratteri: 8 bit non bastano!!!


• Unicode: fino a 232 simboli!!!
• Possono servire 32 bit (4 byte) per carattere...
• I simboli unicode possono essere codificati in vari modi
• UTF-32: ogni simbolo è composto da 32 bit
• UTF-16: ogni simbolo è composto da 16 o più bit (simboli a
lunghezza variabile)
• UTF-8: ogni simbolo è composto da 8 o più bit
• Compatibile con ASCII

8/8
CALCOLATORI
Cenni alle reti logiche
Giovanni Iacca
giovanni.iacca@unitn.it

Lezione basata su materiale preparato


dal Prof. Luigi Palopoli
Cosa sono le reti logiche?
• Fino ad ora abbiamo visto
§ Rappresentazione dell’informazione
§ Aritmetica dei calcolatori
• L’obiettivo principale di questo corso è mostrare
come funziona (e come si progetta) un computer
• Per fare ciò, abbiamo bisogno di fare una piccola
digressione su come si progettano i circuiti logici
(esistono interi corsi dedicati a questo)
Valori logici
• I computer moderni sono realizzati tramite
circuiti elettronici
• Trattandosi di elementi digitali avremo due livelli
fondamentali:
§ Alto, asserito (1): associato alla tensione di
alimentazione Vdd
§ Basso, negato (0): associato alla massa (tensione = 0)
• Altri livelli di tensione sono non significativi e
assunti solo in fase transitoria
Valori logici
Reti logiche
• Le reti logiche sono dei circuiti che trasformano
alcuni valori logici in ingresso in altri valori logici
in uscita
• Le reti logiche sono di due tipi
§ Combinatorie
üRelazione funzionale tra ingresso e uscita
üNon hanno memoria
üL’uscita dipende solo dal valore dell’ingresso
§ Sequenziali
üL’uscita dipende dalla storia degli ingressi passati e non solo
dal valore attuale
üHanno memoria (detta anche stato della rete)
Tabella di verità
• Una possibile maniera di specificare una rete logica
combinatoria è tramite una tabella di verità che elenca
i valori delle uscite in corrispondenza delle varie
combinazioni in ingresso
INPUT OUTPUT
A B C D E F
0 0 0 0 0 0
0 0 1 1 0 0
0 1 0 1 0 0
0 1 1 1 1 0
1 0 0 1 0 0
1 0 1 1 1 0
1 1 0 1 1 0

1 1 1 1 0 1
Algebra di Boole
• Una maniera più compatta è di specificare le funzioni
logiche combinatorie tramite espressioni algebriche
definite con l’algebra di Boole
• Esistono tre operatori di base
§ AND
ü Rappresentato tramite il simbolo di prodotto (•), es. A•B
ü Produce 1 se entrambi gli operandi sono 1, 0 negli altri casi
§ OR
ü Rappresentato tramite il simbolo della somma (+), es. A+B
ü Produce 0 se entrambi gli operandi sono 0, 1 negli altri casi
§ NOT
ü Rappresentato da una barra, es. Ā
ü Ha l’effetto di invertire il valore logico
Algebra di Boole
• Alcune semplici regole ci permettono di manipolare (e
semplificare) facilmente le espressioni logiche
§ Identità: A+0=A, A•1=A
§ Regola «zero e uno»: A + 1 = 1, A•0=0
§ Regola dell’inversa: A + Ā=1, A•Ā=0
§ Regola commutativa: A+B=B+A, A•B=B•A
§ Regola associativa: A+(B+C)=(A+B)+C,
A•(B•C)=(A•B)•C
§ Regola distributiva: A•(B+C)=(A•B)+(A•C),
A+(B•C)=(A+B)•(A+C)
Algebra di Boole
• In più esistono due regole molto importanti,
dette di De Morgan:
A·B =A+B
A+B =A·B

• Queste regole ci dicono che se abbiamo un


elemento logico che implementa l’operazione
NAND (NOT AND), oppure NOR (NOT OR), tutti gli
altri operatori logici si possono ricavare da questo
Algebra di Boole - Esempio
• Torniamo alla tabella che abbiamo visto prima:
INPUT OUTPUT
A B C D E F
0 0 0 0 0 0
0 0 1 1 0 0
0 1 0 1 0 0
0 1 1 1 1 0
1 0 0 1 0 0
1 0 1 1 1 0
1 1 0 1 1 0

1 1 1 1 0 1

• Possiamo verificare facilmente:


D =A+B+C
F =A·B·C
Algebra di Boole - Esempio
• Torniamo alla nostra tabella
INPUT OUTPUT
A B C D E F E vale 1:
0 0 0 0 0 0
0 0 1 1 0 0
§ Se A=1, B=1, C=0
0 1 0 1 0 0 oppure
0
1
1
0
1
0
1
1
1
0
0
0
§ Se A=1, C=1 B = 0
1 0 1 1 1 0 oppure
1 1 0 1 1 0
§ Se B=1, C=1, A= 0
1 1 1 1 0 1

E = (A · B · C) + (A · C · B) + (B · C · A)
• O usando De Morgan
E = (A + B + C) · (A + C + B) · (B + C + A)
Porte logiche
• In effetti, esistono dei circuiti elettronici (cosiddette
«porte logiche») che implementano proprio gli
operatori Booleani fondamentali

AND OR NOT
Porte logiche
• Le porte si possono combinare tra di loro (con il NOT
che viene indicato di solito tramite un cerchio
sull’input corrispondente)

A+B
Alcuni circuiti
• Decoder
Alcuni circuiti
• Multiplexer

• Deviatore che sulla base di un input di


controllo, determina quale degli input passa
Alcuni circuiti
• Multiplexer a N vie

••••

••••

Decoder
Forme canonica SP
• Abbiamo visto calcolare l’espressione logica
equivalente ad una tabella di verità è semplice
• Basta prendere ciascuna riga uguale a 1 e
scrivere un termine di prodotto logico (AND)
dettato dalla configurazione degli ingressi
• A quel punto si può fare la somma (OR) di tutti
i prodotti individuati
Altro esempio
• Consideriamo come ulteriore esempio:
INPUT OUTPUT
A B C D
0 0 0 0
0 0 1 1
0 1 0 1
0 1 1 0
1 0 0 1
1 0 1 0
1 1 0 0
1 1 1 1

D = (A · B · C) + (A · B · C) + (A · B · C) + (A · B · C)
Programmable Logic Array (PLA)
• La struttura che abbiamo visto si compone di due stadi:
la prima è una barriera di AND (cosiddetti anche
«mintermini»), la seconda è una barriera di OR

• La dimensione totale del PLA


è data dalla somma del piano
AND (numero di mintermini)
e del piano OR (numero di
uscite)
• Caratteristiche importanti:
§ Ci sono porte logiche solo per
le configurazioni di ingressi
che producono 1 in uscita
§ Se un mintermine è condiviso
tra varie uscite, lo si può
riutilizzare (basta inserirlo
una sola volta nel piano AND)
Esempio
• Torniamo all’esempio precedente:
INPUT OUTPUT
A B C D E F
0 0 0 0 0 0
0 0 1 1 0 0
0 1 0 1 0 0
0 1 1 1 1 0
1 0 0 1 0 0
1 0 1 1 1 0
1 1 0 1 1 0

1 1 1 1 0 1
Esempio
• Implementazione tramite porte logiche:
D =A+B+C
F =A·B·C
E = (A · B · C) + (A · C · B) + (B · C · A)
Esempio
• Una diversa rappresentazione
D =A+B+C
F =A·B·C
E = (A · B · C) + (A · C · B) + (B · C · A)
Costo
• Le funzioni logiche possono essere implementate in
maniera diversa (più o meno efficiente)
• Per COSTO di una rete logica si intende normalmente la
somma del numero di porte e del numero di ingressi
della rete (indipendentemente dal fatto che siano
positivi o negati)
• E’ possibile trovare delle implementazioni di una rete
che hanno costi diversi
Minimizzazione di funzioni logiche

• La minimizzazione di alcune espressioni logiche è


banale, in altri casi è necessario applicare le regole
algebriche in modo “furbo”
• Esempio:
f (x1 , x2 , x3 ) = x1 x2 x3 + x1 x2 x3 + x1 x2 x3 + x1 x2 x3
= x1 x2 (x3 + x3 ) + x1 x2 (x3 + x3)
= x1 x2 + x1 x2
= (x1 + x1 )x2
= x2
Minimizzazione di funzioni logiche
• Esistono metodi di minimizzazione sistematici
basati sull’applicazione iterativa di queste
regole
• Altri metodi sono basati su rappresentazioni
grafiche (mappe di Karnaugh), ma si applicano
solo a casi più semplici
• Questo argomento si chiama “sintesi logica”,
ed è coperto nel corso di Reti Logiche
Array di elementi logici
• Molto spesso si costruiscono array di elementi che
operano su dati complessi
• Ad esempio come realizzare un multiplexer che
opera su un bus a 32 bit utilizzando elementi a un bit
• BUS: insieme di «fili di ingresso» (ad esempio 32),
che viene visto come un singolo segnale logico
Esempio: Multiplexer a 32 bit
Reti sequenziali
• Le funzioni logiche e le relative reti di implementazione
viste fino ad ora sono note come
“reti combinatorie”
• Le reti combinatorie non hanno una nozione “esplicita”
del tempo e non hanno memoria del passato: in ogni
istante di tempo l’uscita dipende solamente dagli
ingressi nell’istante considerato
• Tuttavia, in molte applicazioni è necessario introdurre
una «memoria» nel sistema...
• Quasi sempre diamo per scontato che un elaboratore sia
in grado di memorizzare informazioni
Reti sequenziali
• La memoria in una rete logica si ottiene con una
“reazione” (o retroazione), ovvero ridirezionando l’uscita
di alcune porte in ingresso ad altre porte del medesimo
stadio (considerando che la rete logica può essere
organizzata in stadi, o strati, successivi), in modo da
formare un “anello ” in cui gli ingressi dipendono dalle
uscite (e viceversa)
• La reazione complica in modo significativo l’analisi e la
sintesi di una rete logica
• La memoria deriva dal fatto che gli ingressi “ricordano” il
passato della rete attraverso il valore delle uscite
passate
Memorizzare un bit
Elemento Bistabile
R-S Latch
q R
Q+ Q+

!q Q– Q–
S
q = 0 or 1

Reset Set Memorizzare


1 1 0 0 0 !q
R 0 R 1 R q
Q+ Q+ Q+

0 0 1 1 1 0 0 q !q
Q– Q– Q–
S S S
Elemento
base di
memoria
(latch)

realizzazione con
due porte NOR
e schema di
“temporizzazione”
della tavola di
verità
Stati indecidibili e temporizzazione

• Dato che i segnali non si propagano in tempo nullo,


l’effetto del cambio di un ingresso si propaga in tempo
finito sulle uscite
• Se le uscite sono reazionate questo può creare
problemi di indecidibilità dello stato della rete logica
(con memoria)
• Gli elementi di memoria sono quindi sempre
temporizzati, cioè sono governati da un segnale
speciale chiamato “clock”
• Un elemento base di memoria temporizzato viene
normalmente indicato come “gated latch”
Ingresso di abilitazione
• Il clock viene inserito come
“ingresso di abilitazione”
attraverso porte AND: se Clk è a
zero la rete reazionata ha gli
ingressi forzati a zero e non può
cambiare stato
• Quando Clk è a uno gli ingressi
della rete reazionata sono gli
ingressi R ed S del circuito
• Circuiti di questo tipo hanno
rappresentazione grafiche
“standard”
Elementi di memoria “reali”:
Latch tipo “D ” e Flip-flop
• Le reti viste prima sono note come latch S-R
(Set-Reset)
• Hanno il difetto di avere uno stato indecidibile
(cioè l’uscita non può essere nota con certezza)
quanto entrambi gli ingressi sono a uno
• In molti casi questo è inaccettabile

• Si può rimediare?
§ latch-D (data)
§ flip-flop
Latch tipo “D”
• Gli ingressi al circuito
base sono ottenuti da
un’unica variabile (di
cui se ne fa il negato)
• Non vi può essere
ambiguità, per
definizione
• Il circuito è abilitato
durante tutta la fase
positiva del clock
Flip-flop
Master-Slave
D
Master
D Q
Qm
Slave
D Q
Qs
Q
• Configurazioni più
Clock Clk Q Clk Q Q
complesse (come
questa) consentono
ad esempio di
(a)
Circuit
ottenere che l’uscita
Clock del circuito commuti
D
esattamente al
Qm

Q = Qs
termine dell’impulso
(b) Timing diagram
di clock

D Q

(c) Graphical
symbol
Struttura

Registri
i7 D
Q+ o7
C
i6 D
Q+ o6
C I O
i5 D
Q+ o5
C
i4 D
Q+ o4
C
i3 D
Q+ o3 Clock
C
i2 D
Q+ o2
C
i1 D
Q+ o1
C
i0 D
Q+ o0
C

Cloc
k

§ Impiegati per registrare delle word di dati


§ Collezione di latch edge-triggered
§ Caricano gli input sul fronte in salita del clock
Operazioni su registri
State = x State = y
Rising y Output = y
Input = y Output = x
x _ clock
_

§ Memorizzano bit
§ La maggior parte delle volte operano come una
barriera tra input e output
§ Sul fronte in salita del clock memorizzano l’input
Vantaggi dell’edge
triggered

• Una metodologia edge triggered permette di


aggiornare lo stato a partire dal quello presente senza
creare delle situazioni di corse
• Questo porta alle macchine a stati in cui:
§ Lo stato successivo dipende da quello presente e dall’input
§ L’output dipende dallo stato presente e dall’input
(macchina di Mealy), o solo dallo stato presente (macchina
di Moore)
Esempio di macchina a stati
Comb. Logic
0
§ Circuito
A
accumulatore
§ A ogni ciclo
L 0 MUX
Out
U

In 1
carica l’input
Load e lo accumula
Clock

Clock

Load

In x0 x1 x2 x3 x4 x5

Out x0 x0+x1 x0+x1+x2 x3 x3+x4 x3+x4+x5


CALCOLATORI
Il linguaggio Assembly

Giovanni Iacca
giovanni.iacca@unitn.it

Lezione basata su materiale preparato


dal Prof. Luca Abeni
Linguaggio macchina ed Assembly

• CPU: “capisce” (e riesce ad eseguire) solo il suo linguaggio macchina


• Sequenza di 0 e 1
• Non proprio utilissimo per noi umani...
• Programmatore: scrive programmi in linguaggi di alto livello
• C, C++, Java, ecc.
• Non proprio comprensibili dalla CPU...
• Come riempire il gap?
• Compromessi fra sequenze di 0 e 1 ed alto livello?
• Assembly!
• Codici mnemonici invece di sequenze di 0 e 1 ⇒ più facile da usare
• Stretta corrispondenza fra istruzioni macchina ed Assembly ⇒
sempre strettamente legato alla CPU

2 / 21
Programmare in Assembly

• Programma Assembly: file ASCII contenente descrizione testuale


delle istruzioni
• Deve essere compilato per essere eseguito dalla CPU
• Assembler: compilatore da Assembly a linguaggio macchina
• Ad ogni istruzione Assembly corrisponde un’istruzione in linguaggio
macchina
• Salvo rare eccezioni (macro, label, riordinamento istruzioni...)
• Le istruzioni Assembly dipendono dalla CPU
• Programmi non portabili
• Linguaggio intermedio o linguaggio di programmazione?

3 / 21
Istruzioni Assembly

• Insieme delle istruzioni (sequenze di bit) riconosciute da una CPU:


Intruction Set Architecture (ISA)
• Non una semplice lista di istruzioni
• Sintassi e Semantica
• Accesso ai dati
• Registri (tipo e numero)
• Modalità di accesso alla memoria (indirizzamento)

• L’ISA di una CPU ne definisce anche il linguaggio Assembly


• In alcuni casi, più ISA per una singola CPU
• Diversi linguaggi Assembly (Intel 32bit vs. Intel 64bit, ecc.)

4 / 21
Funzionamento di una CPU

• Struttura di un programma Assembly → deriva direttamente dal


meccanismo di funzionamento di una CPU
1. Fetch: preleva istruzione dalla memoria
• Dove? Indirizzo memorizzato in apposito registro (Program
Counter PC / Instruction Pointer IP)
2. Decode: decodifica l’istruzione (per capire cosa fare)
3. Execute: esegue l’istruzione (operazione aritmetico / logica,
accesso alla memoria, ecc.)
• Programma ≡ lista di istruzioni macchina eseguite (prevalentemente)
in ordine sequenziale
• Prevalentemente: esistono istruzioni di salto (per rompere
l’esecuzione sequenziale)
• Linguaggio di basso livello: salti, non cicli o selezioni!
• Tutto ciò si riflette sul linguaggio Assembly

5 / 21
Programmi Assembly

• Programma Assembly ≡ lista di istruzioni eseguite (prevalentemente)


in ordine sequenziale
• Prevalentemente operazioni aritmetico / logiche
• Talvolta operazioni di movimento dati
• Ordine prevalentemente sequenziale: esistono operazioni di
controllo del flusso (modificano il valore di PC / IP)
• Le istruzioni Assembly operano su dati (operandi dell’istruzione)
• Operandi di operazione aritmetica o logica
• Dati da muovere (ed indirizzi di memoria da / a cui muovere)
• Nuovi valori per PC / IP
• Ancora: linguaggio di basso livello!

6 / 21
Registri

• Istruzioni Assembly: operano su dati


• Immediati (costanti)
• Contenuti in registri
• Contenuti in memoria (varie modalità di indirizzamento)
• Registro a k bit: batteria di k flip-flop di tipo D
• Banco di registri utilizzabili da istruzioni Assembly
• Cambia da CPU a CPU - definito da ISA
• Registri general-purpose vs. registri specializzati
• In genere, numero di registri da 4 a 64
• Sintassi differente da Assembly ad Assembly
• In alcuni casi nomi simbolici, in altri casi numeri

7 / 21
Istruzioni Assembly

• Istruzione Assembly: codice mnemonico seguito da eventuali operandi


• Operandi: immediati, registri o in memoria
• Alcune ISA pongono vincoli
• Tipi di istruzioni:
• Aritmetico / logiche
• Movimento dati
• Controllo del flusso (salti, ecc.)
• Istruzioni aritmetico / logiche: specificano due operandi ed una
destinazione (talvolta, destinazione implicita)
• Istruzioni che accedono alla memoria: varie modalità di indirizzamento
• Vincoli su istruzioni ed operandi: ISA CISC vs. RISC
• RISC: semplifica l’implementazione della CPU
• CISC: semplifica la scrittura di programmi Assembly

8 / 21
Esecuzione condizionale

• Alcune istruzioni vanno eseguite solo se determinate condizioni si


verificano
• Esempio: istruzioni di controllo del flusso
• Necessario per implementare selezioni e cicli
• In alcune ISA, altre istruzioni possono avere esecuzione
condizionale (ARM: tutte!)
• Come esprimere queste condizioni?
• Confronto fra valori di registri generici (general-purpose)
• Come settare i valori di questi registri?
• Basandosi sui valori di flag contenuti in uno speciale registro
• Come settare i valori di questi flag?

9 / 21
Predicati in Assembly

• In alcune ISA, condizioni basate sul contenuto di registri generici


• Esegui se due registri hanno contenuto uguale (o diverso)
• Come implementare “esegui se il contenuto di un registro è
maggiore del contenuto di un altro registro”?
• Servono istruzioni di confronto!
• In altre ISA, esiste un registro flag
• Vari flag settati da istruzioni aritmetiche e logiche
• Flag settato se il risultato è 0, flag settato se il risultato è negativo,
flag settato in caso di overflow, ecc.
• In alcune ISA, istruzione di confronto set if less than (o simile):
confronta due registri general-purpose e setta un terzo registro in base
al risultato
• In altre ISA, istruzione di confronto cmp: esegue sottrazione settando
flag, ma scarta il risultato

10 / 21
Tipi di istruzioni Assembly

• Istruzioni Aritmetiche e Logiche


• Implementate dalla ALU (Unitá Logica Aritmetica)
• Operandi / Destinazione: solo nei registri o anche in memoria?
• Istruzioni di movimento dati
• Sposta dati fra registri
• Carica costanti (valori immediati) in registri / memoria
• Carica dati da memoria a registri o viceversa
• Istruzioni di salto / controllo del flusso
• Manipolano il contenuto di PC / IP
• Necessaria esecuzione condizionale (selezioni e cicli)
• In più: invocazione di subroutine e ritorno da subroutine
• Necessario salvare il valore di PC / IP prima di cambiarlo...
• Come?

11 / 21
Operandi e destinazioni

• Operazioni aritmetiche e logiche: generalmente due operandi ed una


destinazione
• Istruzioni Assembly con tre argomenti?
• Dove stanno gli operandi? Dove salvare il risultato (destinazione)?
• Due possibili “filosofie” diverse:
1. Tutto nei registri (operandi immediati)
2. Possibilità di avere operandi o destinazione in memoria
• Prima soluzione: implementazione CPU più semplice (e più elegante!)
• Seconda soluzione: linguaggio Assembly più potente e più semplice
da usare
• Talvolta, migliori performance
• La prima soluzione porta ad ISA RISC, la seconda ad ISA CISC

12 / 21
ISA CISC e RISC

• RISC: Reduced Instruction Set Computer


• ISA “più semplice” e regolare
• Istruzioni aritmetico / logiche: no operandi o destinazione in
memoria
• Uniche istruzioni che accedono alla memoria: load e store
• In generale, meno istruzioni e meno potenti
• CISC: Complex Instruction Set Computer
• Maggior numero di istruzioni; istruzioni più “potenti”
• Tutte le istruzioni possono avere operandi (o destinazione) in
memoria

13 / 21
Reduced Instruction Set Computer

• Obiettivo: semplificare al massimo la struttura della CPU


• Istruzioni aritmetico / logiche:
<opcode> <dst>, <arg1>, <arg2>
• <dst>, <arg1>: registri
• <arg2>: registro o valore immediato
• Accessi alla memoria (sostanzialmente, solo due istruzioni):
• load <reg>, <memory location>
• store <memory location>, <reg>
• Conseguenza: servono più registri
• Codifica istruzioni: numero fisso di bit

14 / 21
Complex Instruction Set Computer

• Obiettivo: fornire istruzioni Assembly più potenti / flessibili / performanti


• Istruzioni aritmetico / logiche: possono avere operandi e/o
destinazione in memoria
• Spesso ci sono comunque limiti / vincoli (Intel: al più un operando
o la destinazione in memoria)
• Molte più istruzioni, con comportamenti più complessi
• Codifica istruzioni: numero variabile di bit
• Sintassi meno regolare
• Esempio: per “salvare” alcuni bit di codifica, le istruzioni Intel
hanno destinazione implicita (uguale al secondo argomento)

15 / 21
Meglio CISC o RISC?

• Domanda “filosofica”
• Due tipi di ISA differenti, con obiettivi differenti
• Come in ogni cosa, gli estremismi non sono mai una buona idea...
• Soluzioni “di successo” spesso mescolano i due approcci
• Intel: tipico esempio di ISA CISC, ma ha “rubato” alcune idee a
RISC (numero di registri, cmov, ecc.)
• ARM: ISA RISC “pragmatica” con modalità di indirizzamento più
complesse, molte istruzioni (anche “complesse” e potenti), meno
registri (16)
• Durante il corso vedremo tre ISA: MIPS (RISC “duro e puro”), Intel
(CISC) ed ARM (RISC “pragmatico”)

16 / 21
Accessi alla memoria

• Istruzioni che accedono alla memoria:


• Istruzioni load e store per RISC
• Istruzioni generiche per CISC
• In ogni caso, hanno argomento <memory location>
• Come si esprime <memory location>?
Varie modalità di indirizzamento:
1. Indirizzo di memoria costante espresso come valore immediato
2. Indirizzo di memoria contenuto in un registro
3. Indirizzo di memoria ottenuto shiftando (o comunque
manipolando) il contenuto di un registro
4. Combinazione dei metodi precedenti

17 / 21
Modalità di indirizzamento

• 1. Assoluto: indirizzo codificato nell’istruzione Assembly


• Problematico per RISC... perché?
• 2. Indiretto: registro contenente indirizzo codificato nell’istruzione
Assembly
• Utile per implementare puntatori...
• 3. Base + Spiazzamento (2 + 1): indirizzo ottenuto sommando registro
a valore immediato
• 4a. Base + Indice (2 + 3): indirizzo ottenuto sommando registro a
registro scalato / shiftato
• Utile per implementare array con elementi di dimensione > 1
• 4b. Base + Indice + Spiazzamento (1 + 2 + 3): indirizzo ottenuto
sommando registro, registro scalato / shiftato e valore immediato

18 / 21
Indirizzamento vs. CISC / RISC

• Tradizionalmente, ISA RISC forniscono modalità di indirizzamento +


semplici
• Codificare indirizzi per indirizzamento assoluto è problematico
(istruzioni codificate su numero fisso di bit)
• ISA CISC: indirizzamento con indice shiftato → permette di
“risparmiare” istruzioni nell’accesso ad array
• Eccezione: ARM
• Indirizzamento con indice shiftato
• Pre-incremento / Post-incremento (utile nello scorrere array)

19 / 21
ISA e ABI

• ISA: definisce le istruzioni riconosciute dalla CPU ed i registri


• Numero di registri
• Registri general-purpose vs. registri specializzati
• Come usare i registri?
• Quali registri usare per i dati e quali per gli indirizzi?
• Invocazione di subroutine → i valori dei registri vengono cambiati?
• Come si passano i valori dei parametri ed i valori di ritorno?
• Spesso questo non è forzato dall’hardware...
⇒ ... convenzione software!
• Application Binary Interface (ABI): insieme di convenzioni software
che dicono come usare i registri
• Esempio: convenzioni di chiamata

20 / 21
Convenzioni di chiamata

• Non fanno propriamente parte dell’architettura


• Data una CPU / architettura, si possono usare diverse convenzioni
di chiamata
• Servono per “mettere d’accordo” diversi compilatori / librerie ed
altre parti del Sistema Operativo
• Tecnicamente, sono specificate dall’ABI, non dall’ISA!
• Convenzioni:
• Come / dove passare i parametri: stack o registri?
• Quali registri preservare?
• Quando un programma invoca una subroutine, quali registri
conterranno un certo valore al ritorno dalla subroutine?

21 / 21
CALCOLATORI
Assembl RISC-V
Giovanni Iacca
giovanni iacca unitn it

Luigi Palopoli
luigi palopoli unitn it
In c ion Se
• Per impartire istruzioni al computer bisogna
«parlare» il suo linguaggio
• Il linguaggio si compone di un vocabolario di
istruzioni detto instruction set IS
• I vari tipi di processore hanno ciascuno il
proprio IS.
• Tuttavia le differenze non sono eccessive
Un utile esempio è quello delle inflessioni
regionali di un’unica radice linguistica
In c ion Se
• Come osservato da von Neumann:
“certi insiemi di istruzioni in linea di principio si
prestano a controllare l hardware
• Egli stesso osservava che le considerazioni
davvero decisive sono di natura pratica:
semplicità dei dispositivi richiesti
chiarezza delle applicazioni
velocità di esecuzione
• Queste considerazioni scritte nel sono
straordinariamente valide anche oggi
In e e le ioni
• Nelle prossime lezioni, mostreremo diversi
instruction sets
• Studieremo inoltre il concetto di programma
memorizzato:
Istruzioni e dati sono
memorizzati come numeri
• Considereremo tre IS:
• RISC-V
• Intel
• ARM
Pe ch il RISC-V
• Il motivo per cui mostreremo e faremo esercizi con Intel dovrebbe
essere abbastanza chiaro…
Milioni di PC sono basati su architettura Intel o Intel compatibile
E’ un esempio paradigmatico di architettura CISC

• Vedremo inoltre che ARM è una sorta di via di mezzo tra CISC e RISC
(RISC «pragmatico»
Usata in moltissimi sistemi embedded

• Partiremo però dall’architettura RISC-V, che è invece un’architettura


appunto RISC, che ha origine all’Università di Berkeley nel
Moderna, open-source
Piuttosto usata in alcune applicazioni specifiche
Incominciamo
• Inizieremo dall’IS RISC-V
• Fin dai tempi di von Neumann, era
convinzione diffusa che all’interno di un IS:

Devono necessariamente essere


previste istruzioni per il calcolo
delle operazioni aritmetiche
fondamentali
O e a ioni a i me iche
• E’ dunque naturale che l’architettura RISC V
supporti le operazioni aritmetiche
• Per progettare un IS come quello del RISC V
terremo conto di vari principi ispiratori

P inci i di P ge a i ne n
La em lici à fa i ce la eg la i à
I ioni a i me iche
• Il modo più semplice di immaginare una
istruzione aritmetica è a tre operandi:
a = b + c

• Quindi l’architettura del RISC-V prevede soltanto


istruzioni aritmetiche a tre operandi, ad esempio:

add a, b, c
I ioni i com le e
• Istruzioni più complesse si ottengono a partire dalla
combinazione di istruzioni semplici.
• Esempio (in C :
a = b + c;
d = a ;
• Diventa:
add a, b, c
b d, a,
I ioni i com le e
• Un altro esempio (in C :

a = b + c + d + ;
• Diventa:

add a, b, c
add a, a, d
add a, a,
E em io
• Nei linguaggi ad alto livello possiamo scrivere
espressioni complesse a piacimento:

= ( + ) ( + );

• Quando si traduce a basso livello bisogna per forza


usare sequenze di istruzioni elementari
E em io
• Ad esempio l’espressione precedente verrà
tradotta in istruzioni elementari di questo tipo:

add 0, , # a a ab .
# 0 = +
add 1, , # a a ab .
# 1 = +
b , 0, 1 # = 0 1
Alc ne con ide a ioni
• Nell’assemblatore (assembler RISC V
I commenti iniziano con e continuano fino alla fine
della linea
L’operando “destinatario” dell’operazione è sempre il
primo
• Questo non vale per tutti gli assemblatori
• Ad esempio se si usa gcc come assemblatore
questo segue la sintassi AT T per la quale:
I commenti si fanno a la C: / commento /
L’operando destinatario dell’operazione è messo in
fondo
O e andi
• Fino ad ora abbiamo usato gli operandi come se
fossero “normali” variabili di un linguaggio ad alto
livello…
• … in realtà, nel RISC-V gli operandi di operazioni
aritmetiche sono vincolati ad essere (contenuti
nei registri
• Un registro è una particolare locazione di memoria
che è interna al processore e quindi può essere
reperita in maniera velocissima (un ciclo di clock
Regi i del RISC-V
• Il RISC-V contiene registri a bit
Gruppi di bit detti «parola doppia» (double word
Gruppi di bit detti «parola» (word
• Il vincolo di operare solo tra registri semplifica di molto il
progetto dell’hardware
• Ma perché solo registri?

P inci i di P ge a i ne n
min i n le dimen i ni maggi e la el ci à
• Avere molti registri obbligherebbe i segnali a «spostarsi»
su distanze più lunghe all’interno del processore
• Quindi per effettuare uno spostamento all’interno di un
ciclo di clock saremmo costretti a rallentare il clock
E em io ( i e o)
• Torniamo al nostro esempio:
= ( + ) ( + );
• Il codice con i registri diventa:
add 5, 20, 21 # I a
# 5 a a a
# a d :
# 20+ 21 ( + )
add 6, 22, 23 # Id , 6 c a
# a 22+ 23 ( + )
b 19, 5, 6 # = 5 6
O e a ioni
• Come abbiamo detto, le operazioni logiche e
aritmetiche e si effettuano solo tra registri
• Il problema è che i registri non bastano!
• Occorrono istruzioni di trasferimento che:
prelevino dei dati da locazioni di memoria, e li
carichino nei registri (load
salvino il contenuto dei registri in memoria (store
La memo ia
• La memoria è una sequenza di bit organizzati in gruppi
di ( bit byte

Ciascun byte è associato


ad un indirizzo lineare
progressivo tramite il
quale è possibile
prelevarlo
Wo d e do ble o d
• Per quanto la maggior parte delle architetture permettano
l’accesso a ciascun byte, la maggior parte delle volte si
trasferiscono multipli di o byte, ovvero parole o parole
doppie

Vincolo di allineamento:
è possibile accedere
solo a parole poste a
indirizzi multipli
dell’ampiezza della
parola (vincolo presente
ad es. in Intel x , ma
non in RISC-V
T a fe imen o

• Per caricare una parola doppia, una parola, o un byte,


è necessario specificarne l’indirizzo
• Nell’assemblatore RISC-V l’indirizzo si specifica tramite
una base (in un registro e uno spiazzamento o offset
(costante
• Come vedremo, in altre architetture (es. Intel e ARM
c è molta più flessibilità nello specificare l’indirizzo
Load do ble o d
• Il caricamento di una parola doppia (contenuta
in un certo indirizzo di memoria in un registro
avviene tramite la seguente istruzione:
d 9, 8( 22)
Si sta
operando su Base
una parola Registro in
cui si vuole Spiazzamento
doppia
caricare

• L’effetto di questa istruzione è di caricare in 9 la


parola doppia all’indirizzo dato da 22
E em io
• Supponiamo di volere effettuare l’istruzione:
A[12] = + A[8]
• La traduzione in assembly RISC-V è:
d 9, 64( 22) # a d A 22
# a a a a d a
# c c a a d
# 22 + 8*8
add 9, 21, 9 # 21
d 9, 96( 22) # c
# d 9 A[12],
# a d
# 22 + 12*8
Regi e S illing
• Tipicamente i programmi contengono più variabili che
registri…
• Quello che si fa è caricare le variabili in uso in un dato
momento nei registri e scaricare quelle che non si
usano più in quel momento
• Questa operazione è chiamata register spilling ed è
eseguita dal compilatore che stima il working set ed
inserisce nel codice assembly le operazioni di
load/unload appropriate
• Nell’esempio precedente, in base al codice potrebbe
essere che ad es. l’indirizzo di A serve ancora in 22
nelle istruzioni successive, mentre forse h si potrebbe
scaricare da 21 (fa tutto il compilatore!
O e andi immedia i o co an i
• Molto spesso nelle operazioni aritmetiche
almeno uno dei due operandi è costante
• Un possibile approccio può essere di
memorizzare la costante in un qualche
indirizzo
• Esempio: = + 4;

d 9, I dC 4( 3)
# a c a 4 a d 3 + I dC 4
add 22, 22, 9
O e andi immedia i
• La soluzione che abbiamo visto è piuttosto
inefficiente
• Ciò segue l’idea generale di ende e el ci le
i a i ni i c m ni
• Per questo motivo esistono istruzioni che
permettono di operare con costanti
= + 4; add 22, 22, 4

Immediate
La co an e 0 ( e o)
• Esistono alcune costanti che possono essere di
grande utilità per semplificare alcune
operazioni
Es. per spostare un registro in un altro posso
renderlo destinatario della somma del sorgente
con
• Per questo motivo il RISC-V dedica un registro
ad hoc ( 0 alla costante
N me i
• Prima di parlare del modo in cui le istruzioni sono
codificate attraverso numeri ricordiamo:
Nei calcolatori l unità base di informazione è il bit
Un gruppo di bit può essere associato ad un numero
fino a che rappresenta una cifra nella notazione
esadecimale
Quindi un byte viene rappresentato da due cifre
esadecimali (ciascuna corrispondente a bit
Ad esempio
1001 1101 9D
Cif e e adecimali
Li le e Big Endian
• Quando si memorizza una parola di quattro byte
(o una doppia parola di in una sequenza di
byte posti a indirizzi progressivi, va capito dove va
il byte più significativo e quello meno significativo
• Esempio (esadecimale

0 EA01BD1C

Byte meno
Byte più
significativo
significativo
Li le e Big Endian
• Little Endian (utilizzato ad es. in architetture Intel e
RISC-V
Addr EA
Addr
EA01BD1C Addr BD
Addr C

• Big Endian (utilizzato ad es. in architetture Motorola


e protocolli Internet
Addr C
Addr BD
EA01BD1C Addr
Addr EA
Ra e en a ione delle i ioni
• Come i dati, anche un programma deve essere
memorizzato in forma numerica
• Questo vuol dire che le istruzioni che abbiamo
introdotto simbolicamente (scritte in assembly
prima di essere memorizzate (ed eseguite
devono essere convertite in una serie di numeri
in formato binario (codice macchina
• Cominciamo con un esempio
E em io
• Consideriamo l’istruzione:
add 9, 20, 21

• Per rappresentarla tramite un codice numerico


univoco occorre:
• Un codice numerico che ci dica che si tratta di una
istruzione di somma (add
• Altri codici numerici che ci dicano quali sono gli
operandi sorgente e destinazione
• In RISC-V, ogni istruzione viene codificata in una
parola (ovvero, in bit
Ri l a o
• Il risultato rappresentato in decimale è il
seguente:
bit bit bit bit bit bit

( ( ( ( ( (

Il primo, quarto e sesto


campo codificano Secondo
l’istruzione Primo
operando Risultato
operando

• In RISC-V i registri sono numerati da a e


quindi possono essere specificati come operandi
( bit all’interno del codice dell’istruzione
Cam i delle i ioni RISC-V
• In generale, è utile dare un nome ai vari campi
relativi al codice macchina di un’istruzione
bit bit bit bit bit bit
funz rs rs funz rd codop

• codop: codice operativo dell’istruzione


• funz e funz : codici operativi aggiuntivi
• rs : primo operando sorgente
• rs : secondo operando sorgente
• rd: operando destinazione
T ade-off
P inci i di P ge a i ne n nb n ge
ichiede b ni c m me i
• Nel nostro caso il buon compromesso è di codificare
tutte le istruzioni in bit
• Questa scelta ci costa in termini di:
limite al numero di istruzioni
limite al numero di registri
limite alle modalità di indirizzamento
• … ma ci permette di guadagnare molto in efficienza!
I ioni immedia e
• Abbiamo visto che ci sono istruzioni di
caricamento/salvataggio, e di somma con
costanti, che sarebbero eccessivamente
limitate se usassimo sempre il formato che
abbiamo appena visto (detto R, da registro
• Per questo motivo esiste anche un secondo
formato (detto I, da immediato , utilizzato nei
casi di indirizzamento immediato e di
istruzioni che fanno uso di costanti
I ioni immedia e

bit bit bit bit bit


costante o indirizzo rs funz rd codop

• Ad esempio:
( ( ( ( (
d 9, 64( 22)

rs
rd
E em io
• Supponiamo di voler tradurre in linguaggio
macchina la seguente istruzione:
A[30] = + A[30] + 1;
• La traduzione è la seguente:

d 9, 240( 10)
add 9, 21, 9
add 9, 9, 1
d 9, 240( 10)
in codice macchina
• Cominciamo con il guardare i codici decimali:
in codice macchina
• In binario:
Ria mendo
• Ciascuna istruzione viene espressa come un
numero binario di bit
• Un programma consiste dunque in una
sequenza di numeri binari
• Tale sequenza viene scritta in locazioni
consecutive di RAM
• In momenti diversi, nella stessa RAM,
possiamo rappresentare programmi diversi
Codici delle i ioni i e fino ad
oa
Elenco (quasi completo)
delle istruzioni RISC-V
Elenco (quasi completo)
delle istruzioni RISC-V
Operazioni logiche
• I primi calcolatori operavano solo su parole intere
• Molto presto si manifestò la necessità di operare su
porzioni di parole o addirittura sul singolo bit
• Il RISC-V fornisce alcune istruzioni che permettono
di fare questo in maniera semplificata (rispetto ad
altre architetture) ma efficace
Shift logico
• Consideriamo per prima cosa lo shift logico a sinistra
• L’idea è di inserire degli zeri nella posizione meno significativa
e traslare tutto a sinistra perdendo (nel caso di overflow) i bit
più significativi
• Ad esempio, supponiamo che x19 contenga il valore 9:

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001001

slli x11, x19, 4

00000000 00000000 00000000 00000000 00000000 00000000 00000000 10010000

• L’effetto è di memorizzare in x11 valore 9*2^4=144


Il codice operativo
• Il codice macchina corrispondente all’istruzione:
slli x11, x19, 4
è:

Il numero di
bit di cui
effettuare lo
shift
Esempi
• Come abbiamo visto, l’effetto di questa istruzione è moltiplicare
l’operando per 2^k, dove k è il numero di elementi di cui si fa lo
shift
• Funziona sempre? Dipende…
• Esempio, se x19 contiene: (num. negativo, in complemento a 2)
10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

• Se effettuiamo:
slli x11, x19, 4
• Otteniamo un num. positivo (16):
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00010000

• Il problema è semplicemente che il numero non è rappresentabile!


(overflow)
Esempi
• Consideriamo invece -2 (rappresentato in complemento a 2 su 64 bit):
11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111110

• Se effettuiamo:
slli x11, x19, 1

• Otteniamo, correttamente, -2 * 2^1 = -4

11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111100

• Se rimaniamo nel range di rappresentabilità la moltiplicazione per


potenze di 2 tramite shift funziona bene
Shift a destra
• Analogamente allo shift logico a sinistra, si può avere uno shift
logico a destra
• Esempio:

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00011000

• Se effettuiamo:
srli x11, x19, 4

• Otteniamo:
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
Esempio
• Come abbiamo visto nelle lezioni sull’aritmetica dei calcolatori, lo
shift a destra di k unità corrisponde a dividere per 2^k
• E’ corretto?
• Nell’esempio fatto prima facendo lo shift a destra di 4 bit sul
numero 24 abbiamo ottenuto 1 (che è il risultato della divisione
intera di 24 per 2^4)
• Consideriamo un altro esempio (decimale -2):
11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111110

srli x11, x19, 4


00001111 11111111 11111111 11111111 11111111 11111111 11111111 11111111

• Ciò che si ottiene è un numero positivo e questo nonostante il


risultato dell’operazione sia perfettamente rappresentabile…
Shift aritmetico
• Per risolvere il problema evidenziato si risolve con lo shift
aritmetico a destra
• In sostanza quello che si inserisce a sinistra non sono bit uguali a 0
ma uguali al bit di segno
• Nel nostro esempio:
11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111110

srai x11, x19, 4

11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111


Shift aritmetico
• Notare che lo shift aritmetico non ha nessun
senso fatto a sinistra
§ In quel caso o il risultato è rappresentabile (e lo shift
logico funziona come aritmetico), o non lo è (e non
c’è molto che si possa fare!)
• Altre architetture offrono entrambi i tipi di shift
§ RISC-V essendo RISC fornisce solo l’essenziale!
Altre operazioni logiche
• Il RISC-V offre in aggiunta alle operazioni
logiche appena viste altre operazioni logiche
• Alcune di queste sono:
AND bit a bit
• Le operazioni logiche operano su ciascun bit
• Esempio, supponiamo che x10 e x11 contengano:
00000000 00000000 00000000 00000000 00000000 00000000 00010011 10000001

00000000 00000000 00000000 00000000 00000000 00000000 11111111 00000000

and x9, x10, x11

• Risultato:
00000000 00000000 00000000 00000000 00000000 00000000 00010011 00000000

• L’operazione AND forza alcuni bit a 0 usando come


operando una maschera (i cui bit corrispondenti a
quelli che si vogliono annullare sono settati a 0)
OR bit a bit
• Analogamente è possibile settare a 1 alcuni bit
facendo OR con un operando che abbia 1 nella
posizione corrispondente (maschera)
• Esempio:
§ Supponiamo di volere impostare a 1 i 4 bit più
significativi di una word contenuta in x9

addi x10, x0, 0x000F


slli x10, x10, 60
or x9, x9, x10
Rotazione
• Esempio:
§ Supponiamo di volere «girare» i quattro bit più
significativi del registro x9 sui suoi bit meno
significativi

11010000 00000000 00000000 00000000 11010000 00000000 00000000 00000000


Esempio
• Primo passo
11010000 00000000 00000000 00000000 11010000 00000000 00000000 00000000

srli x10, x9, 60

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001101

• Secondo passo
11010000 00000000 00000000 00000000 11010000 00000000 00000000 00000000

slli x9, x9, 4

00000000 00000000 00000000 00001101 00000000 00000000 00000000 00000000


Esempio
• Terzo passo
srli x9, x9, 4

00000000 00000000 00000000 00000000 11010000 00000000 00000000 00000000

• Quarto passo
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001101

00000000 00000000 00000000 00000000 11010000 00000000 00000000 00000000

or x9, x9, x10

00000000 00000000 00000000 00000000 11010000 00000000 00000000 00001101


OR esclusivo (XOR)
• Oltre a AND e OR abbiamo anche un supporto per l’OR
esclusivo (XOR)
• Lo XOR produce 1 se e solo se i due bit operandi sono
diversi (e zero se sono uguali)
00000000 00000000 00000000 00000000 00000000 01100011 01110000 11001101

00000000 00000000 00000000 00000000 00000000 10011011 01110000 00111101

xor x11, x9, x10

• Risultato:
00000000 00000000 00000000 00000000 00000000 11111000 00000000 11110000
Esempio
• Cosa succede se facciamo?
xor x9, x9, x9

• Il risultato è quello di annullare il valore di x9


• E’ una maniera molto veloce ed efficiente per
annullare un registro
• Lo XOR si può rappresentare (forma SP) come:
• A XOR B = (A AND NOT(B)) OR (NOT(A) AND B)
Lo strano caso del NOT
• Il NOT normalmente è un operatore unario
• Per i progettisti RISC-V tutte le operazioni aritmetiche
hanno tre operandi di tipo registro
• Per questo il NOT non è fornito nativamente dal RISC-V
• Tuttavia, si può ottenere dallo XOR come: NOT(A)=XOR(A,1)
00000000 00000000 00000000 00000000 00000000 01100011 01100011 11001101

11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111

xor x9, x9, x10

• Risultato:
11111111 11111111 11111111 11111111 11111111 10011100 10011100 00110010
Istruzioni per prendere
decisioni
• Una delle caratteristiche fondamentali dei
calcolatori (che li distinguono dalle calcolatrici) è
la possibilità di alterare il flusso del programma al
verificarsi di certe condizioni
§ costrutto if nelle varie forme
• Il linguaggio macchina delle varie architetture
supporta questa possibilità fornendo istruzioni di
«salto condizionato»
• Tipicamente i salti avvengono sulla base di certe
condizioni sui registri
Salto su condizioni
• Le due principali istruzioni di salto condizionato
sono:
beq rs1, rs2, L1
Se il registro rs1 è
uguale al registro
rs2 effettua un
bne rs1, rs2, L1 salto all’istruzione
con etichetta L1

Se il registro rs1 è
diverso dal registro
rs2 effettua un
salto all’istruzione
con etichetta L1
Il costrutto if
• Supponiamo di avere il seguente codice C. Come
viene tradotto?
if (i == j) f = g + h; else f = g – h;
Il costrutto if
• Traduzione costrutto if precedente:
bne x22, x23, ELSE # Salta a ELSE se x22 div. da x23
add x19, x20, x21 # f = g + h
beq x0, x0, ESCI # Salto incondizionato a ESCI
ELSE: sub x19, x20, x21 # f = g - h
ESCI: …

• Alcune osservazioni:
• Le label vengono alla fine tradotte in indirizzi
• Per fortuna questo lavoro noioso è opera del
compilatore!
Cicli
• La struttura che abbiamo presentato può essere usata
anche per realizzare dei cicli
• Consideriamo l’esempio:
while (salva[i] == k)
i += 1;

• Traduzione:
Ciclo: slli x10, x22, 3 # Registro temp. x10 = 8*i
add x10, x10, x25 # Ind. di salva[i] in x10
ld x9, 0(x10) # Carica salva[i] in x9
bne x9, x24, Esci # Esci se raggiunto limite
addi x22, x22, 1 # i = i+1
beq x0, x0, Ciclo
Esci: …
Alcune considerazioni
• Le sequenze di istruzioni tra due salti condizionati
(conditional branch) sono così importanti che
viene dato loro un nome: blocchi di base
§ Blocco di base: seq. di istruzioni che non contiene né
istruzioni di salto (con l’eccezione dell’ultima) né
etichette di destinazione (con l’eccezione della prima)
• Una delle prime fasi della compilazione è di
individuare i blocchi base
• Tutti i cicli in linguaggio ad alto livello vengono
implementati con blocchi di base
Altri confronti
• Oltre al salto su operandi uguali o diversi, è utile avere un salto
anche su operandi minori/maggiori o minori o uguali
• Il RISC-V mette a disposizione diversi tipi di confronti, in
particolare:
Esempio
• Consideriamo il codice
if (i < j) f = g + h; else f = g – h;

• Traduzione
bge x22, x23, ELSE # Salta a ELSE se x22 >= x23
add x19, x20, x21 # f = g + h
beq x0, x0, ESCI # Salto incondizionato a ESCI
ELSE: sub x19, x20, x21 # f = g - h
ESCI: …
Signed e unsigned

• L’esito del confronto è ovviamente diverso se


si tiene conto del segno oppure no
• Per questo motivo troviamo due versioni di blt
e bge (rispettivamente, bltu e bgeu) che
operano su interi senza segno («u» sta per
«unsigned»)
Esempio
• Si supponga che x9 e x10 contengano:
11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

• Cosa produce?
blt x9, x10, L1 salta a L1

• E cosa produce?
bltu x9, x10, L1 non salta a L1
Un piccolo trucco
• Supponiamo di volere controllare se un indice (x20) è fuori
dal limite di un array ([0, x11])
• Ce la possiamo cavare con un solo confronto!
bgeu x20, x11, FuoriLimite

• Perché?
• Se x20 >= x11, avviene il salto a FuoriLimite
• Ma ciò avviene anche se x20 è negativo: interpretando x20
come unsigned, necessariamente sarà x20 >= x11 (si noti
che x11 è necessariamente un num. positivo, in quanto
uguale alla dimensione dell’array-1)
Il costrutto case/switch
• Una possibilità è quella di effettuare una
sequenza in cascata di if-then-else
• In realtà esiste una tecnica diversa:
§ Memorizzare i vari indirizzi del codice da eseguire
in una tabella (una sequenza di word)
§ Caricare in un registro l’indirizzo a cui saltare
§ Fare un salto all’indirizzo puntato dal registro
tramite l’istruzione jalr («jump and link register»,
che vedremo più avanti)
Il costrutto case/switch
• Esempio: Tabella che
memorizza gli indirizzi
dei codici da eseguire

switch(a) {
case 1: <code 1>;
slli x9, x10, 3 # moltiplica per 8
case 2: <code 2>;
add x9, x9, x11 # x11 ind. base TABLE

ld x12, 0(x9) # carica indirizzo
}
jalr x1, x12

Salto all’indirizzo nel


registro x12
Procedure
• L’utilità di un computer sarebbe molto limitata se
non si potessero chiamare procedure (o funzioni)
• Possiamo pensare a una procedura come a una
«scatola nera» che esegue un certo task, senza
che ne conosciamo necessariamente i dettagli
• La cosa importante è definire chiaramente un
protocollo (o convenzione) di chiamata alle
procedure
Protocollo
1. Caricare i parametri di input della procedura
in posti noti a priori
2. Trasferire il controllo alla procedura
1. Acquisire le risorse necessarie
2. Eseguire il compito
3. Caricare i valori di ritorno in posti noti
4. Restituire il controllo al chiamante
3. Prendere il valore di ritorno della procedura e
“cancellare” le tracce (pulire registri, ecc.)
Protocollo
• Il modo in cui questo protocollo viene messo
in pratica dipende dall’architettura e dalle
convenzioni di chiamata del compilatore
• Ci limitiamo qui a pochi cenni per il RISC-V
Protocollo di chiamata
RISC-V
• L’idea di base è di cercare di usare laddove
possibile i registri, perché sono il meccanismo
più veloce per effettuare il passaggio dei
parametri
• Convenzione per il RISC-V:
§ x10-x17 usati per i parametri in ingresso
e per i valori di ritorno
§ x1 indirizzo di ritorno per tornare al
punto di partenza (chiamato anche
ra, ovvero «return address»)
Protocollo di chiamata
RISC-V
• L’indirizzo x1 viene usato in particolare nelle
istruzioni di «jump and link» (jal) e «jump and link
register» (jalr), che effettuano il salto e
contemporaneamente memorizzano in x1
l’indirizzo di ritorno
§ L’indirizzo che viene salvato in x1 è il PC (program
counter) + 4 (istruzione successiva alla jal/jalr)

• Alla fine della procedura sarà sufficiente fare un salto:


jalr x0, 0(x1)
… e se i registri non
bastano?
• In certi casi i registri non bastano perché la procedura ha
più parametri di quanti sono i registri
• In tal caso si uno stack (pila), ovvero una struttura dati
gestita in modalità LIFO in cui è possibile caricare, a partire
da una posizione nota alla procedura (puntata dal registro
x2, chiamato anche sp), i parametri in più
• Lo stack si può usare anche per caricare variabili locali
(tramite un’operazione di push) e salvare dei valori di
registri che dovranno essere ripristinati in seguito
• Alla fine della procedura si può ripulire lo stack (operazione
pop) riportandolo alla situazione precedente
Esempio
• Consideriamo la procedura in C:

long long int esempio_foglia(long long int g,


long long int h, long long int i, long long int j) {
long long int f;
f = (g+h) – (i+j);
return f;
}

• Cerchiamo di capire come verrebbe tradotta


Prologo
• Per prima cosa il compilatore sceglie un’etichetta
associata all’indirizzo di entrata della procedura
(nel nostro caso, esempio_foglia)
§ In fase di collegamento (linking) l’etichetta sarà
collegata a un’indirizzo
• La prima operazione è quella di salvare in
memoria tutti i registri che la procedura usa, in
modo da poterli ripristinare in seguito
• Tale fase è chiamata prologo e potrebbe
richiedere di allocare nello stack anche spazio per
le variabili locali (se i registri non bastano)
Uso dei registri
• Parametri in input:
§ g = x10; h = x11; i = x12; j = x13
• Variabile locale: f = x20
• Usiamo, a titolo di esempio, anche i registri
temporanei x5 e x6
• Supponiamo, sempre a titolo di esempio, di
dover salvare x5, x6 e x20
Prologo
• La procedura usa i registri x5, x6, x20
• Il registro sp punta alla testa dello stack (che cresce verso il basso)
• Il codice del prologo è il seguente: Decrementiamo sp di
24, per far posto a 3
double word

Salvataggio di x5 in
esempio_foglia: addi sp, sp, -24 sp+16
sd x5, 16(sp)
sd x6, 8(sp)
sd x20, 0(sp) Salvataggio di x6 in
sp+8

Salvataggio di x20 in
sp+0
Esecuzione della procedura
• Dopo il prologo, avviene l’esecuzione della procedura:

In x5 salviamo g+h

add x5, x10, x11 In x6 salviamo i+j


add x6, x12, x13
sub x20, x5, x6

In x20: (g+h)-(i+j)
Epilogo
• Dopo aver eseguito la procedura, si ripristinano i
registri usati e si setta il valore di ritorno
Trasferisco x20 in x10
(valore di ritorno)

addi x10, x20, 0 Ripristino i registri


ld x20, 0(sp) prelevando i valori
ld x6, 8(sp) esattamente da dove
ld x5, 16(sp) li avevo messi
addi sp, sp, 24
jalr x0, 0(x1) Ripristino lo stack (tutto
ciò che è sotto sp non è
significativo)
Salto al punto da
dove sono arrivato
Evoluzione dello stack
Stack durante
Stack prima della Stack dopo la
l’esecuzione della
chiamata chiamata
procedura
Un bagno di realtà
• Nessun compilatore farebbe mai questo…
• Infatti
§ I registri temporanei x5 e x6 in realtà non devono essere
salvati durante la chiamata (avremmo potuto evitare due
ld e due sd)
• RISC-V adotta la seguente convenzione:
§ x5-x7 e x28-x31: registri temporanei, che non sono salvati
in caso di chiamata a procedura
§ x8-x9 e x18-x27: registri da salvare il cui contenuto deve
essere preservato in caso di chiamata a procedura
Ulteriori complicazioni
• In realtà le cose possono complicarsi per via di:
§ Variabili locali
§ Procedure annidate
• Questo si risolve usando sempre lo stack e
allocando al suo interno variabili locali e valori
del registro di ritorno x1
• Vediamo un esempio
Procedura ricorsiva
• Consideriamo il seguente esempio
long long int fact(long long int n) {
if (n < 1)
return 1;
else
return n*fact(n-1);
}

• Qui (ma sarebbe lo stesso con una qualsiasi funzione


che chiama un’altra funzione) abbiamo due problemi:
§ Sovrascrittura di x1, che rende impossibile il ritorno una
volta finita la chiamata innestata
§ Sovrascrittura di x10, usato per il parametro n
Procedura ricorsiva
• Soluzione: salvaguardare i registri x1, x10
• Vediamo come…
• Primo step: creiamo spazio nello stack e salviamo x1 e x10
fact:
addi sp, sp, -16 # Allochiamo spazio per due elementi
sd x1, 8(sp) # Salviamo x1
sd x10, 0(sp) # Salviamo x10

• Secondo step, testiamo se n < 1 (ritornando 1 in caso


affermativo):

addi x5, x10, -1 # Calcoliamo x5 = n-1


bge x5, x0, L1 # Se n-1 >= 0, saltiamo a L1
Procedura ricorsiva
• Se n < 1, chiudiamo la procedura:
addi x10, x0, 1 # Metodo del RISC-V per caricare 1 in x10
addi sp, sp, 16 # Ripuliamo lo stack
jalr x0, 0(x1) # Ritorniamo al programma chiamante

notiamo che non ripristiniamo x1 e x10 (come si farebbe


normalmente), perché, essendo l’ultima chiamata della
ricorsione, i loro valori non devono essere più usati (in
quanto n e return address non cambieranno
ulteriormente)
• Se n >= 1, decrementiamo n e richiamiamo fact:
L1:
addi x10, x10, -1 # Decrementiamo n (arg. diventa: n-1)
jal x1, fact # Chiamiamo fact(n-1)
Procedura ricorsiva
• A questo punto ripristiniamo x10 e x1 e ripuliamo lo stack

addi x6, x10, 0 # Carichiamo risultato di fact(n-1) in x6


ld x10, 0(sp) # Ripristino dell’argomento n, x10
ld x1, 8(sp) # Ripristino del reg. di ritorno, x1
addi sp, sp, 16 # Ripuliamo lo stack

• Non ci resta che moltiplicare fact(n-1), che è in x6,


per n, che è in x10, e ritornare

mul x10, x10, x6 # Restituiamo n*fact(n-1)


jalr x0, 0(x1) # Ritorniamo al programma chiamante
Storage class
• Le variabili in C sono in genere associate a locazioni di
memoria, caratterizzate per:
§ Tipo (int, char, float, ecc.)
§ Storage class
• Il C ha due possibili storage class
§ Automatic: variabili locali che hanno un ciclo di vita
collegato alla funzione
§ Static: sopravvivono alle chiamate di procedura. Sono
essenzialmente variabili globali, oppure variabili definite
esplicitamente come static all’interno di una procedura.
• Le variabili statiche sono memorizzate in una zona di
memoria specifica
§ Nel RISC-V, questa zona è accessibile attraverso il registro
x3, chiamato anche gp («global pointer»)
Variabili locali
• L’ultimo problema è costituito dalle variabili locali
• Quando sono poche, si riescono a usare registri
• Quando sono tante (ad esempio struct complesse o
array) non ci sono abbastanza registri
• Quello che si fa è allocare le variabili locali nello
stack (come abbiamo già visto)
Record di attivazione
• Il segmento di stack che contiene registri salvati e variabili locali
relative a una procedura viene chiamato record di attivazione (o
stack frame)
• Le variabili locali vengono individuate tramite un offset a partire
da un puntatore
• Il registro sp è alle volte scomodo come base, in quanto come
abbiamo visto cambia (cresce verso il basso) durante l’esecuzione
della procedura
• Alcuni programmi RISC-V utilizzano il registro x8, chiamato anche
fp, come «frame pointer», ovvero puntatore alla prima parola
doppia dello stack frame di una procedura (mantenuto costante
durante la funzione, per cui può essere usato come base)
Evoluzione dello stack
Dati dinamici
• In aggiunta a variabili globali e variabili locali, abbiamo anche le variabili
dinamiche (quelle che in C/C++ vengono allocate con chiamate
malloc/free, new/delete), salvate nell’«heap» (letteralmente, «cumulo»)
• Il RISC-V adotta il seguente schema di memoria:

Lo stack procede
per indirizzi
decrescenti

L’ heap procede
per indirizzi
crescenti
Convenzioni sui registi
• In base alle convenzioni definite dal RISC-V, l’uso dei registri
può essere riassunto come segue:
Elaborazione
• Per quanto le ricorsioni siano eleganti, spesso
sono causa di varie inefficienze
Never hire a programmer who solves the factorial with a recursion!

• Consideriamo la seguente funzione:

long long int sum(long long int n) {


if (n > 0)
return sum(n-1) + n;
else
return n;
}
Elaborazione
• Consideriamo la chiamata sum (2)

2+1
Sum (2)
Il valore di ritorno viene costruito
tornando su per la catena di chiamate.
n=2 Ogni frame di attivazione deve essere
2+sum(1)
1+0 Sum (1) preservato per produrre il risultato
corretto.

n=1
1+sum(0)
Sum (0)
0

n=0
Elaborazione
• Consideriamo ora il codice così modificato

long long int sum(long long int n, long long int acc) {
if (n > 0)
return sum(n-1, acc+n);
else
return acc;
}

• Stavolta la chiamata ricorsiva è l’ultima cosa che la


procedura fa (si limita a ritornare)
• Questa ricorsione viene detta «di coda»
• Ritornando al nostro esempio, questo diventa sum(2,0)
Elaborazione
• Consideriamo la chiamata sum (2,0)

3
Sum (2,0)
In questo caso lungo i rami di ritorno
non faccio che restituire 3 (valore di
ritorno della chiamata).
n = 2, acc=0 Conservare i dati nel frame di
Sum(1,2)
3 Sum (1,2) attivazione è inutile. Posso usare
un unico frame di attivazione e svolgere
la ricorsione in iterazione.

n = 1, acc= 2
Sum(0, 3)
Sum (0,3)
3

n = 0, acc= 3
Codice
• La procedura ricorsiva viene compilata come segue segue:

sum:
add x5, x0, x11 # Sposta x11 in x5
blt x10, x0, L5 # Se x10 < 0, salta a L5

addi sp, sp, -8 # Prologo: crea spazio nello stack


sd x1, 0(sp) # Prologo: salva x1
add x11, x11, x10 # Somma x10 ad x11 (acc=acc+n)
addi x10, x10, -1 # Decrementa x10 (n=n-1)
jal x1, sum # Ricorsione (modifica x1)

ld x1, 0(sp) # Ripristina x1


addi sp, sp, 8 # Rimetti a posto lo stack
L5:
add x10, x0, x5 # sposta x5 in x10 (valore ritorno)
jalr x0, 0(x1)
Ottimizzazione
• Alcuni compilatori sanno riconoscere la
ricorsione di coda:
§ Il chiamante ritorna subito dopo la jal
§ x5 e gli altri registri non cambiano
§ Il chiamato potrebbe direttamente ritornare al ra
del chiamante
§ In altre parole il chiamante salva ra e lo recupera
per nulla
• Torniamo al nostro esempio della somma…
Ottimizzazione
• Quello che potremmo fare guardando al codice è molto
semplicemente osservare che possiamo sostituire la jalr con
un salto incondizionato a sum, con grandi semplificazioni
sum:
add x5, x0, x11 # Sposta x11 in x5
blt x10, x0, L5 # Se x10 < 0, salta a L5

addi sp, sp, -8 # Prologo: crea spazio nello stack


sd x1, 0(sp) # Prologo: salva x1
add x11, x11, x10 # Somma x10 ad x11 (acc=acc+n)
addi x10, x10, -1 # Decrementa x10 (n=n-1)
jal x0, sum # Ricorsione (NON modifica x1)

ld x1, 0(sp) # Ripristina x1


addi sp, sp, 8 # Rimetti a posto lo stack
L5:
add x10, x0, x5 # sposta x5 in x10 (valore ritorno)
jalr x0, 0(x1)
Ottimizzazione
• Il codice risultante è più corto e anche più efficiente
• Il gcc fa questo con –O2

sum:
add x5, x0, x11 # Sposta x11 in x5
blt x10, x0, L5 # Se x10 < 0, salta a L5

add x11, x11, x10 # Somma x10 ad x11 (acc=acc+n)


addi x10, x10, -1 # Decrementa x10 (n=n-1)
beq x0, x0, sum # Ricorsione (NON modifica x1)

L5:
add x10, x0, x5 # sposta x5 in x10 (valore ritorno)
jalr x0, 0(x1)
Esempi di programmi
assembly RISC-V
Giovanni Iacca

(lezione basata su materiale dei Prof.


Luigi Palopoli, Marco Roveri e Luca Abeni)
Scopo della lezione
• In questa lezione vedremo alcuni esempi di programmi (o frammenti
di programmi) in assembly RISC-V
• Nel proseguo del corso passeremo all’assembly INTEL e ARM e ci
renderemo conto delle differenze
Semplici istruzioni aritmetiche logiche
• Partiamo dal semplicissimo frammento che abbiamo visto a lezione

f = (g + h) − (i + j);
Traduzione RISC-V
• Supponendo che g, h, i, j siano in x19, x20, x21, e x22,
e che si voglia mettere il risultato in x23, la traduzione
è semplicemente

f = (g+h)-(i+j);

add x5, x19, x20


add x6, x21, x22
sub x23, x5, x6
Traduzione RISC-V (v2)
• Supponendo che g, h, i, j siano in x19, x20, x21, e x22,
e che si voglia mettere il risultato in x23, la traduzione
è semplicemente

f = (g+h)-(i+j);

In questa versione è usato


add x23, x19, x20 un registro in meno: il
add x6, x21, x22 risultato intermedio è
memorizzato in x23
sub x23, x23, x6
Accesso alla memoria
• Riguardiamo ancora l’esempio visto a lezione

a[12] = h + a[8];
Traduzione RISC-V
• Supponiamo che h sia in x21 e che il registro base del
vettore a sia in x22

a[12]= h + a[8];

ld x9, 64(x22) // x9 = a[8]


add x9, x21, x9 // x9 = h + a[8]
sd x9, 96(x22) // a[12] = x9
Blocchi condizionali
• Consideriamo il seguente blocco
if (i == j)
f = g + h;
else
f = g – h;
Traduzione RISC-V
• Supponendo di avere f, g, h, i, j nei registri da x19 a x23 avremo

if (i == j)
f = g + h;
else
f = g – h;

bne x22, x23, L2 // se x22 neq x23 vai a L2


add x19, x20, x21 // x19 = g + h
beq x0, x0, L3 // se x0 == x0 vai a L3
L2:
sub x19, x20, x21 // x19 = g - h
L3:

Condizione con disuguaglianza
• Supponiamo ora di avere

if (i < j)
f = g + h;
else
f = g – h;
Traduzione RISC-V
• Supponendo di avere f, g, h, i, j nei registri da x19 a x23 avremo

if (i < j)
f = g + h;
else
f = g – h;

slt x5, x22, x23 // x5 = x22 < x23


beq x5, x0, L2 // se x5 eq x0 vai a L2
add x19, x20, x21 // f = g + h
beq x0, x0, L3 // se x0 == x0 vai a L3
L2:
sub x19, x20, x21 // f = g - h
L3:
….
Traduzione RISC-V (v2)
• Supponendo di avere f, g, h, i, j nei registri da x19 a x23 avremo

if (i < j)
f = g + h;
else
f = g – h;

blt x22, x23, L2 // se x22 < x23 vai a L2


sub x19, x20, x21 // f = g - h
beq x0, x0, L3 // se x0 == x0 vai a L3
L2:
add x19, x20, x21 // f = g + h
L3:
Ciclo while
• Consideriamo il seguente ciclo while

i = 0;
while (a[i] == k)
i += 1;
Traduzione RISC-V
• Supponiamo di avere i in x22, k in x24 e che l’indirizzo base di a sia in x25
i = 0;
while (a[i] == k)
i += 1;

add x22, x0, x0 // i = 0


L1:
slli x10, x22, 3 // x10 = i * 8
add x10, x10, x25 // x10 = indirizzo di a[i]
ld x9, 0(x10) // x9 = a[i]
bne x9, x24, L2 // se a[i] != k vai a L2
addi x22, x22, 1 // i = i + 1
beq x0, x0, L1 // se 0 == 0 vai a L1
L2:

Funzione foglia
• Si definisce “foglia” una funzione che non ne chiama altre
• Se non facciamo delle ottimizzazioni andremo a salvare (prologo) e
ripristinare (epilogo) registri in maniera poco furba
Esempio
typedef long long int int64;
int64 esempio foglia(int64 g, int64 h,
int64 i , int64 j) {
int64 f;
f = (g + h) − (i + j);
return f ;
}

• Abbiamo una sola variabile locale (f) per la quale è


possibile usare un registro
Traduzione RISC-V
• Traduzione tenendo conto che g, h, i, e j corrispondono ai registri da x10 a x13,
mentre f corrisponde a x20
int64 esempio foglia(int64 g, int64 h,
int64 i , int64 j) {
int64 f;
f = (g + h) − (i + j);
return f ;
}

esempio_foglia:
addi sp, sp, -24 // aggiornamento stack per fare posto a tre elementi
sd x5, 16(sp) // salvataggio x5 per usarlo dopo
sd x6, 8(sp) // salvataggio x6 per usarlo dopo
sd x20, 0(sp) // salvataggio x20 per usarlo dopo
add x5, x10, x11 // x5 = g + h
add x6, x12, x13 // x6 = i + j
sub x20, x5, x6 // f = (g+h)- (i+j)
addi x10, x20, 0 // restituzione di f (x10 = x20 + 0)
ld x20, 0(sp) // ripristino x20 per il chiamante
ld x6, 8(sp) // ripristino x5 per il chiamante
ld x5, 16(sp) // ripristino x5 per il chiamante
addi sp, sp, 24 // aggiornamento sp con eliminazione tre elementi
jalr x0, 0(x1) // ritorno al programma chiamante
Traduzione RISC-V (ottimizzata)
• Traduzione tenendo conto che g, h, i, j e che i registri
temporanei non debbano essere salvaguardati
long long int esempio foglia(long long int g, long long int h,
long long int i , long long int j) {
long long int f;
f = (g + h) − (i + j);
return f ;
}

esempio_foglia:
add x5, x10, x11 // x5 = g + h
add x6, x12, x13 // x6 = i + j
sub x7, x5, x6 // f = (g+h)- (i+j)
addi x10, x7, 0 // restituzione di f (x10 = x7 + 0)
jalr x0, 0(x1) // ritorno al programma chiamante
RISC-V Nomi dei registri ed uso
Registro Nome Uso Chi salva

x0 zero Costante 0 N.A.

x1 ra Indirizzo di ritorno Chiamante

x2 sp Stack pointer Chiamato

x3 gp Global pointer ---

x4 tp Thread pointer ---

x5-x7 t0-t2 Temporanei Chiamante

x8 s0/fp Salvato/Puntatore a frame Chiamato

x9 s1 Salvato Chiamato

x10-x11 a0-a1 Argomenti di funzione/valori restituiti Chiamante

x12-x17 a2-a7 Argomenti di funzione Chiamante

x18-x27 s2-s11 Registri salvati Chiamato

x28-x31 t3-t6 Temporanei Chiamante


Traduzione RISC-V Traduzione gcc
• La traduzione gcc è un po’ più articolata, e usa nomi mnemonici per i registri
esempio_foglia:
addi sp, sp, -64
sd ra, 56(sp) // salvataggio ra (x1)
sd s0, 48(sp) // salvataggio s0, cioè fp (x8)
addi s0, sp, 64
sd a0, -24(s0) // salvo registro fp
sd a1, -32(s0)
sd a2, -40(s0)
sd a3, -48(s0)
ld a0, -24(s0)
ld a1, -32(s0)
add a0, a0, a1 // g+h
ld a1, -40(s0)
ld a2, -48(s0)
add a1, a1, a2 // i+j
sub a0, a0, a1 // (g+h)-(i+j)
sd a0, -56(s0)
ld a0, -56(s0)
ld s0, 48(sp)
ld ra, 56(sp)
addi sp, sp, 64
ret
Traduzione RISC-V Traduzione gcc (O2)
• La traduzione gcc diventa più ragionevole richiedendo di ottimizzare
esempio_foglia:
add a0, a0, a1 // g+h
sub a0, a0, a2 // (g+h)-i
sub a0, a0, a3 // ((g+h)-i)-j
ret // macro per jalr x0, 0(ra)
Funzioni non foglia
• Consideriamo il seguente caso più complesso

int64 inc(int64 n)
{
return n + 1;
}
int64 f(int64 x)
{
return inc(x) − 4;
}
Traduzione RISC-V (gcc)
• La traduzione di inc() è simile alla precedente traduzione,
supponendo che n sia in x10 e il risultato vada in x10

long long int inc(long long int n) {


return n + 1;
}

inc:
addi sp, sp, -32 // spazio nello stack
sd x1, 24(sp) // salvo indirizzo di ritorno
sd x8, 16(sp) // salvo frame pointer
addi x8, sp, 32 // frame pointer = inizio frame attivazione
sd x10, -24(x8) // salvo x10
ld x10, -24(x8) // ripristino x10
addi x10, x10, 1 // incremento x10 per predisporre il ritorno
ld x8, 16(sp) // ripristino frame pointer
ld x1, 24(sp) // ripristino return address
addi sp, sp, 32 // ripulisco stack
ret
Traduzione RISC-V
• La traduzione di inc() con ottimizzazioni è la seguente

long long int inc(long long int n) {


return n + 1;
}

inc:
addi x10, x10, 1
jalr x0, 0(x1) // ritorno al programma chiamante
Traduzione RISC-V
• Vediamo ora come il gcc traduce la funzione non
foglia (senza ottimizzazioni)
long long int f(long long int n) {
return inc(n) – 4;
}

f:
addi sp,sp,-32 // estendiamo lo stack
sd ra,24(sp) // salviamo ra
sd s0,16(sp) // salviamo contenuto di s0 (alias per x8)
addi s0,sp,32 // nuovo x8 = sp + 32
sd a0,-24(s0) // salvo in s0 – 24 contenuto di a0 (aka x10, n)
ld a0,-24(s0) // carico in a0 (X10) il contenuto di s0 – 24
call inc // equivale a jal x1, inc
mv a5,a0 // copia risultato a0 della call in a5
addi a5,a5,-4 // decrementa risultato di 4
mv a0,a5 // copia risultato nel registro di ritorno a0
ld ra,24(sp) // recuperiamo ra
ld s0,16(sp) // recuperiamo s0
addi sp,sp,32 // rilasciamo lo stack
ret // ritorniamo a ra
Traduzione RISC-V
• Abilitando le ottimizzazioni di gcc (con O1), le cose
cambiano
long long int f(long long int n) {
return inc(n) – 4;
}

f:
addi sp, sp, -16 // spazio nello stack
sd ra, 8(sp) // memorizzo return address
call inc // jal x1, inc
addi a0, a0, -4 // decremento a0 (x10)
ld ra, 8(sp) // ripristino ra (x1)
addi sp, sp, 16 // dealloco stack
ret
Ordinamento di array
• Passiamo a qualcosa di più complesso. Un algoritmo
noto come «insertion sort»
void sposta(int v[], size_t i) { void ordina(int v[], size_t n) {
size_t j; size_t i;
int appoggio; i = 1;
while (i < n) {
appoggio = v[i]; sposta(v, i);
j = i - 1; i = i+1;
while ((j >= 0) && (v[j] > appoggio)) { }
v[j+1] = v[j]; }
j = j-1;
}
v[j+1] = appoggio;
}
Traduzione RISC-V
• Cominciamo da sposta. Questa volta le cose sono più
complesse. Assumiamo che i parametri siano memorizzati
in a0, a1 rispettivamente.

void sposta(int v[], size_t i) {


size_t j;
int appoggio;

appoggio = v[i];
j = i - 1;

sposta:
slli a4,a1,2 //a4 = i*4
add a5,a0,a4 //a5 = &v[i]
lw a3,0(a5) //a3 = v[i]
addiw a1,a1,-1 //a1 = a1-1 (i-1)
Traduzione RISC-V (continua)
• Ciclo while ((j >= 0) && (v[j] > appoggio)) {
v[j+1] = v[j];
j = j-1;
}

bltz a1,.L2 // se j < 0 esci dal ciclo


lw a4,-4(a5) // a4 = v[i-1]=v[j]
bge a3,a4,.L2 // se appoggio è >= v[j] esci
li a2,-1 // carica -1 in a2
.L3:
sw a4,0(a5) // memorizza v[j] (a4) in v[j+1)
addiw a1,a1,-1 // a1 = a1-1
beq a1,a2,.L4 // salta se a1 = -1
addi a5,a5,-4 // j=j-1
lw a4,-4(a5)
bgt a4,a3,.L3 // salta se v[j] > appoggio
j .L2
Traduzione RISC-V (continua)
• Uscita da sposta
v[j+1] = appoggio;
}

.L4:
li a1,-1
.L2:
addi a1,a1,1
slli a1,a1,2
add a1,a0,a1
sw a3,0(a1)
ret
Traduzione RISC-V (continua)
• Passiamo ora alla funzione ordina. Assumiamo che i
parametri siano memorizzati in a0, a1 rispettivamente.
void ordina(int v[], size_t n) {
size_t i;
i = 1;

li a5,1
ble a1,a5,.L11
addi sp,sp,-32
sd ra,24(sp)
sd s0,16(sp)
sd s1,8(sp)
sd s2,0(sp)
mv s1,a1
mv s2,a0
li s0,1
Traduzione RISC-V (continua)
• Passiamo al loop
while (i < n) {
sposta(v, i);
i = i+1;
}

.L8:
mv a1,s0
mv a0,s2
call sposta
addiw s0,s0,1
bne s1,s0,.L8
Traduzione RISC-V (continua)
• Epilogo ordina

ld ra,24(sp)
ld s0,16(sp)
ld s1,8(sp)
ld s2,0(sp)
addi sp,sp,32
jr ra
.L11:
ret
Copia stringhe
• Consideriamo ora
void copia_stringa(char d[], const char s[]) {
size_t i = 0;

while ((d[i] = s[i]) != `0`) {


i += 1;
}
}
Traduzione RISC-V
• Prologo

void copia_stringa(char d[], const char s[]) {


size_t i = 0;

copia_stringa:
addi sp, sp, -8 // aggiorna stack per inserire un elemento
sd x19, 0(sp) // salva x19
add x19, x0, x0 // i = 0
Traduzione RISC-V (continua)
• Loop

while ((d[i] = s[i]) != `0`) {


i += 1;
}

LoopCopiaStringa:
add x5, x19, x11 // indirizzo di s[i]
lbu x6, 0(x5) // x6 = s[i]
add x7, x19, x10 // indirizzo di d[i]
sb x6, 0(x7) // d[i] = s[i]
beq x6, x0, LoopCopiaStringaEnd // se 0 vai a LoopCopiaStringaEnd
addi x19, x19, 1 // i += 1
jal x0, LoopCopiaStringa // salta a LoopCopiaStringa
LoopCopiaStringaEnd
Traduzione RISC-V (continua)
• Epilogo

LoopCopiaStringaEnd
ld x19, 0(sp) // ripristina contenuto di x19
addi sp, sp, 8 // aggiorna lo stack eliminando un elemento
jalr, x0, 0(x1) // ritorna al chiamante
CALCOLATORI
Cenni ad Assembly Intel

Giovanni Iacca
giovanni.iacca@unitn.it

Lezione basata su materiale preparato


da Luca Abeni, Luigi Palopoli, Fabiano Zenatti e Marco Roveri
Architettura Intel

• Utilizzata sulla maggior parte dei laptop, desktop e server moderni


• Lunga storia
• Dagli anni ’70 (Intel 8080 - 8 bit!)...
• ...Fino ad oggi (Intel Core i7, i9 e simili, 64 bit!)
• E la storia ha dei retaggi
• Evoluzione lenta ma costante, “guardando al passato”...
• Moderne CPU a 64 bit sono ancora in grado di eseguire vecchio codice
ad 8 bit (MSDOS, anyone?)!
• Immaginate complicazioni e complessità che questo comporta...
• Architettura CISC (Complex Instruction Set Computer), contrapposta a
RISC (Reduced Instruction Set Computer) di RISC-V (e MIPS)

2 / 41
Assembly Intel

• CISC → gran numero di istruzioni e modalità di indirizzamento


• Non vedremo tutto in modo esaustivo (ottimi tutorial online)
• Ci focalizzeremo su differenze rispetto a RISC-V
• Compatibilità → varie modalità di funzionamento
• Considereremo solo modalità a 64 bit e relative ISA e ABI
• Esistono vari Assembler, ognuno con sintassi differenti
• Considereremo GNU Assembler (gas)

3 / 41
Registri General Purpose

• Sono tutti preceduti dal prefisso %


• 16 registri a 64 bit (più o meno) general purpose
• Hanno nomi che riflettono gli strascichi di compatibilità con le versioni
precedenti:
• %rax, %rbx, %rcx, %rdx, %rsi, %rdi, %rbp, %rsp
• %r8, ..., %r15
• Note:
• %rsp → stack pointer;
• %rbp → base pointer (puntatore a stack frame)
• %rsi e %rdi derivano da due registri %si e %di della CPU 8086 per la copia
di array (%si → source index, %di → destination index)

4 / 41
Registri General Purpose - 2

I registri %rax ... %rsp estendono registri a 32 bit (%eax ... %esp)

• ...Che a loro volta estendono rispettivi


registri a 16 bit (%ax ... %sp)
• ...Ognuno dei primi quattro registri
(%ax, %bx, %cx, e %dx) sono composti
da 2 registri a 8 bit indicati sostituendo
%x con %h o %l):
• %ax == %ah + %al
• ... Ognuno dei registri r8, ... , r15
estende il rispettivo registro a 32 bit
(r8d, ... , r15d), che a loro volta
estende rispettivo registro a 16 bit
(r8w, ... , r15w) che a loro volta
contiene un registro a 8 bit (r8b, ... ,
r15b).
• In questo caso c’è solo un registro a 8 bit
(byte meno significativo)

5 / 41
Registri “Speciali”

• Instruction Pointer “visibile”: %rip


• Flags register: %rflags (estende %eflags, che estende %flags)
• Set di flag settati da istruzioni logico aritmetiche
CF : Carry Flag → Messo a 1 se risultato è andato in unsigned
overflow o se c’è carry-out
ZF : Zero Flag → Messo a 1 se risultato è zero
SF : Sign Flag → Messo a 1 se risultato è negativo
OF : 2’s Overflow Flag → Messo a 1 se risultato è andato in overflow
• Usati da istruzioni di salto condizionale
• Altri flag controllano il funzionamento della CPU (IF) o contengono altre
informazioni sulla CPU
• Equivalente a Program Status Word (PSW) di altre CPU
• Altri registri “speciali” non sono interessanti per noi ora

6 / 41
Convenzioni di Chiamata

• Non fanno propriamente parte dell’architettura


• Data una CPU / architettura, si possono usare molte diverse
convenzioni di chiamata
• Servono per “mettere d’accordo” diversi compilatori / librerie ed altre
parti del Sistema Operativo
• Tecnicamente, sono specificate dall’ABI, non dall’ISA!!!
• Come / dove passare i parametri
• Stack o registri?
• Quali registri preservare?
• Quando un programma invoca una subroutine, quali registri può
aspettarsi che contengano sempre lo stesso valore al ritorno?

7 / 41
Convenzioni di Chiamata

• Primi 6 argomenti:
• %rdi, %rsi, %rdx, %rcx, %r8 ed %r9
• Altri argomenti (7 → n): sullo stack
• Valori di ritorno:
• %rax e %rdx
• Registri preservati:
• %rbp, %rbx, %r12, %r13, %r14 ed %r15
• Registri non preservati:
• %rax, %r10, %r11, oltre ai registri per passaggio parametri: %rdi,
%rsi, %rdx, %rcx, %r8 ed %r9

8 / 41
Modalità di Indirizzamento - 1

• Istruzioni prevalentemente a 2 operandi


• Secondo operando: destinazione implicita!
• Limitazione rispetto a RISC-V: impossibile specificare due operandi ed una
destinazione diversa
• Sorgente (primo operando)
• Operando Immediato (una costante con $ come prefisso: e.g. $20)
• Operando in Registro (valore di un registro e.g. %rax)
• Operando in Memoria (valore in locazione di memoria e.g. valore ad indirizzo
0x0100A8)
• Destinazione (secondo operando)
• Operando in Registro (un registro come destinazione e.g. %rdx)
• Operando in Memoria (una locazione di memoria specificata dall’indirizzo,
locazione ad indirizzo 0x0AA0E2)
• Possibili operazioni che consentono di scrivere sia su registri che memoria!
• RISC-V: accesso a memoria solo per load e store
• Vincolo: no entrambi gli operandi in memoria

9 / 41
Modalità di Indirizzamento - 2

• Operandi in memoria: varie modalità di indirizzamento


• Semplificando un po’: accesso indiretto
• Accesso a locazione di memoria
• Sintassi:<displ> (<base reg>, <index reg>, <scale>)
• <displacement>: costante (valore immediato) a 8, 16 o 32 bit
• simile a RISC-V - ma RISC-V ha displacement/offset solo a 12 bit

• <base>: valore in registro (come per RISC-V)


• <indice>: valore in registro (semplifica iterazione su array)
• <scala>: valore costante: 1, 2, 4, o 8 (semplifica accesso ad array con
elementi di dimensione 1, 2, 4 o 8 byte)
• Indirizzo <displacement> + <base> + <indice> * <scala>

10 / 41
Indirizzamento - Casi Speciali

• Scala = 1 (no scale): <displ> (<base reg>, <index reg>)


• Niente scala e indice: <displ>( <base reg>)
• Ricorda accesso per RISC-V. Unica differenza: la dimensione in bit
dell’offset/displacement
• Niente scala, indice e displacement (displacement = 0): (<base reg>)
• Niente displacement (displacement = 0): (<base reg>,<index reg>,
<scale>)
• ...

11 / 41
Indirizzamento - limitazioni

• Entrambi gli operandi non possono essere in memoria


• movl 345, (%eax) non è consentito perchè entrambe le destinazioni sono in
memoria: Mem[eax] = Mem[345]
• La scrittura da memoria su memoria viene simulata con due istruzioni che
usano un registro di appoggio:
movl 345, %eax
movl %eax, (%ebx)
• Combinazioni valide sono:
• Imm → Reg
• Imm → Mem
• Reg → Reg
• Mem → Reg
• Reg → Mem

12 / 41
Indirizzamento - riassunto

Nome Forma Esempio Descrizione


Immediata $Num movl $−500, %rax rax = $Num

Accesso Num movl 500, %rax rax = Mem[Num]


diretto
Registro ri movl %rdx, %rax rax = rdx

Accesso (ri ) movl (%rdx), %rax rax = Mem[rdx]


indiretto
Base e Num(ri ) movl 31(%rdx), %rax rax = Mem[rdx+31]
spiazzamento
Indice (rb , ri , s) movl (%rdx, %rcx, 4), %rax rax = Mem[rdx+rcx*4]
scalato
Indice Num(rb , ri , s) movl 35(%rdx, %rcx, 4), %rax rax = Mem[rdx+rcx*4+35]
scalato +
spiazzamento

s è il fattore di scala e può assumere i valori: 1, 2, 4, o 8

13 / 41
Istruzioni Intel

• Troppe per vederle tutte


• Esistono diversi e buoni tutorial online
• Per una lista più o meno dettagliata delle istruzioni
http://en.wikipedia.org/wiki/X86_instruction_listings
• Molte istruzioni “non proprio utili” sono state mantenute per compatibilità!
• In genere, sintassi <opcode> <source>, <destination>
• Carattere finale di opcode (b : 8bit, w : 16bit, l : 32bit, q : 64bit) per indicare
“larghezza” (in bit) degli operandi
• Nomi registri iniziano con “%”
• Valori immediati (costanti) iniziano con “$” (e.g $231)
• Indirizzi diretti (costanti) sono semplici numeri (e.g. “439”)

14 / 41
Istruzioni Più Comuni - 1

• mov: copia dati da sorgente a destinazione


• movsx: copia dati con estensione del segno
• movzx: copia dati con estesione con 0
• add / adc (add with carry)
• sub / sbc (sub with carry)
• mul / imul: signed / unsigned multiplication
• div / idiv: signed / unsigned division
• inc / dec: somma / sottrae 1
• rcl, rcr, rol, ror: varie forme di “rotate”
• sal, sar, shl, shr: shift (aritmetico e logico)
• and / or / xor / not: operazioni booleane bit a bit
• Istruzioni aritmetico / logiche modificano flag (carry, zero, sign, ...)
• Altre istruzioni per modificare flag: clc / stc, cld, cmc...
• neg: complemento a 2 (negazione)
• nop

15 / 41
Istruzioni Più Comuni - 3

• push: inserisce dati sullo stack


• Sintassi: pushq %reg
• Decrementa %rsp di 8
• Scrive %reg nella memoria specificata in %rsp
• Esempio: pushq %rax
• Equivale a: subq $8, %rsp
movq %rax, (%rsp)
• pop: rimuove dati da stack
• Sintassi: popq %reg
• Legge memoria all’indirizzo specificata in %rsp e memorizza valore in %reg
• Incrementa %rsp di 8
• Esempio: popq %rdx
• Equivale a: movq (%rsp), %rdx
addq $8, %rsp

16 / 41
Istruzioni Più Comuni - 2

• cmp e test
• cmp: cmp arg1, arg2
• Compara arg2 con arg1 (e.g. arg2 < arg1, arg2 = arg1, ...)
• Effettua arg2 − arg1 e setta flags del flag register
• arg1 e arg2 non sono modificati (risultato di sottrazione non memorizzato)
• test: test arg1, arg2
• Compara arg2 con arg1 (e.g. arg2 = arg1, ...)
• Effettua arg2 & arg1 e setta flags del flag register
• arg1 e arg2 non sono modificati (risultato di and bit a bit non memorizzato)
• Spesso usato con arg1 = arg2 (e.g. test %eax, %eax per verificare se il valore è 0 o negativo
(ZF o SF).

• Da usarsi prima di salti condizionali

17 / 41
Istruzioni Più Comuni - 3

• jmp : salto incondizionato


• je / jnz / jc / jnc: salti condizionati
• La condizione di salto è stabilita in base ai valori nel flag register (jump if equal,
jump if not zero, jump if carry, ...)
• In generale, “j<condition>”
Istruzione Sinonimo Cond. Flags Descrizione
je label jz label ZF Uguali o zero
jne label jnz label ˜ZF Differenti o Non zero
js label SF Negativo
jns label ˜SF Non Negativo
jg label jnle label ˜(SFˆOF)&˜ZF Signed >
jge label jnl label ˜(SFˆOF) Signed >=
jl label jnge label (SFˆOF) Signed <
jle label jng label (SFˆOF)|ZF Signed <=
ja label jnbe label ˜CF&˜ZF Unsigned >
jae label jnb label ˜CF Unsigned >=
jb label jnae label CF Unsigned <
jbe label jna label CF|ZF Unsigned <=

• Varie istruzioni “condizionali” (conditional move, set on condition, ...)

18 / 41
Istruzioni Più Comuni - 4

• call chiamata a procedura


• Sintassi: call label
• Fa push sullo stack dell’indirizzo della prossima istruzione
• Modifica il program counter per andare all’inizio della procedura desiderata (specificato con una
label)
• Implicitamente esegue: subq $8, %rsp
movq %rip, (%rsp)

• ret: ritorno da procedura


• Sintassi: ret
• Fa pop dallo stack dell’indirizzo di ritorno e lo memorizza in %rip
• Modifica il program counter per andare alla prossima istruzione del chiamante
• Implicitamente esegue: movq (%rsp), %rip
addq $8, %rsp

19 / 41
Istruzioni Più Comuni - Esempi mov

• Sintassi: mov[b,w,l,q] src, dst


• Condizioni iniziali:
Mem[0x00204] = 7654 3210
Mem[0x00200] = fedc ba98
rax = ffff ffff 1234 5678

• movl 0x204, %eax rax = 0000 0000 7654 3210


• movw 0x202, %ax rax = 0000 0000 7654 fedc
• movb 0x207, %al rax = 0000 0000 7654 fe76
• movq 0x200, %rax rax = 7654 3210 fedc ba98
• movb %al, 0x4e5 Mem[0x004e4] = 0000 9800
Mem[0x004e0] = 0000 0000
• movl %eax, 0x4e0 Mem[0x004e4] = 0000 9800
Mem[0x004e0] = fedc ba98

20 / 41
Istruzioni Più Comuni - Esempi mov - 2

• Sintassi: mov[b,w,l,q] src, dst


• Condizioni iniziali:
Mem[0x00204] = 7654 3210
Mem[0x00200] = fedc ba98
rax = ffff ffff 1234 5678

• movl $0xfe1234, %eax rax = 0000 0000 00fe 1234


• movw $0xaa55, %ax rax = 0000 0000 00fe aa55
• movb $20, %al rax = 0000 0000 00fe aa14
• movq $−1, %rax rax = ffff ffff ffff ffff
• movabsq $0x123456789ab, %rax
rax = 0000 0123 4567 89ab
• movq $−1, 0x4e0 Mem[0x004e4] = ffff ffff
Mem[0x004e0] = ffff ffff

21 / 41
Istruzioni Più Comuni - Esempi mov - 3 - Zero/Signed

• Sintassi: mov[b,w,l,q] src, dst


• Condizioni iniziali:
Mem[0x00204] = 7654 3210
Mem[0x00200] = fedc ba98
rdx = 0123 4567 89ab cdef

• movslq 0x200, %rax rax = ffff ffff fedc ba98


• movzwl 0x202, %eax rax = ffff ffff 0000 fedc
• movsbw 0x201, %ax rax = ffff ffff 0000 ffba
• movsbl 0x206, %eax rax = ffff ffff 0000 0054
• movzbq %dl, %rax rax = 0000 0000 0000 00ef

22 / 41
Istruzioni Più Comuni - Esempi add, and, or, sub

• Sintassi: mov[b,w,l,q] src, dst


• Condizioni iniziali:
Mem[0x00204] = 7654 3210
Mem[0x00200] = 0f0f ff00
rdx = ffff ffff 1234 5678
rax = 0000 0000 cc33 aa55

• addl $0x12300, %eax rax = 0000 0000 cc34 cd55


• addq %rdx, %rax rax = ffff ffff de69 23cd
• andw 0x200, %ax rax = ffff ffff de69 2300
• orb 0x203, %al rax = ffff ffff de69 230f
• subw $14, %ax rax = ffff ffff de69 2301
• addl $0x12345, 0x204 Mem[0x00204] = 7655 5555
Mem[0x004e0] = 0f0f ff00

23 / 41
Load Effective Address

• lea: load effective address


• Sintassi: lea src, dest
• Istruzione “strana”, ma talvolta molto utile!
• Nata per calcolare indirizzi (con indirizzamento indiretto) senza fare
accessi
• Usa l’hardware del calcolo di indirizzamento per “normali” operazioni
aritmetiche
• Copia indirizzo di sorgente (calcolato tramite displacement, base, indice e
scala) in registro destinazione
• Calcola l’indirizzo e lo memorizza nel registro di destinazione senza “caricare”
nulla dalla memoria.
• Esempio:
• lea 80(%rdx, %rcx, 2), %rax → %rax = %rdx + 2*%rcx +80
• Viene spesso utilizzata come istruzione aritmetica che effettua
contemporaneamente due somme (un valore immediato e due registri)
shiftando uno degli addendi.

24 / 41
Load Effective Address - 2

Esempio: sommare due registri salvando risultato su un terzo registro


• Su RISC-V se vogliamo sommare x1 e x2 salvando il risultato in x3
• add x3, x1, x2
• Su intel come possiamo sommare %rbx e %rcx salvando il risultato in
%rax?
• Non è possibile specificare un registro destinazione per add diverso dal
secondo operando
• Ma se condiseriamo un accesso a memoria con base %rbx ed indice %rcx...
• ...l’indirizzo acceduto sarebbe %rbx + %rcx
• lea (%rbx, %rcx), %rax
• L’istruzione lea è spesso utilizzata per fare più somme (con eventuali shift)
salvando il risultato in un registro diverso

25 / 41
Load Effective Address - Esempio

• Sintassi: lea src, dst


• Condizioni iniziali:
rcx = 0000 0000 0000 0020
rdx = 0000 0089 1234 4000
rbx = ffff ffff ff00 0300

• leal (%edx,%ecx),%eax rax = 0000 0000 1234 4020


• leaq −8(%rbx),%rax rax = ffff ffff ff00 02f8
• leaq 12(%rdx,%rcx,2),%rax rax = 0000 0089 1234 404c

26 / 41
Load Effective Address - Esempio 2

void f l ( i n t x ) { / / x = %e d i
return 9 * x + 1;
}

Codice non ottimizzato: Codice ottimizzato:

fl : fl :
movl %edi, %eax # tmp = x l e a l 1 ( %edi,%edi, 8 ) , %eax
sall 3 , %eax # tmp *= 8 # eax = 1 + %edi + %edi * 8
addl %edi, %eax # tmp += x ret
addl $ 1 , %eax # tmp += 1
ret

27 / 41
Istruzioni Apparentemente Inutili

• inc <register> / dec <register>: somma / sottrae 1 a registro


• A che servono? Perché non add $1, <register>?
• In RISC-V e ARM ogni istruzione è codificata su 32 bit con numero
costante di bit
• Intel usa codifica binaria con numero variabile di bit
• add $1, <register> richiederebbe di codificare:
• Il codice operativo dell’istruzione add
• Il valore immediato 1 (16, 32 o 64 bit)
• Il registro <register>
• inc non richiede di codificare il valore immediato
• salvo almeno 16 bit!!!
• Questo può spiegare il proliferare di istruzioni “apparentemente” inutili...

28 / 41
Esempio

• Stringa C: array di caratteri terminato da 0


• ASCII: caratteri codificati su byte
• Copia di una stringa:
void c o p i a s t r i n g a ( char * d , const char * s )
{
int i = 0;

while ( ( d [ i ] = s [ i ] ) ! = 0 ) {
i += 1 ;
}
}

• Esempio già visto per Assembly RISC-V...


• Come fare con Assembly Intel (64 bit)?

29 / 41
Accesso a Singoli Byte

• Ricordate? Necessità di copiare byte, non parole...


• Intel fornisce soluzione semplice (ed elegante? Dipende dai punti di vista!)
• I registri rax ... rdx sono composti da “sottoregistri”
• In particolare, al ... dl: registri a 8 bit
• Operazioni mov da e a memoria possono lavorare su dati di diversa
ampiezza (8, 16, 32 e 64 bit)
• Istruzione movb: da memoria a al .. dl o viceversa
• Non c’è bisogno di una istruzione diversa come per RISC-V, è sempre la
stessa mov che lavora su “parti” diverse del registro

30 / 41
Implementazione Assembly: Prologo

• I due parametri d ed s sono contenuti in %rdi e %rsi


• Supponiamo di usare %rax per il contatore i
• Non è un registro preservato...
• Non è necessario salvarlo sullo stack
• Non è necessario alcun prologo; possiamo cominciare col codice C
• Iniziamo: i = 0; → %rax = 0
movq $ 0 , %rax

31 / 41
Implementazione Assembly: Loop

• Ciclo while: copia s[i] in d[i]


• Prima di tutto, carichiamolo in %bl
• Per fare questo, possiamo sfruttare la modalità di indirizzamento
indiretta (base + indice * 2scala , con scala = 0)
• Nessuna necessità di caricare l’indirizzo dell’i-esimo elemento in un
registro, come si faceva per RISC-V

L1 : movb ( % r s i , %rax ) , %bl # I n i z i o Loop

• Ora memorizziamo %bl in d[i]


movb %bl, ( % r d i , %rax )

32 / 41
Implementazione Assembly: Fine del Loop

• Bisogna ora controllare se s[i] == 0


• Se si, fine del loop!

cmpb $ 0 , %bl # c o n f r o n t a %bl con 0 . . .


j e L2 # se sono u g u a l i , s a l t a a L2
# ( esci dal loop ! )

• Se no, incrementa i (che sta in %rax) e cicla...

add $ 1 , %rax
jmp L1 # L1 : l a b e l d i i n i z i o l o o p

• La label L2 implementerà il ritorno al chiamante

33 / 41
Implementazione Assembly: Fine

• Non abbiamo salvato nulla sullo stack: non c’è necessità di epilogo!
• Si può direttamente tornare al chiamante
L2 : r e t

• Mettendo tutto assieme:


. text
. globl copia stringa
copia stringa :
movq $ 0 , %rax
L1 :
movb ( % r s i , %rax ) , %bl
movb %bl, ( % r d i , %rax )
cmpb $ 0 , %bl
j e L2
add $ 1 , %rax
jmp L1
L2 :
ret

34 / 41
35 / 41
Esempi - codice

Codice Assembler Assunzioni/Note

int x, y, z; movl $0x10000004, %ecx &x = 0x10000004


... movl (%ecx), %eax &y = 0x10000008
z = x + y; addl 4(%ecx), %eax &z = 0x1000000c
movl %eax, 8(%ecx)

char a[100]; movl $0x1000000c, %ecx &a = 0x1000000c


... decb 1(%ecx)
a[1]--;

int d[4], x; movl $0x10000010, %ecx &d = 0x10000010


... movl (%ecx), %eax &x = 0x10000020
x = d[0]; movl %eax, 16(%ecx)
x += d[1]; movl 16(%ecx), %eax
addl 4(%ecx), %eax
movl %eax, 16(%ecx)

unsigned int y; movl $0x10000010, %ecx &y = 0x10000010


short z; movl (%ecx), %eax &z = 0x10000014
y = y/4; shrl 2, %eax
z = z << 3; movl %eax, (%ecx)
movw 4(%ecx), %ax
salw 3, %ax
movw %ax, 4(%ecx)

36 / 41
Esempi - codice 2

/ / data = %edi func 1 :


/ / v a l = %esi movl ( %esi ) , %eax
// i = %edx addl ( %edi, %edx, 4 ) , %eax
i n t f u n c 1 ( i n t data [ ] , i n t * v a l , i n t i ) { ret
i n t sum = * v a l ;
sum += data [ i ] ;
r e t u r n sum ;
}

s t r u c t Data { func 2 :
char c ; i n t d ; addb $ 1 , ( %edi )
} subl %esi, 4 ( %edi )
/ / p t r = %edi ret
// x = %esi
void f u n c 2 ( s t r u c t Data * p t r , i n t x ) {
p t r −>c ++;
p t r −>d −= x ;
}
/ / p t r = %edi
// x = %esi

La convenzione per X86 64 richiede che il valore di ritorno di una funzione sia memorizzato in %eax/%rax

37 / 41
Esempi - codice 3

void a b s v a l u e ( i n t x , i n t * r e s ) { a b s v a l u e : t e s t %edi, %edi


i f ( x < 0) jns Lab1
* r e s = −x ; negl %edi
else movl %edi, ( %rsi )
* res = x ; ret
} Lab1 : movl %edi, ( %rsi )
ret

/ / x = %edi, y = %esi, r e s = %rdx func 3 :


void f u n c 3 ( i n t x , i n t y , i n t * r e s ) { cmpl %esi, %edi
if (x < y) jge Lab2
* res = x ; movl %edi, ( %rdx )
else ret
* res = y ; Lab2 :
} movl %esi, ( %rdx )
ret

38 / 41
Esempi - codice 4

/ / x = %edi, y = %esi, r e s = %rdx func 4 :


void f u n c 4 ( i n t x , i n t y , i n t * r e s ) { cmpl $−1 , %edi
i f ( ( x == −1) | | ( y == −1)) j e . L6
* res = y − 1; cmpl $−1 , %esi
else i f ( ( x > 0 ) && ( y < x ) ) j e . L6
* res = x + 1; t e s t l %edi, %edi
else j l e . L5
* res = 0; cmpl %esi, %edi
} j l e . L5
addl $ 1 , %edi
movl %edi, ( %rdx )
ret
. L5 :
movl $ 0 , ( %rdx )
ret
. L6 :
subl $ 1 , %esi
movl %esi, ( %rdx )
ret

/ / a = %edi, b = %esi
i n t avg ( i n t a , i n t b ) { avg : movl %edi, %eax
r e t u r n ( a+b ) / 2 ; addl %esi, %eax
} s a r l 1 , %eax
ret

39 / 41
Esempi - codice 5

/ / s t r = %rdi func 5 :
i n t f u n c 5 ( char s t r [ ] ) { movl $ 0 , %eax
int i = 0; jmp . L2
while ( s t r [ i ] ! = 0){ . L3 :
i ++; addl $ 1 , %eax
} . L2 :
return i ; movslq %eax, %rdx
} cmpb $ 0 , ( %rdi,%rdx )
jne . L3
ret

/ / d a t = % r d i , l e n = %esi func 6 :
i n t func 6 ( i n t dat [ ] , i n t len ) { movl ( %rdi ) , %eax
i n t min = d a t [ 0 ] ; movl $ 1 , %edx
f o r ( i n t i =1; i < l e n ; i ++) { jmp . L2
i f ( d a t [ i ] < min ) { . L4 :
min = d a t [ i ] ; movslq %edx, %rcx
} movl ( % r d i , % r c x , 4 ) , %ecx
} cmpl %ecx, %eax
r e t u r n min ; j l e . L3
} movl %ecx, %eax
. L3 :
addl $ 1 , %edx
. L2 :
cmpl %esi, %edx
j l . L4
ret

40 / 41
Esempi - codice 6 - elevato numero parametri

int caller () { caller :


i n t sum = f 1 ( 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ) ; pushq $8
r e t u r n sum ; pushq $7
} movl $ 6 , %r9d
movl $ 5 , %r8d
i n t f1 ( i n t a1, i n t a2, i n t a3, i n t a4, movl $ 4 , %ecx
i n t a 5 , i n t a 6 , i n t a 7 , i n t a8 ) { movl $ 3 , %edx
r e t u r n a1+a2+a3+a4+a5+a6+a7+a8 ; movl $ 2 , %esi
} movl $ 1 , %edi
call f1
addq $16, %rsp
ret
f1 :
addl %edi, %esi
addl %esi, %edx
addl %edx, %ecx
addl %ecx, %r8d
addl %r8d, %r9d
movl %r9d, %eax
addl 8 ( %rsp ) , %eax
addl 16( %rsp ) , %eax
ret

41 / 41
CALCOLATORI
Cenni ad Assembly ARM

Giovanni Iacca
giovanni.iacca@unitn.it

Lezione basata su materiale preparato


da Luca Abeni, Luigi Palopoli, Fabiano Zenatti e Marco Roveri
Architettura ARM

• Utilizzata sulla maggior parte di tablet, smartphone ed in molti sistemi


embedded
• Architettura nata negli anni ’80
• 1987: Acorn Archimedes
• ARM = Acorn Risc Machine
• 1999: Advanced Risc Machine (nuova azienda!)
• Evoluzione meno complicata rispetto ad Intel
• ARM “nasce” a 32 bit
• Solo recentemente nuova architettura a 64 bit...
• ...Ma non è troppo compatibile col passato!
• Architettura RISC (Reduced Instruction Set Computer) abbastanza
“pragmatica”, che cerca di prendere il meglio dal mondo RISC e dal mondo
Intel
• Forse questa è la chiave del suo successo?

2 / 47
Assembly ARM

• “Strano” RISC con molte modalità di indirizzamento anche potenti


• ISA sviluppato per ovviare ai classici problemi delle architetture RISC
• Altro obiettivo: risparmio energetico
• Densità del codice...
• “Solo” 16 registri, general purpose
• In realtà ARM è una famiglia di CPU, con ISA leggermente differenti
• Non entreremo nei dettagli, ma considereremo solo gli aspetti in
comune fra tutti gli ISA
• Al solito, assembler gnu

3 / 47
Registri ARM

• 16 registri a 32 bit, quasi tutti general purpose


• Nomi: da r0 a r15
• Tecnicamente, r15 non è un registro general purpose
• Alcuni registri accessibili tramite un nome simbolico che ne identifica
l’utilizzo
• r13 == sp (stack pointer)
• r14 == lr (link register)
• Application Program Status Register (apsr) / Current Program Status
Register (cpsr)
• Flags register

4 / 47
Utilizzo dei Registri

• Registri general purpose: r0 ... r14


• Utilizzo non forzato da ISA, ma dipendente da ABI
• r13: spesso usato come stack pointer (sp), ma l’utilizzo dipende da ABI
• Differenza da Intel: in ISA Intel, lo stack pointer deve essere rsp
• r14: talvolta usato come link register (lr: indirizzo di ritorno da subroutine)
• Analogo a ra / x1 di RISC-V
• Registro r15: contiene program counter (pc) e flags (bit 31...28)
• Distinzione chiara fra flag usati per esecuzione condizionale (apsr) ed
altri flag

5 / 47
Flag Register

• eq (equal): esegui se la prec. op. era fra numeri uguali (flag Z vale 1);
• ne (not equal): esegui se la prec. op. era fra numeri diversi (flag Z vale 0);
• hs (higher or same) o cs -carry set- (flag C vale 1);
• lo (lower) o cc -carry clear- (flag C vale 0);
• mi (minus): esegui se risultato ultima op. è negativo (flag N vale 1);
• pl (plus): esegui se risultato ultima op. è positivo (flag N vale 0);
• vs (overflow): esegui se ultima op. è risultata in un overflow (flag V vale 0);
• vc (overflow clear): opposto di vs (esegui se il flag V vale 0);
• hi (higher): esegui se in un’op. di confronto primo operando è maggiore del
secondo, assumendo unsigned (flag C vale 1 e Z vale 0);
• ls (lower or same): esegui se in un op. confronto primo operando è minore o
uguale al secondo, assumendo unsigned (flag C vale 0 o Z vale 1);
• ge (greater or equal): esegui se N vale 1 e V vale 1, o se N vale 0 e V vale 0;
• lt (less than): esegui se N vale 1 e V vale 0, o se N vale 0 e V vale 1;
• gt (greater than): come ge, ma con Z che vale 0;
• le (less or equal): come lt, ma esegue anche se Z vale 1.

6 / 47
Convenzioni di Chiamata

• Non fanno propriamente parte dell’architettura


• Data una CPU / architettura, si possono usare molte diverse
convenzioni di chiamata
• Servono per “mettere d’accordo” diversi compilatori / librerie ed altre
parti del Sistema Operativo
• Tecnicamente, sono specificate dall’ABI, non dall’ISA!!!
• Come / dove passare i parametri
• Stack o registri?
• Quali registri preservare?
• Quando un programma invoca una subroutine, quali registri può
aspettarsi che contengano sempre lo stesso valore al ritorno?
• Nota: per ARM, esistono molte ABI diverse (anche molto diverse!)

7 / 47
Convenzioni di Chiamata

• Primi 4 argomenti:
• r0 ... r3
• Registri non preservati! Utilizzabili anche come registri “temporanei” da
non salvare!
• Altri argomenti (4 → n): sullo stack
• Registri preservati: r4 ... r11
• Eccezione: in alcune ABI r9 non è preservato
• Valori di ritorno: r0 e r1
• I registri che una subroutine può utilizzare senza dover salvare sono quindi
r0, r1, r2, r3 ed r12
• Più eventualmente r9 (dipende da piattaforma / ABI)

8 / 47
Modalità di Indirizzamento - 1

• Istruzioni prevalentemente a 3 argomenti


• Simile a RISC-V, ma meno regolare: ci sono istruzioni (mov, etc...) a 2
argomenti
• Operandi: sinistro e destro
• Operando sinistro: registro
• Operando destro: immediato o registro, eventualmente shiftato
• No operandi in memoria!
• Uniche operazioni che accedono alla memoria: load e store
• Load register / Store register (ldr / str)
• Load / Store multiple (ldm / stm)

9 / 47
Modalità di Indirizzamento - 2

• Operandi in memoria: varie modalità di indirizzamento


• Semplificando un po’:
Base + Offset (immediato)
Base + Indice (eventualmente scalato)
• In più, possibilità di aggiornare il registro base ( [!] )
• Aggiornamento prima dell’accesso (pre indexed) o dopo l’accesso (post indexed)

• i = <base> + {<offset> | <indice (shiftato)> }


• <base>: valore in registro (come per RISC-V)
• <offset>: costante (valore immediato) codificato su 12 bit
• <indice>: valore in registro (semplifica iterazione su array)
• Opzionalmente shiftato o ruotato

• Pre Indexed:
• [rb, #i][!]
• [rb,{+|−}ro[,<shift>]][!]
• Post Indexed:
• [rb],#i
• [rb],{+|-}ro[,<shift>]

10 / 47
Indirizzamento - Casi Speciali

• Pre Indexed con registro indice: [rb,{+|−}ro,<shift>][!]


• <shift> può indicare shift (aritmetici o logici) o rotazioni sul registro ro
• Se non ci sono shift/rotazioni, <shift> può essere omesso
• [rb,{+|−}ro][!]
• Pre Indexed con offset: [rb, #i][!]
• Offset = 0 ⇒ può essere omesso
• [rb][!]
• Post indexed con indice: [rb],{+|-}ro,<shift>
• No shift ⇒ si omette: [rb],{+|-}ro
• Post indexed con offset: [rb],#i
• Offset = 0 ⇒ si omette: [rb]
• Come pre indexed con offset 0 e senza update!!!

11 / 47
Indirizzamento: Esempi esplicativi

• Accesso a memoria tramite registro e offset


• ldr r0, [r1] @ r0 = mem[r1]

• Tre modi di specificare offset:


• Costante:
ldr r0, [ r1, #4] @ r0 = mem[r1 + 4]

• Registro:
ldr r0, [ r1, r2] @ r0 = mem[r1 + r2]

• Scalato:
ldr r0, [ r1, + r2, lsl #8] @ r0 = mem[r1 + r2 << 8]
ldr r0, [ r1, − r2, lsl #8] @ r0 = mem[r1 - r2 << 8]

12 / 47
Indirizzamento: Esempi esplicativi - 2

• Indirizzamento pre-indexed senza writeback: ldr r0, [ r1, #4]

• Indirizzamento pre-indexed con calcolo prima di accedere e writeback:


ldr r0, [ r1, #4]!

• Indirizzamento post-indexed con calcolo dopo accesso e writeback:


ldr r0, [r1] , #4

13 / 47
Indirizzamento: Esempi esplicativi - pre-indexed

• ldr r0, [ r1, #4] @ r0 = mem[r1 + 4]


@ r1 non modificato

14 / 47
Indirizzamento: Esempi esplicativi - pre-indexed writeback

• ldr r0, [ r1, #4]! @ r0 = mem[r1 + 4]


@ r1 = r1 + 4

15 / 47
Indirizzamento: Esempi esplicativi - pre-indexed shifted

• ldr r0, [ r1, r2, lsl #4] @ r0 = mem[r1 + r2 << 4]


@ r1 non modificato

16 / 47
Indirizzamento: Esempi esplicativi - pre-indexed shift writeback

• ldr r0, [ r1, r2, lsl #4]! @ r0 = mem[ r1 + r2 << 4]


@ r1 = r1 + r2 << 4

17 / 47
Indirizzamento: Esempi esplicativi - post-indexed

• ldr r0, [r1] , #4 @ r0 = mem[r1]


@ r1 = r1 + 4

18 / 47
Indirizzamento: Esempi esplicativi - post-indexed shift

• ldr r0, [r1] , r2, lsl #4 @ r0 = mem[r1]


@ r1 = r1 + r2 << 4

19 / 47
Indirizzamento: esempio applicazione

adr r 1 , t a b l e adr r 1 , t a b l e
l o o p : ldr r0, [r1] l o o p : ldr r0, [r1], #4
adr r1, [r1, #4]
@ o p e r a z i o n i su r 0 @ o p e r a z i o n i su r 0
..... .....

20 / 47
Indirizzamento - Tirando le Somme

• Più potente di RISC-V


• Registro indice (scalato)
• Aggiornamento automatico del registro base
• Rispetto ad Intel
• Ha in più l’aggiornamento automatico del registro base
• Ha in meno la possibilità di usare contemporaneamente offset
(displacement) ed indice
• Aggiornamento automatico del registro base: utile per scorrere array

21 / 47
Istruzioni ARM

• Architettura RISC con molte istruzioni


• Non proprio “Reduced”...
• Ampia documentazione su internet
• e.g. http://www.peter-cockerell.net/aalp/html/ch-3.html
• Tutte le istruzioni permettono esecuzione condizionale
• Suffisso eq (equal), ne (not equal), hs (higher or same), lo (lower), mi
(minus), ...
• Condizioni basate sui 4 flag di apsr: n, z, c e v
• Flag aggiornati da
• Istruzioni aritmetico/logiche con suffisso (opzionale) s
• Da apposite istruzioni di confronto (come cmp)

• Nomi registri non hanno prefisso (r0, r1, ..., r15)


• Valori immediati (costanti) iniziano con “#”

22 / 47
Istruzioni Aritmetiche e logiche

• <opcode>[<cond>][s] rd, rl, <r>


<r> := #const | <reg> [, <sor> #const | <reg>]
• Come RISC-V, registro destinazione e 2 operandi
• No argomenti in memoria

• Perché “<r>”?
• Secondo argomento può essere immediato o registro
• differenza da RISC-V: add invece che add ed addi
• Se registro, può essere shiftato o rotato
• lsl (asl), lsr, asr, ror, rrx

• Esempi:
• add r0, r1, #1
• adds r0, r1, r2
• addeq r0, r1, r2, lsl #2
• addeqs r0, r1, r2, lsl r3

23 / 47
Istruzioni Più Comuni - 1

• add / adc (add with carry bit C di CPSR):


add r0, r1, r2 @ r0 = r1 + r2
adc r0, r1, r2 @ r0 = r1 + r2 + C
• sub / sbc (sub with carry bit C di CPSR)
sub r0, r1, r2 @ r0 = r1 - r2
sbc r0, r1, r2 @ r0 = r1 - r2 + C - 1
• rsb / rsc (reverse sub / reverse sub with carry)
rsb r0, r1, r2 @ r0 = r2 - r1
rsc r0, r1, r2 @ r0 = r2 - r1 + C - 1
• A che servono? Perché non usare sub / sbc con operandi invertiti?
• Perché il secondo e terzo argomento di un’istruzione non sono
equivalenti...
• Terzo argomento: possibilità di fare shift / rotazione!!!

24 / 47
Istruzioni Più Comuni - 2

• and / orr / eor : operazioni booleane bit a bit


and r0, r1, r2 @ r0 = r1 and r2
orr r0, r1, r2 @ r0 = r1 or r2
eor r0, r1, r2 @ r0 = r1 xor r2
• bic
bic r0, r1, r2 @ r0 = r1 and not r2
• bic (bit clear): calcola r1 and not(<r2>), ovvero ogni 1 in r2 mette a 0 il
bit corrispondente in r1.

25 / 47
Istruzioni Più Comuni - 3

• mul, mla & friends: varie forme di moltiplicazione


mul r0, r1, r2 @ r0 = r1 * r2
mla r0, r1, r2, r3 @ r0 = r1 * r2 + r3

• Istruzioni di shift e rotazione: tramite manipolazione del terzo argomento


add r0, r1, r2, lsl #4 @ r0 = r1 + r2 << 4
add r0, r1, r2, lsl r4 @ r0 = r1 + r2 << r4

• Nota: molti ARM core NON forniscono una istruzione di divisione tra interi
• L’operazione di divisione è fornita da codice:
i n t d i v i d e ( i n t A , i n t B) {
int Q = 0; int R = A;
while ( R >= B ) {
Q = Q + 1;
R = R − B;
}
r e t u r n Q;
}

26 / 47
Istruzioni Più Comuni - 4

• Movimenti di registri:
• mov
mov r0, r1 @ r0 = r1
mov r0, #21 @ r0 = 21
• mvn: move not. Muove il complemento ad 1 di un registro
mvn r0, r1 @ r0 = not r1
• b: branch (anche salto condizionale, tramite suffisso...)

Branch incondizionato: Branch condizionato:

b label mov r 0 , #0
... loop :
label : . . . ...
add r 0 , r 0 , #1
cmp r 0 , #10
bne l o o p

27 / 47
Istruzioni Più Comuni - 5

• bl: branch and link (per invocazione di subroutine)


• Salva l’indirizzo di ritorno nel link register r14
b l sub ; chiama sub
cmp r 1 , #5 ; r i t o r n a qui
moveq r 1 , #0
...
sub : ; c o d i c e f u n z i o n e sub
...
mov p c , l r ; return

• bx: solo su alcune CPU, equivalente a mov r15, r

28 / 47
Istruzioni Più Comuni - 6

• cmp: setta flags come sub, ma senza risultato!


• Da usarsi prima di salti condizionali
i f ( ( r 0 == r 1 ) && ( r 2 == r 3 ) ) r 4 ++;

; Encoding semplice ; Encoding piu ’ p i c c o l o e v e l o c e


cmp r 0 , r 1 cmp r 0 , r 1
bne s k i p cmpeq r 2 , r 3
cmp r 2 , r 3 addeq r 4 , r 4 , #1
bne s k i p ...
add r 4 , r 4 , #1
skip : . . .

• Altre istruzioni simili: tst (esegue and), teq (xor), cmn (somma), ...
tst r0, r1 @ set cc a r0 and r1
teq r0, r1 @ set cc a r0 xor r1
cmn r0, r1 @ set cc a r0 + r1

29 / 47
Istruzioni Più Comuni - 7

• ldr / str: load / store register


ldr r0, [r1] @ r0 = mem[r1]
str r0, [r1] @ mem[r1] = r0

• Valgono le regole per indirizzamento discusse in precedenza


• Esistono versioni per 32, 16, 8 bit
ldr r0, [r1] ; 32 bit ldrh r0, [r1] ; 16 bit ldrb r0, [r1] ; 8 bit
str r0, [r1] ; 32 bit strh r0, [r1] ; 16 bit strb r0, [r1] ; 8 bit

• ldm / stm: load / store multiple registers


ldm r0, {r1, r2, r3} @ r1 = mem[r0]
@ r2 = mem[r0+4]
@ r3 = mem[r0+8]

stm r0, {r1, r2, r3} @ mem[r0] = r1


@ mem[r0+4] = r2
@ mem[r0+8] = r3
• Consentono di trasferire grandi quantità di dati in modo efficiente
• Utilizzate in prologo/epilogo di funzioni per salvare/ripristinare i registri e
l’indirizzo di ritorno
30 / 47
Load / Store Multiple

• ldm<mode> r [!], <register list> / stm<mode> r, <register


list>
• <mode> può essere
• ia (increment after): registri salvati / recuperati nelle locazioni r, r+4,
r+8, etc...
• ib (increment before): registri salvati / recuperati nelle locazioni r+4,
r+8, r+12, etc...
• da (decrement after): registri salvati / recuperati nelle locazioni r, r−4,
r−8, etc...
• db (decrement before): registri salvati / recuperati nelle locazioni r−4,
r−8, r−12, etc...
• Se ! è specificato, r viene aggiornato di conseguenza
• Nomi alternativi per suffissi: ea (empty ascending), ed (empty
descending), fa (full ascending), fd (full descending)

31 / 47
Load multiple

LDM<mode> [ ! ] Rn, { <r e g i s t e r s > }


IA: addr := Rn
IB: addr := Rn
DA: addr := Rn
DB: addr := Rn IA : Increment After
f o r e a c h Ri i n s o r t e d (< r e g i s t e r s >) IB : Increment Before
IB: addr := addr + 4 DA : Decrement After
DB: addr := addr - 4 DB : Decrement Before
Ri : = Mem[ addr ]
IA: addr := addr + 4
DA: addr := addr - 4
<!>: Rn : = addr

R3
R3 R2
R2 R1
Rn → R1 Rn → Rn → R1 Rn →
R2 R1
R3 R2
R3
32 / 47
Load multiple - 2

• ldmia r0, {r1, r2, r3} or ldmia r0, {r1−r3}

addr data
r0 → 0x010 10
r1 : 10
0x014 20
r2 : 20
0x018 30
r3 : 30
0x01c 40
r0 : 0x010
0x020 50
0x024 60

• ldmia r0!, {r1, r2, r3} or ldmia r0!, {r1−r3}

addr data
r0 → 0x010 10
r1 : 10
0x014 20
r2 : 20
0x018 30
r3 : 30
0x01c 40
r0 : 0x01c
0x020 50
0x024 60
33 / 47
Load multiple - 3

• ldmib r0!, {r1, r2, r3} or ldmib r0!, {r1−r3}

addr data
r0 → 0x010 10
r1 : 20
0x014 20
r2 : 30
0x018 30
r3 : 40
0x01c 40
r0 : 0x01c
0x020 50
0x024 60

• ldmda r0!, {r1, r2, r3} or ldmda r0!, {r1−r3}

addr data
0x010 10
r1 : 60
0x014 20
r2 : 50
0x018 30
r3 : 40
0x01c 40
r0 : 0x018
0x020 50
r0 → 0x024 60
34 / 47
Load multiple - 4

• ldmdb r0!, {r1, r2, r3} or ldmdb r0!, {r1−r3}

addr data
0x010 10
r1 : 50
0x014 20
r2 : 40
0x018 30
r3 : 30
0x01c 40
r0 : 0x018
0x020 50
r0 → 0x024 60

35 / 47
Esempio di uso di load multiple: copia blocchi memoria

• r9: indirizzo della sorgente


• r10: indirizzo della destinazione
• r11: indirizzo fine area della sorgente

loop : ldmia r 9 ! { r 0 − r 7 }
stmia r10 ! { r 0 − r 7 }
cmp r 9 , r11
bne l o o p

36 / 47
Esempio

• Stringa C: array di caratteri terminato da 0


• ASCII: caratteri codificati su byte
• Copia di una stringa:
void c o p i a s t r i n g a ( char * d , const char * s )
{
int i = 0;

while ( ( d [ i ] = s [ i ] ) ! = 0 ) {
i += 1 ;
}
}

• Esempio già visto per Assembly RISC-V ed Intel...


• Come fare con Assembly ARM?

37 / 47
Accesso a Singoli Byte

• Ricordate? Necessità di copiare byte, non parole...


• RISC-V: load byte lb e store byte sb invece di lw e sw
• Intel: soluzione semplice (ed elegante? Dipende dai punti di vista!)
• Registri composti da “sottoregistri” più piccoli
• al ... dl: registri a 8 bit
• movb: da memoria a al .. dl o viceversa
• E ARM?
• Load register (ldr) e store register (str) possono avere suffisso b, h, etc...
• Usiamo ldrb ed strb!

38 / 47
Implementazione Assembly: Prologo

• I due parametri d ed s sono contenuti in r0 ed r1


• Invece di usare un registro per il contatore i, incrementiamo direttamente
r0 ed r1
• Non sono registri preservati...
• Non è necessario salvarli sullo stack
• Si può usare indirizzamento post indexed!!!
• Non è necessario alcun prologo; possiamo cominciare col codice C

39 / 47
Implementazione Assembly: Loop

• Ciclo while: copia s[i] in d[i]


• Prima di tutto, carichiamolo in un registro temporaneo
• Usiamo r3
• Per fare questo, usiamo indirizzamento post indexed (base + indice *
scala, con scala = 1 e poi incrementa base)
• Nessuna necessità di caricare l’indirizzo dell’i-esimo elemento in un
registro, come si faceva per RISC-V

l d r b r 3 , [ r 1 ] , #1

• Ora memoriazziamo r3 in d[i]


s t r b r 3 , [ r 0 ] , #1

40 / 47
Implementazione Assembly: Fine del Loop

• Bisogna ora controllare se s[i] == 0


• Se si, fine del loop!

cmp r 3 , #0 @ c o n f r o n t a r 3 con 0 . . .
beq L2 @ se sono u g u a l i , s a l t a a L2
@ ( esci dal loop ! )

• Se no, cicla...

b copia stringa

• La label L2 implementerà il ritorno al chiamante

41 / 47
Implementazione Assembly: Fine

• Non abbiamo salvato nulla sullo stack: non c’è necessità di epilogo!
• Si può direttamente tornare al chiamante
L2 : mov r 1 5 , r14

• Mettendo tutto assieme:


.text
.globl copia stringa
copia stringa :
l d r b r 3 , [ r 1 ] , #1
s t r b r 3 , [ r 0 ] , #1
cmp r 3 , #0
beq L2
b copia stringa
L2 :
mov r 1 5 , r14

42 / 47
Vediamo GCC...

.text
.global copia stringa
copia stringa :
ldrb r 3 , [ r1 ]
strb r 3 , [ r0 ]
cmp r 3 , #0
bxeq lr
L3 :
ldrb r 3 , [ r 1 , #1 ] !
strb r 3 , [ r 0 , #1 ] !
cmp r 3 , #0
bne L3
bx lr

43 / 47
Esempio

f o r ( i n t i = 0 ; i < 1 0 ; i ++) { mov r 0 , #0 ; r 0 = 0


a [ i ] = 0; adr r 2 , a ; r 2 = &a
} mov r 1 , #0 ; i = 0
l o o p : cmp r 1 , #10 ; i < 10
bge end ; se >= , end
s t r r 0 , [ r 2 , r 1 , l s l #2 ]
; mem[ r 2 + r 1 << 2 ] = 0
add r 1 , r 1 , #1 ; i ++
b loop
end :

i n t gcd ( i n t i , i n t j ) { l o o p : cmp r 0 , r 1
while ( i ! = j ) { subgt r 0 , r 0 , r 1
i f ( i >j ) sublt r 1 , r 1 , r0
i −= j ; bne l o o p
else bx l r
j −= i ;
}
}

44 / 47
Esempio di passaggio parametri con lo stack

int caller () { f1 :
i n t sum = f 1 ( 1 , 2 , 3 , 4 , 5 , 6 ) ; add r0, r 0 , r1
r e t u r n sum ; add r0, r 0 , r2
} add r0, r 0 , r3
ldr r3, [ sp ]
i n t f1 ( i n t a1, i n t a2, i n t a3, i n t a4, add r0, r 0 , r3
i n t a 5 , i n t a6 ) { ldr r3, [ s p , #4 ]
r e t u r n a1+a2+a3+a4+a5+a6 ; add r0, r 0 , r3
} bx lr
caller :
str lr, [ s p , #−4 ] !
sub sp, s p , #12
mov r3, #6
str r3, [ s p , #4 ]
mov r3, #5
str r3, [ sp ]
mov r3, #4
mov r2, #3
mov r1, #2
mov r0, #1
bl f1
add sp, s p , #12
ldr lr, [ sp ] , #4
bx lr

45 / 47
Esempio di divisione come shift e sottrazione per interi positivi

i n t d i v i d e ( i n t A, i n t B) { divide :
int Q = 0 ; i n t R = A; mov r 3 , r0
while ( R >= B ) { cmp r 0 , r1
Q = Q + 1; blt .L4
R = R − B; mov r 0 , #0
} .L3 :
r e t u r n Q; add r 0 , r 0 , #1
} sub r 3 , r 3 , r1
cmp r 1 , r3
ble .L3
bx lr
.L4 :
mov r 0 , #0
bx lr

46 / 47
Versione sofisticata di divisione tra interi positivi

http://www.tofla.iconbar.com/tofla/arm/arm02/index.htm

div :
; divs r 0 , r 1 , r2
cmp r 2 , #0
beq d i v i d e e n d ; check f o r d i v i d e by zero !
mov r 0 , #0 ; c l e a r r 0 t o accumulate r e s u l t
mov r 3 , #1 ; s e t b i t 0 i n r 3 , which w i l l be s h i f t e d l e f t then r i g h t
start :
cmp r 2 , r1
movls r 2 , r 2 , l s l #1
movls r 3 , r 3 , l s l #1
bls start ; s h i f t r 2 l e f t u n t i l i t i s about t o be b i g g e r than r 1 ,
; s h i f t r 3 l e f t i n p a r a l l e l i n o r d e r t o f l a g how f a r we
; have t o go
next :
cmp r1,r2 ; c a r r y s e t i f r1>r 2 ( don ’ t ask why )
subcs r 1 , r 1 , r 2 ; s u b t r a c t r 2 from r 1 i f t h i s would g i v e a p o s i t i v e
; answer
addcs r 0 , r 0 , r 3 ; and add t h e c u r r e n t b i t i n r 3 t o t h e a c c u m u l a t i n g
; answer i n r 0
movs r 3 , r 3 , l s r #1 ; S h i f t r 3 r i g h t i n t o c a r r y f l a g
movcc r 2 , r 2 , l s r #1 ; and i f b i t 0 o f r 3 was z e r o , a l s o s h i f t r 2 r i g h t
bcc next ; I f c a r r y n o t c l e a r , r 3 i s s h i f t e d back t o where
; i t s t a r t e d , and we can end
divide end :
; r 1 h o l d s t h e r e m a i n d e r , i f a n y , r 2 has r e t u r n e d t o t h e v a l u e i t h e l d on
; e n t r y t o t h e r o u t i n e , r 0 h o l d s t h e r e s u l t and r 3 h o l d s z e r o . Both zero
; and c a r r y f l a g s are s e t .
bx l r

47 / 47
Esempi di Programmi Assembly
RISC-V e Intel x86

Giovanni Iacca

(materiale preparato con Luigi Palopoli,


Marco Roveri, e Luca Abeni)

1
Scopo della lezione
• In questa lezione vedremo alcuni esempi di
programmi (o frammenti di programmi) in vari
linguaggi assembly per renderci conto delle
differenze
• Partiremo da assembly RISC-V e Intel
• Successivamente passeremo all’assembly ARM

2
Semplici istruzioni
aritmetiche logiche
• Partiamo dal semplicissimo frammento che
abbiamo visto a lezione

f = (g + h) − (i + j);

3
Traduzione RISC-V
• Supponendo che g, h, i, j siano in x19, x20,
x21, e x22, e che si voglia mettere il risultato
in x23, la traduzione è semplicemente

f = (g+h)-(i+j);

add x5, x19, x20


add x6, x21, x22
sub x23, x5, x6
4
Traduzione RISC-V (v2)
• Supponendo che g, h, i, j siano in x19, x20,
x21, e x22, e che si voglia mettere il risultato
in x23, la traduzione è semplicemente

f = (g+h)-(i+j);

In questa versione è usato


add x23, x19, x20 un registro in meno:
add x6, x21, x22 Il risultato intermedio è
memorizzato in x23
sub x23, x23, x6
5
Traduzione INTEL
• Per INTEL, supponiamo che g, h, i, j siano in rdi,
rsi, rdx, rcx e che si voglia salvare il risultato in rax
• Il problema è come fare la somma a due operandi
e risultato in un terzo operando, cosa possibile
usando l’istruzione lea
f = (g+h)-(i+j);

leaq (%rdi, %rsi), %rax //rax = rdi + rsi


addq %rcx, %rdx //rdx = rdx + rcx
subq %rdx, %rax //rax = rax - rdx
6
Accesso alla memoria
• Riguardiamo ancora l’esempio visto a lezione
assumendo int a[] e int h

a[12] = h + a[8];

7
Traduzione RISC-V

• Supponiamo che h sia in x21 e che il registro


base del vettore a sia in x22

a[12]= h + a[8];

lw x9, 32(x22) // x9 = a[8]


addw x9, x21, x9 // x9 = h + a[8]
sw x9, 48(x22) // a[12] = x9

8
Traduzione INTEL
• Supponiamo di avere h in edi e l’indirizzo di a in rsi.
• Grazie al fatto di poter avere operandi in memoria
stavolta ce la caviamo con due istruzioni

a[12]= h + a[8];

addl 32(%rsi), %edi //edi = edi + a[8]


movl %edi, 48(%rsi) //a[12] = edi

9
Blocchi condizionali
• Consideriamo il seguente blocco
if (i == j)
f = g + h;
else
f = g – h;

10
Traduzione RISC-V
• Supponendo di avere f, g, h, i, j nei registri da
x19 a x23 avremo
if (i == j)
f = g + h;
else
f = g – h;

bne x22, x23, L2 // se x22 neq x23 vai a L2


add x19, x20, x21 // x19 = g + h
beq x0, x0, L3 // se x0 == x0 vai a L3
L2:
sub x19, x20, x21 // x19 = g - h
L3:
… 11
Traduzione INTEL
• Assumiamo che g, h, i, j siano in rdi, rsi, rdx,
rcx e che si voglia salvare f in rax
if (i == j)
f = g + h;
else
f = g – h;

cmpq %rcx, %rdx // i == j??


jne L2
leaq (%rdi, %rsi), %rax // f = g + h
jmp L3
L2:
movq %rdi, %rax // f = g
subq %rsi, %rax // f = f-h (nota complicazione)
L3:
… 12
Traduzione INTEL
(ottimizzata)
• Possiamo fare di meglio usando uno strano oggetto
(move condizionale) che fino ad ora i compilatori
avevano evitato di usare
if (i == j)
f = g + h;
else
f = g – h;

leaq (%rdi, %rsi), %rax //rax = g+h


subq %rsi, %rdi //rdi = g-h
cmpq %rcx, %rdx
cmovne %rdi, %rax //spostiamo se il cmp precedente NE

13
Condizione con
disuguaglianza
• Supponiamo ora di avere:
if (i < j)
f = g + h;
else
f = g – h;

14
Traduzione RISC-V
• Supponendo di avere f, g, h, i, j nei registri da
x19 a x23 avremo
if (i < j)
f = g + h;
else
f = g – h;

slt x5, x22, x23 // x5 = x22 < x23


beq x5, x0, L2 // se x5 eq x0 vai a L2
add x19, x20, x21 // f = g + h
beq x0, x0, L3 // se x0 == x0 vai a L3
L2:
sub x19, x20, x21 // f = g - h
L3:

15
Traduzione RISC-V (v2)
• Supponendo di avere f, g, h, i, j nei registri da
x19 a x23 avremo
if (i < j)
f = g + h;
else
f = g – h;

blt x22, x23, L2 // se x22 < x23 vai a L2


sub x19, x20, x21 // f = g - h
beq x0, x0, L3 // se x0 == x0 vai a L3
L2:
add x19, x20, x21 // f = g + h
L3:

16
Traduzione INTEL
• Assumiamo che g, h, i, j siano in rdi, rsi, rdx,
rcx e che si voglia salvare f in rax
if (i < j)
f = g + h;
else
f = g – h;

cmpq %rcx, %rdx // i == j??


jge L2
leaq (%rdi, %rsi), %rax // f = g + h
jmp L3
L2:
movq %rdi, %rax // f = g
subq %rsi, %rax // f = f-h (nota complicazione)
L3:
17

Traduzione INTEL
(ottimizzato)
• Ancora una volta possiamo usare la move
condizionale
if (i < j)
f = g + h;
else
f = g – h;

leaq (%rdi, %rsi), %rax //rax = g+h


subq %rsi, %rdi //rdi = g-h
cmpq %rcx, %rdx
cmovge %rdi, %rax //spostiamo se il cmp precedente GE

18
Ciclo while
• Consideriamo il seguente ciclo while
i = 0;
while (a[i]==k)
i += 1;

19
Traduzione RISC-V
• Supponendo di avere i in x22, k in x24 e
l’indirizzo base di a sia in x25
i = 0;
while (a[i] == k)
i += 1;

add x22, x0 // i = 0
L1:
slli x10, x22, 2 // x10 = i * 4
add x10, x10, x25 // x10 = indirizzo di a[i]
lw x9, 0(x10) // x9 = a[i]
bne x9, x24, L2 // se a[i] != k vai a L2
addi x22, x22, 1 // i = i + 1
beq x0, x0, L1 // se 0 == 0 vai a L1
L2:
20

Traduzione INTEL
• Stavolta è possibile sfruttare la potenza del CISC
i = 0;
while (a[i]==k)
i += 1;

cmpl (%rsi), %edi // %rsi=&a, %edi=k


jne L2 // ciclo mai eseguito
movq $0, %rax // i = 0
L1:
addq $1, %rax // i++
cmpl %edi, (%rsi, %rax, 4) // k ed a 32 bit
je L1
jmp L3
L2: movq $0, %rax
L3:
… 21
RISC-V Nomi dei Registri
ed uso
Registro Nome Uso Chi salva

x0 zero Costante 0 N.A.

x1 ra Indirizzo di ritorno Chiamante

x2 sp Stack pointer Chiamato

x3 gp Global pointer ---

x4 tp Thread pointer ---

x5-x7 t0-t2 Temporanei Chiamante

x8 s0/fp Salvato/Puntatore a frame Chiamato

x9 s1 Salvato Chiamato

x10-x11 a0-a1 Argomenti di funzione/valori restituiti Chiamante

x12-x17 a2-a7 Argomenti di funzione Chiamante

x18-x27 s2-s11 Registri salvati Chiamato

x28-x31 t3-t6 Temporanei Chiamante

22
Intel Nomi dei Registri ed
Uso
• %rsp → stack pointer;
• %rbp → base pointer
• Primi 6 argomenti:
§ %rdi, %rsi, %rdx, %rcx, %r8 ed %r9
• Altri argomenti (7 → n): sullo
stack
• Valori di ritorno:
§ %rax e %rdx
• Registri preservati:
§ %rbp, %rbx, %r12, %r13, %r14 ed
%r15
• Registri non preservati:
§ %rax, %r10, %r11
§ registri per passaggio parametri:
%rdi, %rsi, %rdx, %rcx, %r8 ed %r9

23
Funzione Foglia
• Si definisce “foglia” una funzione che non ne
chiama altre.
• Le funzioni foglia nel RISC-V, se non
ottimizzate, sono trattate come qualunque
altra funzione
§ occorre salvare (prologo) il return address e
gestire i registri usati come parametri, e
ripristinare (epilogo) tutto quello salvato

24
Esempio
int esempio_foglia(int g, int h,
int i , int j) {
int f;
f = (g + h) − (i + j);
return f ;
}

• Abbiamo una sola variabile locale (f) per la quale


è possibile usare un registro
25
Traduzione RISC-V
• Traduzione tenendo conto che g, h, i, j corrispondono ai
registri da x10 a x13, mentre f corrisponde a x20
int esempio_foglia(int g, int h, int i, int j) {
int f;
f = (g + h) − (i + j);
return f ;
}

esempio_foglia:
addi sp, sp, -24 // aggiornamento stack per fare posto a tre elementi
sd x5, 16(sp) // salvataggio x5 per usarlo dopo
sd x6, 8(sp) // salvataggio x6 per usarlo dopo
sd x20, 0(sp) // salvataggio x20 per usarlo dopo
addw x5, x10, x11 // x5 = g + h
addw x6, x12, x13 // x6 = i + j
subw x20, x5, x6 // f = (g+h)- (i+j)
addi x10, x20, 0 // restituzione di f (x10 = x20 + 0)
ld x20, 0(sp) // ripristino x20 per il chiamante
ld x6, 8(sp) // ripristino x5 per il chiamante
ld x5, 16(sp) // ripristino x5 per il chiamante
addi sp, sp, 24 // aggiornamento sp con eliminazione tre elementi
jalr x0, 0(x1) // ritorno al programma chiamante 26
Traduzione RISC-V
Ottimizzata
• Traduzione tenendo conto che g, h, i, j corrispondono ai registri da
x10 a x13 (aka a0, a1, a2, a3), e che i temporanei possono essere
non salvati/usati.
int esempio_foglia(int g, int h, int i , int j) {
int f;
f = (g + h)- (i + j);
return f ;
}

esempio_foglia:
addw a0, a0, a1 // f = g+h (a0 corrisponde a x10)
addw a3, a2, a3 // a3 = i+j
subw a0, a0, a3 // f = f – a3
ret // alias per jalr x0, 0(x1) o jalr zero, 0(ra)

27
Traduzione INTEL
• Traduzione ottimizzata

int esempio foglia(int g, int h, int i , int j) {


int f;
f = (g + h) − (i + j);
return f ;
}

esempio_foglia:
leal (%rdi,%rsi), %eax
addl %ecx, %edx
subl %edx, %eax
ret

28
Traduzione non ottimizzata
Senza ottimizzazioni il risultato è piuttosto diverso
esempio_foglia:
pushq %rbp //Prologo
movq %rsp, %rbp
movl %edi, -20(%rbp) //g
movl %esi, -24(%rbp) //h
movl %edx, -28(%rbp) //i
movl %ecx, -32(%rbp) //j

movl -20(%rbp), %edx //edx = g


movl -24(%rbp), %eax //eax = h
leal (%rdx,%rax), %ecx //f = ecx = g+h
movl -28(%rbp), %edx //edx = i
movl -32(%rbp), %eax //eax = j
addl %edx, %eax //eax = i+j
subl %eax, %ecx //ecx = g+h – (i+j)
movl %ecx, %eax //eax = ecx

movl %eax, -4(%rbp) //Epilogo


movl -4(%rbp), %eax
popq %rbp
ret
29
Funzioni non foglia
• Consideriamo il seguente caso più complesso

int inc(int n)
{
return n + 1;
}
int f(int x)
{
return inc(x) − 4;
}

30
Traduzione RISC-V
• La traduzione di inc è simile alla precedente traduzione,
supponendo che n è in x10 (aka a0) e risultato in x10 (aka a0)

int inc(int n) {
return n + 1;
}

inc:
addiw a0, a0, 1
ret

31
Traduzione RISC-V
• La traduzione di f richiede più attenzione. Supponiamo anche
qui che n sia in x10 (a0)
int f(int n) {
return inc(n) – 4;
}

f:
addi sp, sp, -16 //Prologo
sd ra, 8(sp)

jal ra, inc


addiw a0, a0, -4

ld ra, 8(sp) //Epilogo


addi sp, sp, 16
ret

32
Traduzione RISC-V
• Con il gcc le cose sono un po’ più complesse e vengono fatte più
operazioni (senza ottimizzazioni).
int f(int n) {
return inc(n) – 4;
}

f: addi sp,sp,-32 // estendiamo stack


sd ra,24(sp) // salviamo ra
sd s0,16(sp) // salviamo contenuto di s0
addi s0,sp,32 // nuovo s0 = s0 + 32
mv a5,a0 // carico in a5 il contenuto di a0
sw a5,-20(s0) // salvo a5 sullo stack
lw a5,-20(s0) // leggo a5 dallo stack
mv a0,a5 // memorizzo a5 in a0
call inc // chiamo inc
mv a5,a0 // copio risultato chiamata su a5
addiw a5,a5,-4 // decremento di 4 il risultato
mv a0,a5 // copio risultato intermedio in a0 per ritorno
ld ra,24(sp) // ripristino ra
ld s0,16(sp) // ripristino s0
addi sp,sp,32 // svuoto stack
jr ra // ritorno 33
Traduzione INTEL
• La traduzione INTEL è più semplice
int inc(int n) {
return n + 1;
}

inc:
leal 1(%rdi), %eax
ret

34
Traduzione INTEL
• La traduzione INTEL è più semplice poiché il salvataggio del
return address è fatto in automatico con la call

int f(int n) {
return inc(n) - 4;
}

f:
call inc
subl $4, %eax
ret

35
Ordinamento di array
• Passiamo a qualcosa di più complesso: un algoritmo
noto come «insertion sort»
void sposta(int v[], size_t i) { void ordina(int v[], size_t n) {
size_t j; size_t i;
int appoggio; i = 1;
while (i < n) {
appoggio = v[i]; sposta(v, i);
j = i - 1; i = i+1;
while ((j >= 0) && (v[j] > appoggio)) { }
v[j+1] = v[j]; }
j = j-1;
}
v[j+1] = appoggio;
}

36
Traduzione RISC-V
• Cominciamo da sposta. Stavolta le cose sono più complesse.
Assumiamo che i parametri siano memorizzati in x10, x11(a0, a1)
rispettivamente. Usiamo a3 per appoggio.
void sposta(int v[], size_t i) {
size_t j;
int appoggio;

appoggio = v[i];
j = i - 1;

sposta:
slli a4,a1,2 //a4 = i*4
add a5,a0,a4 //a5 = &v[i]
lw a3,0(a5) //a3 = v[i]
addiw a1,a1,-1 //a1 = a1-1 (i = i-1)

37
Traduzione RISC-V
continua
• Ciclo
while ((j >= 0) && (v[j] > appoggio)) {
v[j+1] = v[j];
j = j-1;
}

bltz a1,.L2 // se j < 0 esci dal ciclo


lw a4,-4(a5) // a4 = v[i-1]=v[j]
bge a3,a4,.L2 // se appoggio >= v[j] esci
li a2,-1 // carica -1 in a2
.L3:
sw a4,0(a5) // memorizza v[j] (a4) in v[j+1]
addiw a1,a1,-1 // a1 = a1-1
beq a1,a2,.L4 // salta se a1 = -1
addi a5,a5,-4 // j=j-1
lw a4,-4(a5)
bgt a4,a3,.L3 // Salta se v[j] > appoggio
j .L2

38
Traduzione RISC-V
continua
• Uscita da sposta

v[j+1] = appoggio;
}

.L4:
li a1,-1
.L2:
addi a1,a1,1
slli a1,a1,2
add a1,a0,a1
sw a3,0(a1) // v[j+1] = appoggio
ret

39
Traduzione RISC-V
continua
• Passiamo ora alla funzione ordina. I parametri
sono memorizzati in a0, a1 rispettivamente.
void ordina(int v[], size_t n) {
size_t i;
i = 1;

ordina: li a5,1
ble a1,a5,.L11
addi sp,sp,-32
sd ra,24(sp)
sd s0,16(sp)
sd s1,8(sp)
sd s2,0(sp)
mv s1,a1
mv s2,a0
li s0,1 40
Traduzione RISC-V
continua
• Passiamo al loop

while (i < n) {
sposta(v, i);
i = i+1;
}

.L8:
mv a1,s0
mv a0,s2
call sposta
addiw s0,s0,1
bne s1,s0,.L8

41
Traduzione RISC-V
continua
• Epilogo ordina

ld ra,24(sp)
ld s0,16(sp)
ld s1,8(sp)
ld s2,0(sp)
addi sp,sp,32
jr ra
.L11:
ret

42
INTEL
• Riguardiamo lo stesso codice implementato
tramite INTEL
void sposta(int v[], size_t i) {
size_t j;
int appoggio;

appoggio = v[i];
j = i - 1;

sposta: # rdi = v, rsi = i


# rax = j, r10d = appoggio
movq %rsi, %rax
movl (%rdi, %rax, 4), %r10d # appoggio = v[i]
dec %rax
43
INTEL
• Vediamo il ciclo
while ((j >= 0) && (v[j] > appoggio)) {
v[j+1] = v[j];
j = j-1;
}

ciclo:
cmpq $0, %rax # confronta 0 e rax
jl out # esci se j < 0
movl (%rdi, %rax, 4), %r11d # metti v[j] in %r11d
cmpl %r10d, %r11d # confronta v[j] e appoggio
jle out # se v[j] < appoggio esci
movl %r11d, 4(%rdi, %rax, 4)
dec %rax
jmp ciclo
out: 44
INTEL
• Uscita da sposta
v[j+1] = appoggio;
}

out:
movl %r10d, 4(%rdi, %rax, 4)
ret

45
INTEL
• Vediamo la procedura ordina, che non è foglia.
• Il salvataggio sullo stack è semplificato
void ordina(int v[], size_t n) {
size_t i;
i = 1;

ordina: # rdi = v, rsi = n


# rbx = i
pushq %rbx
movq $1, %rbx

46
INTEL
• Il loop
while (i < n) {
sposta(v, i);
i = i+1;
}

loop_ordina:
cmp %rbx, %rsi
jle out_ordina
pushq %rsi
movq %rbx, %rsi
call sposta
popq %rsi
inc %rbx
jmp loop_ordina
out_ordina 47
INTEL
• Epilogo

out_ordina
popq %rbx
ret

48
Copia Stringhe
• Consideriamo ora
void copia_stringa(char d[], const char s[]) {
size_t i = 0;

while ((d[i] = s[i]) != `0`) {


i += 1;
}
}

49
Traduzione RISC-V
• Traduzione RISC-V

void copia_stringa(char d[], const char s[]) {


size_t i = 0;

copia_stringa:
addi sp, sp, -8 // aggiorna stack per inserire un elemento
sd x19, 0(sp) // salva x19
add x19, x0, x0 // i = 0

50
Traduzione RISC-V
continua
• Loop
while ((d[i] = s[i]) != `0`) {
i += 1;
}

LoopCopiaStringa:
add x5, x19, x11 // indirizzo di s[i]
lbu x6, 0(x5) // x6 = s[i]
add x7, x19, x10 // indirizzo di d[i]
sb x6, 0(x7) // d[i] = s[i]
beq x6, x0, LoopCopiaStringaEnd // se 0 vai a LoopCopiaStringaEnd
addi x19, x19, 1 // i += 1
jal x0, LoopCopiaStringa // salta a LoopCopiaStringa
LoopCopiaStringaEnd

51
Traduzione RISC-V
continua
• Chiusura

LoopCopiaStringaEnd
ld x19, 0(sp) // ripristina contenuto di x19
addi sp, sp, 8 // aggiorna lo stack eliminando un elemento
jalr, x0, 0(x1) // ritorna al chiamante

52
Traduzione INTEL
• Inizio
void copia_stringa(char d[], const char s[]) {
size_t i = 0;

copia_stringa:
movzbl (%rsi), %eax
movb %al, (%rdi)
testb %al, %al
je L1
movl $0, %eax

53
Traduzione INTEL
• Loop
while ((d[i] = s[i]) != `0`) {
i += 1;
}

L3:
addl $1, %eax
movslq %eax, %rcx
movzbl (%rsi, %rcx), %edx
movb %dl, (%rdi, %rcx)
testb %dl, %dl
jne L3
L1:
ret

54
Esempi di Programmi Assembly
RISC-V e ARM

Giovanni Iacca

(materiale preparato con Luigi Palopoli,


Marco Roveri, e Luca Abeni)

1
Scopo della lezione
• In questa lezione vedremo alcuni esempi di
programmi (o frammenti di programmi) in vari
linguaggi assembly per renderci conto delle
differenze
• Abbiamo visto esempi in assembly RISC-V e
Intel
• In questa lezione rivedremo esempi RISC-V e
corrispondenti esempi in ARM

2
RISC-V Nomi dei Registri
ed uso
Registro Nome Uso Chi salva

x0 zero Costante 0 N.A.

x1 ra Indirizzo di ritorno Chiamante

x2 sp Stack pointer Chiamato

x3 gp Global pointer ---

x4 tp Thread pointer ---

x5-x7 t0-t2 Temporanei Chiamante

x8 s0/fp Salvato/Puntatore a frame Chiamato

x9 s1 Salvato Chiamato

x10-x11 a0-a1 Argomenti di funzione/valori restituiti Chiamante

x12-x17 a2-a7 Argomenti di funzione Chiamante

x18-x27 s2-s11 Registri salvati Chiamato

x28-x31 t3-t6 Temporanei Chiamante

3
ARM Nomi dei Registri ed
uso
• Nomi: da r0 a r15
§ Tecnicamente, r15 non è un registro general purpose, spesso usato come PC
• Alcuni registri accessibili tramite un nome simbolico che ne iden8fica
l’u8lizzo
§ r13 == sp (stack pointer)
§ r14 == lr (link register)
• Primi 4 argomen8:
§ r0 ... r3
§ registri non preserva@! U@lizzabili anche come registri “temporanei” da non
salvare!
• Altri argomen8 (4 → n): sullo stack
• Registri preserva8: r4 ... r11
§ Eccezione: in alcuni ABI r9 non è preservato
• Valori di ritorno: r0 e r1
• I registri che una subrou8ne può u8lizzare senza doverli salvare sono
§ r0, r1, r2, r3 ed r12
§ più eventualmente r9 (dipende da piaQaforma / ABI)
4
Semplici istruzioni
aritmetiche logiche
• Partiamo dal semplicissimo frammento che
abbiamo visto a lezione

f = (g + h) − (i + j);

5
Traduzione RISC-V
• Supponendo che g, h, i, j siano in x19, x20,
x21, e x22, e che si voglia mettere il risultato
in x23, la traduzione è semplicemente

f = (g+h)-(i+j);

add x5, x19, x20


add x6, x21, x22
sub x23, x5, x6
6
Traduzione RISC-V (v2)
• Supponendo che g, h, i, j siano in x19, x20,
x21, e x22, e che si voglia mettere il risultato
in x23, la traduzione è semplicemente

f = (g+h)-(i+j);

In questa versione è usato


add x23, x19, x20 un registro in meno:
add x6, x21, x22 Il risultato intermedio è
memorizzato in x23
sub x23, x23, x6
7
Traduzione ARM
• Traduzione in ARM pressoché uguale
§ eccetto per il nome dei registri
• La s dopo la add è per settare i flag
§ funzionerebbe anche senza

f = (g+h)-(i+j);

adds r1, r0, r1


adds r3, r2, r3
subs r0, r1, r3

8
Accesso alla memoria
• Riguardiamo ancora l’esempio visto a lezione
assumendo che int a[] e int h.

a[12] = h + a[8];

9
Traduzione RISC-V

• Supponiamo che h sia in x21 e che il registro


base del vettore a sia in x22

a[12]= h + a[8];

lw x9, 32(x22) // x9 = a[8]


addw x9, x21, x9 // x9 = h + a[8]
sw x9, 48(x22) // a[12] = x9

10
Traduzione ARM
• Anche in questo caso la traduzione è molto simile
§ Assumiamo di avere h in r0 e indirizzo di a in r1
• Si usa indirizzamento pre-indexed senza aggiornamento
della base (senza !)

a[12]= h + a[8];

ldr r3, [r1, #32]


add r0, r3, r0
str r0, [r1, #48]

11
Blocchi condizionali
• Consideriamo il seguente blocco

if (i == j)
f = g + h;
else
f = g – h;

12
Traduzione RISC-V
• Supponendo di avere f, g, h, i, j nei registri da
x19 a x23 avremo
if (i == j)
f = g + h;
else
f = g – h;

bne x22, x23, L2 // se x22 neq x23 vai a L2


add x19, x20, x21 // x19 = g + h
beq x0, x0, L3 // se x0 == x0 vai a L3
L2:
sub x19, x20, x21 // x19 = g - h
L3:
… 13
Traduzione ARM
• Questa volta diventa tutto più semplice per via
delle istruzioni condizionali

if (i == j)
f = g + h;
else
f = g – h;

cmp r2, r3
addeq r0, r0, r1
subne r0, r0, r1

14
Condizione con
disuguaglianza
• Supponiamo ora di avere:

if (i < j)
f = g + h;
else
f = g – h;

15
Traduzione RISC-V
• Supponendo di avere f, g, h, i, j nei registri da
x19 a x23 avremo
if (i < j)
f = g + h;
else
f = g – h;

slt x5, x22, x23 // x5 = x22 < x23


beq x5, x0, L2 // se x5 eq x0 vai a L2
add x19, x20, x21 // f = g + h
beq x0, x0, L3 // se x0 == x0 vai a L3
L2:
sub x19, x20, x21 // f = g - h
L3:
16
Traduzione RISC-V (v2)
• Supponendo di avere f, g, h, i, j nei registri da
x19 a x23 avremo
if (i < j)
f = g + h;
else
f = g – h;

blt x22, x23, L2 // se x22 < x23 vai a L2


sub x19, x20, x21 // f = g - h
beq x0, x0, L3 // se x0 == x0 vai a L3
L2:
add x19, x20, x21 // f = g + h
L3:

17
Traduzione ARM
• Nel caso di ARM la traduzione con le istruzioni condizionali
è simile
• Cambiano solo le condizioni... (lt e ge)

if (i < j)
f = g + h;
else
f = g – h;

cmp r2, r3
addlt r0, r0, r1
subge r0, r0, r1

18
Ciclo while
• Consideriamo il seguente ciclo while

i = 0;
while (a[i] == k)
i += 1;

19
Traduzione RISC-V
• Supponendo di tenere i in x22, k in x24 e
l’indirizzo base di a sia in x25
i = 0;
while (a[i] == k)
i += 1;

add x22, x0 // i = 0
L1:
slli x10, x22, 2 // x10 = i * 4
add x10, x10, x25 // x10 = indirizzo di a[i]
lw x9, 0(x10) // x9 = a[i]
bne x9, x24, L2 // se a[i] != k vai a L2
addi x22, x22, 1 // i = i + 1
beq x0, x0, L1 // se 0 == 0 vai a L1
L2:
20

Traduzione ARM
• Assumendo che il valore di k sia inizialmente contenuto in r0, che
l’indirizzo dell’array a sia inizialmente contenuto in r1 e che il valore di i
vada salvato in r0
• Con ARM è possibile usare il pre-indexing per scorrere array
• Il codice è ldr r3, [r1]
cmp r0, r3
bne L2
mov r3, #0
L1:
add r3, r3, #1
i = 0; ldr r2, [r1, #4]!
while (a[i] == k)
cmp r2, r0
i += 1;
beq L1
b L3
L2:
mov r3, #0
L3:
mov r0, r3 21
Funzione Foglia

• Si definisce “foglia” una funzione che non ne chiama altre.

• Le funzioni foglia nel RISC-V, se non ottimizzate, sono


trattate come qualunque altra funzione (occorre salvare il
return address e gestire i registri usati come parametri),
mentre in ARM sono molto più semplici da trattare
§ non occorre salvare il return address, nè avere particolari
accortezze sui registri usati come parametri.

• Prologo ed epilogo quindi in ARM sono dunque semplificati


e lo diventano ancora di più se non usiamo registri da
preservare e variabili da allocare nello stack

22
Esempio
int esempio foglia(int g, int h,
int i , int j) {
int f;
f = (g + h) − (i + j);
return f ;
}

• Abbiamo una sola variabile locale (f) per la quale


è possibile usare un registro
23
Traduzione RISC-V
Ottimizzata
• Traduzione tenendo conto che g, h, i, j corrispondono ai
registri da x10 a x13 (aka a0, a1, a2, a3), e che i temporanei
possono essere non salvati/usati.
int esempio foglia(int g, int h, int i, int j) {
int f;
f = (g + h) − (i + j);
return f ;
}

esempio_foglia:
addw a0, a0, a1 // a0 = g + h
addw a3, a2, a3 // a3 = i + j
subw a0, a0, a3 // f = (g+h)- (i+j)
ret // alias per jalr x0, 0(x1) o jalr zero, 0(ra)

24
Traduzione ARM
• Traduzione molto simile
• Come esempio (non è realmente necessario in questo
caso) rsb viene usato per invertire i due operandi

int esempio foglia(int g, int h, int i , int j) {


int f;
f = (g + h) − (i + j);
return f ;
}

esempio_foglia:
add r0, r0, r1
add r3, r2, r3
rsb r0, r3, r0 // r0=r0-r3 (equivalente a sub r0, r0, r3)
bx lr
25
Funzioni non foglia
• Consideriamo il seguente caso più complesso
int inc(int n)
{
return n + 1;
}
int f(int x)
{
return inc(x) − 4;
}

26
Traduzione RISC-V
• La traduzione di inc è simile alla precedente
traduzione, supponendo che n è in x10 (aka
a0) e risultato in x10 (aka a0)
int inc(int n) {
return n + 1;
}

inc:
addiw a0, a0, 1
ret

27
Traduzione RISC-V senza
ottimizzazioni
• La traduzione di f richiede più attenzione. Supponiamo anche
qui che n è in x10 (a0) e risultato anche esso in x10 (a0).
int f(int n) {
return inc(n) – 4;
}

f:
addi sp, sp, -16 //Prologo
sd ra, 8(sp)

jal ra, inc


addiw a0, a0, -4

ld ra, 8(sp) //Epilogo


addi sp, sp, 16
ret

28
Traduzione ARM
• La traduzione ARM di inc è praticamente identica
• Notare che r0 è sia usato come parametro di
ingresso che come valore di ritorno

int inc(int n) {
return n + 1;
}

inc:
add r0, r0, #1
bx lr

29
Traduzione ARM
• La traduzione ARM può avvalersi del salvataggio multiplo di r4
e lr (con aggiornamento automatico di sp)
• Notare come il ripristino dei registri possa permettere
automaticamente di caricare lr su pc e fare il return
automaticamente
int f(int n) {
return inc(n) - 4;
}

f:
stmfd sp!, {r4, lr}
bl inc
sub r0, r0, #4
ldmfd sp!, {r4, pc}
stmfd = stm full descending = stmdb 30
Ordinamento di array
• Passiamo a qualcosa di più complesso: un algoritmo
noto come «insert sort»
void sposta(int v[], size_t i) { void ordina(int v[], size_t n) {
size_t j; size_t i;
int appoggio; i = 1;
while (i < n) {
appoggio = v[i]; sposta(v, i);
j = i - 1; i = i+1;
while ((j >= 0) && (v[j] > appoggio)) { }
v[j+1] = v[j]; }
j = j-1;
}
v[j+1] = appoggio;
}

31
Traduzione RISC-V
• Cominciamo da sposta. Stavolta le cose sono più complesse.
Assumiamo che i parametri siano memorizzati in x10, x11 (a0, a1)
rispettivamente. Usiamo a3 per appoggio.

void sposta(int v[], size_t i) {


size_t j;
int appoggio;

appoggio = v[i];
j = i - 1;

sposta:
slli a4,a1,2 //a4 = i*4
add a5,a0,a4 //a5 = &v[i]
lw a3,0(a5) //a3 = v[i]
addiw a1,a1,-1 //a1 = a1-1 (i = j-1)

32
Traduzione RISC-V
continua
• Ciclo
while ((j >= 0) && (v[j] > appoggio)) {
v[j+1] = v[j];
j = j-1;
}

bltz a1,.L2 // se j < 0 esci dal ciclo


lw a4,-4(a5) // a4 = v[i-1]=v[j]
bge a3,a4,.L2 // se appoggio >= v[j] esci
li a2,-1 // carica -1 in a2
.L3:
sw a4,0(a5) // memorizza v[j] (a4) in v[j+1]
addiw a1,a1,-1 // a1 = a1-1
beq a1,a2,.L4 // salta se a1 = -1
addi a5,a5,-4 // j=j-1
lw a4,-4(a5)
bgt a4,a3,.L3 // Salta se v[j] > appoggio
j .L2
33
Traduzione RISC-V
continua
• Uscita da sposta
v[j+1] = appoggio;
}

.L4:
li a1,-1
.L2:
addi a1,a1,1
slli a1,a1,2
add a1,a0,a1
sw a3,0(a1) // v[j+1] = appoggio
ret

34
Traduzione RISC-V
continua
• Passiamo ora alla funzione ordina. Assumiamo che i parametri siano
memorizzati in x10, x11 (a0, a1) rispettivamente

void ordina(int v[], size_t n) {


size_t i;
i = 1;

ordina: li a5,1
ble a1,a5,.L11
addi sp,sp,-32
sd ra,24(sp)
sd s0,16(sp)
sd s1,8(sp)
sd s2,0(sp)
mv s1,a1
mv s2,a0
li s0,1

35
Traduzione RISC-V
continua
• Passiamo al loop
while (i < n) {
sposta(v, i);
i = i+1;
}

.L8:
mv a1,s0
mv a0,s2
call sposta
addiw s0,s0,1
bne s1,s0,.L8

36
Traduzione RISC-V
continua
• Epilogo ordina

ld ra,24(sp)
ld s0,16(sp)
ld s1,8(sp)
ld s2,0(sp)
addi sp,sp,32
jr ra
.L11:
ret

37
ARM
• Riguardiamo lo stesso codice implementato
tramite ARM
void sposta(int v[], size_t i) {
size_t j;
int appoggio;

appoggio = v[i];
j = i - 1;

sposta:
mov r2, r1, asl #2 // r2 = i * 4
add r3, r0, r2 // r3 = &v[i]
ldr ip, [r0, r1, asl #2] // ip = r12 (scratch) appoggio = v[i]
subs r1, r1, #1 // j = i - 1
38
ARM
• Vediamo il ciclo
while ((j >= 0) && (v[j] > appoggio)) {
v[j+1] = v[j];
j = j-1;
}

bmi L2 // salta se j < 0


ldr r2, [r3, #-4] // r2 = v[j] = v[i-1]
cmp ip r2
bge L2
L3:
str r2, [r3], #-4
sub r1, r1, #1
cmp r1, #1
beq L2
ldr r2, [r3, #-4]
cmp ip, r2
blt L3 39
ARM
• Uscita da sposta

v[j+1] = appoggio;
}

L2:
add r1, r1, #1
str ip, [r0, r1, asl #2]
bx lr

40
ARM
• Vediamo la procedura ordina, che non è foglia.
• Il salvataggio sullo stack dei registri avviene in un solo passo

void ordina(int v[], size_t n) {


size_t i;
i = 1;

ordina:
cmp r1, #1
bxle lr
stmfd sp!, {r4, r5, r6, lr} // full descending aka db
mov r5, r1
mov r6, r0
mov r4, #1

stmfd = stmdb 41
ARM
• Il loop
• Notare uscita contestuale con rispristino registri
while (i < n) {
sposta(v, i);
i = i+1;
}
ldmfd = ldmia
L8:
mov r1, r4
mov r0, r6
bl sposta
add r4, r4, #1
cmp r5, r4
bne L8
ldmfd sp!, {r4, r5, r6, pc} // full descending 42
Copia Stringhe
• Consideriamo ora
void copia_stringa(char d[], const char s[]) {
size_t i = 0;

while ((d[i] = s[i]) != `0`) {


i += 1;
}
}

43
Traduzione RISC-V
• Traduzione RISC-V
void copia_stringa(char d[], const char s[]) {
size_t i = 0;

copia_stringa:
addi sp, sp, -8 // aggiorna stack per inserire un elemento
sd x19, 0(sp) // salva x19
add x19, x0, x0 // i = 0

44
Traduzione RISC-V
continua
• Loop
while ((d[i] = s[i]) != `0`) {
i += 1;
}

LoopCopiaStringa:
add x5, x19, x11 // indirizzo di s[i]
lbu x6, 0(x5) // x6 = s[i]
add x7, x19, x10 // indirizzo di d[i]
sb x6, 0(x7) // d[i] = s[i]
beq x6, x0, LoopCopiaStringaEnd // se 0 vai a LoopCopiaStringaEnd
addi x19, x19, 1 // i += 1
jal x0, LoopCopiaStringa // salta a LoopCopiaStringa
LoopCopiaStringaEnd

45
Traduzione RISC-V
continua
• Chiusura

LoopCopiaStringaEnd
ld x19, 0(sp) // ripristina contenuto di x19
addi sp, sp, 8 // aggiorna lo stack eliminando un elemento
jalr, x0, 0(x1) // ritorna al chiamante

46
Traduzione ARM
• Inizio
void copia_stringa(char d[], const char s[]) {
size_t i = 0;

copia_stringa:
ldrb r3, [r1]
strb r3, [r0]
cmp r3, #0
bxeq lr

47
Traduzione ARM
• Loop
• Notare come possiamo usare l’aggiornamento del registro
per evitare un registro indice.

while ((d[i] = s[i]) != `0`) {


i += 1;
}

L3:
ldrb r3, [r1, #1]!
strb r3, [r0, #1]!
cmp r3, #0
bne L3
bx lr

48
CALCOLATORI
Toolchain: Come generare applicazioni in
linguaggio macchina

Giovanni Iacca
giovanni.iacca@unitn.it

Lezione basata su materiale preparato


con Luca Abeni, Luigi Palopoli e Marco Roveri
La lingua della CPU

• Una CPU “capisce” e riesce ad eseguire solo il linguaggio macchina


• Linguaggio di (estremamente!) basso livello
• Sequenza di 0 e 1
• Assembly: codici mnemonici (add, addi, etc.) invece di cifre binarie
• Più “gestibile” del linguaggio macchina...
• ... ma sempre troppo complesso per noi!
• In realtà, anche più complesso di quanto visto finora
• In genere, non si programma direttamente in assembly!
• Assembly generato a partire da linguaggio di alto livello...
• Chi fa la conversione? Compilatore!

2 / 25
Compilazione: Esempio

• Esempio di generazione di codice in linguaggio macchina da


linguaggio di alto livello: linguaggio C
1. Preprocessore: gestisce direttive #.... Generalmente,
sostituzione di codice
2. Compilatore: da C ad assembly (file .s)
3. Assembler: da assembly a linguaggio macchina (file .o)
4. Linker: mette assieme codice in linguaggio macchina e librerie per
generare un eseguibile
• Normalmente, un driver gestisce tutto questo in automatico
• Il file eseguibile può ora essere caricato in memoria con un’apposita
system call (in unix, la famiglia exec())

3 / 25
Un compilatore C

• Preprocessore: poco interessante per noi, ignoriamolo

4 / 25
Allocazione della memoria per programmi e dati nel RISC-V

SP → 0000 003f ffff fff016 Stack



Dati dinamici

Dati statici
0000 0000 1000 000016 .data

Testo
PC → 0000 0000 0040 000016 .text

Riservato
0

5 / 25
Usando gcc...

• gcc: gnu compiler collection


• Può compilare vari linguaggi di alto livello...
• ... generando linguaggio macchina per varie CPU (RISC-V
compreso!)
• Vari passaggi ad opera di diversi programmi: cpp, cc, as, ld
• gcc invoca i vari comandi usando i giusti parametri
• Default: invoca tutti i programmi necessari
• gcc -S si ferma dopo aver invocato cc (genera file assembly .s)
• gcc -c si ferma dopo aver invocato as (genera file oggetto .o)

6 / 25
Da C ad assembly

• Dato un file <file>.c, comando gcc -S invoca cc per generare un


file assembly <file>.s
• Sintassi: gcc -S <file>.c [-o <nomefile>]
• Senza opzione -o genera <file>.s
• Con -o salva il risultato della compilazione in <nomefile>

• cc è il Compilatore propriamente detto

• cc conosce l’architettura target (RISC-V, ARM, Intel X86, nel nostro


caso) meglio di un programmatore umano
• Spesso il codice assembly generato da cc è migliore di quello
generato “a mano”
• Possibili diversi livelli di ottimizzazione (-O...)
-O0 No ottimizzazioni, -ON con N ≥ 1 diversi e sofisticati livelli di ottimizzazione
• gcc -c -Q -ON --help=optimizers mostra le ottimizzazioni
abilitate con livello N
Provare con N=0,1, ... e vedere le differenze
• Assembly generato potrebbe essere di non facile lettura
Ottimizzazioni potrebbero riordinare istruzioni per sfruttare al meglio il processore
7 / 25
Da assembly a linguaggio macchina

• Dato un file <file>.c o <file>.s, gcc -c invoca cc e as o solo


as (risp.) per generare un file oggetto <file>.o
• Sintassi: gcc -c <file.{s|c}> [-o <nomefile>]
• Senza opzione -o genera <file>.o
• Con -o salva il risultato della compilazione in <nomefile>

• Assembler
• as fa spesso qualcosa in più rispetto alla semplice sostituzione di
codici mnemonici con sequenze di bit
• Pseudo-istruzioni
• Convertite in istruzioni riconosciute dalla CPU
• Converte numeri da decimale / esadecimale a binario
• Gestisce label
• Gestisce salti: se destinazione troppo lontana, j DEST va
convertita in caricamento di registro + jr
• Genera metadati

8 / 25
Pseudo-istruzioni

• Non corrispondono a vere e proprie istruzioni in Linguaggio Macchina


• Esempio: RISC-V non ha istruzioni native tipo mv fra registri.
• Ma sono utili per il programmatore (o il compilatore)
• L’assembler sa come convertirle in una o più istruzioni macchina
esistenti
• Esempi:
• mv x10, x11 // x10 assume valore di x11

addi x10, x11, 0 // x10 riceve il contenuto di x11 + 0

• li x9, 123 // carica il valore 123 in x9



addi x9, x0, 123 // x9 assume il valore x0 + 123

• j LABEL // Jump non condizionato a LABEL



jal x0, LABEL

• etc.
9 / 25
File oggetto

Composti da segmenti distinti:


• Header
• Specifica dimensione e posizione degli altri segmenti del file oggetto
• Segmenti
• Segmento di testo/Text segment: contiene il codice in linguaggio
macchina
• Segmento dati /Data segment: contiene tutti i dati (sia statici che
dinamici) allocati per la durata del programma (codice)
• Tabella dei simboli/Symbol table
• Associa simboli ad indirizzi (relativi)
• Enumera simboli non definiti (sono in altri moduli)
• Tabella di rilocazione/Relocation table
• Enumera istruzioni che fanno riferimento a istruzioni e dati che dipendono
da indirizzi assoluti (da “patchare”) nel momento in cui il programma viene
caricato in memoria
• Altre informazioni (debugging, etc.)
10 / 25
Da file oggetto ad eseguibili

• Dato un file <file>.c o <file>.s o <file>.o, gcc senza opzioni


-S e -c invoca anche il linker (ld) per generare un eseguibile
• Sintassi: gcc <file.{s|c|o}> [-o <nomefile>]
• Senza opzione -o genera il file a.out (su Windows a.exe)
• Con -o salva l’eseguibile in <nomefile>

• Linker ld: mette assieme uno o più file oggetto, eseguendo le


necessarie rilocazioni
• Decide come codice e dati sono disposti in memoria
• Associa indirizzi assoluti a tutti i simboli
• Risolve simboli che erano lasciati indefiniti in alcuni file .o
• “Patcha” le istruzioni macchina citate nella tabella di rilocazione (in
base agli indirizzi assegnati)
• Scopo di ld è quindi eliminare tabelle dei simboli e tabelle di
rilocazione, generando codice macchina con i giusti riferimenti
• Poiché un simbolo usato in un file può essere definito in un file diverso, ld
mette quindi assieme più file .o

11 / 25
Linker e simboli

• Un linker gestisce vari tipi di simboli:


• Simboli definiti (defined): associati ad un indirizzo (relativo) nella
tabella dei simboli
• Simboli non definiti (undefined): usati in un file (e quindi presenti
nella tabella dei simboli) ma definiti in un file diverso
• Simboli locali (o non esportati): definiti ed usati in un file (quindi
simili a simboli definiti), ma non usabili in altri file
• In tutti i casi, associa un indirizzo assoluto ad ogni simbolo
• Per simboli non definiti, cerca in altri file
• Se non trovato, errore di linking!

12 / 25
Linking in tre passi

1. Disporre in memoria i vari segmenti (.text, .data, etc.) dei file .o


• Segmenti testo uno dopo l’altro, idem per i segmenti dati, etc.
2. In base al passo precedente, assegnare un indirizzo assoluto ad ogni
simbolo contenuto nelle varie tabelle dei simboli
3. In base alle tabelle di rilocazione, correggere le varie istruzioni con gli
indirizzi calcolati

• Il risultato viene poi “incapsulato” in un file eseguibile


• Segmenti (testo, dati, etc.)
• Informazioni per il caricamento in memoria (indirizzo di caricamento dei
segmenti, indirizzo entry point, etc.)
• Altre informazioni (es. per debugging)

13 / 25
Librerie

• Esistono funzioni “predefinite” fornite dal compilatore / sistema


• Definite in file .o inclusi in ogni eseguibile che viene prodotto
• Buon numero di file oggetto linkati “di default” in ogni eseguibile
• Poco pratico!
• Libreria: collezione di file .o
• Invece di linkare un’enormità di file oggetto, si linka un’unica
libreria!
• Librerie: statiche o dinamiche
• Librerie statiche (.a):
• semplici collezioni di file oggetto; ld fa tutto il lavoro!
• Librerie dinamiche (.so):
• ld non fa molto... il vero linking avviene a tempo di esecuzione
(caricamento dinamico)!

14 / 25
Librerie statiche

• ld inserisce nell’eseguibile tutto il codice della libreria utilizzato dal


programma
• La libreria serve solo durante il linking (codice autocontenuto)
• Le dimensioni dell’eseguibile aumentano...
• Esempio: ogni eseguibile contiene una copia del codice di
printf...

• Caricamento del programma da parte del SO: semplice!

15 / 25
Librerie dinamiche

• ld inserisce nell’eseguibile riferimenti alle librerie usate ed alle


funzioni invocate...
• ... ma non le include nell’eseguibile!
• Ogni eseguibile contiene un riferimento ad un linker dinamico
(/lib/ld-linux.so)
• All’esecuzione del programma, viene caricato ed eseguito
/lib/ld-linux.so passandogli il programma stesso come argomento!
• ld-linux.so caricherà quindi l’eseguibile e le librerie (.so) da cui
dipende, e si occuperà di fare il linking
• La libreria serve anche per eseguire il programma (codice non autocontenuto)

• Caricamento del programma da parte del SO: complesso!


• Vantaggi/Svantaggi
• + Le dimensioni dell’eseguibile sono piccole
• + Possibile aggiornare librerie senza ricompilare
• - Il programma non è autocontenuto

16 / 25
Possibile complicazione: “lazy linking”

• A noi informatici piace


complicare le cose...
• ... e siamo pigri!
• Invece di fare le operazioni di
linking a tempo di caricamento,
posporle il piú possibile
• Se un eseguibile è linkato ad una
libreria, ma non ne invoca mai i servizi
a runtime, forse si può evitare di
linkarla...
• Invece di chiamare la vera
funzione, si chiama uno stub
che esegue caricamento,
rilocazione e linking quando
serve
• La seconda volta che si chiama la
procedura, il processo sarà piú
semplice perchè la procedura ora è già
stata caricata

17 / 25
Esempio: funzioni da compilare / linkare

• Programma composto da due file assembly (.s)

file1.o file2.o

.comm x,4,4 .comm y,4,4


... ...
.text .text
.globl func 1 .globl func 2
func 1 : func 2 :
l d x 1 0 , 0 ( x3 ) sd x 1 1 , 0 ( x3 )
j a l x1, 0 j a l x1, 0
... ...

18 / 25
Esempio: file oggetto 1

header campo valore


nome file1
text size 10016
data size 2016
text indirizzo (rel.) istruzione
0 ld x10, 0(x3)
4 jal x1, 0
8 ...
data indirizzo (rel.) simbolo
0 x
... ...
tabella simboli simbolo indirizzo
x *UND*
func 2 *UND*
... ...
tabella rilocazione indirizzo tipo istruzione simbolo
0 ld x
4 jal func 2
Procedura func 1 necessita indirizzo di x da mettere nella ld e indirizzo func 2 da mettere nella jal.
19 / 25
Esempio: file oggetto 2

header campo valore


nome file2
text size 20016
data size 3016
text indirizzo (rel.) istruzione
0 sd x11, 0(x3)
4 jal x1, 0
8 ...
data indirizzo (rel.) simbolo
0 y
... ...
tabella simboli simbolo indirizzo
y *UND*
func 1 *UND*
... ...
tabella rilocazione indirizzo tipo istruzione simbolo
0 sd y
4 jal func 1
Procedura func 2 necessita indirizzo di y per la sd e indirizzo di func 1 per la sua jal.
20 / 25
Linker: mettendo tutto assieme...
Prima file1 poi file2
header campo valore
text size AAA
data size BBB
... ...
text indirizzo istruzione
KKKKKKKKKKKKKKKK16 ld x10, UUU(x3)
LLLLLLLLLLLLLLLL16 jal x1, YYY
... ...
MMMMMMMMMMMMMMMM16 sd x11, VVV(x3)
NNNNNNNNNNNNNNNN16 jal x1, TTT
... ...
data indirizzo simbolo
PPPPPPPPPPPPPPPP16 x
... ...
JJJJJJJJJJJJJJJJ16 y
... ...
Procedura func 1 necessita indirizzo di x da mettere nella ld e indirizzo func 2 da mettere nella jal.
Procedura func 2 necessita indirizzo di y per la sd e indirizzo di func 1 per la sua jal.
21 / 25
Linker: mettendo tutto assieme... (cont.)

• Header
• Text size:
10016 (file1) + 20016 (file2) = 30016
• Data size:
2016 (file1) + 3016 (file2) = 5016

• Disposizione segmenti in memoria 0000 003f ffff fff016 Stack



• text: inizia a 000000000040000016

prima file1 (dimensione 10016 ), Dati dinamici
poi file2 (indirizzo 000000000040010016 )
Dati statici
• data: inizia a 000000001000000016 ; 0000 0000 1000 000016 .data

prima file1 (dimensione 2016 ), Testo


poi file2 (indirizzo 000000001000002016 ) PC=0000 0000 0040 000016 .text

Riservato
• Assegnamento indirizzi a simboli: 0

• func 1: 000000000040000016
func 2: 000000000040010016
• x: 000000001000000016
y: 000000001000002016

22 / 25
Linker: mettendo tutto assieme... (cont.)

header campo valore


text size 30016
data size 5016
... ...
text indirizzo istruzione
000000000040000016 ld x10, UUU(x3)
000000000040000416 jal x1, YYY
... ...
000000000040010016 sd x11, VVV(x3)
000000000040010416 jal x1, TTT
... ...
data indirizzo simbolo
000000001000000016 x
... ...
000000001000002016 y
... ...

23 / 25
Linker: mettendo tutto assieme... (cont.)

• Calcolo valore per jal:


• le istruzioni utilizzano indirizzo relativo al PC, basta fare differenza tra
indirizzo della jal e indirizzo della procedura:
• il campo indirizzo di jal a 40000416 che salta a 40010016 (indirizzo procedura func 2),
conterrà 40010016 − 40000416 = 25210
• il campo indirizzo di jal a 40010416 che salta a 40000016 (indirizzo procedura func 1),
conterrà 40000016 − 40010416 = −26010

• Calcolo offset per ld/sd


• Sono più complessi da calcolare perchè dipendono da indirizzo base (x3,
per semplicità assumiamo x3 = 000000001000000016 ):
• inseriamo 016 nel campo indirizzo di ld a 40000016 per ottenere indirizzo di x
(000000001000000016 )
• inseriamo 2016 (ovvero 3210 ) nel campo indirizzo di sd a 40010016 per ottenere indirizzo
di y (000000001000002016 )
• NOTA: gli indirizzi associati alle operazioni di store vengono gestiti come per le load, ad
eccezione del fatto che il formato istruzioni tipo S rappresenta le costanti diversamente
dal formato I delle load.

24 / 25
Quindi...

header campo valore


text size 30016
data size 5016
... ...
text indirizzo istruzione
000000000040000016 ld x10, 0(x3)
000000000040000416 jal x1, 25210
... ...
000000000040010016 sd x11, 32(x3)
000000000040010416 jal x1, −26010
... ...
data indirizzo simbolo
000000001000000016 x
... ...
000000001000002016 y
... ...

25 / 25
CALCOLATORI
Il processore
Giovanni Iacca
giovanni.iacca@unitn.it

Lezione basata su materiale preparato


dai Prof. Luigi Palopoli e Marco Roveri
Obiettivi
• In questa serie di lezioni cercheremo di capire come è
strutturato un processore
• Le informazioni che vedremo si integrano con quanto visto
sulle reti logiche
• Facciamo riferimento ad un insieme di istruzioni RISC-V
ridotto
§ ISTRUZIONI DI ACCESSO ALLA MEMORIA
ü ld, sd
§ ISTRUZIONI MATEMATICHE E LOGICHE
ü add, sub, and, or
§ ISTRUZIONI DI SALTO
ü beq
• Le altre istruzioni si implementano con tecniche simili
Panoramica generale
• Nell’esecuzione delle istruzioni, per come è
progettata l’ISA, vi sono molti tratti comuni
• Le prime due fasi, per ogni istruzione, sono
§ Prelievo dell’istruzione dalla memoria
§ Lettura del valore di uno o più registri operandi che
vengono estratti direttamente dai campi
dell’istruzione
• I passi successivi dipendono dalla specifica
istruzione ma, fortunatamente, sono molto simili
per ciascuna delle tre classi individuate
Passi per ciascuna classe
• Tutti i tipi di istruzioni considerate usano la ALU (unità
logico aritmetica) dopo aver letto gli operandi
§ Le istruzioni di accesso alla memoria per calcolare l’indirizzo
§ Le istruzioni aritmetico/logiche per eseguire quanto previsto
dall’istruzione
§ I salti condizionati per effettuare il confronto
• Dopo l’uso della ALU il comportamento differisce per le tre
classi
§ Le istruzioni di accesso alla memoria richiedono o salvano il dato
in memoria
§ Le istruzioni aritmetiche/logiche memorizzano il risultato nel
registro target
§ Le istruzioni di salto condizionato cambiano il valore del registro
PC secondo l’esito del confronto
Schema di base
• Di seguito illustriamo la struttura di base della parte
operativa (o datapath) per le varie istruzioni
4. Il risultato della ALU viene
5. Il PC viene usato per memorizzare (o
aggiornato prelevare) un dato, o per
memorizzare in un registro

1. Tutte le istruzioni
usano il PC per 2. Caricamento
prelevare l’istruzione 3. Opera la ALU
degli operandi
Pezzi mancanti
• La figura precedente è incompleta e crea
l’impressione che ci sia un flusso continuo di dati
• In realtà ci sono punti in cui i dati arrivano da
diverse sorgenti e bisogna sceglierne una (punto
di decisione)
• E’ il caso per esempio dell’incremento del PC
§ Nel caso “normale” il suo valore proviene da un
circuito addizionatore (che lo fa puntare alla word
successiva a quella appena letta)
§ Nel caso di salto il nuovo indirizzo viene calcolato a
partire dall’offset contenuto nel campo dell’istruzione
Pezzi mancanti
• Altro esempio
§ Il secondo operando della ALU può provenire dal
banco registri (per istruzioni di tipo R) o dal codice
dell’istruzione stessa (per istruzioni di tipo I)
• Per selezionare quale delle due opzioni
scegliere viene impiegato un particolare rete
combinatoria (multiplexer) che funge da
selettore dei dati
Il multiplexer
• Come abbiamo già visto, il multiplexer ha due (o
più) ingressi dati, e un ingresso di controllo
• Sulla base dell’ingresso di controllo si decide
quale degli input debba finire in output
Ulteriori pezzi mancanti
• Le linee di controllo dei multiplexer vengono impostate
sulla base del tipo di istruzioni
• I vari blocchi funzionali hanno ulteriori ingressi di
controllo
§ La ALU ha diversi ingressi per decidere quale operazione
effettuare
§ Il banco registri ha degli ingressi per decidere se scrivere o
meno in un registro
§ La memoria dati ha degli ingressi per decidere se vogliamo
effettuare letture o scritture
• Per decidere come impiegare i vari ingressi di controllo
abbiamo bisogno di un’unità che funga da “direttore
d’orchestra”
Una figura più completa
Mux per Nel registro
decidere come target
aggiornare il PC memorizziamo
il risultato
della ALU o
quello che
preleviamo
dalla
memoria?

Secondo
Unità che operando
genera i vari registro o
segnali di immediato?
controllo
Informazioni di base
• ASSUNZIONE SEMPLIFICATIVA:
§ Il processore lavora sincronizzandosi con i cicli di
clock
§ Per il momento facciamo l’assunzione
semplificativa che tutte le istruzioni si svolgano in
un singolo ciclo di clock (lungo abbastanza)
• Prima di entrare nella descrizione dei vari
componenti ricordiamo velocemente alcuni
concetti di reti logiche
Reti logiche
• Definiamo rete logica combinatoria un circuito composto di porte
logiche che produce un output che è una funzione (statica)
dell’input
§ Esempio: il multiplexer visto prima
• Vi sono inoltre elementi che chiamiamo «di stato»:
§ In sostanza se a un certo punto salviamo il valore degli elementi di
stato e poi lo ricarichiamo, il computer riparte esattamente da dove si
era interrotto
§ Nel nostro caso elementi di stato sono: registri, memoria dati,
memoria istruzioni
• Gli elementi di stato sono detti sequenziali perché l’uscita a un
ingresso dipende dalla storia (sequenza) degli ingressi precedenti
• Gli elementi di stato hanno (almeno) due ingressi:
§ Il valore da immettere nello stato
§ Un clock con cui sincronizzare le transizioni di stato
Flip-Flop
• Come abbiamo già visto, l’elemento base per memorizzare
un bit è un circuito sequenziale chiamato flip-flop D-latch

In Out
Latch

CLK

• I registri possono essere ottenuti come array di latch (o


in altri modi simili)
• Terminologia
• Asserito: segnale logico a livello alto
• Non Asserito: segnale logico a livello basso
Temporizzazione
• La metodologia di temporizzazione ci dice quando i
segnali possono essere letti o scritti in relazione al
clock
• E’ importante stabilire una temporizzazione
§ Se leggo e scrivo su registro, devo sapere se il dato che
leggo è quello precedente o successivo alla scrittura
• La tecnica di temporizzazione più usata è quella
sensibile ai fronti
§ Il dato viene memorizzato in corrispondenza della salita o
della discesa del fronte di clock
• I dati presi dagli elementi di stato sono relativi al ciclo
precedente
Esempio

Al tempo t l’elemento
di stato 1 viene Il valore viene
Al tempo t+T il valore
aggiornato propagato attraverso
arriva all’elemento di
una rete
stato 2
combinatoria
Considerazioni
• Il tempo di clock T deve essere scelto in modo da
dare tempo ai dati di attraversare la rete
combinatoria
• Nel caso del RISC-V a 64 bit quasi tutti gli
elementi di stato e combinatori hanno ingressi ed
uscite a 64 bit
• La metodologia di memorizzazione sensibile ai
clock permette di realizzare interconnessioni che,
a prima vista, creerebbero dei cicli di retroazione
che renderebbero impredicibile l’evoluzione del
sistema
Esempio
• Consideriamo un caso come questo:

Elemento Logica
di stato combinatoria
s f()

• Se non avessimo temporizzazioni precise dovremmo


scrivere (qualcosa di indecidibile):
s = f(s)
• Grazie alla temporizzazione sensibile al clock abbiamo:
s(t+T) = f(s(t))
che invece produce un’evoluzione ben determinata
Realizzazione del datapath
• Passiamo ora in rassegna le varie componenti che
ci servono per la realizzazione del datapath

Memoria Registro che Addizionatore


dove sono contiene (ALU specializzata)
memorizzate l’indirizzo per incrementare il
le istruzioni dell’istruzione PC
da caricare
Prelievo dell’istruzione
• Usando gli elementi che abbiamo visto possiamo
mostrare come effettuare il prelievo dell’istruzione con
un’apposita circuiteria

Il PC viene
spostato in
avanti di una
word per il
prossimo ciclo

A ogni ciclo di
Il codice
clock viene
dell’istruzione
prelevata
viene reso
l’istruzione
disponibile
puntata dal PC
Istruzioni di tipo R
• Cominciamo dal vedere come vengono eseguite le istruzioni di tipo R
• Abbiamo visto che si tratta di istruzioni aritmetiche/logiche che
operano tra registri e producono un risultato che viene memorizzato
in un registro
• Esempio:
add x5, x6, x7

• Come noto, il codice binario corrispondente ha la forma:

7 bit 5 bit 5 bit 3 bit 5 bit 7 bit


funz7 rs2 rs1 funz3 rd codop
31:25 24:20 19:15 14:12 11:7 6:0 Tipo R
0000000 00111 00110 000 00101 0110011
Blocchi funzionali richiesti
• Per effettuare questi calcoli ho bisogno di due
ulteriori blocchi funzionali.

ALU: effettua
Banco registri: dà
Se abilitato in l’operazione
in output i registri
scrittura scrive nel aritmetica codificata
specificati nel
registro specificato in 4 bit. Setta un bit
registro di lettura
il dato in ingresso in uscita se il
1e2
risultato è zero
Istruzioni load/store
• Consideriamo ora anche le istruzioni load (ld) e store (sd)
• Forma generale

ld x5, offset(x6)
12 bit 5 bit 3 bit 5 bit 7 bit Tipo I
offset rs1 funz3 rd codop

sd x5, offset(x6)
7 bit 5 bit 3 bit 5 bit 7 bit Tipo S
5 bit
Offset rs2 rs1 funz3 Offset codop
[11,5] [4:0]

• Per entrambe si deve calcolare un indirizzo di memoria dato dalla somma


di x6 con l’offset
• Per entrambe occorre leggere dal register file
• Quindi per eseguire queste istruzioni ci servono ancora la ALU e il register
file
Istruzioni load/store
• Notare che l’offset viene memorizzato in un campo a
12 bit che occorrerà estendere a 64 bit (replicando per
42 volte il bit di segno)
• In aggiunta alle componenti che abbiamo visto prima
occorre un’unità di memoria dati dove memorizzare
eventualmente con sd (o da cui leggere con ld)

A differenza della
memoria istruzioni
questa può essere
usata in lettura e
scrittura. Quindi ho
bisogno di
comandi appositi.
Salto condizionato
• L’istruzione di salto condizionato ha la forma

beq x5, x6, offset


7 bit 5 bit 5 bit 3 bit 5 bit 7 bit Tipo SB
Offset rs2 rs1 funz3 Offset codop
[12,10:5] [4:1,11]

• Anche in questo caso bisogna sommare all’attuale PC l’offset a 12 bit (dopo averlo
esteso a 64 bit con segno) che consente di fare salti da -212 a 212.
• Due note:
• L’architettura dell’insieme delle istruzioni specifica che l’indirizzo di base per il
calcolo dell’indirizzo di salto è quello dell’istruzione di salto stessa.
• L’architettura stabilisce che il campo offset sia spostato di 1 bit a sinistra per
fare sì che l’offset codifichi lo spiazzamento in numero di mezze parole
(aumentando lo spazio di indirizzamento dell’offset di un fattore 2 rispetto a
codifica dello spiazzamento in byte).
• La ragione per lo shift di uno (anziché di due) è dovuta alla presenza non
documentata sul libro di istruzioni compresse a 16 bit per alcuni processori
RISC-V.
Salto condizionato
• Nell’esecuzione della beq occorre anche un meccanismo in
base al quale decidere se aggiornare il PC a PC + 4 o a PC +
offset

Calcolo
dell’indirizzo di
salto

La ALU effettua la
sottrazione e se il
risultato è 0 si può
saltare
Progetto di un’unità di elaborazione
• Siccome abbiamo il requisito di eseguire ogni
istruzione in un ciclo di clock, non possiamo usare
un’unità funzionale più di una volta in ogni ciclo
§ Perciò dobbiamo distinguere memoria dati e memoria
istruzioni
• Inoltre occorre condividere il più possibile le varie
unità
§ Perciò occorreranno opportuni multiplexer per poter
selezionare l’input corretto all’unità funzionale tra
quelli possibili
Esempio
• Con questo circuito riusciamo a eseguire istruzioni di Tipo R e
istruzioni di trasferimento da (Tipo I) e alla memoria (Tipo S)
Per istruzione di tipo R:
• ALUSrc = 0
• MemtoReg = 0
• REGwrite = 1
• MemRead = 0
• MemWrite = 0

Per istruzione ld
• ALUSrc = 1
• MemtoReg = 1
• REGwrite = 1
• MemRead = 1
• MemWrite = 0

Per istruzione sd
• ALUSrc = 1
• MemtoReg = X
• REGwrite = 0
• MemRead = 0
• MemWrite = 1
Un esempio più completo

Circuiteria per i
salti condizionati

Con questo
DATAPATH
possiamo fare
istruzioni di tipo R,
memorizzazioni,
caricamento e salti
condizionati
Prima implementazione completa
• Per arrivare a una prima implementazione
completa partiamo dal datapath mostrato e
aggiungiamo la parte di controllo
• Implementeremo le istruzioni
üadd, sub, and, or
üld, sd
übeq
Cominciamo dalla ALU
• La ALU viene impiegata per:
§ Effettuare operazioni logico-aritmetiche (tipo R), compreso slt
§ Calcolare indirizzi di memoria (per sd e ld)
§ Sottrazione per beq
• Per queste diverse operazioni abbiamo una diversa configurazione
degli input di controllo (Linea controllo ALU)

Linea controllo ALU Operazione


0000 AND
0001 OR
0010 Somma
0110 Sottrazione
Ancora sul controllo della ALU
• Per generare i bit di controllo della ALU
useremo una piccola unità di controllo che
riceve in ingresso
§ i campi funz7 e funz3 prelevati dall’istruzione
§ due bit detti «ALUop»
üALUop = 00 -> somma (per istruzioni di sd e ld)
üALUOp = 01 -> sottrazione (per beq)
üALUOp = 10 -> operazione di tipo R (specificata da
funz7 e funz3)
Ancora sul controllo della ALU
Tabella riassuntiva
Decodifica a livelli multipli
• Quello che abbiamo visto è un sistema di
decodifica e generazione dei comandi a due
livelli
§ Livello 1: (unità di controllo) genera i segnali di
controllo ALUOp per l’unità di controllo della ALU
§ Livello 2: (unità di controllo della ALU) genera i
segnali di controllo per la ALU
Unità di controllo dell’ALU
• I segnali di controllo della ALU sono generati
da una rete logica combinatoria (unità di
controllo della ALU)
• Bisognerebbe elencare tutte le combinazioni
di ingresso di ALUop e dei campi funz7 e funz3
(12 bit in tutto)
• Per evitare di elencare tutte le combinazioni
(212 = 4096) useremo X come wildcard (un po’
come * nel filesystem).
Unità di controllo dell’ALU
Tabella di verità (compressa):

ld/sd
beq
add
sub
AND
OR
Unità di controllo principale
• Riguardiamo i campi.
codici
operativi

due registri Registro codice


da leggere target operativo
Unità di controllo principale
• Riguardiamo i campi.
codici
operativi

registro Registro codice


offset per ld
base per ld target operativo
Unità di controllo principale
• Riguardiamo i campi.
registro
registro codice
sorgente
base per sd operativo
per sd

codice
offset per sd
operativo
Unità di controllo principale
• Riguardiamo i campi.
registri
codice
comparazione
operativo
per beq

offset per codice


beq operativo
Panoramica dei segnali di controllo

Nota: nel libro (fig 4.15)


c’è un errore: lo shift è
di 1 non di due!
Tabella riassuntivo dei
segnali di controllo
Schema complessivo
L’unità di controllo
• L’unità di controllo genera tutti i segnali di
controllo (incluso ALUOp)
• Ancora è una rete combinatoria che prende come
input il codice operativo dell’istruzione e genera i
comandi del caso, secondo la seguente tabella
Partiamo dalla ADD
• Per eseguire un’istruzione di tipo R
(ad esempio: add x5, x6, x7) occorre:

1. prelevare l’istruzione dalla memoria e incrementare il PC di 4


2. leggere x6 e x7 dal register file mentre l’unità di controllo
principale calcola il valore da attribuire alle linee di controllo
3. attivare la ALU con in input i dati dal register file usando alcuni
bit del codice operativo per selezionare l’operazione della ALU
(ad esempio: add)
4. memorizzare il risultato nel registro destinazione (x5)

• Tutto questo avviene in un solo ciclo


Passi sul processore
Prelievo:
nel Mux
rimane attivo
il ramo alto

In questo Mux è
attiva la linea
bassa (non uso la
memoria per il
risultato)

Istruzione di tipo
R: il registro Istruzione di tipo
destinazione è R: il controllore
codificato della ALU usa un
in 11 - 7 bit di funz7 e
tutto funz3
Load
• Consideriamo ora l’istruzione
ld x5, offset(x6)
• Fasi
1. la fase di prelievo istruzione ed incremento del PC è
uguale a prima
2. prelevare x6 dal register file
3. la ALU somma il valore letto dal register file i 12 bit del
campo offset dell’istruzione, dotati di segno ed estesi a
64 bit.
4. il risultato della somma viene usato come indirizzo per
memoria dati
5. il dato prelevato dall’unità di memoria dati viene scritto
nel register file nel registro x5
Passi sul processore
Prelievo:
nel Mux rimane
attivo il ramo
alto

In questo Mux è
attiva la linea 1
(secondo input
dato da offset)

In questo Mux,
Il registro stavolta, il dato
destinazione è che passa è
codificato in 11-7 quello sopra
(provenienza
memoria)
Salto condizionale
• Consideriamo ora l’istruzione
beq x5, x6, offset
• Fasi
1. la fase di prelievo istruzione ed incremento del PC è
uguale a prima
2. prelevare x5 e x6 dal register file
3. la ALU sottrare x5 da x6. Il valore del PC viene sommato
ai 12 bit del campo offset dell’istruzione, dotati di segno,
estesi a 64 bit e fatti scorrere di una posizione a sinistra.
Il risultato costituisce l’indirizzo di destinazione del salto
4. la linea Zero in uscita dalla ALU viene usata per decidere
da quale sommatore prendere l’indirizzo successivo da
scrivere nel PC.
Sul processore
Con branch
settato, se il
risultato della ALU
è zero si attiva il
ramo uno

Il registro
target è del La ALU
tutto sottrae I due
irrilevante registri
Implementazione
• Dopo aver capito a cosa servono i vari comandi
possiamo passare all’implementazione secondo
la tabella già vista in precedenza

• Le diverse colonne di questa tabella vanno a


determinare il codice operativo (bit da 0 a 6
dell’istruzione, insieme agli altri codici operativi
addizionali)
Implementazione
• A questo punto gli input e gli output sono
quelli specificati nella seguente tabella:
Considerazioni conclusive
• Abbiamo visto come realizzare un semplice
processore che esegue le istruzioni in un ciclo
• Questo non si fa più perché:
§ A dettare il clock sono le istruzioni più lente
(accesso alla memoria)
§ Se si considerano istruzioni più complesse di
quelle che abbiamo visto, le cose peggiorano
ancora di più (esempio: istruzioni floating point)
§ Non si riesce a fare ottimizzazioni aggressive sulle
operazioni fatte più di frequente
CALCOLATORI
La pipeline
Giovanni Iacca
giovanni.iacca@unitn.it

Lezione basata su materiale preparato


con i Prof. Luigi Palopoli e Marco Roveri
Ripartiamo da questo….
• Abbiamo visto come realizzare un semplice processore
che esegue le istruzioni in un ciclo
• Questo non si fa più nelle implementazioni moderne
perché:
§ A dettare il periodo di clock (comune a tutte istruzioni)
sono le istruzioni più lente (ovvero istruzioni di accesso alla
memoria)
§ Se si mettono istruzioni più complesse di quelle che
abbiamo visto, le prestazioni peggiorano ulteriormente
(esempio gestione operazioni su floating point)
§ Non si riescono a fare ottimizzazioni aggressive sulle cose
fatte più di frequente.
2
Che fare allora?
• Il cosa fare ce lo insegnò Henry Ford con la
catena di montaggio (assembly line):
“If everyone is moving
forward together, then
success takes care of itself.”

“Coming together is a
beginning. Keeping together
is progress. Working together
is success.” 3
Un esempio
• Supponiamo di dover fare il bucato e che questo
consista nelle seguenti attività:

1. Mettere la biancheria nella lavatrice


2. Terminato il lavaggio mettere i panni nell’asciugatrice
3. Terminata l’asciugatura mettere i panni sull’asse da stiro
e procedere alla stiratura
4. Finita la stiratura chiedere al proprio co-inquilino di riporre i
panni

• Abbiamo un grande vantaggio se mentre la


lavatrice lava un bucato, l’asciugatrice ne asciuga
un altro e io ne stiro un altro ancora 4
Una rappresentazione
grafica
Supponendo che ciascuna
fase duri 30 minuti, il ciclo di
lavaggio dura due ore (tra le
18 e le 20 per il primo).
Con il secondo metodo fino
alle 21.30 ne faccio 4.

Notare che il tempo per fare


un bucato (tempo di risposta)
rimane invariato (due ore).
Il throughput passa da 1/4 a 4/7
(miglioramento di 2.3).
Se ci fossero molto più di 4 bucati, si
potrebbe arrivare ad un fattore
di circa 4 di miglioramento.
5
Andiamo su un esempio più serio
• Tornando al RISC-V, le fasi di esecuzione di
un’istruzione sono le seguenti:
1. Prelievo dell’istruzione dalla memoria
2. Lettura dei registri e decodifica dell’istruzione
3. Esecuzione di un’operazione (tipo R) o calcolo di un indirizzo
4. Accesso a un operando nella memoria dati (se richiesto)
5. Scrittura del risultato in un registro (se richiesto)

• Conseguentemente la pipeline dovrà avere


cinque stadi
6
Un esempio
• Limitiamoci per il momento a una pipeline che
sia in grado di effettuare le seguenti
operazioni:
§ LOAD (ld), STORE (sd)
§ Somma (add), Sottrazione (sub), AND (and), OR
(or)
§ Branch on equal (beq)

7
Tempi
• I tempi richiesti per le varie fasi sono i seguenti:

• L’istruzione più lenta è la ld. Quindi ci si deve


adeguare ad essa per la scelta del clock

8
Varie implementazioni
• Diagramma temporale per sequenza istruzioni:
Implementazione
a singolo ciclo

Implementazione
a pipeline

Tempo totale tra fine


prima a fine terza
istruzione passa da
(3*800) a 1400 ps.9
Confronto di prestazioni
• Un confronto di prestazioni può essere fatto
con la seguente formula (valida se la pipeline
opera in condizioni ideali)
Tempo tra due istruzioni senza pipeline
Tempo tra due istruzioni con pipeline =
Numero di stadi della pipeline

• La formula suggerisce che con cinque stadi


dovremmo arrivare a un tempo tra due
istruzioni (inverso del throughput) che
dovrebbe essere 1/5.
10
Osservazioni
• Nell’esempio precedente siamo passati da 2400 a
1400 (miglioramento 1.7) come mai non 5?
• Prima osservazione: per ottenere una prestazione di
1/5 dovremmo portare il clock a 160 ps (non possibile)
perché ci sono alcune fasi che durano 200 ps (al più
4 volte)
• Seconda osservazione: abbiamo considerato poche
istruzioni e non abbiamo fatto in tempo a riempire la
pipeline.

11
Comportamento al limite
• Se consideriamo molte più istruzioni (ad
esempio ne aggiungiamo 1000000 all’esempio
di prima) per un totale di 1000003 si passa da
un tempo di esecuzione di 1000003*800ps a
un tempo di esecuzione di 1400 +
1000000*200ps.
• Se facciamo il rapporto vediamo che
l’incremento prestazionale in termini di
throughput si avvicina al 400%

12
Vantaggi del RISC
Vantaggi delle architetture RISC
• Primo vantaggio: Tutte le istruzioni hanno la stessa lunghezza.
Questo facilita di molto il prelievo (sempre una word).
• Secondo vantaggio: I codici degli operandi sono in posizione
fissa. Questo permette di accedervi leggendo il register file in
parallelo con la decodifica dell’istruzione.
• Terzo vantaggio: Gli operandi residenti in memoria sono
possibili solo per ld/lw e sd/sw. Ciò permette di usare la ALU per
il calcolo di indirizzi (cosa che non sarebbe possibile se
dovessimo usare le ALU in due fasi della stessa istruzione).
• Quarto vantaggio: L’uso di accessi allineati fa sì che gli accessi in
memoria avvengano sempre in un ciclo di trasferimento
(impegnando un solo stadio della pipeline). 13
Hazard
• In condizioni normali la pipeline permette di
eseguire un’istruzione per ciclo di clock.
• Alle volte questo non è possibile per il
verificarsi di «condizioni critiche» (detti
«hazard»).
• Passiamo in rassegna alcune tipologie di
hazard.

14
Hazard Strutturali
• Una condizione di hazard strutturale è una
condizione per la quale l’architettura
dell’elaboratore rende impossibile
l’esecuzione di alcune sequenze di istruzioni in
pipeline.
• Ad esempio, se io disponessi di un’unica
memoria, non potrei nello stesso ciclo,
caricare istruzioni e memorizzare (o prelevare)
operandi dalla memoria.

15
Hazard sui dati
• Questo tipo di hazard si verifica quando la pipeline deve
essere messa in stallo per ottenere delle informazioni dagli
stadi precedenti
• Nell’esempio della stiratura, se mi accorgo che manca un
calzino, devo interrompere la stiratura dell’altro e andare a
cercarlo (bloccando anche le fasi precedenti).
• Nel caso del RISC-V, consideriamo la seguente sequenza

add x1, x2, x3


sub x4, x1, x5

• Il problema è che x1 viene memorizzato nella quinta fase,


mentre la sub dopo ne ha bisogno nella seconda fase…
quindi è costretta ad aspettare per 3 cicli di clock!
16
Come risolverlo?
• L’hazard precedente blocca il completamento
della seconda istruzione per tre cicli di clock
• In certi casi possiamo cavarcela a livello di
compilazione invertendo alcune istruzioni
• Tuttavia il compilatore può risolvere il
problema solo in alcuni casi
• In generale, è utile osservare che non occorre
aspettare di aver memorizzato il risultato

17
Torniamo all’esempio
• Una rappresentazione grafica per l’operazione della
pipeline su una delle istruzioni è la seguente:

Instruction Instruction
Write back
fetch decode
Instruction Accesso alla (scrittura
(quadratino (quadratino
Execute memoria risultato nel
memoria spezzato
register file)
istruzioni) register file)

Ombra = unità usata per istruzione


(a destra: lettura; a sinistra: scrittura) 18
Operand forwarding
• L’operand forwarding (detto anche propagazione, o
anche bypass) viene usato per rendere il risultato
disponibile bypassando l’operazione di write back

Appena il risultato è
disponibile viene
passato avanti all’unita
che lo deve usare 19
Operand forwarding
(continua)
• L’operand forwarding funziona solo se lo stadio a
cui il dato viene propagato è successivo nel tempo
allo stadio dal quale viene prelevato
§ Non si può propagare tra l’uscita della fase di accesso
alla memoria istruzioni e l’ingresso della fase di
esecuzione dell’istruzione successiva:
üOccorrerebbe propagare un dato indietro nel tempo

20
Operand forwarding
(continua)
• Supponiamo di avere sequenza:
ld x1, 0(x2)
sub x4, x1, x5
§ x1 sarebbe disponibile solo dopo il quarto stadio della prima
istruzione
ü Troppo tardi per essere usato come input al terzo stadio della sub
ü Dovremmo imporre uno «stallo della pipeline» della durata di un ciclo
di clock («hazard sui dati» di una load)

21
Hazard sul controllo
• Il terzo tipo di hazard riguarda il controllo
(sostanzialmente i salti condizionati)
• Torniamo all’esempio del bucato
§ Supponiamo che, a seconda del livello di sporco, si
voglia decidere per un lavaggio aggressivo
§ Quello che dovrei fare è verificare la condizione dei
panni all’uscita dell’asciugatrice e su questa base
cambiare le impostazioni
§ … ma nel far questo, si blocca la pipeline (fino a che
non ho finito l’asciugatura non posso procedere al
lavaggio della prossima mandata)
22
Salto condizionato
• Il caso simile a quello del bucato si presenta
con i salti condizionati
• Supponiamo di avere un circuito molto
sofisticato che ci permette di calcolare
l’indirizzo di salto già al secondo stadio
• Comunque, dobbiamo bloccare la pipeline per
uno o due cicli

23
Esempio

Assumiamo che il salto


venga eseguito e che la
destinazione sia la or

Alla fine del prelievo capisco che


è un beq e aspetto per un ciclo
prima di fare il prelievo di quella
successiva 24
Predizione del salto
• Se la pipeline è più lunga, generare questo
stallo risulta in un incremento elevato della
durata che risulta in una riduzione delle
prestazioni
• Quello che si fa è avere dei circuiti che
prevedano i salti
• Ad esempio, nel caso precedente si può
assumere che il salto non venga effettuato, e
poi correggersi in caso contrario

25
Esempio precedente

Si assume
che il salto
non venga
effettuato

26
Circuiteria di branch prediction
• L’esempio che abbiamo appena visto non è
particolarmente sofisticato (funziona bene
solo se il branch non viene effettuato)
• Esistono circuiterie più sofisticate che
permettono di memorizzare l’esito del branch
precedente e assumere che il comportamento
si mantenga coerente

27
Unità di elaborazione finale
e relative unità di controllo

EX/MEM

MEM/WB

28
CALCOLATORI
La gerarchia di memoria
Giovanni Iacca
giovanni.iacca@unitn.it

Lezione basata su materiale preparato


con i Prof. Luigi Palopoli e Marco Roveri
Organizzazione gerarchica
della memoria
• Un elaboratore senza memoria non funziona ...
• La memoria negli elaboratori non è tutta uguale
• Per decenni il sogno di ogni programmatore è
stato quello di avere *tanta* memoria con
accessi ultrarapidi
• Esistono compromessi tra costo, prestazioni e
dimensione della memoria

2
Basics ...
• La memoria serve a contenere dati, bisogna poter
leggere e scrivere in memoria...
• La memoria indirizzata direttamente (memoria
principale, memoria cache):
§ è di tipo volatile, cioè il suo contenuto viene perso se si
spegne l’elaboratore
§ è limitata dallo spazio di indirizzamento del processore
• La memoria indirizzata in modo indiretto (memoria
periferica):
§ è di tipo permanente: mantiene il suo contenuto anche
senza alimentazione
§ ha uno spazio di indirizzamento “software” non limitato
dal processore 3
Basics ...

• Le informazioni nella memoria principale


(indirizzamento diretto) sono accessibili al
processore in qualsiasi momento
• Le informazioni nella memoria periferica
(indirizzamento indiretto) devono prima essere
trasferite nella memoria principale
• Il trasferimento dell’informazione tra memoria
principale e memoria periferica è mediato dal
software (tipicamente il Sistema Operativo)
4
Terminologia

• Tempo di accesso: tempo richiesto per una


operazione di lettura/scrittura nella memoria
• Tempo di ciclo: tempo che intercorre tra l’inizio di
due operazioni consecutive (es. due read) tra
locazioni diverse; in genere leggermente
superiore al tempo di accesso
• Accesso Casuale:
§ non vi è alcuna relazione o ordine nei dati
memorizzati
§ tipico delle memorie a semiconduttori
5
Terminologia
• Accesso Sequenziale:
§ l’accesso alla memoria è ordinato o semi-ordinato
§ il tempo di accesso dipende dalla posizione
§ tipico dei dischi e dei nastri
• RAM: Random Access Memory
§ memoria scrivibile/leggibile a semiconduttori
§ tempo di accesso indipendente dalla posizione
dell’informazione
• ROM: Read Only Memory
§ memoria a semiconduttori in sola lettura
§ accesso casuale o sequenziale
6
Memoria principale

• Connessione “logica” con il processore


Processor Memory
k-bit
address bus
MAR
n-bit
data bus Up to 2 k addressable
MDR locations
Control lines
(R/W, MFC, etc.) Word length = n bits

MAR: Memory Address Register


MDR: Memory Data Register
MFC: Memory Function Completed
7
Memorie RAM a semiconduttori

• Memorizzano singoli bit, normalmente organizzati in


byte e/o word
• Data una capacità N (es. 512 Kbit) la memoria può
essere organizzata in diversi modi a seconda del
parallelismo P (es. 1, 4, 8)
§ 512K X 1
§ 128K X 4
§ 64K X 8
• L’organizzazione influenza il numero di pin di I/O del
circuito integrato (banco) che implementa la
memoria
8
Organizzazione dei bit in un
banco di memoria 16 X 8
b7 b ¢7 b
1 b¢ b
0 b¢
1 0
W
0




FF FF
A0 W1




A
1 Address Memory
decoder • • • • • • cells
A2 • • • • • •
• • • • • •
A3

W15


Sense / Write Sense / Write Sense / Write R /W


circuit circuit circuit CS

CS: Circuit Select


Data input /output lines: b7 b1 b0

9
Memorie Statiche (SRAM)

• Sono memorie in cui i bit possono essere


tenuti indefinitamente (posto che non manchi
l’alimentazione)
• Estremamente veloci (tempo di accesso di
pochi ns)
• Consumano poca corrente (e quindi non
scaldano)
• Costano care perché hanno molti componenti
per ciascuna cella di memorizzazione
10
SRAM: cella di memoria
b “interruttori” b¢

T T
1 2
X Y

Word line
(indirizzi)

Bit lines
(dati)

11
SRAM: lettura e scrittura
• b’ = NOT(b): i circuiti di terminazione della linea di bit (sense/write
circuit) interfacciano il mondo esterno che non accede mai
direttamente alle celle
• La presenza contemporanea di b e NOT(b) consente un minor tasso di
errori
• Scrittura: la linea di word è alta e chiude T1 e T2; il valore presente su b
e b’, che funzionano da linee di pilotaggio, viene memorizzato nel latch
a doppio NOT
• Lettura: la linea di word è alta e chiude T1 e T2, le linee b e b’ sono
tenute in stato di alta impedenza: il valore nei punti X e Y viene
“copiato” su b e b’
• Se la linea di word è bassa, T1 e T2 sono interruttori aperti: il consumo è
praticamente nullo

12
RAM dinamiche (DRAM)
• Sono le memorie più diffuse nei PC e simili
• Economiche e a densità elevatissima (in pratica 1
solo componente per ogni cella)
§ la memoria viene ottenuta sotto forma di carica di un
condensatore
• Hanno bisogno di un aggiornamento (refresh)
continuo del proprio contenuto che altrimenti
“svanisce” a causa delle correnti parassite
• Consumi elevati a causa del rinfresco continuo
13
DRAM: cella di memoria

Bit line “interruttore”

Word line

T
Sense C
Amplifier

14
DRAM: lettura e scrittura

• Scrittura: la linea di word è alta e chiude T, il valore


presente su b viene copiato sul condensatore C
(carica il transistor)
• Lettura: la linea di word è alta e chiude T, un
apposito circuito (sense amplifier) misura la
tensione su C
§ se è sopra una certa soglia data, pilota la linea b alla
tensione nominale di alimentazione, ricaricando C
§ se è sotto la soglia data, mette a terra la linea b
scaricando completamente il condensatore C
15
DRAM: tempi di rinfresco
• Nel momento in cui T viene aperto, il
condensatore C comincia a scaricarsi (o caricarsi,
anche se più lentamente, a causa delle resistenze
parassite dei semiconduttori)
• E’ necessario “rinfrescare” la memoria prima che
i dati “spariscano”
basta fare un ciclo di lettura
• In genere il chip di memoria contiene un circuito
per il refresh (lettura periodica di tutta la
memoria); l’utente non si deve preoccupare del
problema
16
DRAM: multiplazione degli indirizzi

• Data l’elevata integrazione delle DRAM il numero


di pin di I/O è un problema
• E’ usuale multiplare nel tempo l’indirizzo delle
righe e delle colonne negli stessi fili
• Normalmente le memorie non sono indirizzabili
al bit, per cui righe e colonne si riferiscono a byte
e non a bit
• Es. una memoria 2M X 8 (21 bit di indirizzo) può
essere organizzata in 4096 righe (12bit di
indirizzo) per 512 colonne (9bit di indirizzo) di 8
bit ciascuno 17
Organizzazione di una DRAM 2M X 8

RAS

Row Row 4096 ´ (512 ´ 8)


address
latch decoder cell array

A 20 - 9 ⁄ A 8 - 0 Sense / Write CS
circuits R /W

Column
address Column
latch decoder

CAS D7 D0 18
DRAM: modo di accesso veloce

• Spesso i trasferimenti da/per la memoria


avvengono a blocchi (o pagine)
• Nello schema appena visto, vengono selezionati
prima 4096 bytes e poi tra questi viene scelto
quello richiesto
• E’ possibile migliorare le prestazioni
semplicemente evitando di “riselezionare” la riga
ad ogni accesso se le posizioni sono consecutive
• Questo viene chiamato “fast page mode” (FPM) e
l’incremento di prestazioni può essere significativo
19
DRAM sincrone (SDRAM)

• Le DRAM viste prima sono dette “asincrone” perché non


esiste una precisa temporizzazione di accesso, ma la dinamica
viene governata dai segnali RAS e CAS
• Il processore deve tenere conto di questa potenziale
“asincronicità”
§ in caso di rinfresco in corso può essere fastidiosa
• Aggiungendo dei buffer (latch) di memorizzazione degli
ingressi e delle uscite si può ottenere un funzionamento
sincrono, disaccoppiando lettura e scrittura dal rinfresco, e si
può ottenere automaticamente un accesso FPM pilotato dal
clock

20
Organizzazione base di una SDRAM
Refresh
counter

Row
address Row Cell array
latch decoder
Row/Column
address
Column Column Read/Write
address circuits & latches
counter decoder

Clock
R AS Mode register
CAS and Data input Data output
timing control register register
R /W
CS

Data

21
SDRAM: esempio di accesso in FPM

Clock

R /W

RAS

CAS

Address Row Col

Data D0 D1 D2 D3

22
Velocità e prestazione

• Latenza: tempo di accesso ad una singola parola


§ è la misura “principe” delle prestazioni di una memoria
§ dà un’indicazione di quanto il processore dovrebbe
poter aspettare un dato dalla memoria nel caso
peggiore
• Velocità o “banda”: velocità di trasferimento
massima in FPM
§ molto importante per le operazioni in FPM che sono
legate all’uso di memorie cache interne ai processori
§ è anche importante per le operazioni in DMA, posto
che il dispositivo periferico sia veloce
23
Double-Data-Rate SDRAM (DDR-SDRAM)

• DRAM sincrona che consente il trasferimento dei dati


in FPM sia sul fronte positivo che sul fronte negativo
del clock
• Latenza uguale a una SDRAM normale
• Banda doppia
• Sono ottenute organizzando la memoria in due
banchi separati
§ uno contiene le posizioni pari: si accede sul fronte
positivo
§ l’altro quelle dispari: si accede sul fronte negativo
• Locazioni contigue sono in banchi separati e quindi si
può fare l’accesso in modo interlacciato 24
CALCOLATORI
La gerarchia di memoria
Giovanni Iacca
giovanni.iacca@unitn.it

Lezione basata su materiale preparato


con i Prof. Luigi Palopoli e Marco Roveri
Gerarchia di memoria
• Abbiamo visto i vari blocchi di memoria con
diverse caratteristiche di velocità e capacità
• Ma ritorniamo a come ottenere velocità e
capacità insieme
• Il problema è noto da molto tempo….

Idealmente si desidererebbe una memoria


indefinitamente grande, in cui ogni particolare parola
risulti immediatamente disponibile […]

Burks, Goldstine, Von Neumann, 1946 2


Gestione “piatta”
• Immaginiamo di essere un impiegato che
lavora al comune
• Per effettuare il mio lavoro ho bisogno di
avere accesso ad un archivio dove sono
presenti le varie pratiche
• Ogni volta che mi serve una pratica vado a
prenderla, ci opero su, e poi la rimetto a posto

3
Gestione “piatta”
• Per accedere alla pratica scrivo su un bigliettino lo
scaffale dove la pratica può essere trovata, lo
affido a un attendente, e aspetto che me la porti
• Osservazioni:
§ la mia capacità di memorizzazione è molto grande
§ la gran parte del mio tempo (direi il 90%) la spreco
aspettando che l’attendente vada a prendere le
pratiche
§ Sicuramente non è una gestione efficiente del mio
tempo
• Posso essere più veloce?

4
Gestione “veloce”
• In alternativa posso tenere le pratiche sul mio
tavolo e operare solo su quelle
• Osservazioni
§ Sicuramente non perdo tempo (non ho da
aspettare attendenti che vadano in su e in giù)
§ Tuttavia il numero massimo di pratiche che
possono gestite è molto basso
• Posso operare su più dati?

5
Capre e cavoli
• Per riuscire ad avere al tempo stesso velocità e
ampiezza dell’archivio, posso fare due
osservazioni:
1. Il numero di pratiche su cui posso
concretamente lavorare in ogni giornata è
limitato
2. Se uso una pratica, quasi sicuramente dovrò
ritornare su di essa in tempi brevi… tanto
vale tenersela sul tavolo

6
Un approccio gerarchico
• L’idea è che ho un certo numero di posizioni sulla
mia scrivania
§ Man mano che mi serve una pratica la mando a
prendere
§ Se ho la scrivania piena faccio portare a posto quelle
pratiche che non mi servono più per fare spazio
• In questo modo posso contare su un’ampia
capacità di memorizzazione, *ma* la maggior
parte delle volte accedo ai dati molto
velocemente

7
Torniamo ai calcolatori
• Fuor di metafora, all’interno di un calcolatore
possiamo dare al processore (e ai programmi)
l’illusione di avere uno spazio di memoria
molto grande ma con grande velocità
• Questo è possibile grazie a due princìpi
§ Principio di località spaziale
§ Principio di località temporale

8
Località temporale
• Quando si fa uso di una locazione, la si riutilizzerà
presto con elevata probabilità
• Esempio.
Ciclo: slli x10, x22, 3
add x10, x10, x25
ld x9, 0(x10)
bne x9, x24, Esci
addi x22, x22, 1
beq x0, x0, Ciclo
Esci: …. Queste istruzioni vengono
ricaricate ogni volta che si
esegue il ciclo

9
Località spaziale
• Quando si fa riferimento a una locazione, nei
passi successivi si farà riferimento a locazioni
vicine
Ciclo: slli x10, x22, 3
add x10, x10, x25
ld x9, 0(x10)
bne x9, x24, Esci
addi x22, x22, 1 ISTRUZIONI: la modalità di
beq x0, x0, Ciclo esecuzione normale è il
Esci: …. prelievo di istruzioni
successive
DATI: Quando si scorre un
array si va per word
successive
10
Qualche dato
Facciamo riferimento a qualche cifra (relativa al 2012)
Tecnologia di Tempo di accesso tipico $ per GB (2010) $ per GB (2012)
Memoria
SRAM 0.5-2.5 ns $2000 - $5000 $500 - $1000
DRAM 50 – 70 ns $20 - $75 $10 - $20
Memoria flash 70 – 150 ns $4 - $12 $0.75 - $1
Dischi magnetici 5 000 000 – 20 000 000 ns $0.2 - $2 $0.05 - $0.1

Queste cifre suggeriscono l’idea della gerarchia di


memoria
•Memorie piccole e veloci vicino al processore
•Memorie grandi e lente lontane dal processore
11
Gerarchia di memoria
• Struttura base

12
Struttura della gerarchia

13
Esempio
• Due livelli

14
Terminologia
• Blocco: unità minima di informazione che può essere presente o
assente in ciascun livello
§ Un faldone nell’esempio di un archivio
• Hit rate: Frequenza di successo = frazione degli accessi in cui trovo il
dato nel livello superiore
§ Quante volte trovo il faldone che mi serve nella scrivania
• Miss Rate: 1.0 – Hit rate: frazione degli accessi in cui non trovo il
dato nel livello superiore
§ Quante volte devo andare a cercare un faldone in archivio
• Tempo di hit: Tempo che occorre per accedere al dato quando lo
trovo nel livello superiore
§ Quanto mi ci vuole a leggere un documento nel faldone sulla scrivania
• Penalità di miss: quanto tempo mi ci vuole per accedere al dato se
non lo trovo nel livello superiore
§ Tempo per spostare il faldone dall’archivio alla scrivania + tempo di
accesso al documento nel faldone (dopo che arriva sulla scrivania)
15
Considerazioni
• La penalità di miss è molto maggiore del
tempo di hit (e anche del trasferimento in
memoria di un singolo dato)
§ Da cui il vantaggio
• Quindi è importante ridurre frequenza di miss
§ In questo ci aiuta il principio di località che il
programmatore deve sfruttare al meglio

16
Cache
• Cache: posto sicuro [nascosto] dove riporre le
cose
• Nascosto perché il programmatore non vede
la cache direttamente (non vi accede)
§ L’uso della cache è interamente trasparente
• L’uso della cache fu sperimentato per la prima
volta negli anni ‘70 e da allora è stato
largamente adottato in tutti i calcolatori

17
Un esempio semplice
• Partiamo da un semplice esempio in cui i blocchi di
cache siano costituiti da una sola word
• Supponiamo che a un certo punto il processore
richieda la parola Xn che non è in cache
X4 X4
X1 X1
Xn-2 Xn-2
Xn-1 Xn-1
Cache Miss
X2 X2
Xn
X3 X3

18
Domande
• Come facciamo a capire se un dato richiesto è
nella cache?
• Dove andiamo a cercare per sapere se c’è?
• Cache a mappatura diretta: a ogni indirizzo della
memoria corrisponde una precisa locazione della cache
• Possibilità: indirizzo locazione dove un indirizzo è
mappato = (indirizzo blocco) modulo (numero di
blocchi in cache)
• Se la dimensione (=numero di blocchi) della cache è
potenza di due, è sufficiente prendere i bit meno
significativi dell’indirizzo in numero pari al
logaritmo in base due della dimensione della cache19
Esempio
• Se la nostra cache dispone di 8 parole devo
prendere i tre bit meno significativi

20
Problema
• Siccome molte parole posso essere mappate
sullo stesso blocco di cache, come facciamo a
capire se, in un dato momento, vi si trova
l’indirizzo che serve a noi?
• Si ricorre a un campo, detto tag, che contiene
un’informazione sufficiente a risalire al blocco
correntemente mappato in memoria
• Ad esempio possiamo utilizzare i bit più
significativi di una parola, che non sono usati per
la mappatura sulla cache, per trovare la locazione
di memoria corrispondente all’indirizzo mappato
da quel blocco di cache.
21
Validità
• Usiamo i bit più significativi (due nell’esempio
fatto) per capire se nel blocco di cache
memorizziamo l’indirizzo richiesto
• Inoltre abbiamo un bit di validità che ci dice se
quello che memorizziamo in un blocco di
cache in un certo momento sia o meno valido

22
Esempio

Accesso a
10110:
Miss
Accesso a
10010:
Accesso a miss
11010:
Miss

Accesso a
11010: hit
23
Esempio RISC-V a 64 bit
• Esempio
§ Indirizzo su 64 bit
§ Cache a mappatura diretta
§ Dimensioni della cache: 2n blocchi, di cui n bit usati
per l’indice
§ Dimensione del blocco di cache: 2m parole, ossia
2m+2 byte (m bit usati per individuare una parola nel
blocco), due bit per individuare un byte in una parola

In questo caso la dimensione del tag è data da:


64-(n+m+2)

24
Schema di risoluzione
• Un indirizzo viene risolto in cache con il seguente
schema (n=10, m=0, dimensione tag = 52):

Se il campo tag
è uguale ai 52 bit più
significative
dell’indirizzo e se il bit
di validità è 1, allora si
ha una hit e si da il dato
al processore,
altrimenti scatta una
miss.

25
Esempio
• Si consideri una cache con 64 blocchi di 16 byte
ciascuno. A quale numero di blocco corrisponde
l’indirizzo 1200 espresso in byte?
• Blocco identificato da:
(indirizzo blocco) modulo (numero blocchi in cache)
• Dove:
Indirizzo del Dato in byte
Indirizzo Blocco = Byte per blocco

26
Esempio
• Quindi l’indirizzo del blocco è 1200/16=75
• Blocco contente il dato è: 75 modulo 64 = 11
0 16 1200
1 17 1201 MEMORIA
.... .... PRINCIPALE
.. ... ...
15 31 1215

0 1 75

0 0 0 0
1 1 1 1 MEMORIA
.... ....
.. ... ... ... CACHE
15 15 15 15
0 1 11 63 27
Trade-off
• Blocchi di cache molto grandi esaltano la località
spaziale e da questo punto di vista diminuiscono
le probabilità di miss
• Tuttavia, a parità di dimensioni della cache avere
pochi blocchi diminuisce l’efficacia nello
sfruttamento della località temporale
• Quindi abbiamo un trade-off
• Inoltre avere dei miss con blocchi grandi porta a
un costo di gestione alto (bisogna spostare molti
byte)
28
Frequenza delle miss
Miglior trade-off:
fenomeno più
evidente per
cache piccole

29
Gestione delle miss
• La presenza di una cache non modifica molto il
funzionamento del processore (con pipeline)
fino a che abbiamo delle hit
§ Il processore non si «accorge» neanche della
presenza della cache
§ In caso di miss, bisogna generare uno stallo nella
pipeline e gestire il trasferimento da memoria
principale alla cache (ad opera della circuiteria di
controllo)

30
Gestione delle miss
• Ad esempio, per una miss sulla memoria
istruzioni, bisognerà:
1. Inviare il valore PC – 4 alla memoria (PC viene
incrementato all’inizio, quindi la miss è su PC-4)
2. Comandare alla memoria di eseguire una lettura
e attenderne il completamento
3. Scrivere il blocco che proviene dalla memoria
della cache aggiornando il tag
4. Far ripartire l’istruzione dal fetch, che stavolta
troverà l’istruzione in cache

31
Scritture
• Gli accessi in lettura alla memoria dati avvengono con
la stessa logica
• Gli accessi in scrittura sono un po’ più delicati perché
possono generare problemi di consistenza
• Una politica è la cosiddetta write-through
§ Ogni scrittura viene direttamente effettuata in memoria
principale (sia che si abbia una hit che una miss)
§ In questo modo non ho problemi di consistenza, ma le
scritture sono molto costose
§ Posso impiegare un buffer di scrittura (una coda in cui
metto tutte le scritture che sono in attesa di essere
completate)

32
Scritture
• Un’altra possibile politica è la write-back
§ Se il blocco è in cache le scritture avvengono
localmente in cache e l’update viene fatto solo
quando il blocco viene rimpiazzato (o quando una
locazione nel blocco viene acceduta da un altro
processore su architetture multi-core)
§ Questo schema è conveniente quando il
processore genera molte scritture e la memoria
non ce la fa a «stargli dietro»

33
Esempio
FastMath Intrinsity
(basato su architettura MIPS)

• Cache di 16K
• 16 parole per
blocco
• Possibilità di
operare in
write-through o
in write-back

34
Esempio
FastMath Intrinsity
(basato su architettura MIPS)

Tipiche performance misurate su benchmark SPEC CPU2000

35
Cache associative
• Le cache a mappatura diretta sono piuttosto
semplici da realizzare
• Tuttavia hanno un problema: se ho spesso bisogno
di locazioni di memoria che si mappano sullo
stesso blocco, ho cache miss in continuazione
• All’estremo opposto ho una cache completamente
associativa
§ Posso mappare qualsiasi blocco in qualsiasi blocco di
cache

36
Cache completamente associativa
• Il problema per le cache completamente
associative è che devo cercare ovunque il dato (il
tag è tutto l’indirizzo del blocco)
• Per effettuare la ricerca in maniera efficiente,
devo farla su tutti i blocchi in parallelo
• Per questo motivo ho bisogno di n comparatori
(uno per ogni blocco di cache) che operino in
parallelo
• Il costo HW è così alto che si può fare solo per
piccole cache
37
Cache set-associativa
• Le cache set-associative sono un via di mezzo tra le due
che abbiamo visto
• In sostanza ogni blocco di memoria può essere
mappato su una linea di n blocchi diversi di cache (n
«vie»)
• Quindi combiniamo due idee
§ Associamo ciascun blocco di memoria a una certa linea (e
quindi a uno degli n blocchi di quella linea su cui possiamo
mappare il blocco di memoria)
§ All’interno della linea, effettuiamo una ricerca parallela
come se avessimo una cache completamente associativa

38
Mappatura del blocco
• In una cache a mappatura diretta un blocco di memoria
viene mappato in un blocco di cache dato da:
(indirizzo blocco) modulo (numero blocchi della cache)

• In una cache set-associativa un blocco di memoria


viene mappato nella linea data da:
(indirizzo blocco) modulo (numero linee della cache)

• Quindi, per trovare il blocco all’interno della linea


dobbiamo confrontare (in parallelo) il tag del blocco con
tutti i tag dei blocchi di quella linea

39
Posizione del blocco

Blocco #12 solo nella linea


0 = (12 mod 4), blocco 0 o 1
Blocco #12 solo in (ipotizzando cache a 2 vie, Blocco #12 può
posizione 4 (12 mod 8) ovvero 4 linee da 2 blocchi) essere ovunque 40
Varie configurazioni

41
Schema per cache a 4 vie

42
Vantaggi dell’associatività

Aumentando l’associatività abbiamo vantaggi (frequenza di miss) e


svantaggi (complessità). La scelta viene fatto tenendo presente
questo trade-off
43
Un problema in più
• Nelle cache a mappatura diretta quando ho
una cache miss sicuramente so chi sostituire
(l’unico blocco in cui posso mapparmi)
• Nelle cache associative ho più scelte: se la
linea è piena chi sostituisco?
• Varie politiche
§ FIFO
§ Least Recently Used
Richiede una serie di bit
in più per contare
l’ultimo accesso 44
Ok, ma a che ci serve
questa roba?
Numero di istruzioni
per elemento da
ordinare

Tempo di esecuzione

Cache miss

45
CALCOLATORI
I/O
Giovanni Iacca
giovanni.iacca@unitn.it

Lezione basata su materiale preparato


con i Prof. Luigi Palopoli e Marco Roveri
La necessità di comunicare
• Un calcolatore è completamente inutile senza la
possibilità di caricare/salvare dati e di comunicare con
l’esterno
• I dispositivi di I/O devono essere
§ espandibili
§ eterogenei
• I dispositivi di I/O sono molto vari e la tipologia di
prestazione è diversa
§ In alcuni casi interessa il tempo di accesso (la latenza) e il
tempo di risposta
ü Es. dispositivi interattivi come tastiere o mouse
§ In altri casi siamo interessati al throughput
ü Caso di dischi o interfacce di rete
2
Un semplice schema
• I dispositivi sono collegati al processore da un
dispositivo di comunicazione chiamato bus

3
Classificazione
• I dispositivi di I/O sono di vario tipo e possono
essere classificati in vari modi
§ Comportamento: che operazioni posso effettuare
con il dispositivo (R/W)
§ Partner: può essere un uomo o una macchina
§ Velocità di trasferimento

4
Esempi

5
Prestazioni
• A seconda del tipo di applicazione, posso
essere interessato a diverse prestazioni
§ Ad esempio per un sistema di streaming mi
interessa il throughput
§ Per un sistema bancario, mi può servire
massimizzare il numero di file di piccole
dimensioni su cui opero contemporaneamente

6
Connessione tra
processori e periferiche
• Le connessioni avvengono tramite delle strutture di
comunicazione chiamate bus
• Esistono due tipi di bus
§ Bus processore/memoria:
ü specializzati, corti e veloci
§ Bus I/O
ü possono essere lunghi e permettono il collegamento con periferiche
eterogenee
ü tipicamente, non sono collegati alla memoria in maniera diretta ma
richiedono un bus processore/memoria o un bus di sistema
• Nelle prime architetture avevamo un unico grosso bus
parallelo che collegava tutto
• Per problemi di clock e frequenze ora si usano architetture
di comunicazione più complesse fatte di più bus paralleli
condivisi e di bus seriali punto/punto
7
Terminologia
• Transazione di I/O
§ Invio indirizzo e spedizione o ricezione dei dati
• Input
§ Trasferimento di dati da una periferica verso la
memoria dove il processore può leggerla
• Output
§ Trasferimento dalla memoria al dispositivo

8
Bus sincrono
• Tra le linee di controllo deve avere clock
• Le comunicazioni avvengono con un
protocollo collegato al ciclo di clock
• Esempio: dato richiesto al clock n viene messo
sul bus al clock n+5

9
Bus sincrono: funzionamento base

Bus clock

Address and
command

Data

t0 t1 t2

Indica valori validi Bus cycle


(1/0, alto/basso)
su una struttura parallela
10
Bus sincrono
• Pro
§ Molto semplice da implementare (piccola
macchina a stati finti)
§ Molto veloce (pochi segnali di controllo)
• Contro
§ Poca robustezza al drift del clock
§ Tutte le periferiche devono andare alla velocità del
clock

11
Bus asincrono
• Per ovviare agli inconvenienti discussi si tende
a usare interconnessioni asincrone
• In sostanza non abbiamo più un clock e tutte
le transazioni sono governate da una serie di
segnali di handshake
• Questo richiede l’introduzione di apposite
linee di controllo per segnalare inizio e fine di
transazioni, ma permette di collegare
periferiche a velocità diversa

12
Ciclo di un bus asincrono
T ime

Address
and command

Master-ready

Slave-ready

Data

t0 t1 t2 t3 t4 t5

Bus cycle 13
Bus asincrono
• Pro
§ Consente di essere robusto rispetto a ritardi
§ Consente di comunicare con periferiche di tipo diverso
• Contro
§ Lento nelle interazioni (diversi segnali di controllo
devono circolare per riuscire a comunicare)
§ Circuiteria di gestione del protocollo complessa
• Spesso si usano tecnologie ibride (in cui c’è un
segnale di clock) ma prevalentemente asincrone
14
Tecnologie (asincrone) attuali

15
Esempio x86
HUB per il
controllo
memoria

HUB per
connessione
ad altri bus

Trend più
recente:
incorporare il
tutto all’interno
del processore

16
Prospettiva del programmatore
• Rimane da capire
§ Come trasformare una richiesta di I/O in un comando per la
periferica
§ Come trasferire i dati
§ Qual è il ruolo del sistema operativo?
• Riguardo al SO occorre osservare
§ Programmi che condividono il processore condividono anche il
sistema di I/O
§ I trasferimenti dati vengono spesso effettuati usando interrupt,
che hanno un impatto sulle funzionalità del SO
ü Quindi devono essere eseguiti in una particolare modalità del
processore (supervisor) cui solo il codice del kernel può accedere
§ Il controllo di operazioni I/O spesso si interseca con
problematiche di concorrenza

17
Funzionalità richieste al SO
• Garantire che un dato utente abbia accesso ai
dispositivi di I/O cui ha diritto (permessi) di
accedere
• Fornire comandi di alto livello per gestire le
operazioni di basso livello
• Gestire le interruzioni generate dai dispositivi di
I/O (in maniera simile a quanto avviene con le
eccezioni generate nei programmi)
• Ripartire l’accesso a ciascun dispositivo in
maniera equa tra i vari programmi che lo
richiedono
18
Requisiti
• Per implementare le funzionalità appena
discusse occorre
§ Rendere possibile al SO di inviare comandi alle
periferiche
§ Rendere possibile ai dispositivi notificare la
corretta esecuzione di un’operazione
§ Consentire trasferimenti diretti di dati tra
dispositivo e memoria

19
Come impartire i comandi ai dispositivi
• Questo si fa fornendo sulle relative linee di
bus alcune «parole» di controllo
• Può essere fatto in due modi:
§ Scrivendo/leggendo in particolari locazioni di
memoria (memory mapped I/O)
§ Tramite alcune istruzioni speciali (dedicate all’I/O)

20
Esempio
• Scrivendo una particolare parola in una locazione di memoria
associata al dispositivo
§ Il sistema di memoria ignora la scrittura
§ Il controllore di I/O intercetta l’indirizzo particolare e trasmette il dato
al dispositivo sotto forma di comando
• Queste particolari locazioni di memoria NON sono accessibili ai
programmi utente ma solo al sistema operativo (quindi occorre una
chiamata di sistema che faccia commutare il processore in modalità
supervisore)
• Il dispositivo stesso può usare queste locazioni per trasmettere dati
o pre-segnalare il suo stato
• Ad esempio posso chiedere la stampa di un carattere a terminale, e
a stampa finita un particolare bit di un registro di stato mappato in
memoria verrà commutato (a 1 per indicare la corretta stampa)

21
Come trasmettere/ricevere i dati

• La modalità più semplice per trasferire i dati è


la cosiddetta attesa attiva (polling)
• In sostanza si manda un comando di
lettura/scrittura alla periferica e poi si fa un
ciclo di attesa testando il bit di stato per
vedere quando il dato è pronto

22
Esempio
• Input: lettura dalla tastiera in x7
lui x5, 0xffff #ffff0000 Memory Map
Waitloop: lw x6, 0(x5) #control
andi x6, x6,0x0001
beq x6, x0, Waitloop #ffff0000 input control reg
lw x7, 4(x5) #data #ffff0004 input data reg
#ffff0008 output control reg
• Output: stampa del dato da x7 #ffff000C output data reg
lui x5, 0xffff #ffff0000
Waitloop: lw x6, 8(x5) #control
andi x6,x6,0x0001
beq x6,x0, Waitloop
sw x7, 12(x5) #data
• Questo ciclo di attesa è chiamato polling

23
Costo del polling
• Consideriamo un processore a 500Mhz e
supponiamo che occorrano 400 cicli di clock
per un’operazione di polling. Qual è il costo
percentuale?
§ Esempio 1: Mouse. Per non perdere movimenti da
parte dell’utente occorre acquisire il dato 30 volte
al secondo.
§ Esempio 2: Hard disk. I dati vengono trasferiti in
blocchi di 16 byte a 8MB/s senza la possibilità di
perdite.

24
Esempio 1 (Mouse)
• Cicli di clock al secondo spesi per il polling
= 30 * 400 = 12000 clocks/sec
§ % Processor for polling:
12*103/500*106 = 0.002%
Þ Fare polling sul mouse «ruba» un utilizzo di processore
trascurabile

Questo overhead viene pagato sempre, sia


che ci sia il trasferimento, sia che non ci sia
25
Esempio 2 (Hard disk)
• Numero di volte/sec che occorre fare cicli di
attesa per non perdere dati:
= 8 MB/s /16B = 500K polls/sec
• Spesa in cicli di clock/sec
= 500K * 400 = 200,000,000 clocks/sec
• % processore
200*106/500*106 = 40%
Þ Inaccettabile perché pagata sempre

26
CALCOLATORI
I/O
Giovanni Iacca
giovanni.iacca@unitn.it

Lezione basata su materiale preparato


con i Prof. Luigi Palopoli e Marco Roveri
Considerazioni sul polling
• L’attesa attiva fa perdere tempo al processore che
dedica cicli macchina a letture inutili
• Il polling può essere usato quando le operazioni
di I/O avvengono con velocità di trasferimento
predeterminata (es. applicazioni di controllo) e
comunque il processore ha poco altro da fare
• Sicuramente se i dati vengono trasferiti con
elevati bitrate il ciclo di attesa attiva dura poco
• In altri casi lo spreco è inaccettabile e per questo
motivo è stato inventato l’I/O a interruzione di
programma (interrupt driven I/O)
2
Organizzazione memoria SPIM (MIPS)
Memory
ffff fffc
• Partiamo da Mem Map I/O
Kernel
una tipica Code & Data
$sp 8000 0080
organizzazione Stack
di memoria (ad
es. quella
dell’emulatore 230
words
SPIM) Dynamic data
$gp 1000 8000 (® 1004 0000)
Static data 1000 0000
User
Code
PC 0040 0000
Reserved
http://spimsimulator.sourceforge.net/ 0000 0000 3
Controllo del terminale in SPIM (INPUT)
• Osserviamo da vicino le locazioni per il
controllo del terminale (INPUT)

7 0
Receiver data unused
(0xffff0004)
received byte
(read only)
1 0
Receiver control unused
(0xffff0000)
interrupt enable ready
(read only)

4
Interruzioni di programma
• Un’interruzione I/O è un segnale usato per segnalare al processore che la
periferica è pronta ad eseguire il trasferimento richiesto
§ Le interruzioni possono avere diverso grado di urgenza (è possibile definire
priorità).
§ Occorre un modo per segnalare al processore quale periferica richiede
l’interruzione.
• Le interruzioni I/O sono sempre asincrone rispetto all’esecuzione delle
istruzioni
§ Non esistono particolari istruzioni assembly per eseguire le interruzioni.
Un’interruzione può arrivare mentre una qualsiasi istruzione viene eseguita, e
dà comunque modo di terminare l’esecuzione dell’istruzione.
ü Il programmatore può spesso differire l’esecuzione dell’interruzione a un momento più
conveniente (es. sezioni non interrompibili nel codice del kernel).
• Vantaggio
§ Non occorre interrompere l’esecuzione del programma se non quando il dato
può essere effettivamente riferito in memoria.
• Svantaggio – occorre un hardware speciale per:
§ Permettere ai dispositivi di I/O di generare un’interruzione.
§ Rilevare l’interruzione, salvare lo stato del processore per eseguire una
particolare routine di servizio (Interrupt Service Routine, ISR) e poi riprendere
dal punto dove si era interrotto.
5
Input a interruzione di programma
1. Interrupt
Processor dall’input add
sub user
and program
2.1 Salva PC or
beq
Memory Receiver
2.3 Servi
2.2 Salta l’interrupt
Keyboard alla ISR
lbu input
sb interrupt
... service
2.4 Ritorno al jr routine
Nel mezzo c’è una codice utente
commutazione in modalità
«Supervisore» (solo il SO può memory
gestire l’interruzione)

6
Esempio controllo terminale SPIM
1. La periferica indica con un’interruzione che ha un nuovo carattere
dalla tastiera nell’opportuno registro di ricezione
Receiver data unused 65 Byte
(0xffff0004) ricevuto
ü Contestualmente il bit pronto viene messo a uno nel registro di controllo

Receiver control unused 1 1®0


(0xffff0000)
interrupt enable ready

2. Il processo utente viene interrotto trasferendo il controllo a una ISR


che copia il dato in memoria utente
ü All’atto della lettura il bit ready viene ri-azzerato
ü Notare che prima di effettuare il trasferimento il bit interrupt enable era stato
posto a 1 per abilitare le interruzioni

7
Che ci si guadagna?
• Ritorniamo all’esempio di prima dell’hard disk e supponiamo che un
interrupt costi 500 cicli di clock (plausibile che costi di più del
polling)
• Se le interruzioni vengono generate alla frequenza di polling
§ Disk Interrupts/sec = 8 MB/s /16B
= 500K interrupts/sec
§ Disk Polling Clocks/sec = 500K * 500
= 250,000,000 clocks/sec
§ % Processor: 250*106/500*106= 50%
§ Sembrerebbe che non ci sia guadagno…anzi
• Tuttavia se l’hard disk è attivo solo per il 5% del tempo, gli interrupt
generati saranno il 5% e la spesa di processore sarà:
5% * 50%=2.5%

L’overhead si paga solo quando vengono


effettivamente generate richieste 8
Eccezioni in generale
System
user program Exception
Exception Handler
normal control
flow: sequential,
jumps, branches,
calls, returns
return from
exception

• Eccezione = trasferimento (non programmato)


del controllo del programma
§ Il sistema effettua delle azioni per gestire le eccezioni
üAd esempio deve sapere dove registrare il punto di
interruzione e dove salvare lo stato, e poi (a eccezione finita)
come riprendere dal punto immediatamente successivo al
punto di interruzione
9
Tre tipi di eccezioni
• Interrupts
§ Causate da eventi esterni (I/O)
§ Asincrone
§ Possono essere gestite nello spazio tra due istruzioni
§ Semplicemente sospendono il programma e riprendono dal punto in cui era
stato interrotto

• Traps (Eccezioni)
§ Causate da eventi interni al programma
ü Condizioni eccezionali (ad es. arithmetic overflow, undefined instr.)
ü Errori (ad es. hardware malfunction, memory parity error, segmentation fault)
ü Fault (ad es. non-resident page – page fault)
§ Sincrone all’esecuzione del programma
§ Gestite da un trap handler
§ E’ possibile riprovare ad eseguire l’istruzione che ha causato l’eccezione o
abortire il programma

• Environment call/break
§ La environment call (istruzione ecall) è causata da una esplicita richiesta di un
servizio di sistema (stampa di un carattere, un intero, …) attraverso esplicita
ecall.
§ La environment break (istruzione ebreak) è causata da una esplicita chiamata a
ebreak per motivi diagnostici o di debug (ad es. la break nel GDB) 10
Gestione delle eccezioni
Exception Handler
• Esistono vari metodi per gestire le eccezioni
§ Salto diretto all’indirizzo della routine di gestione
§ Vettore di interruzione

• In entrambi gli approcci lo stato della


macchina deve essere preservato
§ Ad es. salvando il contenuto dei registri nello stack
o in appositi registri

11
Salto diretto ad indirizzo
della routine di gestione
• Salto diretto ad un indirizzo
specifico: Mem
causa SCAUSE
PC <- ind_proc_gest
ind_proc_gest STVEC
§ Nel RISC-V Routine di
ü Indirizzo di gestione è contenuto nel gestione
registro speciale STVEC
ü La causa dell’eccezione è
memorizzata in registro speciale
SCAUSE

• Vantaggi:
§ Non è necessario fare un accesso in
memoria per prelevare l’indirizzo
della routine di gestione
• Svantaggi
§ Nella routine di gestione occorre
analizzare la causa dell’eccezione

12
Vettore di interruzione
• Memorizzo in una tabella gli indirizzi
delle Routine di Gestione per ogni
possible causa:
causa SCAUSE

Vettore di Interruzione
PC <- Mem[base + causa*4] Ind. routine 0 ind_proc_gest STVEC
Ind. routine 1
• Nel RISC-V causa*4
§ Indirizzo base delle routine di gestione
delle eccezioni contenuto in registro Ind. routine m
speciale STVEC
§ La causa dell’eccezione è memorizzata Routine di
in registro speciale SCAUSE gestione
• Vantaggi
§ La causa dell’eccezione è nota ed
utilizzata per identificare la routine
relativa di gestione
• Svantaggi
§ E’ necessario accedere alla memoria
per prelevare l’indirizzo della routine di
gestione

13
Salvataggio dello stato della
macchina al verificarsi di una
eccezione
• Vari approcci
§ Salvataggio sullo stack
§ Salvataggio in registri ausiliari (sia visibili che non)
§ Salvataggio in registri speciali

• Nel RISC-V
§ Salvataggio sullo stack e su registri speciali
üSEPC, SCAUSE, SSTATUS, STVAL (o BadVaddr), …

14
Supporto alla gestione delle eccezioni
• Il RISC-V ha vari registri a 64 bit
nel RISC-V
§ SEPC - contiene indirizzo dell’istruzione che ha generato l’eccezione
§ SSTATUS - contiene i bit di abilitazione globale degli interrupt
§ SCAUSE - I bit 63 e [3-0] codificano le possibili sorgenti di eccezione
ü Illegal instruction = {0, 2}
ü Breakpoint = {0, 3}
ü Time interrupt = {1, 5}
ü External interrupt = {1,9}
ü …
§ STVAL – Supervisor Trap Value – contiene l’indirizzo al quale si è verificato un riferimento
errato alla memoria
§ SIE – Supervisor Interrupt Enable – specifica abilitazioni più fini degli interrupt in attesa
§ SIP – Supervisor Interrupt Pending – monitoraggio degli interrupt in attesa
§ STVEC – Supervisor Trap Vector – Indirizzo base della lista dei vettori di interrupt
§ SSCRATCH – Registro per salvataggi temporanei

• Questi registri si trovano in un banco interno al processore chiamato Control


Status Register (CSR)

• Al verificarsi di una eccezione la parte di controllo della CPU modifica questi


registri
§ Note:
ü In fase di fetch il PC è aggiornato a PC+4. L’istruzione colpevole da memorizzare in SEPC è quindi il PC
prima dell’incremento
ü In PC viene (sovra-)scritto direttamente il nuovo valore (PC <- STVEC) 15
Status Register
8 5 10
SSTATUS
SPP SPIE SIE

• Il livello di privilegio può essere S- per Supervisor o U- per User


§ Questi sono rispettivamente codificati con i valori 1 e 0
§ Chi si trova ad un certo livello non può sapere se esistono livelli di privilegio superiori (tale
informazione non è accessibile)

• SIE (S- Interrupt Enable) abilita globalmente gli interrupt a quel dato livello – per
non «entrare in loop» viene subito disabilitato (0)

• SPIE (S- Previous Interrupt Enable) indica lo stato precedente del bit SIE al
momento in cui si verifica l’eccezione

• SPP (S- Previous Privilege mode) indica il livello di privilegio precedente al


momento in cui si verifica l’eccezione

16
Controllo fine degli
interrupt
9 5 10
SIE
SEIE STIE SSIE
9 5 10
SIP
SEIP STIP SSIP

• SxIE sono bit di abilitazione più fine degli interrupt


• SxIP sono bit di indicazione di interrupt pendenti (in attesa)

• x si riferisce alla possibile sorgente dell’eccezione


§ x = E à interrupt esterno
§ x = T à interrupt dal timer
§ x = S à interrupt software (ovvero eccezione)
17
Ingresso ed Uscita in modalità
Supervisor
• Al verificarsi di una eccezione
PC SEPC Salvataggio dello stato della
macchina: valori correnti nei
Current Privilege Level SSTATUS.SPP rispettivi registri dei valori
SSTATUS.SIE SSTATUS.SPIE precedenti

E poi…
STVEC PC Salto alla routine di gestione

1 Current Privilege Level Modalità supervisor

0 SSTATUS.SIE Interrupt disabilitati

• All’esecuzione della SRET1


SEPC PC Ripristino dello stato della
macchine: valori precedenti nei
SSTATUS.SPP Current Privilege Level rispettivi registri dei valori
SSTATUS.SPIE SSTATUS.SIE correnti

1Tipicamente si usa SRET per ritornare da una routine di gestione interrupt o eccezione
18
SCAUSE Register
63 3 0
SCAUSE
Int CODE

• Int (1 bit) se vale 1, la sorgente è un interrupt, se vale 0 la sorgente è una


eccezione

• Code (4 bits) codifica la ragione dell’eccezione


§ 0 - Instruction address misaligned
§ 2 - Illegal instruction
§ 3 - Breakpoint
§ 4 - Load address misaligned
§ 5 - Load address fault
§ 6 - Store address misaligned
§ 7 - Store address fault
§ 8 - Environment call from U-mode
§ 9 - Environment call from S-mode
§ C - Instruction page fault
§ D - Load page fault
§ …

19
Lettura/Scrittura di SEPC,
SCAUSE, STVEC, STVAL, …
• Per modificare i contenuti dei registri speciali CSR:
§ CSR Atomic Read & Write (/Immediate): CSRRW rd, csr, rs CSRRWI rd, csr, imm
§ CSR Atomic Read & Set bits (/Immediate): CSRRS rd, csr, rs CSRRSI rd, csr, imm
§ CSR Atomic Read & Clear bits (/Immediate): CSRRC rd, csr, rs CSRRCI rd, csr, imm

• Esempi
CSRRW t0,stvec,t1 # t0 ß stvec, stvec ß t1
CSRRW x0,sscratch,t1 # sscratch ß t1 (in questo caso rd = x0: CSR non letto)

CSRRS t0,sstatus,t1 # t0 ß sstatus, sstatus ß sstatus OR t1


CSRRS t0,sstatus,x0 # t0 ß sstatus (in questo caso rs = x0: CSR non scritto)

CSRRSI t0,sie,32 # t0 ß sie e mette a 1 il bit-5 (2^5) di sie (interrupt timer)


CSRRSI t0,sie,512 # NON possibile (l’immediato deve stare su 5 bit)

CSRRC t0,sie,t1 # t0 ß sie, sie ß sie AND NOT(t1)


# es. se t1=2^9+2^5=544, azzera i bit 9 e 5 di sie
# (disabilita interrupt esterni e interrupt timer)

• Nota
§ Se il vecchio valore di CSR non serve, CSRRS/CSRRSI e CSRRC/CSRRCI vengono chiamate con rd=x0
20
Supporto del RISC-V per la
gestione delle eccezioni
• I soli tipi di eccezioni che possono essere generate
nell’implementazione della CPU analizzata sono:
§ Esecuzione di una istruzione non valida
§ Malfunzionamenti hardware
• In caso di eccezione il processore deve:
§ Salvare l’indirizzo dell’istruzione che ha generato l’eccezione nel
registro program counter dell’eccezione (SEPC - supervision exception
program counter register)
§ Salvare la causa dell’eccezione nel registro causa dell’eccezione
(SCAUSE – supervisor cause exception register)
§ Trasferire il controllo ad un indirizzo specifico del sistema operativo
per gestire l’eccezione
§ Terminata la gestione ripristinare lo stato del processore e ritornare
all’esecuzione precedente (se possibile)
• Le eccezioni sono trattate nel RISC-V come se fossero degli hazard
sul controllo
21
Supporto del RISC-V per la
gestione delle eccezioni (cont.)
• Per la gestione di una eccezione in IF si usa stesso meccanismo di gestione degli
errori di predizione per i salti (convertendo l’istruzione in una nop)

• Introduciamo un nuovo segnale di controllo ID.Flush, messo in OR con il segnale di


stallo che proviene dall’unità rilevamento hazard, in modo da eliminare l’istruzione
dallo stadio ID e realizzare così uno stallo

• Introduciamo un nuovo segnale EX.Flush, che pilota un nuovo multiplexer per


mettere a 0 i segnali di controllo di questo stadio

• Assumendo che l’indirizzo della prima istruzione del codice di gestione delle
eccezioni sia 0000 0000 1C09 0000esa, per saltare al codice di gestione
dell’eccezione occorre aggiungere una linea che porta il valore
0000 0000 1C09 0000esa al multiplexer che seleziona il nuovo valore del PC

• Salviamo infine l’indirizzo dell’istruzione che ha causato l’eccezione nel registro


SEPC

22
Supporto del RISC-V per la
gestione delle eccezioni (cont.)
• Problema:
§ Se non si interrompe l’istruzione prima della fine della sua esecuzione
non sarà più possibile vedere il valore originale del registro di
destinazione (ad es. x1, che potrebbe essere «sporcato» dalla
destinazione dell’istruzione che ha generato eccezione)
ü Se supponiamo che eccezione venga riconosciuta nello stadio EX si può
utilizzare EX.Flush per prevenire che l’istruzione scriva il registro destinazione
nello stadio WB

• Nota:
§ Molte eccezioni richiedono di completare normalmente l’esecuzione
dell’istruzione che ha causato l’eccezione
ü Il modo più semplice per ottenere ciò consiste nell’eliminare l’istruzione e farla
eventualmente ripartire dall’inizio dopo che l’eccezione è stata gestita

23
Supporto del RISC-V per la
gestione delle eccezioni (cont.)

Nota:
errore
sul libro

STVEC = 0000 0000 1C09 0000esa


24

Potrebbero piacerti anche