Need for Speed

Need for Speed

Exercício de aprendizagem

Introdução

Interagir com a linguagem C: structs

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

Alinhamento

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.

Convenção de chamada

Em geral, os valores são passados para as funções e devolvidos por elas como seria de esperar:

  • Os valores inteiros, incluindo endereços de memória e enums, são passados em registos de uso geral (GPR).
  • Os valores de vírgula flutuante são passados em registos 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
Caution

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.

Instruções

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.

Note

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.

1. Cria um carro telecomandado

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[]);

2. Cria uma nova pista

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);

3. Cria um evento de corrida

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);

4. Confirma a participação de um carro na corrida

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);

5. Cria um torneio

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);
Editar via GitHub A ligação abre numa nova janela ou separador
x86-64 Assembly Exercism

Estás pronto para começar Need for Speed?

Inscreve-te no Exercism para aprenderes e dominares x86-64 Assembly com 22 conceitos130 exercícios, e mentoria humana real, tudo grátis.