হাতে লেখা অ্যাসেম্বলি পুনরাবৃত্তিতে ভরপুর। একই ছোট ইন্সট্রাকশনের ধারা সামান্য ভিন্নতাসহ বারবার ফিরে আসে, আর বয়লারপ্লেট কমানোর মতো বিমূর্ততা খুব কমই থাকে। শুধু তা-ই নয়, অ্যাসেম্বলিতে কোড লেখার সময় এমন মান সামলাতে হয় যাদের স্পষ্ট কোনো অর্থ নেই। নামযুক্ত প্যারামিটার নেই, আর ভিত্তি যন্ত্রের বেশিরভাগ খুঁটিনাটি প্রোগ্রামারকে নিজ হাতেই সামলাতে হয়।
ট্র্যাক যে অ্যাসেম্বলার ব্যবহার করে, সেই NASM দুটি সমস্যার জন্যই কিছু সহায়ক টুল দেয়। এসব টুল চলে অ্যাসেম্বল টাইমে, অর্থাৎ কোনো ইন্সট্রাকশন প্রসেসরে পৌঁছানোর আগেই। সোর্স টেক্সট মেশিন কোডে রূপান্তরিত হওয়ার আগেই কীভাবে তা নতুন করে লেখা হবে, সেটা এরা অ্যাসেম্বলারকে জানিয়ে দেয়।
এসব টুলের মধ্যে সবচেয়ে গুরুত্বপূর্ণ হলো ম্যাক্রো, যা অ্যাসেম্বলারের জন্য এক ধরনের রেসিপি হিসেবে ভাবা যায়। যে জায়গায় ব্যবহার করা হয়, ঠিক সেখানেই এগুলো প্রসারিত হয়, যেন কোডটা হাতে সেখানেই লেখা হয়েছিল।
ম্যাক্রো প্যারামিটার হিসেবে কিছু লিটারাল টোকেন নিতে পারে, ফলে বিভিন্ন প্রসঙ্গে এগুলো পুনরায় ব্যবহার করা যায়।
তবে এগুলো ফাংশন নয়: কোনো call নেই, ret নেই, কোডের অন্য কোনো বিন্দুতে নিয়ন্ত্রণ হস্তান্তরও নেই।
সবচেয়ে সরল সংজ্ঞাটি %define দিয়ে একটি মান বা ছোট একটি এক্সপ্রেশনকে একটি নাম দেয়:
%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)
এর পরে WIDTH যেখানে যেখানে আছে, অ্যাসেম্বলির আগেই তা 8 দিয়ে বদলে যায়।
বদলটা টেক্সটগত, তাই নামটি যেখানেই থাকুক, অপারেন্ডে হোক বা অন্য কোনো সংজ্ঞার ভেতরে, সেখানেই তা বসে যায়।
সংখ্যাগত কনস্ট্যান্টের জন্য %define-এর বদলে equ ব্যবহার করাই ভালো।
%define দিয়ে ঘোষণা করা একক-লাইন ম্যাক্রো কেবল ব্যবহারের সময়ই প্রসারিত হয়, আর তা পুনরায় অ্যাসাইন বা আনডিফাইন করা যেতে পারে।
অন্যদিকে একটি equ কনস্ট্যান্ট সংজ্ঞায়নের জায়গাতেই মূল্যায়িত হয় এবং কখনো পরিবর্তিত হয় না।
একটি %define নামযুক্ত প্যারামিটারও নিতে পারে, যা ছোট একটি ইনলাইন ফাংশনের মতো আচরণ করে:
%define double(x) ((x) + (x))
mov eax, double(WIDTH) ; assembled as mov eax, ((8) + (8))
পুরো বডি এবং প্রতিটি প্যারামিটার, দুটোকেই বন্ধনীতে মুড়ে দিন।
%define square(x) x*x দিয়ে square(1+1) কলটি প্রসারিত হয়ে 1+1*1+1 হয়, যার মান 3।
%define square(x) ((x)*(x)) লেখা হলে একই কল প্রসারিত হয়ে ((1+1)*(1+1)) হয়, যার মান 4।
%define দিয়ে সংজ্ঞায়িত নাম কেস-সংবেদনশীল।
চাইলে %idefine রূপটি নামটিকে কেস-অসংবেদনশীল করে দেয়।
কোডের নানা অংশকে অর্থবহ করে তুলতে একক-লাইন ম্যাক্রো কাজে লাগে। সতর্কতার সাথে ব্যবহার করলে এটি পড়ার সহজতা বাড়ায় এবং টাইপো হওয়ার সম্ভাবনা কমায়। উদাহরণস্বরূপ, কোনো ফাংশনের ভেতরে নির্দিষ্ট কাজে ব্যবহৃত একটি রেজিস্টারকে তার উদ্দেশ্য দিয়েই সংজ্ঞায়িত করা যায়:
%define COUNTER rcx
...
.loop:
...
dec COUNTER
jnz .loop
...
যেকোনো সংখ্যক লাইনের গোটা কোড-ধারা %macro দিয়ে ধরা হয়, আর %endmacro দিয়ে বন্ধ করা হয়।
নামের পরে একটি সংখ্যা বোঝায় ম্যাক্রোটি কতটি আর্গুমেন্ট নেয়, যা শূন্যও হতে পারে।
আর্গুমেন্ট পাঠানোর সময় সেগুলো কমা (,) দিয়ে আলাদা করা হয়:
%macro example_macro 0
...
%endmacro
বহু-লাইন ম্যাক্রোতে আর্গুমেন্টের নাম থাকে না।
যে ক্রমে পাঠানো হয়, সেই ক্রমে এদের %1, %2 ইত্যাদি নামে ডাকা হয়:
; Multiplies two values, returning the result in the first argument
%macro mult_two 2 ; %1 = accumulator, %2 = value
imul %1, %2
%endmacro
mult_two eax, ecx ; multiplies eax and ecx and returns the result into eax
mult_two r8, r9 ; multiplies r8 and r9 and returns the result into r8
মনে রাখবেন, ম্যাক্রোর বডিতে যে কাজগুলো করা হয়, তার জন্য আর্গুমেন্টগুলো বৈধ সিনট্যাক্স হতে হবে।
এখানে mult_two eax, ecx আর mult_two r8, r9 দুটোই অ্যাসেম্বল হয়, কিন্তু mult_two eax, cl হবে না, কারণ imul eax, cl সংজ্ঞায়িত নয়।
সাধারণ লেবেল সংজ্ঞায়িত করা কোনো ম্যাক্রো দুবার ব্যবহার করলেই ভেঙে পড়ে, কারণ তখন লেবেলটিও দুবার সংজ্ঞায়িত হয়ে যাবে।
একবারের বেশি কল করা হতে পারে এমন ম্যাক্রোর ভেতরে লেবেল ব্যবহার করতে হলে লেবেলটির আগে %% বসাতে হয়।
এটি অ্যাসেম্বলারকে নির্দেশ দেয়, প্রতিবার প্রসারণের সময় এই লেবেলকে আলাদা, অনন্য একটি নাম দিতে:
; clobbers rcx
%macro clear_n 2 ; %1 = base address, %2 = count of dwords to clear
xor ecx, ecx
%%loop:
mov dword [%1 + rcx*4], 0
inc ecx
cmp ecx, %2 ; %2 is compared with ecx: it is an immediate or a 32-bit operand
jb %%loop
%endmacro
clear_n rdi, 4 ; gets its own %%loop
clear_n rsi, 8 ; a different %%loop, no clash
times ব্যবহার করে এক লাইনের কোড ব্লক পুনরাবৃত্তি করা সম্ভব।
মেমোরিতে একই মান বারবার ঘোষণা করার সময় এটি বিশেষভাবে কাজে লাগে:
section .data
negative_block: times 16 db -1
; this assembles to 16 db -1 in sequence, i.e., an array of 16 bytes, all equal to -1
বহু-লাইনের কোড পুনরাবৃত্তির জন্য বদলে %rep ব্যবহার করা যায়:
%rep 2 ; number of repetitions: 2
add rcx, 32
mov dword [rsi + rcx], 0
%endrep
মনে রাখবেন, পুনরাবৃত্তির সংখ্যা %rep-এর ঠিক পরেই লেখা হয়, আর পুনরাবৃত্ত ব্লক বন্ধ করতে একটি %endrep থাকে।
রান-টাইম লুপ যেমন কাউন্টার ব্যবহার করে, অনেকটা তেমনি %rep ব্লকের ভেতরে অ্যাসেম্বল-টাইম কাউন্টার ব্যবহার করা সাধারণ ব্যাপার।
অ্যাসেম্বল-টাইম ইন্টিজার কনস্ট্যান্ট equ দিয়ে ঘোষণা করা গেলেও, অ্যাসেম্বল-টাইম ইন্টিজার ভ্যারিয়েবলের জন্য আমরা বদলে %assign ব্যবহার করি।
%define-এর বিপরীতে %assign সঙ্গে সঙ্গে একটি সংখ্যায় মূল্যায়িত হয় এবং পুনরায় অ্যাসাইন করা যায়:
xor eax, eax
%assign i 0
%rep 4
add eax, i ; add 0, then 1, then 2, then 3
%assign i i + 1
%endrep
প্রিপ্রসেসর চলা শেষ হলে লুপটি আর থাকে না, শুধু চারটি add ইন্সট্রাকশন পড়ে থাকে, যেখানে 0, 1, 2 আর 3 ইমিডিয়েট গেঁথে দেওয়া থাকে।
এটি অ্যাসেম্বলারের করা লুপ আনরোলিং: কোনো কাউন্টার রেজিস্টার নেই, কোনো ব্রাঞ্চ নেই, শুধু সোজা-সরল কোড।
প্রিপ্রসেসরের জন্য নানা ধরনের কন্ডিশনাল আছে। এরা কয়েকটি আলাদা পথের মধ্যে একটি বেছে নেয় এবং নিজের বডি ঠিক সেই জায়গাতেই প্রসারিত করে, তবে কেবল তখনই যখন নিজের শর্ত সত্য হয়।
%if ... %elif ... %else ... %endif একটি সংখ্যাগত পরীক্ষা থেকে একটি শাখা বেছে নেয়:
%if WIDTH > 4
mov eax, 2
%elif WIDTH > 2
mov eax, 1
%else
mov eax, 0
%endif
ম্যাক্রোগুলো প্রায়ই আলাদা একটি ফাইলে রাখা হয় এবং যেসব সোর্সের দরকার, সেগুলোতে %include দিয়ে টেনে আনা হয়:
%include "patterns.mac"
নাম দেওয়া ফাইলটি অ্যাসেম্বলির আগে ঠিক সেই জায়গায় হুবহু ঢুকিয়ে দেওয়া হয়, যেন তার ভেতরের সবকিছু সেখানেই টাইপ করা হয়েছিল। এতে সংজ্ঞাগুলো এক জায়গায় থাকে এবং কয়েকটি সোর্স ফাইল সেগুলো ভাগ করে নিতে পারে।
ম্যাক্রো ফাইলটি যদি আলাদাভাবে অ্যাসেম্বল করা হতে পারে, তবে তার .asm ফাইল এক্সটেনশন থাকা উচিত।
মনে রাখবেন, ম্যাক্রোর কোনো লিংকেজ থাকে না।
ম্যাক্রো ফাইলটি অ্যাসেম্বল করা হলেও তার ম্যাক্রোগুলো কেবল সেই অন্য ফাইলের কাছেই দৃশ্যমান, যে ফাইলটি %include দিয়ে এটিকে ধরে।
নাহলে কেবল %include করার জন্য রাখা ফাইলে যেকোনো ফাইল এক্সটেনশনই চলতে পারে।
.asm বা .mac, দুটোর যেকোনোটির ব্যবহারই একটি প্রচলিত রীতি।
আপনি সেই ম্যাক্রো লাইব্রেরিটি রক্ষণাবেক্ষণ করেন, যার ওপর একটি ম্যাগাজিনের কম্পোজিশন ইঞ্জিন দাঁড়িয়ে আছে। ইঞ্জিনটি নিজে, অর্থাৎ প্রতিটি পৃষ্ঠা সাজানো ফাংশনগুলো, নির্দিষ্ট এবং আপনি সেটি লেখেন না। আপনি লেখেন কেবল সেই অ্যাসেম্বল টাইমের যন্ত্রাংশ, যা দিয়ে এটি তৈরি হয়।
এই অনুশীলনীতে আপনাকে কোনো ফাংশন লিখতে হবে না, শুধু ম্যাক্রো। এরপর এই ম্যাক্রোগুলো অন্য একটি ফাইলে আগে থেকেই ডিফাইন করা ফাংশনে অন্তর্ভুক্ত ও কল করা হয়।
আপনার চারটি কাজ আছে।
প্রতিটি ম্যাক্রো তার কাজে দেওয়া নির্দিষ্ট নাম ও আর্গুমেন্টের ক্রম অনুযায়ী কল করা হয়, তাই সেগুলো হুবহু লিখুন। নাহলে ফাইলটি অ্যাসেম্বল হবে না।
ম্যাগাজিনের ফরম্যাট কয়েকটি নির্দিষ্ট সংখ্যা, আর লেআউটের বাকি অংশ সেগুলোকে শুধু নাম ধরে উল্লেখ করে। অ্যাসেম্বল টাইমে আপনাকে প্রতিটির একটি পাঠযোগ্য নাম দিতে হবে:
COLUMN_COUNT হলো 4
COLUMN_STRIDE হলো 18
SPACE হলো 0x20, ফাঁকা বাইটটিRULE হলো 0x3D, = বাইটটিRULE_WIDTH হলো 4
PLAIN হলো 0x2E, . বাইটটিRULED হলো 0x2D, - বাইটটিBOXED হলো 0x23, # বাইটটিইঞ্জিন এগুলোকে উপরের ক্রমেই রিপোর্ট করে।
out = {4, 18, 32, 61, 4, 46, 45, 35} // the eight named values, in order
fill_run ম্যাক্রোটি লিখুনইঞ্জিন একটি গ্যালি ফাঁকা করে এবং একটি একক বাইটের রান স্ট্যাম্প করে ম্যাস্টহেড রুল বসায়। সেই ধাপটিকে একটি মাল্টি-লাইন ম্যাক্রো হিসেবে রূপ দিন, যা রান ভরাতে হলেই ইঞ্জিন কল করতে পারে।
প্রথমে rcx রেজিস্টারের জন্য একটি পাঠযোগ্য নাম COUNTER ডিফাইন করুন।
তারপর fill_run ম্যাক্রোটি ডিফাইন করুন, যা একটি অ্যারেকে একটি বাইটের N কপি দিয়ে ভরাবে।
এটি এই ক্রমে তিনটি আর্গুমেন্ট নেয়:
fill_run rdi, rax, SPACE
fill_run rdx, RULE_WIDTH, RULE
fill_run ম্যাক্রো COUNTER-কে ইচ্ছেমতো ওভাররাইট করতে পারে, কিন্তু অন্য কোনো রেজিস্টার নয়।
মনে রাখবেন, বাইটের সংখ্যা একটি ৬৪-বিট জেনারেল-পারপাস রেজিস্টারে, যেমন rax বা rsi, অথবা একটি ইমিডিয়েট হিসেবে, যেমন 10 বা 4, পাস করা হতে পারে।
আপনি ধরে নিতে পারেন যে এই সংখ্যাটি কখনোই ঋণাত্মক নয়।
এই ম্যাক্রোটি দুটি ভিন্ন ফাংশনে তিনবার কল করা হয়।
length = 8
page = {32, 32, 32, 32, 32, 32, 32, 32}
width = 8
page = {61, 61, 61, 61, 32, 32, 32, 32} // '====' then spaces
lay_columns ম্যাক্রোটি লিখুনগ্রিড ফরম্যাটের জন্য নির্দিষ্ট, তাই কলামের শুরুগুলো অ্যাসেম্বল টাইমেই হিসাব করা যায়।
lay_columns ম্যাক্রোটি ডিফাইন করুন, যা COLUMN_COUNT-সংখ্যক অফসেট লেখে, প্রতিটি একটি ৬৪-বিট ইন্টিজার।
এটি এই ক্রমে দুটি আর্গুমেন্ট নেয়:
lay_columns rsi, rax
গন্তব্য অ্যাড্রেসে লেখার অফসেট সংখ্যা নির্দিষ্ট এবং COLUMN_COUNT-এর সমান।
প্রতিটি অফসেট একটি ৬৪-বিট মান, যা offset[i] = margin + i * COLUMN_STRIDE দিয়ে হিসাব করা হয়, যেখানে i হলো 0 থেকে শুরু হওয়া অফসেটের ইনডেক্স, এবং margin হলো দ্বিতীয় আর্গুমেন্ট।
এই ম্যাক্রো তার দুটি রেজিস্টার আর্গুমেন্ট নষ্ট করে দিতে পারে, কিন্তু অন্য কিছু নয়। এটি একবার কল করা হয়।
margin = 3
out = {3, 21, 39, 57}
set_header ম্যাক্রোটি লিখুনতিনটি স্থায়ী সেকশনের প্রতিটির হেডার স্টাইল ফরম্যাট দ্বারা নির্দিষ্ট, তাই বাইটটি অ্যাসেম্বল টাইমেই জানা থাকে।
set_header ডিফাইন করুন, যা এই ক্রমে দুটি আর্গুমেন্ট নেয়:
0 হলো প্লেইন, 1 হলো রুলড, এবং 2 হলো বক্সড।গন্তব্য অ্যাড্রেস হলো একটি ইফেক্টিভ মেমরি অ্যাড্রেসের এক্সপ্রেশন।
এটি একটি বেস রেজিস্টার নেয়, যার সাথে 1, 2, 4 বা 8 দিয়ে স্কেল করা একটি ইনডেক্স রেজিস্টার যোগ করা যেতে পারে, এবং একটি ইমিডিয়েট:
set_header rdx+8*rcx+2, 1
রানটাইম ব্রাঞ্চের বদলে কন্ডিশনাল অ্যাসেম্বলি (%if / %elif / %else) দিয়ে হেডার বাইটটি (PLAIN, RULED, বা BOXED) বেছে নিন।
এই ম্যাক্রো কোনো রেজিস্টার ওভাররাইট করা উচিত নয়। এটি একই ফাংশনে তিনবার কল করা হয়।
pitch = 4
out = {46, 0, 0, 0, 45, 0, 0, 0, 35} // '.', '-', '#'
Exercism-এ সাইন আপ করুন, x86-64 Assembly ট্র্যাকের 22টি কনসেপ্ট130টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।