Need For Speed

Need For Speed

学習演習

はじめに

C言語とのインターフェース:構造体

C言語では、構造体はさまざまなフィールドからなる複合型です。 各フィールドは、それぞれ独自の型とサイズを持ちます。

たとえば、次のコードは3つの整数からなる型を定義します。内訳は4バイトのものが2つ、8バイトのものが1つです:

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

構造体のフィールドの1つが配列である場合、その配列に割り当てられたすべてのバイトは構造体の一部になります。 一方、フィールドがその配列へのポインターにすぎない場合は、配列は別の場所に格納する必要があります:

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バイトの境界を守る必要があります。 つまり、構造体内の任意の2つのフィールドの間には、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レジスターで渡されます。

一方、構造体は特別なケースです。 経験則として、構造体内のすべてのフィールドはメモリー上で隣接した領域を占めるため、可能な限り1つのレジスターにまとめて配置する必要があります:

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を使って浮動小数点から整数へ変換すると、基になるバイトの構成が変わります。

ビット表現を変えずに、生のバイトをxmmレジスターからGPRへ移動するには、movd(32ビット値の場合)とmovq(64ビット値の場合)を使えます。

ただし、サイズが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. レースイベントを作成する

1つのレースには最大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を定義します。 開始時点では、レースへの参加が確定した車は1台もありません:

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

4. レースへの車の参加を確定する

車のオーナーがレースへの参加を申し込んだとき、リストにまだ空きがあれば、その参加が確定します。

x86-64アセンブリで関数add_participantを定義します。 レースにもう1台分の空きがあれば、その車はリストに追加され、カウンターが更新されて、参加が確定します。 空きがなければ、レースの主催者は、今回は参加できないことを車のオーナーに伝えなければなりません。

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

Need For Speedを始める準備はできましたか?

Exercismに登録すれば、22個のコンセプト130個の演習、そして本物の人間によるメンタリングとともに、x86-64 Assemblyを学んでマスターできます。すべて無料です。