La costruzione dei Programmi

Utilizzo delle subroutine

Le capacità delle singole istruzioni del linguaggio macchina sono estremamente limitate. Costruire un programma per eseguire un compito complesso a partire da questi componenti primitivi può sembrare un'impresa ardua. Sono sicuro che ormai puoi immaginare la soluzione: i compiti complessi possono essere scomposti in compiti più semplici, che a loro volta possono essere ulteriormente suddivisi, fino a quando il processo si riduce a operazioni banali. Anche se al momento può sembrarti improbabile, per qualsiasi attività che un computer può eseguire, questo processo di scomposizione alla fine deve arrivare fino alle operazioni di base fornite dal linguaggio macchina.

I cicli e le istruzioni condizionali offrono due modi per raggruppare le istruzioni in strutture più grandi e significative. Da un certo punto di vista, un ciclo è solo una sequenza di bit, in cui i bit alla fine codificano un'istruzione di salto. Ma per chi ha progettato il programma, le istruzioni all'interno del ciclo svolgono un compito significativo, e il ciclo nel suo complesso esegue un'attività leggermente più complessa: “Esegui questa operazione [il contenuto del ciclo] ripetutamente.” Allo stesso modo, le decisioni possono essere utilizzate per costruire blocchi di programma che dicono: “Se si verifica una certa condizione, allora esegui questa operazione; altrimenti esegui quest'altra,” dove entrambe le operazioni sono blocchi più semplici che svolgono compiti significativi.

La maggior parte dei linguaggi macchina fornisce un altro meccanismo, le subroutine, per supportare la costruzione dei programmi attraverso il concetto di suddivisione. È inevitabile che un programmatore consideri certe sequenze di istruzioni all'interno di un programma come esecutrici di specifici sottocompiti dell'operazione complessiva del programma. Senza subroutine, questa suddivisione esisterebbe solo nella mente del programmatore; con le subroutine, essa può riflettersi nella struttura fisica del programma.

Una subroutine è una sequenza di istruzioni memorizzata a partire da una certa posizione in memoria. (Non potrebbe essere altrimenti.) Ciò che la rende speciale è la disponibilità di un'istruzione del linguaggio macchina, chiamata "salto alla subroutine", che dice in pratica: “Vai ad eseguire la subroutine che inizia all'indirizzo di memoria N e, una volta terminata, torna alla posizione attuale nel programma e continua da lì.” In altre parole, quando si incontra un'istruzione di salto alla subroutine, l'intera subroutine viene eseguita, dopodiché il computer ritorna alla posizione nel programma dove era stato chiamato il salto alla subroutine e prosegue l'esecuzione.

Un salto alla subroutine è simile a un semplice salto all'inizio della subroutine, con la differenza che prima di eseguire il salto, il valore attuale del program counter (il registro che tiene traccia dell'istruzione in esecuzione) viene memorizzato da qualche parte affinché possa essere recuperato quando necessario. Ripristinare il valore del program counter equivale a riportare il computer alla posizione in cui si trovava quando la subroutine è stata chiamata. La subroutine deve terminare con un'altra istruzione, chiamata "ritorno dalla subroutine". L'effetto di questa istruzione è recuperare il valore salvato del program counter e ripristinarlo, completando così il ritorno alla posizione corretta nel programma.

Nota che, poiché il computer "ricorda" dove deve tornare dopo l'esecuzione della subroutine, la stessa subroutine può essere chiamata da diverse parti del programma. Il computer riuscirà sempre a tornare nel posto giusto.

Osserva cosa sta accadendo qui: la singola istruzione di salto alla subroutine funge da sostituto per l'intera subroutine. Puoi pensare a quella singola istruzione come a un comando che esegue l'intera operazione specificata dalla subroutine, per quanto complessa essa sia. È quasi come se fosse stata aggiunta una nuova istruzione al linguaggio macchina per eseguire quell'operazione. Ed è esattamente così che il programmatore dovrebbe pensarla: la subroutine è un blocco significativo che, una volta costruito, può essere riutilizzato per costruire strutture più complesse.

