Strutture dati in C e loro traduzione in assembler MIPS 1 Direttive

Transcript

Strutture dati in C e loro traduzione in assembler MIPS 1 Direttive
Strutture dati in C e loro traduzione in assembler
MIPS
1 Direttive assembler per l'allocazione dei dati
Prima di iniziare a trattare il problema dell'allocazione delle varie strutture
dati, introduciamo le direttive assembler che servono per allocare dati globali
nel segmento dei dati, ovvero successivamente alla direttiva .data.
Le direttive hanno a che fare con dati di tipo elementare (int, float, double,
char), e possono essere impiegati anche per allocare sequenze di dati elementari
gia inizializzati oppure non inizializzati.
E' possibile specicare, prima delle direttive stesse, delle etichette seguite dai
due punti. Le etichette di fatto corrispondono agli indirizzi di memoria in corrispondenza dei quali i dati verranno allocati dall'assemblatore. Le etichette
possono poi essere usare dalle istruzioni per far riferimento alle aree di memoria
relative, ad esempio possono essere riferite dalle istruzioni di lw e sw per leggere
e scrivere, rispettivamente, in memoria.
Le principali direttive per l'allocazione di dati inizializzati sono le seguenti:
.half
.word
.byte
.float
.double
.asciiz
h1,...,hn
w1,...,wn
b1,...,bn
f1,...,fn
d1,...,dn
str
#
#
#
#
#
#
dich.
dich.
dich.
dich.
dich.
dich.
di
di
di
di
di
di
una
una
una
una
una
una
sequenza di n half-word;
sequenza di n word (interi)
sequanza di byte
sequenza di float
sequenza di double
stringa costante (terminata da 0)
dove le liste alla destra delle direttive corrispondono alle inizializzazioni delle
rispettive locazioni di memoria.
I vari elementi delle liste sono allocati in memoria in locazioni contigue.
Gli indirizzi dei vari elementi sono scelti in modo da rispettare degli allineamenti
pressati. In particolare .half alloca i vari elementi su indirizzi multipli di 2,
mentre .word, .float e .double su indirizzi multipli di 4. Le direttive .byte
e .asciiz, poiche allocano dati di lunghezza 1B, non devono rispettare alcun
allineamento.
Si noti che lw/sw devono essere usati per accedere dati (4B) allineati alla parola
(ind. multipli di 4), lh/sh per accedere dati (2B) allineati alla mezza parola
(ind. multipli di 2), ed inne lb/sb per accedere dati (1B) allineati al byte.
1
Se l'indirizzo impiegato non rispetta l'allineamento relativo, si ottiene un'errore
(eccezione causata da allineamento errato).
La direttiva .align n puo invece essere impiegata per rompere lo schema di
allineamento automatico imposto dalla direttiva seguente. In particolare, essa
impone che il prossimo dato venga allineato con un indirizzo multiplo di 2n .
Quindi .align 2 impone l'allineamento alla parola, mentre .align 0 rimuove
tutti gli allineamenti automatici imposti da .half,.word, ecc.
Inne, la direttiva .space n alloca uno spazio di n byte nel segmento dei dati.
L'allineamento ssato da .space puo essere controllato con la direttiva .align
vista sopra.
2 Array mono-dimensionali (vettori)
Si considerino i due vettori seguenti:
int a[10];
char b[5];
Essi possono essere allocati in memoria rispettando gli allineamenti con le seguenti direttive:
.data
.align 2
a: .space 40
b: .space 5
Gli elementi dei due array sono allocati in memoria in maniera contigua. Tramite
la direttiva .align 2 abbiamo forzato l'allineamento dei vari elementi dell'array
a[] alla word (4B).
Per esempio, se i due array sono allocati il primo a partire dall'indirizzo 0, il
secondo dall'indirizzo 40, abbiamo il seguente layout di memoria:
------------------------------------------------------|
a[0]
|
a[1]
| ...
------------------------------------------------------0
4
8
------------------------------------------------------| b[0] | b[1] | b[2] | b[3] | b[4] | b[5] | b[6] | ...
------------------------------------------------------40
41
42
43
44
45
46
47
I due esempi seguenti illustrano un paio di funzioni C, con relativa traduzione
MIPS, che restituiscono il valore dell'elemento di indice ind di un array di interi
e di caratteri. Relativamente al codice assembler illustrato in Esempio 2.1, si
noti la moltiplicazione per 4 (sll $a1, $a1, 2), che serve per tener conto della
dimensione degli interi (4B).
2
Esempio 2.1
Funzione C
int elem(int a[], int ind)
{
return(a[ind));
}
Esempio 2.2
Funzione C
Traduzione MIPS
Commenti
elem: sll $a1, $a1, 2
add $v0, $a0, $a1
lw $v0, 0($v0)
jr $ra
La funzione e scritta in accordo alla politica caller-save.
I parametri a e ind sono passati rispettivamente in $a0 e $a1.
Il risultato della funzione e
restituito in $v0.
Traduzione MIPS
Commenti
char elem(char a[], int ind)
elem: add $v0, $a0, $a1
{
lb $v0, 0($v0)
return(a[ind));
jr $ra
}
La funzione e scritta in accordo alla politica caller-save.
I parametri a e ind sono passati rispettivamente in $a0 e $a1.
Il risultato della funzione e
restituito in $v0.
3 Array bi-dimensionali
La convenzione impiegata dal compilatore C per allocare array bi-dimensionali
in memoria lineare e' quella di memorizzare il vettore per righe.
Ovvero, prima tutti gli elementi della riga 0, di seguito e in maniera contigua
tutti quelli della riga 1, ecc.
Si consideri ad esempio l'array:
int a[10][2];
che puo essere allocato in memoria rispettando gli allineamenti con le seguenti
direttive:
.data
a:
.align 2
.space 80
L'array e poi allocato in memoria dall'assemblatore a partire da un indirizzo
multiplo di 4. Se il primo elemento della prima riga (a[0][0]) e allocato a
partire dall'indirizzo 0, abbiamo il seguente layout:
----------------------------------------------------------------------|
a[0][0]
|
a[0][1]
|
a[1][0]
| .....
----------------------------------------------------------------------0
4
8
12
Per accedere l'elemento a[i][j] e quindi necessario calcolare il displacement
a partire dall'indirizzo corrispondente all'etichetta a.
Si consideri, a proposito, che il primo elemento della riga i-esima, a meno di
3
calcoli che riguardano la dimensione del dato, e memorizzato a partire dal displacement: i*num colonne.
Per selezionare l'elemento j-esimo della riga medesima basta quindi sommare j al displacement precedente, ottenendo quindi un diplacement uguale a:
i*num colonne+j.
Poiche nel processore MIPS l'indirizzamento e al Byte, per avere il displacement
corretto bisogna anche moltiplicare per la dimensione del tipo di dato dell'array
stesso: (i*num colonne+j)*size.
Esempio 3.1
Funzione C
int el2(int a[][2],
int i1, int i2)
{
return(a[i1][12));
}
Traduzione MIPS
el2:
sll $t0, $a1, 3
# i1*8
sll $t1, $a2, 1
# i2*2
addu $t0, $a0, $t0
addu $t0, $t1, $t0
lw $v0, 0($t0)
jr $ra
Commenti
La funzione e scritta in accordo alla politica caller-save.
I parametri a e i1 e i2 sono
passati, rispettivamente, in
$a0, $a1 e $a2.
Nel calcolo del displacement da sommare a $a0
bisogna considerare che
num colonne=2, mentre
size=4, per cui l'espressione
(i*num colonne+j)*size
diventa i*8+j*2.
Il risultato della funzione e
restituito in $v0.
4 Strutture
Attraverso le struct e possibile in C denire collezioni di dati di tipo diverso,
e riferire tali collezioni con uno stesso nome.
Ad esempio, questa e una denizione di struttura C:
struct elem {
char c;
int i[2];
double d;
};
Alternativamente:
typedef struct {
char c;
int i[2];
double d;
} type_elem;
Nel primo caso, per denire una nuova variabile var di tipo struct
basta dichiararla come segue:
4
elem,
struct elem var;
mentre nel secondo caso, avendo ridenito un nuovo tipo
dichiararla come segue:
type elem,
basta
type_elem var;
Nel seguito useremo sempre la prima convenzione, senza usare typedef.
L'allocazione della variabile var di tipo struct elem in memoria prevede
l'allocazione contigua dei vari campi della struttura a partire da un indirizzo di
partenza.
Solitamente i vari campi della struttura vengono pero allocati in memoria rispettando l'allineamento denito dai tipi associati ai campi stessi. Questo puo
portare ad alcuni sprechi di spazio tra un campo e l'altro.
Ad esempio, se gli int e i double devono essere allineati alla word (4B), la
variabile var di tipo struct elem verra cos memorizzata (considerando che il
primo campo della struttura e allocato a partire dall'indirizzo 0):
------------------------------------------------------------------------| c | //// |
i[0]
|
i[1]
|
d
| ....
------------------------------------------------------------------------0
4
8
12
16
20
sprecando 3B in piu per l'allineamento tra il campo c ed il campo i[].
Inoltre, poiche e possibile denire array di strutture, sempre per rispettare gli
allineamenti e spesso necessario sprecare spazio tra una struttura e la successiva
all'interno dell'array.
Ad esempio, se deniamo il seguente array ar[], i cui elementi sono di tipo
struct elem:
struct elem {
int k;
char c;
};
struct elem ar[10];
l'array verra cos memorizzato (considerando che il primo campo elemento dell'array sia allocato a partire dall'indirizzo 128, per cui l'etichetta ar corrispondera a 128):
------------------------------------------------------------------------|
k
| c | ///// |
k
| c | ///// |
k
| ....
------------------------------------------------------------------------128
132
136
140
144
148
La dimensione della struttura sara quindi 8B, considerando i byte nali (padding), che garantiscono l'allineamento del prossimo elemento dell'array, come
parte integrante della struttura stessa. Questo tipo di gestione della memoria
puo essere vericato da C tramite il seguente comando:
5
printf("%d\n", sizeof(struct elem));
Possimo ora dare una regola generale per scegliere l'allineamento dell'indirizzo iniziale di una struttura e garantire che:
tutti i campi della struttura sia allineati opportunamente;
l'indirizzo della struttura stessa sia anche l'indirizzo del primo campo della
struttura stessa;
i vari campi della struttura si possano individuare tramite un displacement
costante a partire dall'indirizzo iniziale.
Per eettuare questa scelta, basta considerare i tipi di tutti i campi contenuti nella struttura, scegliendo come allineamento dell'indirizzo iniziale della
struttura l'allineamento piu severo tra quelli deniti dai vari campi della struttura stessa. Per esempio, se abbiamo un campo char (allineamento a 1B), un
campo short int (allineamento a 2B), ed un campo int (allineamento a 4B),
l'indirizzo iniziale della struttura dovra essere un multiplo di 4 (allineamento
alla word=4B).
4.1 Accesso ai campi delle strutture
Si consideri la seguente struttura:
struct elem {
char c;
int i[2];
double d;
};
In accordo alla regola data precedentemente, se i double devono essere allianeati
alla word, l'indirizzo iniziale di una variabile di tipo struct elem deve essere
anch'esso allineato alla word. Considerando che questo indirizzo iniziale sia 128:
------------------------------------------------------------------------| c | ////// |
i[0]
|
i[1]
|
d
| ....
------------------------------------------------------------------------128
132
136
140
144
148
Vediamo ora tre semplici esempi di funzioni che accedono ai vari campi di
una struttura di tipo struct elem, il cui puntatore e passato come parametro
delle funzioni stesse.
Esempio 4.1
Funzione C
char ret_c(struct elem *ptr)
{
return( ptr->c );
}
Traduzione MIPS Commenti
ret_c:
lb $v0, 0($a0)
jr $ra
6
La funzione e scritta in accordo
alla politica caller-save.
Il parametro ptr e passato in
$a0. Il risultato della funzione
e restituito in $v0.
Esempio 4.1.2
Funzione C
int *ret_i(struct elem *ptr)
{
return( ptr->i );
/* Il comando di sopra
e' equivalente a:
return ( &(ptr->i[0]) );
*/
}
Esempio 4.1.3
Funzione C
int ret_i1(struct elem *ptr)
{
return( ptr->i[1] );
}
Traduzione MIPS Commenti
La funzione e scritta
in accordo alla politica
ret_i:
addiu $v0, $a0, 4
jr $ra
caller-save.
Il parametro ptr e passato in $a0. Il risultato della funzione e
restituito in $v0.
Traduzione MIPS Commenti
La funzione e scritta
in accordo alla politica
ret_i1:
lw $v0, 4($a0)
jr $ra
caller-save.
Il parametro ptr e passato in $a0. Il risultato della funzione e
restituito in $v0.
4.2 Campi puntatore all'interno di strutture
La struttura denita precedentemente aveva come caratteristica che il vettore di interi int i[2] era completamente contenuto all'interno della struttura
ed aveva lunghezza ssa. Se invece volessimo implementare una lista di strutture dove, per ogni struttura, il vettore i[] avesse lunghezza variabile, in C
dovremmo dichiarare:
struct elem {
char c;
int *i;
double d;
};
Se poi volessimo, a tempo di esecuzione, allocare un vettore di due interi,
puntato dal puntatore i contenuto all'interno di una variabile di tipo struct
elem, dovremmo scrivere:
...
struct elem var;
...
var.i = (struct elem *) malloc(2 * sizeof(int));
dove malloc(2 * sizeof(int)) alloca un'area di memoria contigua (di cui
restituisce l'indirizzo) di dimensione 8B.
Considerando che la variabile var deve essere allineata alla word (es. 128),
e che l'area di memoria restituita da malloc() sia anch'essa allineata alla word
7
(es. 1024), l'assegnamento di sopra (var.i
situazione:
= ....)
produrrebbe la seguente
------------------------------------------------------------------------| c | ////// |
i=1024
|
d
| ....
---------------------|--------------------------------------------------128
132
|
136
140
144
148
|
\/
--------------------------|
i[0]
|
i[1]
|
--------------------------1024
1028
Nota che, anche se abbiamo cambiato la denizione della struttura, per riferire
l'indirizzo dell'array i[] continueremo ad usare la notazione var.i, mentre per
riferire il secondo elemento dell'array i[] continueremo ad usare la notazione
var.i[1].
La traduzione assembler e invece completamente diversa. Per illustrare questo,
rivediamo due degli esempi visti precedentemente: Esempio 4.1.2 ed Esempio 4.1.3. Negli esempi rivisti, la denizione di struct elem e quella nuova,
con l'array i[] allocato dinamicamente a tempo di esecuzione.
Esempio 4.2.1
Funzione C
int *ret_i(struct elem *ptr)
{
return( ptr->i );
/* Il comando di sopra
e' equivalente a:
return ( &(ptr->i[0]) );
*/
}
Esempio 4.2.2
Funzione C
int ret_i1(struct elem *ptr)
{
return( ptr->i[1] );
}
Traduzione MIPS Commenti
La funzione e scritta
in accordo alla politica
ret_i:
lw $v0, 4($a0)
jr $ra
caller-save.
Il parametro ptr e passato in $a0. Il risultato della funzione e
restituito in $v0.
Traduzione MIPS Commenti
ret_i1:
lw $v0, 4($a0)
lw $v0, 4($v0)
jr $ra
8
La funzione e scritta
in accordo alla politica
caller-save.
Il parametro ptr e passato in $a0. Il risultato della funzione e
restituito in $v0.