Nel linguaggio C, una struct è un tipo composito, formato da campi diversi. Ogni campo ha il proprio tipo e la propria dimensione.
Per esempio, questo definisce un tipo formato da tre interi, due da 4 byte ed uno da 8 byte:
struct example {
int32_t first_four_byte_integer;
uint32_t second_four_byte_integer;
uint64_t eight_byte_integer;
};
Una struct può anche contenere altre struct:
struct example_2 {
struct example first;
struct example second;
};
Se uno dei campi della struct è un array, tutti i byte allocati per l'array fanno parte della struct. Se invece il campo è solo un puntatore all'array, allora l'array deve essere memorizzato altrove:
struct with_array {
char string[20];
};
// all 20 bytes of string are inside struct with_array
struct with_pointer {
const char *string;
};
// only the 8-byte pointer to the string is inside struct with_pointer
L'allineamento di un tipo è una potenza di due non negativa (1, 2, 4, 8, ...) che rappresenta un requisito sul suo indirizzo. Ogni oggetto di quel tipo deve trovarsi ad un indirizzo che sia un multiplo del suo allineamento.
Per un tipo primitivo, l'allineamento è di solito uguale alla sua dimensione; quindi, per esempio, un int32_t di solito ha un allineamento di 4 byte.
Questo significa che int32_t deve essere allocato su confini di 4 byte.
L'allineamento di una struct, invece, è l'allineamento massimo tra i suoi campi.
Quindi, per esempio, la struct example definita sopra avrebbe un allineamento di 8 byte, dato che questo è l'allineamento di uint64_t.
Se l'allineamento di un campo della struct è minore di quello della struct, dopo quel campo possono essere inseriti dei byte di padding. Questo garantisce che i campi successivi e la struct nel suo insieme rispettino i requisiti di allineamento. In pratica, questo significa che, anche se quei byte di padding contribuiscono alla dimensione della struct, non vengono usati nel valore della struct:
struct example_3 {
char one_byte; // alignment of 1
int four_byte; // alignment of 4
};
// example_3 has alignment of 4 (the same as int)
// it occupies 8 bytes in space:
// 1 byte for field 'one_byte'
// 3 padding bytes (with undefined value)
// 4 bytes for field 'four_byte'
Nota che nella struct definita sopra, il campo 'one_byte' ha allineamento 1 e può essere collocato a qualsiasi indirizzo. Dato che il campo 'four_byte' ha allineamento 4, anche i campi della struct devono rispettare i confini di 4 byte. Questo significa che tra due campi qualsiasi della struct deve esserci un numero di byte multiplo di 4. Dato che 'one_byte' occupa solo 1 byte, dopo di esso vengono inseriti 3 byte di padding per rispettare questo confine.
I campi adiacenti vengono raggruppati quando possibile, per ridurre al minimo il numero di byte di padding necessari:
struct two_padding {
char one; // alignment of 1
char two; // alignment of 1
int three; // alignment of 4
};
// struct two_padding has alignment of 4 (the same as int)
// it occupies 8 bytes in space:
// 1 byte for field 'one'
// 1 byte for field 'two'
// 2 padding bytes
// 4 bytes for field 'three'
Questo raggruppamento deve rispettare anche l'allineamento di ogni campo:
struct one_padding {
char one; // alignment of 1
int16_t two; // alignment of 2
int three; // alignment of 4
};
// struct one_padding has alignment of 4 (the same as int)
// it occupies 8 bytes in space:
// 1 byte for field 'one'
// 1 padding byte
// 2 bytes for field 'two'
// 4 bytes for field 'three'
// Since 'two' has alignment of 2, it must be placed in a 2-byte boundary
// So, there must be 1 padding byte between 'one' and 'two'
Questo significa che cambiare l'ordine dei campi in una struct può a volte portare a una differenza nella sua dimensione:
struct example_4 {
int64_t one;
short two;
int32_t three;
};
// example_4 has alignment of 8 (the same as int64_t)
// it occupies 16 bytes in space:
// 8 bytes for field 'one'
// 2 bytes for field 'two'
// 2 padding bytes
// 4 bytes for field 'three'
// two' and 'three' are adjacent and can be grouped to sum up 8 bytes with only 2 bytes of padding
struct example_5 {
int16_t one;
int64_t two;
unsigned int three;
};
// example_5 also has alignment of 8 (the same as int64_t)
// however, it occupies 24 bytes in space:
// 2 bytes for field 'one'
// 6 bytes for padding
// 8 bytes for field 'two'
// 4 bytes for field 'three'
// 4 padding bytes
// 'one' can't be grouped with 'two' because their sum would be higher than the struct's alignment
// 'two' and 'three' also can't be grouped, for the same reason
// so padding bytes must be inserted in full after each field with less than 8 bytes in alignment
Nota che l'allineamento di un array è uguale all'allineamento del suo tipo sottostante:
struct array_example {
char string[10]; // alignment of 1
};
// array_example has alignment of 1 (it can be placed at any address)
// it occupies 10 bytes in space, one for each char in 'string'
Gli elementi di un array possono essere raggruppati allo stesso modo in cui lo sarebbero se non facessero parte di un array:
struct group_array {
int16_t arr[3];
int8_t one_byte;
int32_t four_byte;
};
// group_array has alignment of 4 because this is the alignment of 'four_byte'
// it occupies 12 bytes in space:
// 4 bytes for arr[0] and arr[1]
// 2 bytes for arr[2]
// 1 byte for 'one_byte'
// 1 padding byte (arr[2] is grouped with 'one_byte')
// 4 bytes for 'four_byte'
È importante tenere conto di eventuali byte di padding quando si accede ad una struct in assembly.
In generale, i valori vengono passati alle funzioni e restituiti da esse come ci si aspetterebbe:
xmm.Le struct, però, sono un caso speciale. Come regola generale, tutti i campi di una struct occupano spazio adiacente in memoria e dovrebbero essere collocati in un unico registro quando possibile:
struct single_type {
int16_t first;
int16_t second;
int16_t third;
int16_t fourth;
};
// this struct occupies 8 bytes
// it is entirely passed in the same GPR register:
// the field 'first' is passed on the lowest 2 bytes
// each subsequent field is passed in the following 2 bytes
Se una struct ha campi con valori interi ed anche campi con valori in virgola mobile, ogni 8 byte consecutivi della struct vanno classificati separatamente:
struct mixed_types {
double first;
int second;
int third;
};
// this struct occupies 16 bytes
// the field 'first' is passed in the first 8 bytes of a xmm register
// the fields 'second' and 'third' are passed in the same GPR register:
// the field 'second' is passed on the lowest 4 bytes
// the field 'third' is passed in the next 4 bytes
Se interi e valori in virgola mobile si trovano negli stessi 8 byte, il tipo è intero e l'intero blocco di 8 byte (compreso il valore in virgola mobile) viene collocato in un GPR:
struct mixed_types {
int32_t integer;
float floating_point;
};
// this struct occupies 8 bytes
// it is entirely passed in the same GPR register:
// the field 'integer' is passed on the lowest 4 bytes
// the field 'floating_point' is passed on the next 4 bytes
Quando si converte un valore in virgola mobile in un intero usando cvtsi2ss o cvtsi2sd, l'organizzazione dei byte sottostanti cambia.
Puoi usare movd (per i valori a 32 bit) e movq (per i valori a 64 bit) per spostare i byte grezzi da un registro xmm ad un GPR, senza cambiare la rappresentazione in bit.
Tuttavia, le struct di dimensione superiore a 16 byte vengono di solito collocate in memoria, indipendentemente dal tipo di ogni 8 byte:
struct big_struct {
uint8_t first;
double second;
int64_t third;
};
// this struct occupies 24 bytes, including 7 padding bytes between 'first' and 'second'
// it is entirely passed in memory
Quando una struct viene passata come argomento ad una funzione, occupa tutti i registri necessari. Se invece è collocata in memoria, si usa lo stack.
Regole simili valgono per le struct restituite da una funzione.
Di solito vengono restituite prima in rax e rdx (se di valore intero) o in xmm0 e xmm1 (se di valore in virgola mobile).
Se invece sono collocate in memoria, chi chiama deve riservare loro dello spazio e passare l'indirizzo di questa posizione di memoria come primo argomento implicito della funzione (in rdi).
La funzione memorizza poi la struct in questo spazio e ne restituisce l'indirizzo in rax.
In questo esercizio organizzerai gare tra vari tipi di auto radiocomandate. Ogni auto ha le proprie caratteristiche e gareggia su molte piste diverse nel corso della stagione del torneo.
Le funzioni in questo esercizio sono dichiarate in un file sorgente C con le firme fornite. Il codice in assembly x86-64 deve seguire queste specifiche.
Hai definito una struct car_t per rappresentare un'auto radiocomandata:
struct car_t {
char name[10];
int16_t speed;
float battery;
};
Consenti di creare un'auto radiocomandata definendo una funzione new_car in assembly x86-64.
Il campo battery inizia sempre con il valore 100.0.
struct car_t new_car(short speed, const char name[]);
Le piste sono rappresentate da una struct track_t:
struct track_t {
enum surface_t surface;
size_t distance;
};
Il tipo enum surface_t tiene traccia delle diverse superfici di una pista:
enum surface_t {
ASPHALT,
SAND,
ICE,
CLAY
};
Consenti di creare una nuova pista definendo una funzione new_track in assembly x86-64.
struct track_t new_track(enum surface_t surface, size_t distance);
Fino a 6 auto partecipano a ogni gara, percorrendo un numero specificato di giri su una pista.
Le gare sono rappresentate da una struct race_t:
struct race_t {
struct track_t track;
uint8_t num_of_laps;
struct car_t cars[6];
uint8_t num_of_running_cars;
};
Definisci una funzione new_race in assembly x86-64.
All'inizio, non ci sono auto la cui partecipazione sia confermata per la gara:
struct race_t new_race(struct track_t track, uint8_t num_of_laps);
Quando il proprietario di un'auto chiede che partecipi a una gara, la sua partecipazione viene confermata solo se l'array non è già pieno.
Definisci una funzione add_participant in assembly x86-64.
Se c'è spazio per un altro partecipante nella gara, l'auto viene aggiunta all'array, aggiornando il contatore, e la sua partecipazione viene confermata.
Altrimenti, gli organizzatori della gara devono informare il proprietario dell'auto che questa volta non può partecipare.
bool add_participant(struct race_t *race, struct car_t car);
Tieni traccia delle gare programmate per la stagione in una struct tournament_t:
struct tournament_t {
struct race_t races[20];
size_t num_of_races;
};
Definisci una funzione add_race in assembly x86, per aggiungere una gara all'array del torneo e aggiornarne anche il contatore.
Puoi considerare che ci sarà sempre spazio sufficiente nell'array races per la nuova gara.
void add_race(struct tournament_t *tournament, struct race_t race);
Iscriviti a Exercism per imparare e padroneggiare x86-64 Assembly con 22 concetti130 esercizi e il mentoring di persone reali, tutto gratis.