追求速度

追求速度

学习练习

简介

与 C 语言交互:结构体

在 C 语言中,结构体是一种复合类型,由不同的字段组成。每个字段都有自己的类型和大小。

例如,下面定义了一种由三个整数组成的类型,其中两个占 4 字节,一个占 8 字节:

struct example {
    int32_t first_four_byte_integer;
    uint32_t second_four_byte_integer;
    uint64_t eight_byte_integer;
};

结构体也可以包含其他结构体:

struct example_2 {
    struct example first;
    struct example second;
};

如果结构体的某个字段是数组,那么为该数组分配的所有字节都属于这个结构体。但是,如果该字段只是指向数组的指针,那么数组必须存储在别处:

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

对齐

类型的对齐是一个非负的 2 的幂(1、2、4、8……),表示对其地址的一项要求。该类型的每个对象都必须位于对齐值的倍数地址上。

对于基本类型,对齐值通常等于它的大小,因此,举例来说,int32_t通常具有 4 字节的对齐。这意味着int32_t必须分配在 4 字节的边界上。

不过,结构体的对齐值是其所有字段中对齐值的最大值。因此,举例来说,上面定义的struct example的对齐值是 8 字节,因为这就是uint64_t的对齐值。

如果某个结构体字段的对齐值小于结构体本身的对齐值,就可能在该字段之后插入填充字节。这样可以确保后续字段以及整个结构体都满足对齐要求。实际上,这意味着这些填充字节虽然会计入结构体的大小,却不参与结构体的值:

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'

注意,在上面定义的结构体中,字段 'one_byte' 的对齐值为 1,可以放在任意地址上。由于字段 'four_byte' 的对齐值为 4,结构体中的字段也必须遵守 4 字节边界。这意味着结构体中任意两个字段之间都必须相隔 4 的倍数字节。由于 'one_byte' 只占 1 字节,因此要在它后面插入 3 个填充字节,以满足这一边界要求。

只要可以合并,相邻的字段就会被分到一组,以尽量减少所需的填充字节数:

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'

这种合并也必须遵守每个字段的对齐要求:

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'

这意味着,改变结构体中字段的顺序,有时会导致它的大小不同:

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

注意,数组的对齐值与其底层类型的对齐值相同:

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'

数组的元素可以像它们不属于数组那样进行合并:

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'

在汇编中访问结构体时,务必考虑一切可能的填充字节,这一点很重要。

调用约定

一般来说,值传入函数和从函数返回的方式都符合人们的预期:

  • 整数值(包括内存地址和枚举)通过通用寄存器(GPR)传递。
  • 浮点值通过xmm寄存器传递。

不过,结构体是一种特殊情况。通常来说,结构体中的所有字段在内存中占用相邻的空间,应尽可能放入单个寄存器:

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

如果结构体既有整数值的字段,也有浮点值的字段,那么结构体中每个连续的 8 字节都应单独分类:

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

如果整数和浮点值位于同一个 8 字节内,该类型就视为整数,整个 8 字节(包括其中的浮点值)都会放入 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

使用cvtsi2ss或cvtsi2sd把浮点数转换为整数时,底层字节的组织方式会发生变化。

你可以用movd(用于 32 位的值)和movq(用于 64 位的值)把原始字节从xmm寄存器移动到 GPR,而不改变位表示。

但是,大小超过 16 字节的结构体通常会放入内存,无论其中每个 8 字节是什么类型:

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

结构体作为实参传给函数时,会使用所需数量的寄存器。如果它被放入内存,则改用栈来传递。

结构体作为函数的返回值时也适用类似的规则。如果结构体是整数值,通常优先通过rax和rdx返回;如果是浮点值,则通过xmm0和xmm1返回。但如果它被放入内存,调用方就必须为它预留空间,并把这块内存的地址作为函数的隐式第一个实参(放在rdi中)传进去。随后,函数把结构体存入这块空间,并在rax中返回它的地址。

说明

在这个练习中,你将组织各种遥控车之间的比赛。每辆车都有自己的特性,在整个锦标赛赛季的比赛中驶过许多不同的赛道。

Note

本练习中的函数在一个 C 源文件中按照给出的签名声明。 你的 x86-64 汇编代码必须遵循这些规范。

1. 创建一辆遥控车

你已经定义了一个结构体 car_t 来表示遥控车:

struct car_t {
    char name[10];
    int16_t speed;
    float battery;
};

通过用 x86-64 汇编定义函数 new_car 来创建遥控车。 字段 battery 始终以值 100.0 开始。

struct car_t new_car(short speed, const char name[]);

2. 创建新赛道

赛道用结构体 track_t 表示:

struct track_t {
    enum surface_t surface;
    size_t distance;
};

enum surface_t 记录了赛道的不同路面:

enum surface_t {
    ASPHALT,
    SAND,
    ICE,
    CLAY
};

通过用 x86-64 汇编定义函数 new_track 来创建新赛道。

struct track_t new_track(enum surface_t surface, size_t distance);

3. 创建一场比赛

每场比赛最多有 6 辆车参加,在赛道上跑指定的圈数。 比赛用结构体 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;
};

用 x86-64 汇编定义函数 new_race。 开始时,比赛还没有已确认的参赛车辆:

struct race_t new_race(struct track_t track, uint8_t num_of_laps);

4. 确认一辆车参赛

当车主提出让某辆车参加比赛时,只有在名单还没有满的情况下,这辆车的参赛才会得到确认。

用 x86-64 汇编定义函数 add_participant。 如果比赛中还能再容纳一辆参赛车辆,就把这辆车加入名单,更新计数器,它的参赛就得到确认。 否则,赛事组织者必须告知车主,这次无法参赛。

bool add_participant(struct race_t *race, struct car_t car);

5. 创建一场锦标赛

你用一个结构体 tournament_t 记录本赛季安排好的比赛:

struct tournament_t {
    struct race_t races[20];
    size_t num_of_races;
};

用 x86 汇编定义函数 add_race,把一场比赛加入锦标赛的数组,并更新它的计数器。 你可以认为 races 数组里总是有足够的空间容纳新的比赛。

void add_race(struct tournament_t *tournament, struct race_t race);
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
x86-64 Assembly Exercism

准备好开始 追求速度 了吗?

注册 Exercism,借助 22 个概念130 个练习 和真人导师指导,学习并掌握 x86-64 Assembly,全部免费。