1.05 Lezione 5 - follen99/ArchitetturaDeiCalcolatori GitHub Wiki

📅 03-17

Recap lezione 4

Ogni istruzione è rappresentata in binario, ed ogni istr è una stringa di bit che va a finire in memoria insieme al resto dei dati (in un sistema moderno).

A volta i programmi potrebbero operare su altri programmi, in modo che l'utput di uno sia l'input dell'altro.

Compatibilità binaria: se ho un programma scritto per un certo processore, ed ho un altro processore che utilizza lo stesso set di istruzioni (ovvero binary compatible) posso passare il programma da un sistema all'altro.

Operazioni logiche

L'ALU viene chiamata in questo modo, perchè esegue operazioni aritmetiche, ma anche logiche:

le operazioni logiche permettono delle manipolazioni di bit:

  • Shift a sinistra: prendere una stringa di bit e shiftarla a sinistra aggiungendo degli zeri in coda.
  • Shift a destra: corrispettivo dello shift a sinistra

Il RISC v dispone del "pacchetto" minimo indispensabile per eseguire queste operazioni.

Molto spesso lo shift viene utilizzato per effettuare le divisioni per 2: se shifto una stringa di bit di una posizione verso destra, ottengo la divisione per due di quel numero. Gli shift logici non preservano il segno, mentre quelli aritmetici si. Gli shift vengono inoltre usati per effettuare delle moltiplicazioni per potenze di due, quindi per risparmiare tempo i processori, automaticamente sostituiscono le moltiplicazioni con una serie shift.

Il RISC non dispone di operazioni di NOT, che però può essere ottenuta con le operazioni precedenti.

Operazioni AND

Le operazioni di AND possono essere usate per mascherare un numero binario; infatti se eseguo l'AND bit a bit tra due binari, dove porrò "1" nella seconda stringa, posso preservare il valore della stringa originaria; questo perché, ovviamente, 1AND1 = 1, ma 1AND0 = 0.

Operazioni OR

Se volessi impostare solo alcuni bit ad 1 in un registro, lasciando gli altri inalterati. Ad esempio voglio che 4 particolari bit di una sequenza devono essere 1 indipendentemente dall'altra stringa di bit.

Per effettuare questa operazione si utilizza un'operazione di OR.

Operazioni XOR - NOT

Visto che non ho un'operazione di NOT nel RISC, come posso "crearmela"?

Basta utilizzare un XOR con una string di bit composta da soli 1. Questo perchè:

1 XOR 1 0
1 XOR 0 1

Possiamo inoltre utilizzare l'XOR per effettuare il complemento a due:

  1. Complemento bit a bit (XOR con stringa di 1)
  2. Sommare 1

Operazioni condizionali

Sono le istruzioni che permettono di implementare i vari if, while, for, ecc, che troviamo nei linguaggi ad alto livello.

beq rs1, rs2, L1

  • If(rs1 == rs2) branch to instruction label L1

bne rs1, rs2, L1

  • If(rs1 != rs2) branch to instruction label L1

Come scrivere gli if statements in assembly

bne s6, s7, Else
add s3, s4, s5
beq zero, zero, Exit	# non condizionale

Else: sub s3, s4, s5
Exit:	...

Il blocco Else viene eseguito solo se il contenuto di s6 ed s7 non è uguale; se invece s6==s7 allora viene eseguita l'addizione.

Gli indirizzi delle Etichette vengono calcolate automaticamente dall'assembler, quindi ci basta porre delle etichette simboliche.

🏁 0:58

Come scrivere i loop in assembly

while (save[i] == k) 
  i +=1;

Codice scritto in assembly

Loop: slli a0, s6, 3	# l'offset viene calcolato precedentemente
			add a0, a0, s9 	# devo costruire l'indirizzo save[i] sommando ad a0 l'indirizzo di partenza dell'array posto in s9
			ld	s1, 0(a0)		# prendo la dw in a0 (ovvero save[i])
			bne s1, s8, Exit	# se s1≠s8 esco
			addi s6, s6, 1		# incremento
			beq	zero, zero, Loop
			
Exit:	...

slli a0, s6, 3 sto shiftando i contenuti di s6 a sinistra di 3 posizioni (23 = 8) e lo salvo in A0, sta semplicemente moltiplicando i x 8; spostarmi nella i-esima posizione significa prendere i, moltiplicarlo per 8 e sommarlo all'indirizzo di partenza dell'array.

Basic Block

E' un blocco di istruzioni in cui si rimane sicuramente durante l'esecuzione. Non ci sono quindi salti, ed è eseguito in modo sequenziale.

Questi blocchi sono di interesse per i compilatori, proprio perché sono i blocchi su cui possono essere realizzate delle ottimizzazioni. Il compilatore cerca quindi di "incastrare meglio" i blocchi, per ottimizzare dove è possibile.

Altri operatori condizionali

Abbiamo ovviamente anche altri operatori condizionali:

  • blt rs1, rs2, L1 branch less than
  • bge rs1, rs2, L1 branch grater equal

Signed vs Unsigned

Ci sono degli operatori che trattano le stringhe di bit nei registri come signed, ed altre come unsigned.

Le istruzioni RISC Signed/unsigned sono:

Signed bge blt
Unsigned bltu bltu

🏁 03-17 1:21

