Architettura del processore

A.S. 2018-2019

Istituto di Istruzione Superiore G. Marconi

Sistemi e Reti

Benvenuti ragazzi!

Questo materiale riepiloga quanto detto a lezione negli scorsi giorni.

Le risposte alle domande dei compiti in classe sono tutte incluse in questo materiale.

I riferimenti al libro di testo sono tra parentesi quadre, come ad esempio [pag. 1].

Il testo in questi box sono per approfondimento. La lettura di questo materiale non è strettamente necessaria per le verifiche, ma è consigliata per vostra cultura personale e in quanto sapere indispensabile per chi vuole diventare un informatico.

Buon studio e buon lavoro.

Che cos'è un computer?

Prima di parlare del processore, dobbiamo avere bene in mente che cos'è un computer, ed in particolare che cos'è un computer oggi come lo conosciamo noi.

Computer = processore + memoria (breve storia)

Volendo semplificare al massimo, i computer che usiamo di solito sono composti da due unità fondamentali distinte:

  • processore, che esegue calcoli ed operazioni
  • memoria, che immagazzina le informazioni (dati e istruzioni) che devono essere usate dal processore

Questo tipo di divisione ha una storia che percorre tutto il secolo scorso. Nasce dalla macchina ideale di Turing, descritta da Alan Turing nel 1936. [pagg. 2-3 incluso il box "Macchina di Turing"]

La storia di Alan Turing è molto interessante, recentemente ne hanno tratto un film chiamato The Imitation Game. Nato a Londra nel 1912, è considerato uno dei padri dell'informatica ed è stato un grande matematico; viene anche considerato il fondatore dell'intelligenza artificiale (molto famoso è il Test di Turing, presentato in un articolo del 1950). Curiosità: Turing fu trovato morto l'8 giugno 1954 per avvelenamento nel suo letto, accanto a lui c'era una mela con un morso; probabilmente si trattò di un suicidio a causa della persecuzione legale per omosessualità. Anche se non c'è una fonte ufficiale, si dice che Steve Jobs decise di scegliere la mela morsicata come logo della sua azienda proprio in omaggio a Turing.

Il modello ideale (cioé non realizzabile nella pratica) di Turing fu ripreso da John von Neumann (si legge fòn nòimann) che realizzò nel 1949 la prima macchina digitale programmabile, chiamata EDVAC.

John von Neumann nacque a Budapest (Ungheria) nel 1903 ma naturalizzato americano dopo il suo trasferimento negli USA a causa delle persecuzione nazista. Anche lui è considerato uno dei padri dell'informatica ed uno dei più importanti scienziati del '900. Il suo contributo alla scienza fa dalla matematica alla statistica, alla fisica quantistica e, ovviamente, all'informatica che stava nascendo proprio in quel periodo. Fu uno dei più grandi sostenitori della bomba atomica e del suo utilizzo militare nel Progetto Manhattan; questo fervore lo rese una figura controversa nel dopoguerra.

Ancora oggi la maggior parte dei computer che utilizziamo si basa sull'architettura di Von Neumann: come abbiamo detto all'inizio, l'unità di computazione è nettamente separata da quella di memorizzazione dei dati e delle istruzioni.

Non tutti i sistemi di elaborazione delle informazioni funzionano con questa separazione: ad esempio, il cervello è costituito da una rete neurale (un insieme di sinapsi collegate fra loro) che ha la funzione sia di processare che di memorizzare le informazioni. Vedi la ricerca svolta da Lorenzo Dionisi a riguardo.

Può un computer pensare? Domanda molto difficile a cui rispondere, ovviamente. Per sapere lo stato dell'arte in questo momento sul tema, vi consiglio di seguire il corso Introduction to Philosophy dell'Università di Edinburgo. Se volete arrivare subito alla parte su mente, cervello e computer, potete saltare direttamente alla settimana 4; vi consiglio comunque di seguire tutto il corso, è molto interessante. Il corso è gratuito, in lingua inglese con sottotitoli disponibili in italiano.

Raspberry PI

