Na linguagem C, uma struct é um tipo composto, formado por diferentes campos. Cada campo tem o 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. No entanto, se o campo for apenas um ponteiro para o array, então o array tem de ser guardado noutro sítio:
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 um requisito sobre o seu endereço. Todos os objetos desse tipo têm de estar num endereço que seja múltiplo do seu alinhamento.
Para um tipo primitivo, o alinhamento é normalmente igual ao seu tamanho, por isso, por exemplo, um int32_t tem habitualmente um alinhamento de 4 bytes.
Isto significa que int32_t tem de ser alocado em fronteiras de 4 bytes.
O alinhamento de uma struct, no entanto, é o alinhamento máximo de qualquer um dos seus campos.
Assim, por exemplo, a struct example, definida acima, teria um alinhamento de 8 bytes, já que este é o alinhamento de uint64_t.
Se o alinhamento de um campo da struct for menor do que o da struct, podem ser inseridos bytes de preenchimento depois desse campo. Isto garante que os campos seguintes e a struct como um todo cumprem os requisitos de alinhamento. Na prática, isto significa que, embora esses bytes de preenchimento contribuam para o tamanho da struct, 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'
Repara 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 têm de respeitar fronteiras de 4 bytes. Isto significa que tem de haver um número de bytes múltiplo de 4 entre quaisquer dois campos da struct. Como 'one_byte' ocupa apenas 1 byte, são então inseridos 3 bytes de preenchimento depois dele para respeitar essa fronteira.
Os campos adjacentes são agrupados sempre que possível, de modo a 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'
Este agrupamento também tem de 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'
Isto significa que alterar a ordem dos campos numa struct pode, por vezes, 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
Repara que o alinhamento de um array é o mesmo que o alinhamento 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 que 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 ter em conta quaisquer bytes de preenchimento possíveis sempre que se acede a uma struct em assembly.
Em geral, os valores são passados para as funções e devolvidos por elas como seria de esperar:
xmm.As structs, no entanto, 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 num único registo 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 vírgula flutuante, cada bloco consecutivo de 8 bytes na struct deve ser classificado 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 haja inteiros e valores de vírgula flutuante nos mesmos 8 bytes, o tipo é inteiro e os 8 bytes completos (incluindo o valor de vírgula flutuante) são colocados num 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 se faz uma conversão de vírgula flutuante para inteiro com cvtsi2ss ou cvtsi2sd, isso altera a organização dos bytes subjacentes.
Podes usar movd (para valores de 32 bits) e movq (para valores de 64 bits) para mover bytes em bruto de um registo xmm para um GPR, sem alterar a representação de bits.
No entanto, as structs com mais de 16 bytes de tamanho são normalmente colocadas na memória, independentemente do tipo de cada bloco de 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 a uma função, são usados tantos registos quantos os necessários. Se for colocada na memória, usa-se a pilha em vez disso.
Aplicam-se regras semelhantes às structs devolvidas por uma função.
Normalmente, são devolvidas primeiro em rax e rdx (se forem de valor inteiro) ou em xmm0 e xmm1 (se forem de valor de vírgula flutuante).
Se forem colocadas na memória, no entanto, quem chama tem de reservar espaço para ela e passar o endereço dessa localização de memória como primeiro argumento implícito da função (em rdi).
A função guarda então a struct nesse espaço e devolve o seu endereço em rax.
Neste exercício vais organizar corridas entre vários tipos de carros telecomandados. Cada carro tem as 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 num ficheiro de código C com as assinaturas fornecidas. O teu código de assembly x86-64 tem de seguir essas especificações.
Definiste uma struct car_t para representar um carro telecomandado:
struct car_t {
char name[10];
int16_t speed;
float battery;
};
Permite criar um carro telecomandado definindo uma função new_car em assembly x86-64.
O campo battery começa sempre 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
};
Permite criar uma nova pista definindo uma função new_track em assembly x86-64.
struct track_t new_track(enum surface_t surface, size_t distance);
Em cada corrida participam até 6 carros, dando um número especificado de voltas a 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;
};
Define uma função new_race em assembly x86-64.
No início, não há carros participantes confirmados para a 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 numa corrida, a sua participação só é confirmada se a lista ainda não estiver cheia.
Define uma função add_participant em assembly x86-64.
Se houver espaço para mais um participante na corrida, o carro é adicionado à lista, o contador é atualizado e a sua participação fica confirmada.
Caso contrário, os organizadores da corrida têm de informar o dono do carro de que não pode participar desta vez.
bool add_participant(struct race_t *race, struct car_t car);
Registas as corridas agendadas para a temporada numa struct tournament_t:
struct tournament_t {
struct race_t races[20];
size_t num_of_races;
};
Define uma função add_race em assembly x86, para adicionar uma corrida ao array do torneio e atualizar também o contador.
Podes considerar que haverá sempre espaço suficiente no array races para a nova corrida.
void add_race(struct tournament_t *tournament, struct race_t race);
Inscreve-te no Exercism para aprenderes e dominares x86-64 Assembly com 22 conceitos130 exercícios, e mentoria humana real, tudo grátis.