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.