Sebbene il linguaggio macchina sia il linguaggio nativo del computer, la maggior parte dei programmatori non scrive mai un programma direttamente in linguaggio macchina. Invece, utilizzano quelli che vengono chiamati linguaggi ad alto livello, come Python, Java o C. I programmi scritti in questi linguaggi devono essere tradotti in linguaggio macchina prima che il computer possa eseguirli, ma la traduzione è un processo automatico eseguito da programmi chiamati compilatori. Di conseguenza, il programmatore non ha alcun contatto diretto con il linguaggio macchina, così come una persona che utilizza un elaboratore di testi non ha alcun contatto con la struttura dei bit che rappresentano il documento nel computer.

Il linguaggio macchina è solitamente una preoccupazione per chi progetta i computer o, come in questo caso, per chi cerca di comprenderne il funzionamento, piuttosto che per chi li usa semplicemente o scrive programmi per essi. Tuttavia, anche quando studierai linguaggi di alto livello, la tua esperienza con il linguaggio macchina non sarà stata inutile. Sebbene i linguaggi ad alto livello siano molto più facili da usare rispetto al linguaggio macchina, si basano sulle stesse funzionalità discusse in questa sezione, tra cui il trasferimento di dati, le operazioni aritmetiche di base, i cicli, le decisioni e le subroutine.

Esempi di utilizzo di subroutine

Un primo esempio di una subroutine per l'esecuzione dell'operazione di modulo (resto di una divisione): il programma subroutine-example.s invoca la subroutine per calcolare 48 modulo 5 (risultato 3):


# Call the subroutine to compute 48 % 5

li $1 48  # A
li $2 5   # B
li $3 100 # Address of subroutine
jalr $0 $15

# Now, $1 contains the answer

add $2 $1 $0  # copy answer to register 2
li $1 2       # syscal number for print integer
syscall       # print the answer

li $1 0 
syscall  # HALT

# Subroutine to computer A % B.
# Inputs:  A in Register 1, B in Register 2
# Output:  A % B in Register 1, computed as A - (A/B)*B
# Return address:  Must be saved in regiser 15
# Uses: Register 3

@100  # Subroutine starts at location 100

div $3 $1 $2    #  S = A/B
mul $3 $3 $2    #  R = R * B = (A/B)*B
sub $1 $1 $3    #  A = A - R = A - (A/B)*B
jalr $0 $15

Un altro esempio di utilizzo delle subroutine in un programma che calcola il numero degli spazi in una stringa data in input, count-spaces.s:

li $1 3       # read a string, store at location 256
lui $2 1
li $3 100
syscall

lui $1 1      # call subroutine, passing address of string,
li $2 100
jalr $15 $2

# register 1 now holds the number of spaces in the string

add $2 $1 $0  # print the answer
li $1 2
syscall

li $1 0       # halt
syscall


@100 # subroutine to count spaces in a string, address of string in $1;
     # uses registers 1 through 5, and register 15 for return address;
     # return value in register 1

li $4 32      # code for space char
li $5 1       # constant 1

add $2 $1 $0  # address now in register 2
li $1 0       # counter

# start of loop
lw $3 $2      # read char into register 3
beqz $3 5     # if char is zero, done 
sub $3 $3 $4
bnez $3 1     # if char is not 32, skip next instruction
add $1 $1 $5  # increment counter
add $2 $2 $5  # increment address
beqz $0 -7    # back to start of loop

jalr $0 $15   # return

Esempio di programma che utilizza la subroutine per il calcolo dell'area di un quadrato data la dimensione del lato:

Areaquadrato=latolatoArea_{quadrato} = lato * lato

Sotto il codice del semplice programma che calcola l'area di un quadrato con lato di valore 7, poi quella di uno con lato di 10 e poi quella di uno con lato di 3, sempre utilizzando la subroutine. Nel nostro programma il risultato dell'invocazione della subroutine viene messo nel registro 10, mentre il valore dell'input, il lato del quadrato di cui si vuole calcolare l'area, nel registro 2. Che registri utilizzare è una scelta del programmatore.

