En langage C, une structure est un type composite, formé de différents champs. Chaque champ possède son propre type et sa propre taille.
Par exemple, ceci définit un type formé de trois entiers, deux de 4 octets et un de 8 octets :
struct example {
int32_t first_four_byte_integer;
uint32_t second_four_byte_integer;
uint64_t eight_byte_integer;
};
Une structure peut aussi contenir d'autres structures :
struct example_2 {
struct example first;
struct example second;
};
Si l'un des champs de la structure est un tableau, tous les octets alloués pour ce tableau font partie de la structure. Cependant, si le champ n'est qu'un pointeur vers le tableau, alors le tableau doit être stocké ailleurs :
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'alignement d'un type est une puissance de deux non négative (1, 2, 4, 8, ...) qui représente une contrainte sur son adresse. Tout objet de ce type doit être situé à une adresse qui est un multiple de son alignement.
Pour un type primitif, l'alignement est généralement égal à sa taille, donc, par exemple, un int32_t a généralement un alignement de 4 octets.
Cela signifie qu'un int32_t doit être alloué à des frontières de 4 octets.
L'alignement d'une structure, en revanche, est l'alignement maximal de l'un de ses champs.
Ainsi, par exemple, la structure struct example, définie ci-dessus, aurait un alignement de 8 octets, puisque c'est l'alignement de uint64_t.
Si l'alignement d'un champ de la structure est inférieur à celui de la structure, des octets de remplissage peuvent être insérés après ce champ. Cela garantit que les champs suivants et la structure dans son ensemble respectent les contraintes d'alignement. En pratique, cela signifie que, bien que ces octets de remplissage contribuent à la taille de la structure, ils ne sont pas utilisés dans la valeur de la structure :
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'
Note que dans la structure définie ci-dessus, le champ 'one_byte' a un alignement de 1 et peut être placé à n'importe quelle adresse. Comme le champ 'four_byte' a un alignement de 4, les champs de la structure doivent aussi respecter des frontières de 4 octets. Cela signifie qu'il doit y avoir un nombre d'octets multiple de 4 entre deux champs quelconques de la structure. Comme 'one_byte' n'occupe qu'un seul octet, 3 octets de remplissage sont insérés après lui pour respecter cette frontière.
Les champs adjacents sont regroupés chaque fois que possible afin de minimiser le nombre d'octets de remplissage nécessaires :
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'
Ce regroupement doit aussi respecter l'alignement de chaque champ :
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'
Cela signifie que changer l'ordre des champs dans une structure peut parfois entraîner une différence de taille :
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
Note que l'alignement d'un tableau est le même que l'alignement de son type sous-jacent :
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'
Les éléments d'un tableau peuvent être regroupés de la même manière que s'ils ne faisaient pas partie d'un tableau :
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'
Il est important de tenir compte des éventuels octets de remplissage lorsqu'on accède à une structure en assembleur.
En général, les valeurs sont passées aux fonctions et renvoyées par celles-ci comme on peut s'y attendre :
xmm.Les structures, cependant, sont un cas particulier. En règle générale, tous les champs d'une structure occupent un espace adjacent en mémoire et devraient être placés dans un seul registre chaque fois que possible :
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
Si une structure a des champs avec des valeurs entières et aussi des champs avec des valeurs à virgule flottante, chaque bloc consécutif de 8 octets de la structure doit être classifié séparément :
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
Si des entiers et des valeurs à virgule flottante se trouvent dans le même bloc de 8 octets, le type est entier et l'ensemble du bloc de 8 octets (y compris la valeur à virgule flottante) est placé dans 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
Lorsqu'une conversion est effectuée d'une valeur à virgule flottante vers un entier à l'aide de cvtsi2ss ou cvtsi2sd, cela modifie l'organisation des octets sous-jacents.
On peut utiliser movd (pour les valeurs de 32 bits) et movq (pour les valeurs de 64 bits) pour déplacer des octets bruts d'un registre xmm vers un GPR, sans changer la représentation binaire.
Cependant, les structures de plus de 16 octets sont généralement placées en mémoire, quel que soit le type de chaque bloc de 8 octets :
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
Lorsqu'une structure est passée comme argument à une fonction, elle utilise autant de registres que nécessaire. Si elle est placée en mémoire, la pile est utilisée à la place.
Des règles similaires s'appliquent aux structures renvoyées par une fonction.
Elles sont généralement renvoyées d'abord dans rax et rdx (s'il s'agit d'une valeur entière) ou xmm0 et xmm1 (s'il s'agit d'une valeur à virgule flottante).
Si elles sont placées en mémoire, cependant, l'appelant doit réserver de l'espace pour elles et passer l'adresse de cet emplacement mémoire comme premier argument implicite à la fonction (dans rdi).
La fonction stocke alors la structure dans cet espace et renvoie son adresse dans rax.
Dans cet exercice, tu vas organiser des courses entre différents types de voitures radiocommandées. Chaque voiture a ses propres caractéristiques et participe à des courses sur de nombreuses pistes différentes tout au long de la saison du tournoi.
Les fonctions de cet exercice sont déclarées dans un fichier source C avec les signatures fournies. Ton code en assembleur x86-64 doit respecter ces spécifications.
Tu as défini une structure car_t pour représenter une voiture radiocommandée :
struct car_t {
char name[10];
int16_t speed;
float battery;
};
Définis une fonction new_car en assembleur x86-64 pour créer une voiture radiocommandée.
Le champ battery commence toujours avec la valeur 100.0.
struct car_t new_car(short speed, const char name[]);
Les pistes sont représentées par une structure track_t :
struct track_t {
enum surface_t surface;
size_t distance;
};
L'enum surface_t permet de suivre les différentes surfaces d'une piste :
enum surface_t {
ASPHALT,
SAND,
ICE,
CLAY
};
Définis une fonction new_track en assembleur x86-64 pour créer une nouvelle piste.
struct track_t new_track(enum surface_t surface, size_t distance);
Jusqu'à 6 voitures participent à chaque course, en parcourant un nombre de tours donné sur une piste.
Les courses sont représentées par une structure 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;
};
Définis une fonction new_race en assembleur x86-64.
Au départ, aucune voiture participante n'est confirmée pour la course :
struct race_t new_race(struct track_t track, uint8_t num_of_laps);
Quand le propriétaire d'une voiture demande qu'elle participe à une course, sa participation n'est confirmée que si la liste n'est pas déjà pleine.
Définis une fonction add_participant en assembleur x86-64.
S'il reste de la place pour un participant de plus dans la course, la voiture est ajoutée à la liste, ce qui met à jour le compteur, et sa participation est confirmée.
Sinon, les organisateurs de la course doivent informer le propriétaire de la voiture qu'elle ne peut pas participer cette fois-ci.
bool add_participant(struct race_t *race, struct car_t car);
Tu gardes la trace des courses programmées pour la saison dans une structure tournament_t :
struct tournament_t {
struct race_t races[20];
size_t num_of_races;
};
Définis une fonction add_race en assembleur x86 pour ajouter une course au tableau du tournoi et mettre à jour son compteur.
Tu peux considérer qu'il y aura toujours assez de place dans le tableau races pour la nouvelle course.
void add_race(struct tournament_t *tournament, struct race_t race);
Inscris-toi sur Exercism pour apprendre et maîtriser x86-64 Assembly avec 22 concepts130 exercices, et un vrai mentorat humain, le tout gratuitement.