Na linguagem C, uma struct é um tipo composto, formado por campos diferentes. Cada campo tem seu próprio tipo e tamanho.
Por exemplo, isto define um tipo formado por três inteiros, dois de 4 bytes e um de 8 bytes:
struct example {
int32_t first_four_byte_integer;
uint32_t second_four_byte_integer;
uint64_t eight_byte_integer;
};
Uma struct também pode conter outras structs:
struct example_2 {
struct example first;
struct example second;
};
Se um dos campos da struct for um array, todos os bytes alocados para o array fazem parte da struct. Porém, se o campo for apenas um ponteiro para o array, o array precisa ser armazenado em outro lugar:
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
O alinhamento de um tipo é uma potência de dois não negativa (1, 2, 4, 8, ...) que representa uma exigência sobre seu endereço. Todo objeto desse tipo precisa estar em um endereço que seja múltiplo do seu alinhamento.
Para um tipo primitivo, o alinhamento geralmente é igual ao seu tamanho, então, por exemplo, um int32_t costuma ter alinhamento de 4 bytes.
Isso significa que int32_t precisa ser alocado em limites de 4 bytes.
O alinhamento de uma struct, porém, é o maior alinhamento entre os seus campos.
Então, por exemplo, a struct example, definida acima, teria um alinhamento de 8 bytes, já que esse é o alinhamento de uint64_t.
Se o alinhamento de um campo da struct for menor que o da struct, bytes de preenchimento podem ser inseridos depois desse campo. Isso garante que os campos seguintes e a struct como um todo atendam aos requisitos de alinhamento. Na prática, isso significa que, embora esses bytes de preenchimento contribuam para o tamanho da struct, eles não são usados no valor da 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'
Repare que, na struct definida acima, o campo 'one_byte' tem alinhamento de 1 e pode ser colocado em qualquer endereço. Como o campo 'four_byte' tem alinhamento de 4, os campos da struct também precisam respeitar limites de 4 bytes. Isso significa que deve haver um número de bytes múltiplo de 4 entre dois campos quaisquer da struct. Como 'one_byte' ocupa apenas 1 byte, 3 bytes de preenchimento são inseridos depois dele para respeitar esse limite.
Campos adjacentes são agrupados sempre que possível, para minimizar o número de bytes de preenchimento necessários:
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'
Esse agrupamento também precisa respeitar o alinhamento de cada 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'
Isso significa que mudar a ordem dos campos de uma struct às vezes pode levar a uma diferença no seu tamanho:
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
Repare que o alinhamento de um array é o mesmo do seu tipo subjacente:
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'
Os elementos de um array podem ser agrupados da mesma forma como se não fizessem parte de um 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 levar em conta quaisquer bytes de preenchimento possíveis sempre que você acessar uma struct em assembly.
Em geral, os valores são passados para as funções e retornados delas como se espera:
xmm.As structs, porém, são um caso especial. Como regra geral, todos os campos de uma struct ocupam espaço adjacente na memória e devem ser colocados em um único registrador sempre que possível:
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 uma struct tiver campos com valores inteiros e também campos com valores de ponto flutuante, cada 8 bytes consecutivos da struct devem ser classificados separadamente:
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
Caso inteiros e valores de ponto flutuante estejam nos mesmos 8 bytes, o tipo é inteiro e os 8 bytes inteiros (incluindo o valor de ponto flutuante) são colocados em um 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 uma conversão de ponto flutuante para inteiro é feita usando cvtsi2ss ou cvtsi2sd, isso muda a organização dos bytes subjacentes.
Você pode usar movd (para valores de 32 bits) e movq (para valores de 64 bits) para mover bytes brutos de um registrador xmm para um GPR, sem alterar a representação de bits.
No entanto, structs com mais de 16 bytes de tamanho geralmente são colocadas na memória, independentemente do tipo de cada 8 bytes:
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 uma struct é passada como argumento para uma função, ela usa tantos registradores quanto forem necessários. Se ela for colocada na memória, a pilha é usada em vez disso.
Regras semelhantes se aplicam a structs retornadas de uma função.
Elas geralmente são retornadas primeiro em rax e rdx (se forem de valor inteiro) ou em xmm0 e xmm1 (se forem de valor de ponto flutuante).
Se forem colocadas na memória, porém, quem chama precisa reservar espaço para ela e passar o endereço desse local de memória como um primeiro argumento implícito para a função (em rdi).
A função então armazena a struct nesse espaço e retorna seu endereço em rax.
Neste exercício, você vai organizar corridas entre vários tipos de carros de controle remoto. Cada carro tem suas próprias características e corre em muitas pistas diferentes, em corridas ao longo da temporada do torneio.
As funções deste exercício são declaradas em um arquivo fonte em C com as assinaturas fornecidas. O seu código em x86-64 assembly precisa seguir essas especificações.
Você definiu uma struct car_t para representar um carro de controle remoto:
struct car_t {
char name[10];
int16_t speed;
float battery;
};
Permita criar um carro de controle remoto definindo uma função new_car em x86-64 assembly.
O campo battery sempre começa com o valor 100.0.
struct car_t new_car(short speed, const char name[]);
As pistas são representadas por uma struct track_t:
struct track_t {
enum surface_t surface;
size_t distance;
};
O enum surface_t guarda os diferentes tipos de superfície de uma pista:
enum surface_t {
ASPHALT,
SAND,
ICE,
CLAY
};
Permita criar uma nova pista definindo uma função new_track em x86-64 assembly.
struct track_t new_track(enum surface_t surface, size_t distance);
Até 6 carros participam de cada corrida, dando um número determinado de voltas em uma pista.
As corridas são representadas por uma 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;
};
Defina uma função new_race em x86-64 assembly.
No início, não há carros com participação confirmada na corrida:
struct race_t new_race(struct track_t track, uint8_t num_of_laps);
Quando o dono de um carro pede que ele participe de uma corrida, a participação só é confirmada se a lista ainda não estiver cheia.
Defina uma função add_participant em x86-64 assembly.
Se houver espaço para mais um participante na corrida, o carro é adicionado à lista, o contador é atualizado e a participação dele é confirmada.
Caso contrário, os organizadores da corrida precisam informar ao dono do carro que ele não pode participar desta vez.
bool add_participant(struct race_t *race, struct car_t car);
Você acompanha as corridas agendadas para a temporada em uma struct tournament_t:
struct tournament_t {
struct race_t races[20];
size_t num_of_races;
};
Defina uma função add_race em x86-assembly, para adicionar uma corrida ao array do torneio e também atualizar o contador.
Você pode considerar que sempre haverá espaço suficiente no array races para a nova corrida.
void add_race(struct tournament_t *tournament, struct race_t race);
Crie sua conta no Exercism para aprender e dominar x86-64 Assembly com 22 conceitos130 exercícios e mentoria humana de verdade, tudo de graça.