在 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-assembly 程式碼必須遵循這些規格。
你已經定義了一個 struct 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[]);
賽道以 struct 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 輛車參加,在賽道上跑完指定的圈數。
比賽以 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;
};
在 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);
你用 struct tournament_t 記錄這個賽季預定的比賽:
struct tournament_t {
struct race_t races[20];
size_t num_of_races;
};
在 x86-assembly 中定義函式 add_race,把一場比賽加入錦標賽的陣列,同時更新它的計數器。
你可以假設 races 陣列一定有足夠的空間容納新的比賽。
void add_race(struct tournament_t *tournament, struct race_t race);