در زبان C، یک ساختار یک نوع ترکیبی است که از فیلدهای مختلف تشکیل شده است. هر فیلد نوع و اندازهی خودش را دارد.
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
همترازی یک نوع، توانی از دو و نامنفی است (۱، ۲، ۴، ۸، ...) که نشاندهندهی الزامی روی آدرس آن است. هر شیء از آن نوع باید در آدرسی قرار بگیرد که مضربی از همترازی آن باشد.
برای یک نوع اولیه، همترازی معمولاً برابر با اندازهی آن است، بنابراین، برای مثال، یک int32_t معمولاً همترازی ۴ بایتی دارد.
این یعنی int32_t باید در مرزهای ۴ بایتی تخصیص داده شود.
اما همترازی یک ساختار، بیشترین همترازی میان فیلدهای آن است.
پس برای مثال، struct example که در بالا تعریف شد، همترازی ۸ بایتی خواهد داشت، چون این همترازی 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 همترازی ۱ دارد و میتواند در هر آدرسی قرار بگیرد.
چون فیلد four_byte همترازی ۴ دارد، فیلدهای داخل ساختار نیز باید مرزهای ۴ بایتی را رعایت کنند.
این یعنی باید بین هر دو فیلد در ساختار، تعدادی بایت باشد که مضربی از ۴ است.
چون one_byte فقط ۱ بایت را اشغال میکند، سپس ۳ بایت پرکننده بعد از آن درج میشود تا این مرز رعایت شود.
فیلدهای مجاور هرگاه ممکن باشد گروهبندی میشوند تا تعداد بایتهای پرکنندهی لازم به کمینه برسد:
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
اگر یک ساختار فیلدهایی با مقادیر صحیح و همچنین فیلدهایی با مقادیر شناور داشته باشد، هر ۸ بایت متوالی در ساختار باید جداگانه دستهبندی شود:
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
اگر مقادیر صحیح و شناور در همان ۸ بایت قرار بگیرند، نوع آن صحیح است و کل ۸ بایت (شامل مقدار شناور) در یک 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 (برای مقادیر ۳۲ بیتی) و movq (برای مقادیر ۶۴ بیتی) استفاده کنید تا بایتهای خام را از یک رجیستر xmm به یک GPR منتقل کنید، بدون تغییر نمایش بیتی.
اما ساختارهایی با حجم بیش از ۱۶ بایت معمولاً در حافظه قرار میگیرند، صرفنظر از نوع هر ۸ بایت:
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 شما باید این مشخصات را رعایت کند.
یک struct به اسم car_t تعریف کردهاید تا یک ماشین کنترلی را نمایش دهد:
struct car_t {
char name[10];
int16_t speed;
float battery;
};
با تعریف یک تابع به اسم new_car در اسمبلی x86-64، امکان ساخت یک ماشین کنترلی را فراهم کنید.
فیلد 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
};
با تعریف یک تابع به اسم new_track در اسمبلی x86-64، امکان ساخت یک پیست جدید را فراهم کنید.
struct track_t new_track(enum surface_t surface, size_t distance);
در هر مسابقه حداکثر ۶ ماشین شرکت میکنند و تعداد مشخصی دور را در یک پیست میرانند.
مسابقهها با یک 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;
};
یک تابع به اسم new_race در اسمبلی x86-64 تعریف کنید.
در ابتدا، هیچ ماشین شرکتکنندهای برای مسابقه تأیید نشده است:
struct race_t new_race(struct track_t track, uint8_t num_of_laps);
وقتی صاحب یک ماشین درخواست میکند که آن ماشین در مسابقهای شرکت کند، شرکتش فقط در صورتی تأیید میشود که فهرست از قبل پر نباشد.
یک تابع به اسم add_participant در اسمبلی x86-64 تعریف کنید.
اگر جای یک شرکتکنندهی دیگر در مسابقه وجود داشته باشد، ماشین به فهرست اضافه میشود، شمارنده بهروزرسانی میشود و شرکت آن تأیید میگردد.
در غیر این صورت، برگزارکنندگان مسابقه باید به صاحب ماشین اطلاع دهند که این بار نمیتواند شرکت کند.
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;
};
یک تابع به اسم add_race در اسمبلی x86 تعریف کنید تا یک مسابقه را به آرایهی تورنمنت اضافه کند و شمارندهی آن را هم بهروزرسانی کند.
میتوانید فرض کنید که همیشه فضای کافی در آرایهی races برای مسابقهی جدید وجود دارد.
void add_race(struct tournament_t *tournament, struct race_t race);
در Exercism ثبتنام کنید تا x86-64 Assembly را همراه با 22 مفهوم130 تمرین و مربیگری انسانی واقعی یاد بگیرید و در آن استاد شوید، همهی اینها رایگان.