Fondamenti di Informatica - Architettura degli elaboratori PDF

Summary

These notes provide an overview of digital computer architectures, covering topics such as processors, memories, and peripherals. It discusses the central processing unit (CPU) and its components, along with the concepts of instruction set architecture (ISA) and the differences between RISC and CISC architectures.

Full Transcript

Fondamenti di Informatica Architettura degli elaboratori Carmine Dodaro, Università della Calabria Dal libro di testo1: “Un calcolatore digitale è un sistema in cui processori, memorie e dispositivi periferici sono connessi tra loro”. In generale, sono connessi tramite bus (architettura bus-oriente...

Fondamenti di Informatica Architettura degli elaboratori Carmine Dodaro, Università della Calabria Dal libro di testo1: “Un calcolatore digitale è un sistema in cui processori, memorie e dispositivi periferici sono connessi tra loro”. In generale, sono connessi tramite bus (architettura bus-oriented). Da questa frase si possono comprendere gli elementi principali di un calcolatore digitale: Processori Memorie Periferiche Bus 1A. Tanenbaum e T. Austin: Architettura dei calcolatori - Un approccio strutturale. Organizzazione dei sistemi di calcolo CPU Memoria centrale Disco Periferica 1 Periferica 2 Unità di Controllo Unità aritmetico-logica Registri Bus Il processore o CPU (Central Processing Unit) ha il ruolo di eseguire i programmi contenuti nella memoria centrale. Il processo di funzionamento è abbastanza semplice: le istruzioni dei programmi sono nella memoria centrale, la CPU le preleva e le esegue in sequenza. La CPU è composta da diversi elementi, tra cui: L’unità di controllo (CU), che si occupa di prelevare le istruzioni dalla memoria e di determinare il tipo dell’istruzione prelevata. L’unità aritmetico-logica (ALU), che si occupa di eseguire le operazioni aritmetico-logiche per eseguire le istruzioni. Una memoria molto veloce, composta da registri, che viene utilizzata principalmente per memorizzare i risultati temporanei e altre informazioni di controllo. Ogni registro ha una funzione e una dimensione. I due registri principali sono il Program Counter (PC), che contiene l’indirizzo in memoria dell’istruzione successiva da prelevare, e l’Instruction Register (IR), che contiene l’istruzione che si sta eseguendo. Processori La CPU esegue le istruzioni attraverso una serie di passi (detto ciclo macchina o ciclo fetch-decode-execute): Preleva l’istruzione dalla memoria e la inserisce nell’IR. Modifica il PC inserendo l’indirizzo dell’istruzione seguente. Determina il tipo dell’istruzione prelevata al punto 1. Se l’istruzione usa un blocco di dati dalla memoria, determina dove si trovano i dati. Se necessario, copia il blocco di dati dalla memoria all’interno di uno dei registri. Esegue l’istruzione. Ritorna al punto 1. Esecuzione dell’istruzione Abbiamo visto che il processore esegue istruzioni, ma deve sapere che cosa fare e come farlo. Queste istruzioni devono essere standardizzate per permettere la comunicazione tra hardware e software. Il set di istruzioni è l’insieme di comandi che il processore può eseguire direttamente, quindi determina cosa può fare il processore e come interagisce con la memoria e le periferiche. Ci sono diversi tipologie di istruzioni: Aritmetiche: Addizione, sottrazione, moltiplicazione. Logiche: AND, OR, NOT. Controllo: Salti condizionali, chiamate di subroutine. L’ISA (Instruction Set Architecture) è il livello che definisce: Quali istruzioni può eseguire il processore. Come le istruzioni sono rappresentate in binario. Interfaccia tra software e hardware. L’ISA permette al software (programmi) di comunicare con l’hardware (CPU). Un’istruzione semplice come ADD R1, R2 (somma dei registri R1 e R2) viene tradotta in un comando binario che il processore esegue. Set di istruzioni RISC e CISC sono tipologie di architetture di processori. Queste architetture riguardano la progettazione hardware e la gestione del set di istruzioni del processore, ovvero il modo in cui le operazioni sono eseguite a livello fisico. RISC (Reduced Instruction Set Computer) impiega un set di istruzioni ridotto e più semplice, progettato per eseguire operazioni con istruzioni più rapide e semplici. CISC (Complex Instruction Set Computer) utilizza un set di istruzioni ampio e complesso, che consente al processore di eseguire operazioni complesse con una singola istruzione. Architetture dei processori: RISC vs CISC Caratteristiche: Set di istruzioni complesso e ampio. Ogni istruzione può eseguire operazioni complesse (es. caricamento di dati e calcolo in un’unica istruzione). Vantaggi: Minor numero di istruzioni per eseguire un compito. Riduzione della dimensione del programma. Svantaggi: Maggiore complessità del processore. Maggiore consumo di energia. Esempi: Famiglia x86 di Intel (Core™ i3/i5/i7/i9) e i processori AMD come Ryzen™ CISC Caratteristiche: Set di istruzioni semplice e limitato. Ogni istruzione esegue un’unica operazione semplice (es. caricamento di dati, calcolo). Progettato per eseguire più istruzioni per ciclo di clock. Vantaggi: Maggiore efficienza e velocità di esecuzione. Maggiore parallelismo. Svantaggi: Maggiore numero di istruzioni per compiti complessi. Potenziale aumento della dimensione del programma. Esempi: ARM, MIPS, RISC-V RISC Per migliorare le prestazioni di una CPU si può aumentare la velocità di clock (intuitivamente, il numero di operazioni che si possono eseguire ogni secondo), ma qualsiasi architettura hardware ha dei limiti fisici che non si possono superare. Per questo motivo, spesso, si ricorre più che altro al parallelismo, che può essere sfruttato: a livello di istruzione, dove ogni singola istruzione lo utilizza per fare in modo che la CPU ne possa elaborare molte di più al secondo; a livello di processore, dove più CPU lavorano insieme sullo stesso problema. Nel primo caso, si possono utilizzare delle tecniche per velocizzare il prelievo delle istruzioni dalla memoria. Una delle prime tecniche usate si chiama prefetching e consiste nel dividere l’esecuzione in due parti distinte: il prelievo dell’istruzione dalla memoria e l’esecuzione dell’istruzione stessa. Il concetto di pipeline estremizza questa strategia. Infatti, l’esecuzione non si divide più in due fasi, ma in un numero molto più alto di fasi (anche più di 10) che sono eseguite in parallelo. Ogni fase è gestita da un componente hardware dedicato. Parallelismo Al punto 1, S1 preleva l’istruzione 1 dalla memoria. Al punto 2, S2 decodifica l’istruzione 1 prelevata precedentemente e S1 preleva l’istruzione 2. Al punto 3, S3 preleva gli operandi per l’istruzione 1, S2 decodifica l’istruzione 2 e S1 preleva l’istruzione 3.... Pipeline Unità di esecuzione dell’istruzione Unità di fetch dell’istruzione Unità di decodifica dell’istruzione Unità di fetch degli operandi Unità di memorizzazione del risultato S1 S2 S3 S4 S5 S1: 1 2 3 4 5 6 7 8 9 S2: 1 2 3 4 5 6 7 8 S3: 1 2 3 4 5 6 7... S4: 1 2 3 4 5 6 S5: 1 2 3 4 5 1 2 3 4 5 6 7 8 9 Tempo La memoria principale (RAM, da Random Access Memory) è quel componente del calcolatore dove vengono memorizzati i programmi e i dati. Le memorie sono formate da celle che contengono delle informazioni. Ogni cella è identificata da un numero, cioè l’indirizzo, che può essere utilizzato dal programma per riferirsi alla cella. Gli indirizzi vanno da 0 a n-1, dove n è il numero di celle disponibili. Ogni cella è formata dallo stesso numero di bit. La RAM può essere byte-addressable, cioè con celle da 1 byte, oppure word-addressable, cioè con celle di dimensioni pari alla default word size, che è il numero massimo di bit su cui una CPU può lavorare: Un calcolatore a 32 bit ha registri a 32 bit e istruzioni in grado di gestire parole a 32 bit (word pari a 4 byte). Un calcolatore a 64 bit ha registri a 64 bit e istruzioni in grado di gestire parole a 64 bit (word pari a 8 byte). Quasi tutti i produttori usano celle a 8 bit (1 byte). Memoria principale Memoria principale 0 0 1 1 1 0 1 1 0 1 2 3 4 5 6 7 8 9 … Indirizzo Cella a 8 bit Se una cella contiene k bit, può rappresentare 2k elementi. Se un indirizzo ha k bit, il massimo numero di celle indirizzabili è 2k. All’interno di una parola, in che ordine si memorizzano i dati? Si usa il termine endianness per riferirsi alla convenzione scelta per definire questo ordine e ci sono due ordinamenti più diffusi: Little endian: si memorizzano da destra verso sinistra. Big endian: si memorizzano da sinistra verso destra. Esempio. Supponiamo di voler memorizzare in una memoria byte-addressable il seguente dato di 4 byte: 00000000 00001111 01010101 11111111 Ordinamento dei byte in memoria: endianness 0 0 0 0 0 0 0 0 0 4 0 0 0 0 1 1 1 1 8 0 1 0 1 0 1 0 1 12 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 4 0 1 0 1 0 1 0 1 8 0 0 0 0 1 1 1 1 12 0 0 0 0 0 0 0 0 Little endian Big endian Vantaggi Facilità nelle operazioni aritmetiche. Quando si leggono i dati di una parola (word) multi-byte, il byte meno significativo si trova all’indirizzo più basso. Questo corrisponde direttamente all’ordine delle operazioni aritmetiche nei registri. Compatibilità con i tipi variabili di dati: In molte architetture, il little-endian consente di trattare dati multi-byte come array di byte con un accesso semplice al valore più piccolo senza bisogno di ulteriori operazioni. Dominanza nell’industria: Utilizzato su piattaforme come x86/x86-64 e ARM (nella maggior parte dei casi). Favorisce l’interoperabilità con queste architetture, che rappresentano la maggioranza dei dispositivi consumer. Svantaggi Interoperabilità: Quando i dati vengono trasmessi o memorizzati su sistemi che utilizzano big-endian (es. alcuni protocolli di rete), è necessaria una conversione, che introduce complessità. Umanamente meno intuitivo: La rappresentazione in memoria non corrisponde all’ordine naturale dei numeri. Little-endian: vantaggi e svantaggi Vantaggi Interoperabilità con protocolli di rete: Molti protocolli di rete (come TCP/IP) e formati di dati standard usano big-endian, noto anche come network byte order. La corrispondenza diretta riduce la necessità di conversioni. Più leggibile per l’essere umano: l’ordine di memorizzazione dei byte in memoria corrisponde all’ordine naturale di lettura dei numeri, rendendolo più intuitivo per il debug e la visualizzazione. Svantaggi Operazioni aritmetiche più complesse: Le operazioni hardware come l’incremento di numeri multi-byte richiedono una gestione più complessa, poiché il byte meno significativo non è al primo indirizzo. Meno comune nell’hardware moderno: La maggior parte delle piattaforme moderne (x86, ARM) usa little-endian per impostazione predefinita. L’uso di big-endian può quindi richiedere adattamenti software o hardware. Big-endian: vantaggi e svantaggi Quando un programma C, C++ o Assembly viene eseguito, gli viene riservata una parte di memoria. Tale memoria è suddivisa in 6 sezioni (o segmenti) per memorizzare in modo organizzato le diverse informazioni che servono ad eseguire correttamente il programma. Questa organizzazione viene detta memory layout. Le sezioni sono organizzate in memoria con un ordine ben preciso: prima si trovano 4 sezioni che hanno dimensioni fissate, stabilite in fase di compilazione, e di seguito 2 sezioni la cui dimensione può variare nel corso dell’esecuzione del programma. Memoria dedicata a un programma FFFFFFFFFFFFFFFF … Stack Attualmente non usata Heap Dati non inizializzati.bss Dati in sola lettura.rodata Dati inizializzati.data Codice del programma.text … 000000000000000 Le prime 4 sezioni sono:.text memorizza le istruzioni in linguaggio macchina del programma. È in sola lettura per evitare modifiche accidentali e prevenire problemi in fase di esecuzione o attacchi..data, sezione dei dati inizializzati. Memorizza le variabili globali che sono state inizializzate..rodata, sezione dei dati read-only. Memorizza le costanti globali (non possono cambiare valore nel corso dell’esecuzione del programma)..bss, sezione dei dati non inizializzati. Memorizza le variabili globali che NON sono state inizializzate. Queste variabili verranno automaticamente inizializzate a 0 all’avvio del programma. Il nome si può ricordare come Better to Save Space, ovvero variabili a cui è bene riservare spazio in memoria. Queste sezioni hanno una dimensione fissata, codificata nel file eseguibile. Il sistema operativo sa quanto deve dedicare a ciascuna di esse in base alle istruzioni che compongono il programma e alle variabili globali che sono dichiarate al suo interno. Memoria dedicata a un programma Heap è la sezione dedicata alle variabili dinamiche (es. le sequenze di elementi con dimensione variabile). In base a quanta memoria sarà necessaria in fase di esecuzione del programma, questa sezione crescerà o diminuirà. Memoria dedicata a un programma: heap FFFFFFFFFFFFFFFF … Stack Attualmente non usata Heap Dati non inizializzati.bss Dati in sola lettura.rodata Dati inizializzati.data Codice del programma.text … 000000000000000 Stack è la sezione dove si memorizzano le variabili locali ovvero quelle dichiarate localmente all’interno di un blocco di codice, come ad esempio il corpo di una funzione, di un for, di un if, etc. Inoltre, lo stack viene usato per memorizzare i parametri passati alle funzioni e il return address ovvero l’indirizzo dell’istruzione successiva da cui riprendere l’esecuzione quando una funzione termina. Lo stack cresce verso il basso, dove è presente memoria non utilizzata e ha un funzionamento a pila: LIFO (Last In First Out), cioè l’ultimo elemento inserito è il primo ad essere estratto. L’operazione di inserimento (push) ed estrazione (pop) avviene sempre e solo dallo stesso verso, ovvero dal basso. Il lato in cui si inserisce ed estrae viene detto top (o cima) dello stack. Memoria dedicata a un programma: stack FFFFFFFFFFFFFFFF … Stack Attualmente non usata Heap Dati non inizializzati.bss Dati in sola lettura.rodata Dati inizializzati.data Codice del programma.text … 000000000000000 #include int a = 10; // Variabile intera inizializzata int b; // Variabile intera non inizializzata const int c = 1; // Costante intera int f1(int n) { // Funzione f1: riceve un parametro intero e restituisce un intero int m = a * n; // Salva in m il risultato della moltiplicazione di a per n return m; // Restituisce il valore di m } // L’esecuzione parte da questa funzione main int main() { // Restituisce un intero int x = f1(2); // Chiama f1 e salva il risultato nella variabile x printf("X:%d\n", x); // Stampa il valore di x return 0; // La restituzione di 0 è una convenzione per indicare che non ci sono errori } Esempio #include int a = 10; int b; const int c = 1; int f1(int n) { int m = a * n; return m; } int main() { int x = f1(2); printf("X:%d\n", x); return 0; } Esempio Stack x = ? Memoria non utilizzata Heap Dati non inizializzati.bss, contiene: b=0 Dati in sola lettura.rodata, contiene: c=1 Dati inizializzati.data, contiene: a=10 Codice del programma.text, contiene le varie istruzioni tradotte in linguaggio macchina top dello stack Attualmente, x non ha ancora un valore. #include int a = 10; int b; const int c = 1; int f1(int n) { int m = a * n; return m; } int main() { int x = f1(2); printf("X:%d\n", x); return 0; } Esempio Stack x = ? Indirizzo istruzione successiva Memoria non utilizzata Heap Dati non inizializzati.bss, contiene: b=0 Dati in sola lettura.rodata, contiene: c=1 Dati inizializzati.data, contiene: a=10 Codice del programma.text, contiene le varie istruzioni tradotte in linguaggio macchina top dello stack L’istruzione int x = f1(2) corrisponde a una serie di istruzioni in linguaggio macchina, Qui, viene inserito l'indirizzo della prossima istruzione da eseguire in linguaggio macchina. #include int a = 10; int b; const int c = 1; int f1(int n) { int m = a * n; return m; } int main() { int x = f1(2); printf("X:%d\n", x); return 0; } Esempio Stack x = ? Indirizzo istruzione successiva 2, cioè il parametro passato alla funzione f1 Memoria non utilizzata Heap Dati non inizializzati.bss, contiene: b=0 Dati in sola lettura.rodata, contiene: c=1 Dati inizializzati.data, contiene: a=10 Codice del programma.text, contiene le varie istruzioni tradotte in linguaggio macchina top dello stack Da qui inizia la porzione di stack dedicata alla funzione f1. In generale, la porzione di stack dedicata all’invocazione di una funzione è detta stack frame. #include int a = 10; int b; const int c = 1; int f1(int n) { int m = a * n; return m; } int main() { int x = f1(2); printf("X:%d\n", x); return 0; } Esempio Stack x = ? Indirizzo istruzione successiva 2, cioè il parametro passato alla funzione f1 m = 20 Memoria non utilizzata Heap Dati non inizializzati.bss, contiene: b=0 Dati in sola lettura.rodata, contiene: c=1 Dati inizializzati.data, contiene: a=10 Codice del programma.text, contiene le varie istruzioni tradotte in linguaggio macchina top dello stack Da qui inizia la porzione di stack dedicata alla funzione f1. In generale, la porzione di stack dedicata all’invocazione di una funzione è detta stack frame. #include int a = 10; int b; const int c = 1; int f1(int n) { int m = a * n; return m; } int main() { int x = f1(2); printf("X:%d\n", x); return 0; } Esempio Stack x = ? Indirizzo istruzione successiva Memoria non utilizzata Heap Dati non inizializzati.bss, contiene: b=0 Dati in sola lettura.rodata, contiene: c=1 Dati inizializzati.data, contiene: a=10 Codice del programma.text, contiene le varie istruzioni tradotte in linguaggio macchina top dello stack #include int a = 10; int b; const int c = 1; int f1(int n) { int m = a * n; return m; } int main() { int x = f1(2); printf("X:%d\n", x); return 0; } Esempio Stack x = 20 Memoria non utilizzata Heap Dati non inizializzati.bss, contiene: b=0 Dati in sola lettura.rodata, contiene: c=1 Dati inizializzati.data, contiene: a=10 Codice del programma.text, contiene le varie istruzioni tradotte in linguaggio macchina top dello stack #include int a = 10; int b; const int c = 1; int f1(int n) { int m = a * n; return m; } int main() { int x = f1(2); printf("X:%d\n", x); return 0; } Esempio Stack x = 20 Indirizzo istruzione successiva Memoria non utilizzata Heap Dati non inizializzati.bss, contiene: b=0 Dati in sola lettura.rodata, contiene: c=1 Dati inizializzati.data, contiene: a=10 Codice del programma.text, contiene le varie istruzioni tradotte in linguaggio macchina top dello stack Come visto precedentemente, si intende l’indirizzo della prossima istruzione in linguaggio macchina. #include int a = 10; int b; const int c = 1; int f1(int n) { int m = a * n; return m; } int main() { int x = f1(2); printf("X:%d\n", x); return 0; } Esempio Stack x = 20 Indirizzo istruzione successiva Parametri e variabili della funzione printf Memoria non utilizzata Heap Dati non inizializzati.bss, contiene: b=0 Dati in sola lettura.rodata, contiene: c=1 Dati inizializzati.data, contiene: a=10 Codice del programma.text, contiene le varie istruzioni tradotte in linguaggio macchina top dello stack In questo caso, lo stack frame è dedicato alla funzione printf. #include int a = 10; int b; const int c = 1; int f1(int n) { int m = a * n; return m; } int main() { int x = f1(2); printf("X:%d\n", x); return 0; } Esempio Stack x = 20 Indirizzo istruzione successiva Memoria non utilizzata Heap Dati non inizializzati.bss, contiene: b=0 Dati in sola lettura.rodata, contiene: c=1 Dati inizializzati.data, contiene: a=10 Codice del programma.text, contiene le varie istruzioni tradotte in linguaggio macchina top dello stack In generale, le CPU sono sempre state più veloci rispetto alle memorie, quindi nel momento in cui prelevano le istruzioni dalla memoria centrale devono aspettare un certo numero di cicli prima di ottenere il risultato. Per realizzare delle memorie veloci come le CPU queste devono essere collocate sul chip della CPU, poiché il bus per la memoria è troppo lento. Ma questo pone due limiti: i costi molto alti e la dimensione di un chip di CPU. Per questa ragione, spesso si combinano memorie centrali molto capienti ma lente con una piccola quantità di memoria molto veloce. Quest’ultima è chiamata cache. L’idea di funzionamento è molto intuitiva: la CPU cerca prima le parole che gli servono all’interno della memoria cache, se non le trova allora la richiede alla memoria centrale. Quando si preleva una parola dalla memoria centrale, oltre a prendere la parola stessa, si prendono anche un certo numero di parole vicine e si memorizzano nella cache. Questo meccanismo funziona perché spesso le istruzioni che si eseguono in sequenza sono memorizzate all’interno di indirizzi vicini in memoria centrale. Memoria cache Per calcolare le prestazioni complessive della cache si può utilizzare una semplice formula dove: c è il tempo di accesso alla cache. m è il tempo di accesso alla memoria centrale. h è la frazione di riferimenti che possono essere letti dalla cache invece che dalla memoria centrale. Il tempo medio di accesso si calcola come c + (1 - h) m. Se h fosse vicino a 1, cioè la maggior parte dei riferimenti sono letti dalla cache e non dalla memoria centrale, allora il tempo medio di accesso sarebbe pari a circa c (quindi la velocità della cache). Se h fosse vicino a 0, cioè la maggior parte dei riferimenti sono letti dalla memoria centrale, allora il tempo medio di accesso sarebbe pari a circa c + m (quindi la velocità della memoria centrale più il tempo speso a cercare inutilmente la parola nella cache). Memoria cache La progettazione della cache presenta diversi problemi: La grandezza della cache. Più è grande e migliori sono le prestazioni, ma anche i suoi costi, quindi non è semplice trovare un bilanciamento tra i due. La scelta della linea di cache. La linea di cache è il blocco minimo di dati che la cache può leggere o scrivere dalla memoria principale e ha influenza sulle prestazioni. Infatti, una linea più grande carica più dati vicini, quindi meno accessi alla memoria (più velocità), ma se quei dati vicini non vengono usati, si sprecano spazio e tempo. Una linea più piccola riduce lo spreco di spazio nella cache, caricando solo ciò che serve, ma potrebbe causare più accessi alla RAM se servono dati vicini. Tipicamente, si usano valori come 64 byte o 128 byte, che funzionano bene per la maggior parte dei programmi, ma dipende dai tipi di applicazioni. I programmi con dati vicini beneficiano di linee più grandi, mentre i programmi con dati non contigui beneficiano di linee più piccole. La scelta se mantenere un’unica cache per le istruzioni e i dati oppure avere due cache specializzate (architettura Harvard). La prima avrebbe il vantaggio di una progettazione più semplice, ma le CPU moderne usano il secondo approccio perché la pipeline permettono che l’accesso alla cache sia parallelo. La scelta sul numero di cache da avere (tipicamente tre livelli). Memoria cache I processori moderni hanno tre livelli principali di cache (L1, L2, L3) e, in alcuni casi, una cache di livello superiore (L4). Cache L1 (Livello 1) è la più vicina al core del processore ed è anche estremamente veloce. È molto piccola ed è specifica per ogni core del processore (quindi non condivisa). Cache L2 (Livello 2) è più grande della L1 ma leggermente più lenta (sebbene sia ancora molto veloce). In base all’architettura può essere dedicata per core o condivisa tra più core. Cache L3 (Livello 3) è una cache condivisa tra tutti i core del processore. In genere è più lenta di L1 e L2, ma più grande. Cache L4 (Livello 4) è presente in alcuni processori ad alte prestazioni ed è solitamente condivisa tra il processore e altre unità, come la GPU integrata. I livelli di cache lavorano in modo gerarchico: Il processore cerca prima il dato nella cache L1. Se non lo trova (cache miss), cerca nella L2, poi nella L3, e infine accede alla RAM. Nota: ogni livello è più grande ma più lento del precedente. Memoria cache Le istruzioni Come visto precedentemente, il calcolatore comprende delle istruzioni all’interno di un insieme di istruzioni (instruction set). I linguaggi dei calcolatori sono molto simili tra di loro, perché spesso sono progettati con gli stessi principi fondamentali. Quindi, in genere, dopo aver visto un linguaggio non è particolarmente complesso passare a un altro linguaggio. Di seguito useremo un insieme di istruzioni chiamato RISC-V, poiché è uno standard aperto rilasciato sotto licenza open source. Instruction set Nel RISC-V gli operandi devono essere contenuti nei registri (32 registri in totale, x0–x31) per poter eseguire delle operazioni (dove x0 contiene sempre il valore 0). Alla memoria si accede attraverso istruzioni di trasferimento dati. Nel RISC-V si usa l’indirizzamento al byte e ogni parola ha grandezza 4 byte, questo significa che due variabili successive hanno indirizzi in memoria a distanza 4. Operandi RISC-V 0 byte 0 1 byte 1 2 byte 2 3 byte 3 4... Word Il linguaggio assembly prevede diverse istruzioni. Per le operazioni aritmetiche: add rd, rs1, rs2 significa somma il contenuto del registro rs1 e rs2 e salva il risultato nel registro rd. sub rd, rs1, rs2 significa sottrai il contenuto del registro rs1 e rs2 e salva il risultato nel registro rd. addi rd, rs1, 20 significa somma il contenuto del registro rs1 con 20 e salva il risultato nel registro rd. In genere viene usata per sommare delle costanti. Linguaggio assembly RISC-V Il linguaggio assembly prevede diverse istruzioni. Per il trasferimento dati: istruzione load word: lw rd, offset(rs1) significa sposta una parola dalla memoria al registro, dove rd è il registro di destinazione, rs1 è il registro che contiene un indirizzo di base e offset indica lo scostamento in byte rispetto a rs1. Esempio: supponiamo che in memoria l’indirizzo 0 contenga il valore 42 e il registro x1 contenga 0 (l’indirizzo base). Il codice: lw x2, 0(x1) si occupa di prendere il contenuto dell’indirizzo 0 (quindi 42) e di salvarlo nel registro x2. Supponiamo, inoltre, che in memoria l’indirizzo 4 contenga il valore 35. Il codice: lw x3, 4(x1) si occupa di prendere il contenuto dell’indirizzo 4 (quindi 35) e di salvarlo nel registro x3. istruzione store word: sw rs2, offset(rs1) significa sposta una parola dal registro alla memoria, dove rs2 è il registro che contiene il dato da scrivere in memoria, rs1 è il registro che contiene un indirizzo di base e offset indica lo scostamento in byte rispetto a rs1. Esempio: supponiamo che il registro x2 contenga il valore 42 e il registro x1 contenga 0 (l’indirizzo base). Il codice: sw x2, 0(x1) si occupa di prendere il contenuto del registro x2 (quindi 42) e di salvarlo in memoria all’indirizzo 0. Linguaggio assembly RISC-V Il linguaggio assembly prevede diverse istruzioni. Per il trasferimento dati ci sono anche altre istruzioni simili a quelle precedenti: istruzione che legge mezza parola: lh rd, offset(rs1) istruzione che memorizza mezza parola: sh rs2, offset(rs1) istruzione che legge un byte: lb rd, offset(rs1) istruzione che memorizza un byte: sb rs2, offset(rs1) Linguaggio assembly RISC-V Il linguaggio assembly prevede diverse istruzioni. Per le operazioni logiche: and rd, rs1, rs2 effettua l’and bit a bit tra rs1 e rs2 e salva il risultato in rd. or rd, rs1, rs2 effettua l’or bit a bit tra rs1 e rs2 e salva il risultato in rd. xor rd, rs1, rs2 effettua lo xor bit a bit tra rs1 e rs2 e salva il risultato in rd. esistono le varianti andi, ori, xori dove rs2 non è un registro ma una costante. Linguaggio assembly RISC-V Il linguaggio assembly prevede diverse istruzioni. Per lo scorrimento: sll rd, rs1, rs2 effettua uno scorrimento logico a sinistra, dove rs1 è il registro che contiene il valore da spostare, rs2 è il registro che contiene il numero di posizioni di spostamento (al max 32) e rd è il registro dove verrà salvato il risultato. Il risultato è che ogni bit di rs1 viene spostato a sinistra di un numero di posizioni specificato in rs2, gli spazi lasciati a destra sono riempiti con 0 e i bit più significativi che “escono” vengono scartati. Esempio: supponiamo che x1 contenga: 00000000000000000000000000001011 (11 in decimale) e x2 contenga 00000000000000000000000000000010 (2 in decimale). sll x3, x1, x2 sposta i bit di x1 di 2 posizioni a sinistra: 00000000000000000000000000101100 (44 in decimale) e salva il risultato in x3. Lo spostamento a sinistra è un’operazione molto efficiente per moltiplicare un numero per 2rs2. Infatti, prima abbiamo moltiplicato 11 e 22. srl rd, rs1, rs2 effettua uno scorrimento logico a destra (in modo simile al precedente). Linguaggio assembly RISC-V Il linguaggio assembly prevede diverse istruzioni. Per effettuare dei salti condizionati alle altre istruzioni: beq rs1, rs2, offset se rs1 e rs2 hanno lo stesso valore vai all’istruzione che si trova offset istruzioni avanti o indietro. Quindi, offset specifica l’indirizzo relativo a cui saltare se la condizione è soddisfatta. Esempio: beq x1, x2, L1 # Se x1 == x2, salta all’etichetta L1 addi x3, x0, 1 # (Eseguita solo se x1 != x2) L1: addi x3, x0, 0 # (Eseguita se il salto avviene) bne rs1, rs2, offset, come prima ma va all’istruzione se rs1 e rs2 sono diversi. blt rs1, rs2, offset, come prima ma va all’istruzione se rs1 < rs2. bge rs1, rs2, offset, come prima ma va all’istruzione se rs1 >= rs2. Linguaggio assembly RISC-V Il linguaggio assembly prevede diverse istruzioni. Per effettuare dei salti incondizionati: jal rd, offset, dove rd è il registro di destinazione in cui viene salvato l’indirizzo di ritorno (il PC dell’istruzione successiva) e offset è lo spostamento che specifica l’indirizzo relativo (espresso in parole da 4 byte) a cui saltare. jalra rd, offset(rs1), dove rd è il registro di destinazione in cui viene salvato l’indirizzo di ritorno, rs1 è il registro che contiene l’indirizzo base a cui saltare e offset è un valore costante che viene sommato al contenuto di rs1 per calcolare l’indirizzo di salto. Si utilizza per le funzioni: jal ra, func # Salta alla funzione "func", salva il ritorno in ra # Istruzioni successive func: # Corpo della funzione jalr ra, 0(ra) # Torna al punto di chiamata usando il valore in "ra" Linguaggio assembly RISC-V Supponiamo di avere un’istruzione del tipo add rd, rs1, rs2 e di volerla convertire in linguaggio macchina. L’istruzione add rd, rs1, rs2 appartiene al formato R (R sta per registro) delle istruzioni RISC-V. Quindi, questa viene rappresentata in questo formato a 32 bit: Da assembly al linguaggio macchina funz7 rs2 rs1 funz3 rd codop 7 bit 5 bit 5 bit 3 bit 5 bit 7 bit dove: codop: è il codice operativo, quindi l’operazione alla base dell’istruzione rs1, rs2 e rd: il primo registro, il secondo registro e il registro destinazione funz3 e funz7: due codici operativi aggiuntivi Quindi, un’istruzione del tipo: add x9, x20, x21 viene convertita come segue: Da assembly al linguaggio macchina 0 21 20 0 9 51 7 bit 5 bit 5 bit 3 bit 5 bit 7 bit dove: 51 corrisponde alle operazioni aritmetiche nel formato R funz3 pari a 0 corrisponde all’operazione add funz7 pari a 0 indica l’operazione add senza overflow gli altri sono i numeri dei registri. Questi vengono convertiti in binario: 0000000 10101 10100 000 01001 0110011 7 bit 5 bit 5 bit 3 bit 5 bit 7 bit Quindi, l’istruzione del tipo: add x9, x20, x21 corrisponde alla sequenza binaria: 00000001010110100000010010110011 che successivamente viene rappresentata in esadecimale per avere le istruzioni in modo più compatto e leggibile rispetto alla forma binaria: 0000 0001 0101 1010 0000 0100 1011 0011 0 1 5 A 0 4 B 3 Da assembly al linguaggio macchina Quindi l’istruzione add x9, x20, x21 corrisponde all’istruzione in linguaggio macchina 0x015A04B3. Caratteristica RISC-V MIPS ARM v7 ARM v8 (AArch64) x86 (IA-32) x86-64 Filosofia Open-source Proprietaria Proprietaria Proprietaria Proprietaria Proprietaria Lunghezza istruzioni Fissa: 32 bit (16 bit opzionale) Fissa: 32 bit Variabile 16/32 bit Fissa: 32/64 bit Variabile: 1-15 byte Variabile: 1-15 byte Set base di istruzioni Minimo (modulare: estensioni) Moderato Moderato Più ampio rispetto a ARM v7 Ampio Molto ampio Supporto floating-point Opzionale Separato in coprocessori (FPU) Integrato Integrato Integrato Integrato Registri 32 registri general-purpose 32 registri general-purpose 16 registri (R0-R15) 31 registri (X0-X30) 8 registri (EAX, EBX, ecc. 16 registri (RAX, RBX, ecc.) Endianess LE BE/LE LE (BE opzionale) LE LE LE Architettura RISC RISC RISC RISC CISC CISC Target principale Open computing (embedded, HPC) Embedded, educazione Mobile, embedded Server, desktop, mobile, embedded Desktop Server, desktop Confronto tra varie architetture

Use Quizgecko on...
Browser
Browser