Il codice di calcolo_area_quadrato_subroutine.s che esegue il calcolo delle tre aree, invocando tre volte la subroutine posta all'indirizzo 100 di memoria:



# il registro 1 contiene indirizzo della subroutine 
li $1 100

li $2 7

# il registro 9 dopo esecuzione jalr il valore del PC di ritorno
# il registro 1 contiene l'indirizzo della subroutine e viene caricato
# nel PC (Program Counter) 
jalr $9 $1

# controlla il valore registro 10 dovrebbe essere 49 (7 * 7)

li $2 10
jalr $9 $1

# controlla il valore registro 10 dovrebbe essere 100 (10 * 10)

li $2 3
jalr $9 $1

# controlla il valore registro 10 dovrebbe essere 9 (3 * 3)

li $1 0         # trap number for Halt
syscall         # halt

# il registro 2 contiene il valore del lato
# il registro 10 contiene il risultato dell'operazione (valore dell'area)
@100
mul $10 $2 $2
jalr $0 $9

Scriviamo due funzioni per il calcolo dell'area e la circonferenza del cerchio dato come input il valore del raggio: siccome secondo le formule moltiplicato per la costante Π\Pi ( =3,141= 3,141 approssimato alla terza cifra decimale).

Areacerchio=raggioraggioΠArea_{cerchio} = raggio * raggio * \Pi
Circonferenzacerchio=2raggioΠCirconferenza_{ cerchio}= 2 * raggio * \Pi

Non disponendo dei numeri frazionari sulla macchina Larc possiamo approssimare la costante con il numero intero 3141 e poi il risultato del calcolo dividerlo per 1000 approssimando così all'intero.

Il codice con la funzione del calcolo dell'area approssimato del cerchio, calcolo_area_cerchio_subroutine.s:


# costante 1000 (0x3E8 in esadecimale) nel registro 4
lui $4 3
li $5 0xE8 
lui $6 0xFF
sub $5 $5 $6

add $4 $4 $5  # caricata la costante nel registro 4

# costante 3141 (0xC45 in esadecimale) nel registro 6
lui $6 0xC
li $7 0x45     # siccome < 0x80 non fa l'estensione di segno

add $6 $6 $7   # caricata la costante nel registro 6

li $1 2    # chiamata di funzione con raggio = 2
li $8 50
jalr $9 $8
# nel registro 2 controlla il risultato approssimato del calcolo area

li $1 5    # chiamata di funzione con raggio = 5
li $8 50
jalr $9 $8
# nel registro 2 controlla il risultato approssimato del calcolo area

li $1 0         # trap number for Halt
syscall         # halt

# calcola area cerchio
# registro 1 contiene l'input, valore del lato
# registro 2 contiene l'output - valore intero dell'area
@50
add $2 $0 $1
mul $2 $2 $2    
mul $2 $2 $6
div $2 $2 $4
jalr $0 $9

Nell'esempio viene prima calcolata l'area del cerchio di raggio 2 e risulta 223141/1000=122*2*3141/1000 = 12 (l'operazione div è la divisione intera, ad esempio, come divisione intera 5/2=25/2 = 2 ).

La seconda chiamata a subroutine, nel programma, calcola l'area del cerchi con raggio 5. Possiamo notare che il calcolo non risulta corretto anche se tutto sembra corretto. Eseguendo i passaggi del calcolo 553141=785255*5*3141 = 78525, abbiamo il problema che questo numero non più essere contenuto in modo corretto nei registri di questa architettura (i registri sono di 16 bit quindi possono contenere interi che vanno da 0 a 2161=655352^{16} -1 = 65535) quindi il risultato dell'operazione, essendo maggiore della capacità del registro, causa l'overflow e nel registro viene messo il numero 12989 ( perché 78525=65536+12989 78525 = 65536 + 12989 ).

Last updated