Chiamata di procedure

  1. Sono all'interno di un unità di programma; ad un certo punto richiamo la funzione f() con dei parametri;
  2. Salto ad un altro pezzo di programma, dove la funzione f() è locata. Questa funzione definirà delle variabili e compierà operazioni.
  3. Quando la funzione ha finito di eseguire, ci sarà un return, che mi porterà all'istruzione immediatamente successiva a quella della chiamata di funzione.

Il tutto viene eseguito tramite lo stack, infatti, andando a salvare sullo stack dei punti di ritorno possiamo effettuare il pop() dallo stack e tornare al punto della chiamata di funzione.

A livello di linguaggio assembly devo:

  1. Preparare i parametri
  2. Eseguo un salto e devo ricordare da qualche parte il punto di ritorno, noto come ra, ovvero return address.
  3. Quando la funzione finisce recupera il ra e torna dove è stata chiamata la funzione iniziale.

Quali registri usare?

I registri che vengono utilizzati nella programmazione assembly per passare i parametri sono i registri "A", ovvero argument.

Più precisamente, a0-a1 fungono da doppia funzione, servono sia come parametri che come risultato, mentre a2-a7 vengono usati solo per i parametri.

Esiste un registro apposito, ra, che contine il punto di ritorno della funzione, che ci permette di tornare dalle funzioni.

Istruzioni per chiamata di funzioni

L'istruzione che ci permette di effettuare dei "salti" è:

jal ra, Label

Questa funzione ha due parametri, ra, ovvero il registro di ritorno, ed un altro parametro label che è proprio il label a cui l'istruzione effettuerà il salto.

Per tornare indietro, si usa l'istruzione:

jalr zero, 0(ra)

Salta all'indirizzo specificato (in questo caso o(ra)) e salva il ra in "zero", ovvero un registro che vale sempre 0, essenzialmente inutile (non salva quindi il ra). Questa istruzione è "leggermente strana" (per via di "zero") semplicemente per una questione di uniformità dell'istruzione, ma il primo parametro (zero) non serve a nulla.

Esempio procedura foglia

Una procedura foglia è una procedura che non richiama altre procedure. E' importante che sia una funzione foglia, perchè per avere una procedura non foglia, la questione sarebbe più complicata.

leaf_example:
	addi sp, sp, -24
	sd	 s0, 16(sp)
	sd	 s1, 8(sp)
	sd	 s4, 0(sp)
	
	add	s0, a0, a1
	add	s1, a2, a3
	sub	s4, s0, s1
	addi	a0, s4, 0
	
	ld	s4, 0(sp)
	ld	s1, 8(sp)
	ld 	s0, 10(sp)
	addi sp, sp, 24
	
	jalr zero, 0(ra)
Come funziona lo stack

Lo stack pointer punta all'ultima posizione occupata dello stack (in memoria); quando parte il programma viene destinata una parte di memoria allo stack. Lo stack parte da un indirizzo più grande e si estende via via che viene usato verso indirizzi più piccoli (in questo processore).

Oltre allo stack è presente anche un heap, che viene usato per allocare dinamicamente la memoria. E' una struttura dati di tipo diverso, dove non esiste un vero e proprio ordine di ingresso ed uscita.

Queste due strutture dati si contendono la memoria attribuita al programma, infatti man mano che queste vengono "popolate" si estendono l'una verso l'altra, possibilmente senza mai incontrarsi.

Come salvare sullo stack

Per salvare sullo stack i registri s0, s1, s4, dobbiamo "fare spazio" per 3 posizioni da 8 byte l'una, quindi in totale 24 byte.

Per fare spazio, quindi, spostiamo il puntatore allo stack sp di -24 byte con addi sp, sp, -24.

Successivamente posso salvare i registri sullo stack con sd s0, 16(sp) ...

Come copiare un registro in un altro

Questa operazione è supportata da altri processori tramite l'istruzione move (mv). Il RISC, però, non dispone di questa istruzione;

Per risolvere il problema si utilizza semplicemente un'addizione del tipo: addi a0,s4,0 in modo da spostare s4 in a0. La stessa cosa si può fare usando l'add normale: add a0,s4,zero, dove "zero" è il registro zero del risc.

Ritornare e ripristinare lo stack

Per uscire dalla funzione ci basta ripristinare lo stack, andando a prelevare i dati che erano salvati precedentemente nei registri s0, s1, s4, dallo stack. Successivamente, ci basta chiamare jalr zero, 0(ra).

Perchè fare tutti questi movimenti con lo stack?

Sullo stack, prima di entrare nella funzione, non è salvato nulla, quindi non è il suo contenuto che ci interessa (come potrebbe accadere per il ra).

Usiamo lo stack per salvare i contenuti dei registri save (s0, s1, s4), in modo tale da poterli usare (invece di usare i registri temporanei, 't') a nostro piacimento, senza andare ad intaccare un'altra possibile funzione o programma che li stava usando per salvare altri dati.

Quindi?

I registri save sono stati utilizzati solo a puro scopo didattico, per far vedere come salvare il loro contenuto all'interno dello stack. usando i registri temporanei il codice avrebbe potuto essere:

leaf_example:	
	add	t0, a0, a1
	add	t1, a2, a3
	sub	t4, t0, s1
	addi	a0, t4, 0
	
	jalr zero, 0(ra)

🏁 FINE LEZIONE 5

⚠️ **GitHub.com Fallback** ⚠️