Nello studio del computer abbiamo la fortuna di poter utilizzare i nostri dispositivi per poter verificare in prima persona gli argomenti. Per semplicità, per questo corso ho scelto di usare la Raspberry PI, una single-board computer dal prezzo accessibile (circa 40€) e dalle prestazioni adatte alla maggior parte degli usi domestici. L'ultimo modell ad oggi (ottobre 2018) è la Raspberry Pi 3 B+.

Raspberry Pi è stata sviluppata nel Regno Unito dalla Raspberry Pi Foundation. La presentazione al pubblico è avvenuta il 29 febbraio 2012. E' attualmente la single-board computer più diffusa sul mercato, è molto facile trovare progetti di tutti i tipi che ne fanno uso.

Single-board computer significa che tutte le componenti del computer sono sulla stessa scheda. Come dicevamo nel capitolo precedente, a noi interessano particolarmente processore e memoria.

Analizziamo la Raspberry: per sapere a cosa corrispondono i vari componenti, possiamo leggere il codice stampato sopra ad ognuno di essi e fare una ricerca su Internet. Dopo una breve ricerca possiamo trovare che il processore è quello cerchiato nell'immagine qui sotto, con la scritta "Broadcom".

Dov'è la RAM? Per trovarla dobbiamo girare la scheda e guardare sul retro. Il chip è quello grande con scritto "Elpida".

Accedere al terminale

Raspberry PI personale

Se avete a disposizione a casa una Raspberry, potete usarla per questo corso, è la scelta raccomandata.

Se non l'avete già fatto, scaricate e installate il sistema operativo seguendo la guida ufficiale. Avrete bisogno di una scheda SDCard; vi consiglio una scheda da 8, 16 o 32 GB.

Una volta accesa, aprite un terminale premendo sull'icona con lo schermino nero nella barra in alto, a sinistra. Vi comparirà una schermata simile a quella qui sotto.

Raspberry PI remota

Se non avete una Raspberry, potete usare quella della scuola collegandovi via SSH. Potete collegarvi da desktop con Google Chrome o da smartphone con Termius.

Desktop

Aprite Chrome e installate l'estensione Secure Shell Extension. Vi comparirà l'icona del terminale in alto a destra.

Cliccateci sopra, andate su Connect Dialog, sia aprirà una finestra come quella qui sotto.

Inserite username e hostname che vi sono stati forniti a lezione.

Smartphone

Istruzioni valide sia per iOS (iPhone, iPad) che Android.

Scaricate l'applicazione Termius ed apritela.

Cliccate sul + in basso a destra, quindi "New Host", come Alias mettere "Raspberry Marconi" e in hostname, username e password quelli che vi sono stati forniti a lezione.

Android in locale

Se non avete una Raspberry ma volete comunque fare gli esercizi in locale, senza una connessione Internet, potete usare un qualsiasi smartphone Android. Vi basta installare l'applicazione Termux per avere un terminale molto simile a quello della Raspberry. Per usare i tasti speciali (es. ctrl, esc, tab, etc.), molto utili da terminale, potete consultare questa guida; TL;TR premete la combinazione "Volume up + q".

Computer Linux

Se avete un computer con il sistema operativo Linux (es. Ubuntu o Mint), potete provare ad usarlo per questo corso. I risultati potrebbero essere un po' diversi ed alcuni comandi potrebbero non funzionare, ma con un po' di buona volontà si riesce a far tutto :)

Analisi software e hardware

Dal terminale voi avete pieno controllo di tutto il sistema della vostra macchina. Cominciamo con richiedere le informazioni essenziali attraverso il seguente comando. ATTENZIONE: i comandi nelle guide come questa per convenzione vengono preceduti dal carattere dollaro $, per far capire che è un comando da terminale; voi non dovete copiare il dollaro! Tutto ciò che segue e che non è preceduto dal dollaro, è l'output del terminale (cioé quello che viene restituito dal comando).

$ uname -a
Linux raspberrypi 4.9.80-v7+ #1098 SMP Fri Mar 9 19:11:42 GMT 2018 armv7l GNU/Linux

