極速快感

極速快感

學習練習

簡介

與 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-assembly 程式碼必須遵循這些規格。

1. 建立遙控車

你已經定義了一個 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[]);

2. 建立新賽道

賽道以 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);

3. 建立賽事

每場比賽最多有 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);

4. 確認車輛參賽

當車主要求讓車輛參加比賽時,只有在名單還沒額滿的情況下,參賽資格才會被確認。

在 x86-64 組合語言中定義函式 add_participant。 如果比賽還有一個參賽名額,就把這輛車加入名單、更新計數器,並確認它的參賽資格。 否則,賽事主辦單位必須告知車主,這輛車這次無法參賽。

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

5. 建立錦標賽

你用 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);
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
x86-64 Assembly Exercism

準備好開始 極速快感 了嗎?

註冊 Exercism,透過 22 個概念130 個練習 和真人引導來學習並精通 x86-64 Assembly,全部免費。