在 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'
在汇编中访问结构体时,务必考虑一切可能的填充字节,这一点很重要。
一般来说,值传入函数和从函数返回的方式都符合人们的预期:
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
使用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中返回它的地址。
在这个练习中,你将组织各种遥控车之间的比赛。每辆车都有自己的特性,在整个锦标赛赛季的比赛中驶过许多不同的赛道。
本练习中的函数在一个 C 源文件中按照给出的签名声明。 你的 x86-64 汇编代码必须遵循这些规范。
你已经定义了一个结构体 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[]);
赛道用结构体 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);
每场比赛最多有 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);
当车主提出让某辆车参加比赛时,只有在名单还没有满的情况下,这辆车的参赛才会得到确认。
用 x86-64 汇编定义函数 add_participant。
如果比赛中还能再容纳一辆参赛车辆,就把这辆车加入名单,更新计数器,它的参赛就得到确认。
否则,赛事组织者必须告知车主,这次无法参赛。
bool add_participant(struct race_t *race, struct car_t car);
你用一个结构体 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);