সাধারণত অপারেটিং সিস্টেম (OS) একটি প্রোগ্রামের জন্য মেমরিকে একটি সাধারণ বিন্যাসে সাজিয়ে দেয়:
| ঠিকানা | মেমরি অঞ্চল |
|---|---|
| উচ্চ | স্ট্যাক |
| ... | |
| হিপ | |
| রিড-অ্যান্ড-রাইট সেগমেন্ট | |
| কোড/রিড-ওনলি সেগমেন্ট | |
| নিম্ন | সংরক্ষিত |
সেগমেন্টে ভাগ করা মেমরি আবার সেকশনগুলোতে সাজানো থাকে, আর প্রতিটি সেকশনের অনুমতিও আলাদা।
এখন পর্যন্ত আমরা যে ফাংশনগুলো ডিফাইন করেছি, সেগুলো সবই ছিল section .text-এ। এই সেকশনে থাকে রিড-ওনলি এক্সিকিউটেবল ডেটা। অন্য সেকশনগুলো ব্যবহার করা হয় ডেটা ভ্যারিয়েবল ডিক্লেয়ার করার জন্য; এগুলো রিড-ওনলি বা রিড-অ্যান্ড-রাইট হতে পারে, কিন্তু এক্সিকিউটেবল নয়।
ইনিশিয়ালাইজড ডেটা ডিক্লেয়ার করা হয় section .data-এ।
NASM-এ (The Netwide Assembler, এই ট্র্যাকের অ্যাসেম্বলার), একটি ইনিশিয়ালাইজড ভ্যারিয়েবলের থাকে একটি নাম, ডেটার সাইজ বোঝানো একটি ডিরেক্টিভ, আর কমা দিয়ে আলাদা করা মানের একটি তালিকা।
এগুলোর প্রতিটিকে একটি স্পেস দিয়ে আলাদা করা হয়, আর লেবেলের পরে চাইলে একটি : থাকতে পারে।
প্রধান ডিরেক্টিভগুলো এবং তাদের সংশ্লিষ্ট ডেটা সাইজ:
| ডিরেক্টিভ | সাইজ |
|---|---|
| db | ১ বাইট |
| dw | ২ বাইট |
| dd | ৪ বাইট |
| dq | ৮ বাইট |
যেমন, এটি space নামের একটি বাইট ভ্যারিয়েবল ডিক্লেয়ার করে যার মান 10:
section .data
space db 10
section .data-এ ডিক্লেয়ার করা ভ্যারিয়েবলগুলো মিউটেবল, অর্থাৎ এগুলো রিড-অ্যান্ড-রাইট।
এগুলোর স্টোরেজ ডিউরেশনও স্ট্যাটিক, অর্থাৎ প্রোগ্রাম চালু থাকার পুরো সময় জুড়ে এগুলো থাকে।
section .rodata অনেকটা section .data-এর মতো।
দুটি সেকশনেই ইনিশিয়ালাইজড ডেটা থাকে, যেগুলো একইভাবে ডিক্লেয়ার করা হয় এবং যাদের স্টোরেজ ডিউরেশনও একই।
এদের মধ্যে প্রধান পার্থক্য হলো section .rodata-এর ডেটা ইমিউটেবল, অর্থাৎ রিড-ওনলি।
equ দিয়ে ডিফাইন করা কনস্ট্যান্ট section .rodata-এ ডিফাইন করা কনস্ট্যান্টের চেয়ে আলাদা।
equ দিয়ে ডিফাইন করা কনস্ট্যান্ট মেমরিতে জায়গা দখল করে না, বরং অ্যাসেম্বলার সরাসরি তার মান বসিয়ে দেয়।
আসলে এটি ওই মানের একটি প্লেসহোল্ডার মাত্র।
অন্যদিকে, section .rodata-এ ডিফাইন করা কনস্ট্যান্ট সত্যিই মেমরিতে সংরক্ষিত থাকে এবং এর একটি ঠিকানা থাকে।
ডিক্লেয়ার করা ডেটার সাথে একটি নাম যুক্ত থাকতে হয়। এই নামটিকে বলা হয় লেবেল।
লেবেল হলো একটি সিম্বল, যা মেমরিতে ডেটার নির্দিষ্ট ঠিকানা এনকোড করে। x86-64-এ ঠিকানা হলো ৬৪-বিট মান।
NASM-এ কোনো ডেটাকে সরাসরি তার লেবেল দিয়ে অ্যাক্সেস করতে গেলে বরাদ্দ করা মেমরি পাওয়া যায় না, বরং পাওয়া যায় তার ঠিকানা:
section .data
example dq 27 ; this declares a 8-byte variable initialized with 27
section .text
fn:
mov rax, example ; this stores the address of the declared variable in rax, not its contents
...
মেমরি ঠিকানার ভেতরের কনটেন্ট অ্যাক্সেস করতে হলে সেটিকে ডিরেফারেন্স করতে হয়। এটিকে বলা হয় ইনডিরেকশন।
NASM-এ এটি করা হয় [] দিয়ে:
section .data
example dq -27 ; this declares a 8-byte variable initialized with -27
section .text
fn:
mov rax, [example] ; this dereferences example and access the value stored in memory (-27)
...
তবে এমন কিছু পরিস্থিতি আছে, যেখানে ডিরেফারেন্স করা মেমরির সাইজ নিয়ে অস্পষ্টতা তৈরি হতে পারে। সেসব ক্ষেত্রে এই সাইজ উল্লেখ করে একটি প্রিফিক্স ব্যবহার করতে হয়।
একটি সাধারণ x86-64 প্রোগ্রামে সবচেয়ে গুরুত্বপূর্ণ প্রিফিক্সগুলো এবং তাদের সাইজ:
| প্রিফিক্স | সাইজ |
|---|---|
| byte | ১ বাইট |
| word | ২ বাইট |
| dword | ৪ বাইট |
| qword | ৮ বাইট |
একই লোড সাইজ স্পষ্টভাবে উল্লেখ করেও লেখা যায়:
mov rax, qword [example] ; same dereference, size stated explicitly
মেমরি ডিরেফারেন্স করার সময় সবসময় একটি প্রিফিক্স ব্যবহার করা ভালো অভ্যাস।
মেমরিতে লেখাও একইভাবে করা হয়, একটি ঠিকানা ডিরেফারেন্স করে:
section .data
example1 db 10 ; example1 is a 1-byte memory location initialized with value 10
example2 dq -456 ; example2 is a 8-byte memory location initialized with value -456
example3 dd 54 ; example3 is a 4-byte memory location initialized with value 54
section .text
fn:
mov byte [example1], 20 ; example1 now has value 20
mov qword [example2], rdx ; example2 now has value equal to the contents in rdx
mov dword [example3], eax ; example3 now has value equal to the contents in eax
মনে রাখবেন, বেশিরভাগ ইনস্ট্রাকশনে আগে রেজিস্টারে কনটেন্ট লোড না করেই আপনি মেমরি অপারেন্ড ব্যবহার করতে পারেন। তবে সাধারণত সোর্স ও ডেস্টিনেশন, দুটি অপারেন্ডেই একসাথে এগুলো ব্যবহার করা যায় না, দুটির মধ্যে কেবল একটি ব্যবহার করা যায়:
section .data
example4 dw 4
example5 dq -8
example6 dd 15
section .text
fn:
add word [example4], 5 ; example4 is now a 2-byte memory location with the value 4 + 5 = 9
imul rax, qword [example5] ; rax = rax * (-8)
; this is not possible -> sub dword [example6], dword [example6]
যদিও একটি ভ্যারিয়েবলের ঠিকানা রেজিস্টারে রাখতে mov ব্যবহার করা যায়, এর জন্য বিশেষভাবে তৈরি একটি ইনস্ট্রাকশনও আছে: lea।
এই ইনস্ট্রাকশন একটি মেমরি-ফর্ম অপারেন্ড ব্যবহার করে, কিন্তু এটি মেমরি পড়ে না। বরং এটি ইফেক্টিভ অ্যাড্রেস এক্সপ্রেশনটি হিসাব করে ফলাফলটি ডেস্টিনেশন অপারেন্ডে লেখে:
lea rax, [example] ; this stores the address of 'example' in rax
মেমরি ঠিকানা হিসাব করে রেজিস্টারে রাখতে lea ব্যবহার করাই বেশি স্বাভাবিক।
মেমরি লোকেশন অ্যাক্সেস করার সময় NASM-এর ডিফল্ট আচরণ হলো অ্যাবসোলিউট অ্যাড্রেস তৈরি করা, অর্থাৎ নির্দিষ্ট মেমরি ঠিকানা।
নিরাপত্তার কারণে এক্সিকিউটেবল অনেক সময় PIE (Position Independent Executable) হিসেবে বিল্ড করা হয়, যেখানে মেমরি অঞ্চলগুলো এলোমেলো ঠিকানায় বসানো হয়।
একটি PIE-তে লিংকের সময় কোনো ভ্যারিয়েবলের চূড়ান্ত ঠিকানা জানা থাকে না।
তাই কোড বরং rip নামের একটি বিশেষ রেজিস্টারের মান থেকে অফসেট হিসেবে ঠিকানা হিসাব করে; rip পরের যে ইনস্ট্রাকশনটি এক্সিকিউট হবে সেটির দিকে ইঙ্গিত করে।
এটিকেই সাধারণত RIP-রিলেটিভ অ্যাড্রেসিং বলা হয়।
NASM-এ rel অপারেটর দিয়ে আপনি RIP-রিলেটিভ অ্যাক্সেস চাইতে পারেন:
mov rax, qword [rel variable]
সোর্স ফাইলের একেবারে উপরে default rel লিখে রিলেটিভ অ্যাড্রেসিংকে ডিফল্টও বানানো যায়।
এই ট্র্যাকের সব অনুশীলনী PIE হিসেবে কম্পাইল ও লিংক করা হয়, তাই রিলেটিভ ঠিকানা তৈরি করতে rel ব্যবহার করা উচিত।
যেকোনো সেকশনে (যেমন .text, .data, .rodata) ডিফাইন করা লেবেল (ফাংশন ও ডেটা) একই সোর্স ফাইলের ভেতরে দৃশ্যমান থাকে।
যদি global ডিক্লেয়ার করা হয়, তবে সেগুলো অন্য সোর্স ফাইল থেকেও দৃশ্যমান থাকে।
উল্টোদিকে, অন্য সোর্স ফাইলে ডিফাইন করা লেবেল extern ডিক্লেয়ার করা থাকলে বর্তমান সোর্স ফাইলে দৃশ্যমান থাকে।
এক্ষেত্রে অ্যাসেম্বলিতে ডেটার সাইজের কোনো ইঙ্গিত থাকে না, এটি আগে থেকেই জানা থাকতে হয়।
default rel
section .data
global number1 ; 'number1' is a variable visible to other source files
number1 db 200
extern number2 ; 'number2' is a variable visible to the current source file, but defined in another
section .text
extern sum ; sum is a function visible to the current source file, but defined in another
fn:
mov dil, byte [number1]
mov sil, byte [number2]
call sum
...
আপনার বন্ধু José একটি স্থানীয় স্কুলের শিক্ষক। রং কীভাবে মিলিয়ে ভিন্ন ভিন্ন রং তৈরি করা যায়, তা দেখানোর জন্য তিনি কিছু মজার পরীক্ষার একটি আইডিয়া পেয়েছিলেন।
সেই পরীক্ষাগুলোর জন্য তিনি আপনার সাহায্য চেয়েছিলেন।
এই অনুশীলনীতে একটি রংকে একটি ৩২-বিট (৪-বাইট) সংখ্যা দিয়ে প্রকাশ করা হয়, যা তার RGB মান এনকোড করে।
একটি RGB মান তিনটি চ্যানেল নিয়ে গঠিত: Red, Green এবং Blue (অর্থাৎ লাল, সবুজ আর নীল), যার প্রতিটি ৮ বিট (১ বাইট) জায়গা নেয়।
চতুর্থ বাইটটি সাধারণত Alpha চ্যানেলের জন্য সংরক্ষিত থাকে, তবে এই অনুশীলনীতে এর মান খালি (0) থাকবে।
প্রতিটি রঙের মান আগেই একটি টেবিলে সংরক্ষিত আছে, যা অন্য একটি সোর্স ফাইলে ডিফাইন করা হয়েছে। এই টেবিলে প্রতিটি রং একটি অনন্য অ্যাড্রেস দিয়ে চিহ্নিত করা হয়।
get_color_value নামের একটি ফাংশন ডিফাইন করুন, যা একটি রঙের ৩২-বিট মান রিটার্ন করে।
এই ফাংশনটি প্যারামিটার হিসেবে রঙের টেবিলে ওই রঙের একটি বৈধ অ্যাড্রেস নেয়।
get_color_value(black)
// => 0
ইঙ্গিত - ৩২ বিট সমান ৪ বাইট।
বিভিন্ন রং মেশানোর জন্য José প্রথমে একটি বেস রং ঠিক করবেন এবং কেবল তার সাথে মেশানো সেকেন্ডারি রংটিই পরিবর্তন করবেন।
add_base_color নামের একটি ফাংশন ডিফাইন করুন, যা একটি রঙের ৩২-বিট মান base_color ভ্যারিয়েবলে সংরক্ষণ করে, যাতে পরে তা ব্যবহার করা যায়।
এই ফাংশনের কোনো রিটার্ন ভ্যালু নেই এবং এটি প্যারামিটার হিসেবে রঙের টেবিলে রঙটির অ্যাড্রেস নেয়।
base_color ভ্যারিয়েবলটি আপনাকেই ডিফাইন করতে হবে এবং এটি অন্য সোর্স ফাইল থেকেও ব্যবহার করা যেতে হবে।
একই সময়ে একটির বেশি বেস রং থাকবে না। নতুন কোনো বেস রং যোগ করা হলে পুরোনোটি বাতিল হয়ে যায়।
ডিফল্টভাবে, প্রোগ্রাম শুরুর সময় base_color ভ্যারিয়েবলটিকে white-এর ৩২-বিট মান দিয়ে ইনিশিয়ালাইজ করা উচিত, যা হলো 0xFFFFFF00.
ইঙ্গিত - NASM শুরুতে 0x ব্যবহার করে হেক্সাডেসিমালে লেখা সংখ্যা গ্রহণ করে, যেমন 0xFFFFFF00.
José প্রাইমারি রং ব্যবহার করে অনেক রকম মিশ্রণ তৈরি করতে চান, তাই দ্রুত ব্যবহারের জন্য তিনি সেগুলো আলাদা করে রাখতে চান।
যেহেতু তিনি রং প্রকাশ করতে RGB ব্যবহার করছেন, প্রাইমারি রংগুলো হলো:
RED, যার মান 0xFF000000.GREEN, যার মান 0x00FF0000.BLUE, যার মান 0x0000FF00.এই রংগুলোর প্রতিটির জন্য একটি করে কনস্ট্যান্ট ডিফাইন করুন। এই কনস্ট্যান্টগুলো অন্য সোর্স ফাইল থেকেও ব্যবহার করা যেতে হবে।
অন্য একটি সোর্স ফাইলে ডিফাইন করা combining_function অনুযায়ী রংগুলো মেশানো উচিত।
এই ফাংশনটি প্যারামিটার হিসেবে base_color এবং তার সাথে মেশানো সেকেন্ডারি রঙের ৩২-বিট মান নেয়।
এটি মেশানো রঙের ৩২-বিট মান রিটার্ন করে।
make_color_combination নামের একটি ফাংশন ডিফাইন করুন, যা দুটি রং মেশায় এবং ফলাফলটি মেমরিতে সংরক্ষণ করে।
এই ফাংশনের কোনো রিটার্ন ভ্যালু নেই এবং এটি এই ক্রমে প্যারামিটার নেয়:
লক্ষ্য করুন, combining_function আপনি যে রেজিস্টারগুলো ব্যবহার করছেন সেগুলোর মান পরিবর্তন করে দিতে পারে।
ফাংশনটি কল করার আগে আপনার প্রয়োজনীয় প্রতিটি ভ্যারিয়েবল মেমরিতে সংরক্ষণ করতে ভুলবেন না।
Exercism-এ সাইন আপ করুন, x86-64 Assembly ট্র্যাকের 22টি কনসেপ্ট130টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।