Need For Speed

Need For Speed

লার্নিং অনুশীলনী

ভূমিকা

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

অ্যালাইনমেন্ট

একটি টাইপের অ্যালাইনমেন্ট হলো দুইয়ের একটি নন-নেগেটিভ ঘাত (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 ব্যবহার করে ফ্লোটিং-পয়েন্ট থেকে ইন্টিজারে কনভার্সন করলে অন্তর্নিহিত বাইটগুলোর বিন্যাস বদলে যায়।

বিট রিপ্রেজেন্টেশন না বদলে 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-assembly কোডকে অবশ্যই ওই স্পেসিফিকেশন মেনে চলতে হবে।

1. একটি রিমোট কন্ট্রোলড গাড়ি তৈরি করুন

রিমোট কন্ট্রোলড গাড়ি প্রকাশ করতে আপনি একটি struct car_t ডিফাইন করেছেন:

struct car_t {
    char name[10];
    int16_t speed;
    float battery;
};

x86-64 assembly-এ 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 assembly-এ new_track নামের একটি ফাংশন ডিফাইন করে নতুন ট্র্যাক তৈরি করা সম্ভব করুন।

struct track_t new_track(enum surface_t surface, size_t distance);

3. একটি রেসিং ইভেন্ট তৈরি করুন

প্রতিটি রেসে সর্বোচ্চ ৬টি গাড়ি অংশ নেয় এবং একটি ট্র্যাকে নির্দিষ্ট সংখ্যক ল্যাপ চলে। রেস বোঝানো হয় একটি 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 assembly-এ new_race নামের একটি ফাংশন ডিফাইন করুন। শুরুর দিকে রেসটির জন্য কোনো নিশ্চিত অংশগ্রহণকারী গাড়ি থাকে না:

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

4. রেসে একটি গাড়ির অংশগ্রহণ নিশ্চিত করুন

কোনো গাড়ির মালিক যখন সেটির একটি রেসে অংশগ্রহণের অনুরোধ করেন, তখন অ্যারেটি আগে থেকেই পূর্ণ না থাকলে তবেই তার অংশগ্রহণ নিশ্চিত হয়।

x86-64 assembly-এ 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

Need For Speed শুরু করতে প্রস্তুত?

Exercism-এ সাইন আপ করুন, x86-64 Assembly ট্র্যাকের 22টি কনসেপ্ট130টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।