Analizziamo alcune cose importanti di quello che ci restituisce:

  • Linux: kernel del sistema operativo (la parte del sist.operativo a stretto contatto con l'hardware)
  • raspberrypi: nome della nostra macchina che viene visualizzato quando connesso in rete
  • ...
  • armv7l: architettura del processore
  • GNU/Linux: nome completo del sistema operativo

Possiamo vedere qui che il processore ha una archiettura di tipo ARM.

Se usate il vostro smartphone Android, avrete dei risultati leggermente diversi ma comunque simili.

Analizziamo meglio il processore attraverso il comando lscpu:

$ lscpu
Architecture:          armv7l
Byte Order:            Little Endian
CPU(s):                4
On-line CPU(s) list:   0-3
Thread(s) per core:    1
Core(s) per socket:    4
Socket(s):             1
Model:                 4
Model name:            ARMv7 Processor rev 4 (v7l)
CPU max MHz:           1400.0000
CPU min MHz:           600.0000
BogoMIPS:              38.40
Flags:                 half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32

Qui ci sono tutte le informazioni dettagliate sul processore. In particolare possiamo vedere che il processore ha 4 core (numero di CPU) e una velocità massima di 1400 MHz.

Su Android, il comando lscpu va installato: se provate a lanciarlo, vi darà le istruzioni per farlo. Di solito si installa lanciando il comando pkg install util-linux. Se non dovesse funzionare, provate prima a lanciare il comando pkg update.

Il Processore

Cominciamo ad esaminare come lavora un processore più nel dettaglio.

Prima di tutto va compreso che, anche se può fare cose molto complesse, il processore sa eseguire ad ogni passo solo operazioni molto semplici. Volendo estremizzare, il processore fa fondamentalmente le seguenti cose:

  • leggere e scrivere dalla memoria RAM
  • fare operazioni logiche su numeri binari, ad esempio AND, OR, NOT, etc.
  • fare operazioni aritmetiche su numeri binari, in particolare la somma

Le applicazioni complesse che conosciamo si basano su queste operazioni elementari. Il vantaggio dei processori è che riescono a fare in un secondo miliardi di queste operazioni: ad esempio il processore della Raspberry ha un clock di esecuzione di 1.4 GHz, quindi riesce ad eseguire un miliardo e quattrocento milioni di operazioni in un secondo, più o meno.

Il cervello funziona in modo molto diverso: ha una frequenza che va da 4 a 40 Hz, ma ad ogni iterazione fa moltissime operazioni.

Un'altra cosa fondamentale da capire è che il processore capisce solo numeri binari. Non esistono nient'altro che uno e zeri all'interno del processore. Questo è dovuto al fatto che all'interno ci sono collegamenti elettrici e porte logiche, che possono per l'appunto avere solo i valori di acceso (passa corrente) o spento (non passa corrente). Tutto quello che non è binario deve essere convertito per poter essere utilizzato. E' per questo che è fondamentale conoscere bene l'aritmetica binaria per poter comprendere come funziona una CPU.

Per impratichirvi con le porte logiche, vi consiglio di giocare a Circuit Scramble. Per iOS non li conosco, se avete un'app da proporre fatemi sapere che l'aggiungo qui!

La memoria

Il secondo componente fondamentale dei computer è la memoria.

Prima di tutto una precisazione: quando ora parliamo di memoria, intendiamo un tipo specifico chiamato memoria primaria: generalmente è costituita dalla RAM, ovvero Random Access Memory, memoria ad accesso casuale.

Diversi tipi di RAM. Fonte: wikipedia

La caratteristica principale è che, come dice il nome, si può accedere a qualsiasi parte di questo tipo di memoria semplicemente conoscendone la posizione, che è rappresentato da un numero intero. Questo numero viene anche detto indirizzo. Al contrario, altri tipi di memoria come l'hard-disk, devono essere acceduti in maniera sequenziale: per poter accedere ad una certa porzione di memoria, bisogna prima scorrere parte della memoria precedente: questo la rende ovviamente molto più lenta.

Possiamo immaginare la RAM come un lunghissimo nastro, diviso in caselle, proprio come il nastro della macchina di Turing.

Un esempio di macchina di Turing. Fonte: wikipedia

Come dicevamo, la caratteristica di questa memoria è che si può accedere a qualsiasi casella, basta saperne l'indirizzo. Per convenzione e comodità, si usano i numeri esaedecimali per indicare l'indirizzo.

Torniamo al caso della nostra Raspberry. Per sapere la dimensione della memoria, possiamo usare il seguente comando da terminale.

$ cat /proc/meminfo
MemTotal:         949580 kB
MemFree:          335800 kB
MemAvailable:     698964 kB
Buffers:          196460 kB
Cached:           218844 kB
SwapCached:            0 kB
Active:           282936 kB
Inactive:         290512 kB
...

Potete eseguire lo stesso comando anche sul vostro dispositivo Android.

L'output del comando è molto lungo, ho omesso la parte finale per brevità. Il dato che ci interessa in particolare è il primo: memoria totale pari a 949580 kB. Per convertirlo in byte, dobbiamo sapere quanto vale un kB: normalmente il prefisso k sta per kilo e significa 1000, ma in questo caso il kernel (che gestisce /proc/meminfo) usa una convenzione sua e vale 1024 :(.

Calcoliamo il numero totale di byte della memoria, e convertiamolo in esadecimale:

949580 kB = 949580 * 1024 = 972369920 (in base 10) = 0x39F53000 (in base 16)

Quindi, noi avremo un "nastro" di memoria RAM a cui possiamo accedere usando un indirizzo da 0x00000000 a 0x39F53000.

Somma di interi

Prendiamo come esempio una semplice operazione e proviamo a capire come deve essere trasformata per poter essere eseguita da un processore.

Ipotizziamo che vogliamo sommare due numeri interi. Come dobbiamo fare? Ricordiamoci che il processore può solo leggere e scrivere sulla memoria, ed effettuare semplici operazioni. Il processore inizia e finisce in uno stato "vuoto", quindi tutto quello che deve fare e tutti i dati che usa devono essere già in memoria prima dell'esecuzione del programma.

Proviamo a scomporre in piccoli passi:

  1. leggo dalla memoria il primo addendo, e lo copio in un registro interno, ad esempio regA
  2. leggo dalla memoria il secondo addendo, e lo copio in un altro registro interno, ad esempio regB
  3. leggo dalla memoria l'operazione che devo eseguire, in questo caso somma
  4. eseguo l'operazione
  5. scrivo il risultato in un registro, per ottimizzare posso riutilizzare il regA (così uso solo due registri in totale)
  6. copio il contenuto di regA in memoria.

OK, sembra che ci siamo. Proviamo a vedere se funziona effettivamente così. Per verificare, utilizzeremo il linguaggio C perché è quello che ha meno "passaggi" dal codice sorgente al compilato.

int main() {
  int a = 12;
  int b = 32;
  int c = a + b;
  return 0;
}

Per salvare questo file in locale, utilizzate l'editor nano.

$ nano

Ricopiate il codice sopra, salvatelo con ctrl-o con il nome main.c, ed uscite dall'editor con ctrl-x.

Un alternativa più professionale a nano è vim, lo trovate già installato sulla Raspberry. Un tutorial efficace e divertente per imparare a usare vim è Vim Adventures.

Ora compiliamo il file con gcc e ci facciamo generare l'assembly con l'opzione -S:

$ gcc -S main.c

Otteniamo un output simile al seguente:

.arch armv6
.eabi_attribute 28, 1
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 2
.eabi_attribute 30, 6
.eabi_attribute 34, 1
.eabi_attribute 18, 4
.file	"main.c"
.text
.align	2
.global	main
.syntax unified
.arm
.fpu vfp
.type	main, %function
main:
@ args = 0, pretend = 0, frame = 16
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
str	fp, [sp, #-4]!
add	fp, sp, #0
sub	sp, sp, #20
mov	r3, #12
str	r3, [fp, #-8]
mov	r3, #32
str	r3, [fp, #-12]
ldr	r2, [fp, #-8]
ldr	r3, [fp, #-12]
add	r3, r2, r3
str	r3, [fp, #-16]
mov	r3, #0
mov	r0, r3
add	sp, fp, #0
@ sp needed
ldr	fp, [sp], #4
bx	lr
.size	main, .-main
.ident	"GCC: (Raspbian 6.3.0-18+rpi1+deb9u1)

Potete eseguire questi passi anche da Android. Dovrete installare prima gli strumenti di sviluppo con il comando pkg install build-essential.

La parte che ci interessa è quella all'interno della sezione main:.

mov	r3, #12
str	r3, [fp, #-8]
mov	r3, #32
str	r3, [fp, #-12]
ldr	r2, [fp, #-8]
ldr	r3, [fp, #-12]
add	r3, r2, r3
str	r3, [fp, #-16]

Le operazioni che può eseguire il processore sono quelle più a sinistra:

  • mov: move, copia un valore costante in un registro
  • str: store register, copia il valore dal registro alla memoria ad un certo indirizzo
  • ldr: load register, copia il valore dalla memoria al registro ad un certo indirizzo

Analizziamo il codice assembly nel dettaglio:

  • mov r3, #12: assegna il valore 12 al registro r3
  • str r3, [fp, #-8]: copia il valore del registro r3 nell'indirizzo di memoria -8
  • mov r3, #32: assegna il valore 32 al registro r3
  • str r3, [fp, #-8]: copia il valore del registro r3 nell'indirizzo di memoria -12
  • ldr r2, [fp, #-8]: copia il dato all'indirizzo di memoria -8 nel registro r2
  • ldr r3, [fp, #-12]: copia il dato all'indirizzo di memoria -12 nel registro r3
  • add r3, r2, r3: effettua la somma dei registri r3 e r2 ed assegna il risultato a r3
  • str r3, [fp, #-16]: copia il valore del registro r3 nell'area di memoria -16

Concettualmente, è così che funziona un processore: anche le operazioni molto complesse vengono suddivise in piccoli parti fino ad arrivare ad un risultato molto simile a quello presentato.

Arithmetic Logic Unit

Facciamo un ulteriore passo avanti verso la comprensione di come funziona nel dettaglio l'elaborazione dei dati in un computer. Nella pagina precedente abbiamo visto che uno dei comandi che può interpretare il processore è add.

Ricordiamo ancora una volta che una delle linee guida più importanti nella progettazione del processore è l'efficienza: operazioni molto comuni come la somma deve essere effettuata nel modo più veloce teoricamente possibile. Per questo motivo è stata ideata un'unità dedicata, chiamata Arithmetic Logic Unit (ALU).

Rappresentazione grafica di una ALU.
Fonte: Lambtron - Own work, CC BY-SA 4.0, Link

L'idea di ALU è stata proposta da von Neumann stesso insiema al primo computer, EDVAC. La prima implementazione su circuito integrato è del 1967.

Come funziona in pratica? L'ALU all'interno ha un circuito disegnato appositamente per fare alcune operazioni logiche (es. AND, OR, NOT, XOR) e matematiche (es. somma di binari, complemento a due). Esiste un progetto su Minecraft che spiega nel dettaglio come è possibile realizzare un ALU completamente funzionante usando le pietre rosse (Redstone) e relativi componenti. Un video di una ALU realizzata in pietra rossa è disponibile qui.

Somma di interi

La somma di interi in binario funziona esattamente come la somma in decimale che abbiamo imparato alle elementari, con la sola differenza che:

  • abbiamo solo due simboli disponibili 0 e 1 (non dieci)
  • il riporto deve essere fatto quando si supera il valore 1 (e non 9)

Ad esempio:

// Quanto fa 6+5?
2 --converto in binario--> 10
3 --converto in binario--> 11

// 1. Metto i numeri in colonna
  10 +
  11 =
------


// 2. Sommo la colonna più a destra
 10 +
 11 =
------
  1

// 3. Passo alla colonna successiva: 1+1 fa 0 col riporto di 1
 10 +
 11 =
------
101   --converto in decimale--> 5  OK, ci torna!

Rappresentazione dei numeri negativi

Come rappresentiamo i numeri negativi? Ricordiamoci che il processore capisce solo 0 e 1, quindi non abbiamo modo di scrivere il segno "-" davanti ad un numero.

Una soluzione che sembra semplice, ed è stata utilizzata nei primi anni dell'informatica (anni '50 e '60 del secolo scorso) è quella di usare il primo bit del numero come segno. Questo rappresentazione è chiamata complemento a uno. Ipotizziamo di avere una rappresentazione del nostro numero binario a 4 bit:

// Primo bit 0, numero positivo
0101  --> +5
// Primo bit 1, numero negativo
1101  --> -5

Questa rappresentazione ha diversi inconvenienti, principalmente:

  • non è semplicissimo matematicamente fare somme
  • il valore 0 ha due rappresentazioni: 0000 e 1000

Complemento a due

Attualmente tutti i processori e relative ALU utilizzano un altro sistema, chiamata complemento a due. Per rappresentare un numero negativo, si usano i seguenti passi:

  1. prendo il valore positivo
  2. inverto tutti i bit
  3. sommo 1

Esempio:

// Voglio rappresentare il numero -5
// 1. prendo il valore positivo
0101
// 2. inverto i bit
1010
// 3. aggiungo 1
1011

Quindi la rappresentazione di -5 in complemento a due è 1011.

Per tornare da rappresentazione binaria in complemento a due a decimale, basta fare l'operazione inversa:

// Quanto vale 1001?
// 1. sottraggo 1
1000
// 2. inverto i bit
0111 --> +7
// 3. cambio di segno
+7 --> -7

Somma con numeri negativi

In questo modo fare le somme tra numeri è "gratis": posso eseguire la somma come abbiamo già imparato, ricordandoci che una sottrazione può sempre essere riscritta come una somma tra numeri con segno.

// Quanto fa 7-5?
// La sottrazione può essere riscritta come: 7+(-5)
// Converto i numeri in binario
7 --> 0111
-5 --> 1011
// Eseguo la somma
0111 +
1011 =
------
0010   --> +2    OK, ci torna!

Per approfondimenti ed esercizi, vi consiglio di consultare questo tutorial.

Hack & Security

Come qualsiasi altro componente di un sistema, fisico o software, anche i processori hanno delle vulnerabilità che devono essere conosciute.

Le vulnerabilità dei processori hanno fatto notizia all'inizio di quest'anno con la divulgazione di due criticità che coinvolgono moltissimi processori attualmente in commercio: Meltdown e Spectre.

Meltdown e Spectre sono chiamati side-channel attacks perché sfruttano l'implementazione fisica di alcuni algoritmi, e si contrappongono principalmente ai brute force attacks che invece si basano, come dice il nome, su attacchi a "forza bruta".

Meltdown e Spectre si basano su alcune strategie di ottimizzazione del processore. Come abbiamo detto all'inizio di questo capitolo, il numero di operazioni al secondo che può fare un processore equivale più o meno alla sua frequenza, ad esempio 1.2 GHz. Oltre una certa frequenza di esecuzione non si può andare, per motivi fisici e quantistici. Per aumentare le prestazioni si usano quindi altri metodi, in particolare:

  • out-of-order processing: le istruzioni vengono riordinate per cercare di eseguirne il più possibile ad ogni ciclo
  • branch prediction: il corpo all'interno dei condizionali (es. if) viene eseguito prima di fare il controllo della condizione stessa, sempre per cercare di fare più operazioni ad ogni ciclo e non lasciare mai il processore sotto-utilizzato.

Non tutti i processori usano queste strategie: ad esempio i Cortex-A7 e Cortex-A53 non le usano e non sono quindi vulnerabili a Meltdown e Spectre.

Per appofondimenti su questo tema, si consiglia caldamente la lettura di questo post.