পাঠ ২৭ · ৫৭-এর মধ্যে · মডিউল ৫
Home / Courses / Computer Architecture & Digital Logic / ইনস্ট্রাকশন সেট আর্কিটেকচার

অ্যাসেম্বলি ল্যাঙ্গুয়েজ বেসিকস

Assembly language basics
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • অ্যাসেম্বলি ল্যাঙ্গুয়েজ ও অ্যাসেম্বলারের সংজ্ঞা এবং L24-এর ইনস্ট্রাকশন এনকোডিং-এর সাথে সরাসরি সম্পর্ক
  • এই কোর্সের সরল ইনস্ট্রাকশন সেট — ADD, ADDI, LOAD, STORE, BEQ — এবং প্রতিটির সিনট্যাক্স
  • একটি হাই-লেভেল কন্ডিশনাল স্টেটমেন্ট হাতে অ্যাসেম্বলিতে অনুবাদ করার পদ্ধতি, শুধুমাত্র BEQ ব্যবহার করে শাখা-নিয়ন্ত্রণ (branch control) তৈরি করা
  • Python দিয়ে একটি ছোট্ট assemble() ও simulate() লেখা, এবং তা দিয়ে branch-taken ও branch-not-taken — দুটো পথই সঠিক প্রমাণ করা

১ · অ্যাসেম্বলি ও অ্যাসেম্বলার — L24-এর সরাসরি সম্প্রসারণ

L24-এ দেখেছেন একটি মেশিন ইনস্ট্রাকশন আসলে একটি বাইনারি সংখ্যা, যার নির্দিষ্ট ফিল্ড আছে — opcode, রেজিস্টার, immediate ইত্যাদি। কিন্তু 10001100010000010000000000000100-এর মতো বাইনারি স্ট্রিং হাতে লেখা বা পড়া বাস্তবে অসম্ভব। অ্যাসেম্বলি ল্যাঙ্গুয়েজAssembly Languageমেশিন ইনস্ট্রাকশনের মানুষ-পঠনযোগ্য টেক্সট রূপ। এই সমস্যার সমাধান — প্রতিটি ইনস্ট্রাকশনকে ADD r3, r1, r2-এর মতো একটি সংক্ষিপ্ত, পঠনযোগ্য টেক্সট লাইনে প্রকাশ করা যায়, যা প্রায় সবসময় ঠিক একটি মেশিন ইনস্ট্রাকশনের সাথে এক-এক মিলে যায় — এটি একটি হাই-লেভেল ল্যাঙ্গুয়েজের (যেমন C বা Python) একটি লাইনের চেয়ে সম্পূর্ণ ভিন্ন, কারণ একটি হাই-লেভেল লাইন কম্পাইল হয়ে একাধিক মেশিন ইনস্ট্রাকশনে পরিণত হতে পারে।

অ্যাসেম্বলারAssemblerঅ্যাসেম্বলি টেক্সটকে বাস্তব বাইনারিতে রূপান্তরকারী টুল। হলো সেই টুল যা এই অ্যাসেম্বলি টেক্সটকে পড়ে বাস্তব বাইনারি মেশিন কোড বানায় — মূলত এটি L24-এর encode_r_type() / encode_i_type() ফাংশনগুলোই স্বয়ংক্রিয়ভাবে চালায়, শুধু ইনপুট হিসেবে টেক্সট লাইন পার্স করে ফিল্ডগুলো বের করে নেয়।

২ · এই কোর্সের সরল ইনস্ট্রাকশন সেট

ADD rd, rs1, rs2
R-type — rd = rs1 + rs2 (দুটো রেজিস্টার যোগ)
ADDI rd, rs1, imm
I-type — rd = rs1 + imm (একটি ধ্রুবক যোগ)
LOAD rd, offset(rs1)
rd = mem[rs1 + offset] — রেজিস্টার-ইনডাইরেক্ট-উইথ-ডিসপ্লেসমেন্ট (L25 পুনরায়)
STORE rs2, offset(rs1)
mem[rs1 + offset] = rs2 — মেমরিতে লেখা
BEQ rs1, rs2, offset
rs1 == rs2 হলে PC-কে offset পরিমাণ এগিয়ে লাফ দেয় — নাহলে স্বাভাবিকভাবে পরের ইনস্ট্রাকশনে যায়

লক্ষ করুন — LOAD ও STORE-এর offset(rs1) অংশটি ঠিক L25-এর indexed/displacement addressingeffective address = base রেজিস্টারের মান + একটি ধ্রুবক অফসেট — অ্যারে ইনডেক্সিং-এর পেছনের প্রকৃত হার্ডওয়্যার মেকানিজম।-এর বাস্তব সিনট্যাক্স রূপ।

৩ · উদাহরণ: if (a == b) { c = a + 1; } হাতে অ্যাসেম্বলিতে অনুবাদ

ধরুন r1 = a, r2 = b, r3 = c — এই রেজিস্টার বরাদ্দ (register allocation) নিয়েই শুরু করতে হয়। সমস্যা হলো, এই কোর্সের সরল ইনস্ট্রাকশন সেটে কোনো "unconditional jump" বা "branch-not-equal" নেই — শুধু BEQ (branch if equal) আছে। তাহলে "a != b হলে যোগফলটা এড়িয়ে যাও" — এই যুক্তি কীভাবে বানাবেন শুধু BEQ দিয়ে?

সমাধানটি L26-এর একটি গুরুত্বপূর্ণ ডিজাইন সিদ্ধান্তকে কাজে লাগায় — রেজিস্টার r0 সবসময় হার্ডওয়্যার্ডভাবে ০ পড়ে। তাই BEQ r0, r0, offset সবসময়ই সত্য (r0 == r0 == 0 সবসময়) — এটি একটি "unconditional branch" হিসেবে কাজ করে! নিচের তিন-লাইনের প্রোগ্রামটি লক্ষ করুন —

হাতে-অনুবাদ করা অ্যাসেম্বলি প্রোগ্রাম

0: BEQ r1, r2, 1 — a==b হলে ২ ইনস্ট্রাকশন এগিয়ে (instr 2-এ) লাফ দাও।
1: BEQ r0, r0, 1 — সবসময় সত্য → instr 2 এড়িয়ে instr 3-এ (প্রোগ্রামের শেষে) লাফ দাও।
2: ADDI r3, r1, 1 — c = a + 1 (শুধু instr 0 থেকে সরাসরি লাফ দিলেই এখানে পৌঁছানো যায়)।

a==b হলে: instr 0-এর BEQ সত্য হয়ে সরাসরি instr 2-তে লাফায় — instr 1 (unconditional skip) কখনো চলেই না, তাই c = a+1 বাস্তবায়িত হয়। a!=b হলে: instr 0-এর BEQ মিথ্যা হয়ে instr 1-এ যায়, যা সবসময়-সত্য BEQ হওয়ায় instr 2-কে এড়িয়ে প্রোগ্রামের শেষে চলে যায় — c অপরিবর্তিত থাকে। নিচের কোড সেলে এই দুটো পথই আলাদাভাবে চালিয়ে যাচাই করা হয়েছে।

নিচের কোড সেলে একটি ছোট্ট assemble() ফাংশন এই টেক্সট লাইনগুলো পার্স করে L24-স্টাইল encode_i_type()/encode_r_type() ব্যবহার করে বাস্তব বাইনারিতে প্যাক করে, এবং একটি simulate() ফাংশন সেই ইনস্ট্রাকশনগুলো এক এক করে চালিয়ে রেজিস্টার/মেমরি স্টেট বদলায় — ঠিক যেভাবে একটি বাস্তব CPU চালাত।

Python
# -- এটি বাস্তব হার্ডওয়্যার নয়, একটি টয় assembler + simulator: রেজিস্টার ও মেমরি স্রেফ Python dict/list হিসেবে সিমুলেট করা হয়েছে

OPCODES = {'ADD': 0, 'ADDI': 1, 'LOAD': 2, 'STORE': 3, 'BEQ': 4}

def encode_r_type(opcode, rd, rs1, rs2):
    """L24-স্টাইল: opcode(4bit) | rd(4bit) | rs1(4bit) | rs2(4bit) -- একটি ১৬-বিট ইনস্ট্রাকশনে প্যাক"""
    return (opcode << 12) | (rd << 8) | (rs1 << 4) | rs2

def encode_i_type(opcode, rd, rs1, imm):
    """L24-স্টাইল: opcode(4bit) | rd(4bit) | rs1(4bit) | imm(4bit) -- শিক্ষার সুবিধার্থে ছোট্ট ৪-বিট imm ফিল্ড"""
    return (opcode << 12) | (rd << 8) | (rs1 << 4) | (imm & 0xF)

def assemble(lines):
    """অ্যাসেম্বলি টেক্সট লাইনগুলো পার্স করে ইনস্ট্রাকশন dict + বাস্তব বাইনারি এনকোডিং -- দুটোই তৈরি করে"""
    program = []
    for raw_line in lines:
        line = raw_line.split(';')[0].strip()  # কমেন্ট বাদ
        if not line:
            continue
        parts = line.replace(',', ' ').split()
        mnemonic = parts[0].upper()
        args = parts[1:]

        if mnemonic == 'ADD':
            rd, rs1, rs2 = (int(a[1:]) for a in args)
            enc = encode_r_type(OPCODES['ADD'], rd, rs1, rs2)
            program.append({'op': 'ADD', 'rd': rd, 'rs1': rs1, 'rs2': rs2, 'encoded': enc})
        elif mnemonic == 'ADDI':
            rd, rs1 = int(args[0][1:]), int(args[1][1:])
            imm = int(args[2])
            enc = encode_i_type(OPCODES['ADDI'], rd, rs1, imm)
            program.append({'op': 'ADDI', 'rd': rd, 'rs1': rs1, 'imm': imm, 'encoded': enc})
        elif mnemonic == 'LOAD':
            rd = int(args[0][1:])
            off_str, rs1_str = args[1].split('(')
            offset, rs1 = int(off_str), int(rs1_str.rstrip(')')[1:])
            enc = encode_i_type(OPCODES['LOAD'], rd, rs1, offset)
            program.append({'op': 'LOAD', 'rd': rd, 'rs1': rs1, 'imm': offset, 'encoded': enc})
        elif mnemonic == 'STORE':
            rs2 = int(args[0][1:])
            off_str, rs1_str = args[1].split('(')
            offset, rs1 = int(off_str), int(rs1_str.rstrip(')')[1:])
            enc = encode_i_type(OPCODES['STORE'], rs2, rs1, offset)
            program.append({'op': 'STORE', 'rs2': rs2, 'rs1': rs1, 'imm': offset, 'encoded': enc})
        elif mnemonic == 'BEQ':
            rs1, rs2 = int(args[0][1:]), int(args[1][1:])
            offset = int(args[2])
            enc = encode_i_type(OPCODES['BEQ'], rs2, rs1, offset)
            program.append({'op': 'BEQ', 'rs1': rs1, 'rs2': rs2, 'imm': offset, 'encoded': enc})
    return program

def simulate(program, registers, memory, max_steps=20):
    registers[0] = 0  # r0 সবসময় ০ -- L26-এর হার্ডওয়্যার্ড-জিরো নিয়ম
    pc, steps, trace = 0, 0, []
    while 0 <= pc < len(program) and steps < max_steps:
        ins = program[pc]
        next_pc = pc + 1
        if ins['op'] == 'ADD':
            result = registers[ins['rs1']] + registers[ins['rs2']]
            if ins['rd'] != 0:
                registers[ins['rd']] = result
            trace.append(f"PC={pc}: ADD  r{ins['rd']} = r{ins['rs1']}+r{ins['rs2']} = {result}")
        elif ins['op'] == 'ADDI':
            result = registers[ins['rs1']] + ins['imm']
            if ins['rd'] != 0:
                registers[ins['rd']] = result
            trace.append(f"PC={pc}: ADDI r{ins['rd']} = r{ins['rs1']}+{ins['imm']} = {result}")
        elif ins['op'] == 'BEQ':
            equal = registers[ins['rs1']] == registers[ins['rs2']]
            if equal:
                next_pc = pc + 1 + ins['imm']
            trace.append(
                f"PC={pc}: BEQ  r{ins['rs1']}({registers[ins['rs1']]}) == "
                f"r{ins['rs2']}({registers[ins['rs2']]}) ? {equal} -> PC হবে {next_pc}"
            )
        pc = next_pc
        steps += 1
    return trace

asm_lines = [
    "BEQ r1, r2, 1",
    "BEQ r0, r0, 1",
    "ADDI r3, r1, 1",
]
program = assemble(asm_lines)
print("এনকোড হওয়া বাইনারি:", [ins['encoded'] for ins in program])

print("\n--- কেস ১: a == b (a=5, b=5) ---")
regs1 = [0, 5, 5, 999, 0, 0, 0, 0]   # r3 আগে থেকেই 999 (সেন্টিনেল) -- বদলালো কিনা দেখার জন্য
for line in simulate(program, regs1, {}):
    print(" ", line)
print(f"ফলাফল: c = r3 = {regs1[3]}  (প্রত্যাশিত: a+1 = 6)")

print("\n--- কেস ২: a != b (a=5, b=3) ---")
regs2 = [0, 5, 3, 999, 0, 0, 0, 0]
for line in simulate(program, regs2, {}):
    print(" ", line)
print(f"ফলাফল: c = r3 = {regs2[3]}  (প্রত্যাশিত: অপরিবর্তিত, 999 -- ADDI এড়িয়ে গেছে)")

assert regs1[3] == 6, "কেস ১ ভুল!"
assert regs2[3] == 999, "কেস ২ ভুল -- ব্রাঞ্চ ঠিকমতো স্কিপ করেনি!"
print("\nদুটো কেসই সঠিক -- BEQ ব্রাঞ্চ সঠিকভাবে এক্সিকিউট/স্কিপ করছে।")

    
লক্ষ করুন কেস ২-তে r3 সেন্টিনেল মান 999-ই থেকে যায় — এটিই প্রমাণ করে যে ADDI r3, r1, 1 ইনস্ট্রাকশনটি আসলেই এক্সিকিউট হয়নি, স্রেফ "ভুল উত্তর হয়নি" এমন না — ব্রাঞ্চটি সত্যিই instr 2-কে এড়িয়ে গেছে তা কোড-স্তরে যাচাই করা হলো।
মূল কথা · Key takeaway

অ্যাসেম্বলি ল্যাঙ্গুয়েজ ও অ্যাসেম্বলার হলো L24-এর বাইনারি এনকোডিং-এর মানুষ-বান্ধব মুখ। একটি সরল ইনস্ট্রাকশন সেট (এমনকি মাত্র একটি কন্ডিশনাল ব্রাঞ্চ, BEQ) দিয়েই — হার্ডওয়্যার্ড-জিরো রেজিস্টারের মতো চতুর কৌশল ব্যবহার করে — সম্পূর্ণ কন্ডিশনাল লজিক বানানো সম্ভব। M6-এ এই একই ইনস্ট্রাকশনগুলো এবার একটি বাস্তব ডেটাপাথের ভেতর দিয়ে কীভাবে প্রবাহিত হয় তা দেখবেন।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ এই ইনস্ট্রাকশন সেটে শুধু BEQ (branch if equal) আছে, কিন্তু "branch if not equal" নেই কেন সমস্যা হলো না?

কারণ r0 সবসময় ০ পড়ে (L26), তাই BEQ r0, r0, offset সবসময় সত্য — এটিকে "unconditional jump" হিসেবে ব্যবহার করা যায়। এই দুটো প্রিমিটিভ (conditional BEQ + unconditional BEQ-via-r0) মিলিয়েই যেকোনো if/else যুক্তি বানানো সম্ভব — বাস্তব RISC ISA-গুলোও (যেমন RISC-V) ঠিক এই কৌশল ব্যবহার করে, আলাদা কোনো "JUMP" ইনস্ট্রাকশন ছাড়াই।

প্র ০২ উপরের প্রোগ্রামে যদি "BEQ r0, r0, 1" (instr 1) লাইনটি বাদ দেওয়া হতো, তাহলে a != b কেসে কী ভুল হতো?

instr 1 বাদ দিলে, a != b কেসে instr 0-এর BEQ মিথ্যা হয়ে সরাসরি instr 1-এ (এখন যেটা ADDI) চলে যেত — অর্থাৎ c = a+1 ভুলভাবেই এক্সিকিউট হয়ে যেত, যদিও a != b। instr 1-এর unconditional-skip লাইনটিই আসল "না-হলে এড়িয়ে যাও" যুক্তিটি বাস্তবায়ন করে — এটি বাদ দিলে if-condition-এর পুরো অর্থই হারিয়ে যায়।

প্র ০৩ assemble() ফাংশনটি সরাসরি একটি dict তৈরি না করে কেন encoded ফিল্ডে একটি বাস্তব বাইনারি সংখ্যাও রাখল?

কারণ এটিই দেখায় যে অ্যাসেম্বলার শুধু "বোধগম্য গঠন" তৈরি করে না — এটি প্রকৃতপক্ষে সেই একই বাইনারি বানায় যা বাস্তব হার্ডওয়্যার ইনস্ট্রাকশন মেমরিতে সংরক্ষণ করত (L24-এর encode_r_type/encode_i_type-এর সরাসরি পুনরায় ব্যবহার)। dict অংশটি এই পাঠের সিমুলেটরের সুবিধার জন্য (দ্রুত এক্সিকিউট করার জন্য), কিন্তু encoded সংখ্যাটিই প্রমাণ করে যে এটি নিছক কল্পনা নয়, প্রকৃত এনকোডিং প্রক্রিয়া।

অনুশীলন

  1. চিন্তা করুন: STORE rs2, offset(rs1) ইনস্ট্রাকশনের এনকোডিং-এ rs2-কে "rd" ফিল্ডে রাখা হয়েছে (যদিও STORE আসলে কোনো রেজিস্টারে লেখে না)। এই ফিল্ড-পুনর্ব্যবহার কৌশলটি কেন ব্যবহারিক?

    এতে সব ইনস্ট্রাকশনের এনকোডিং ফরম্যাট এক থাকে (opcode|field2|field3|field4) — হার্ডওয়্যারের ডিকোডার সার্কিট (L11) তখন প্রতিটি ইনস্ট্রাকশনের জন্য একই ফিক্সড বিট-পজিশন থেকে ফিল্ড বের করতে পারে, opcode অনুযায়ী আলাদা লজিক লাগে না — এটিই RISC-স্টাইল ইনস্ট্রাকশন এনকোডিং-এর একটি বাস্তব সরলীকরণ কৌশল (বাস্তব RISC-V-এর SB-format ঠিক এই একই কৌশল ব্যবহার করে)।

  2. পরীক্ষা করুন: কোড সেলে asm_lines-এ "LOAD r4, 2(r1)" একটি নতুন লাইন যোগ করার কথা ভাবুন (এখনো কোড পরিবর্তন করবেন না) — এটি সঠিকভাবে চলতে memory dict-এ কী থাকা দরকার?

    এই LOAD ইনস্ট্রাকশনটি ঠিকানা r1 + 2-এ থাকা মান পড়বে। কেস ১-এ r1=5, তাই এটি ঠিকানা ৭-এ পড়বে — memory = {7: কোনো মান} না থাকলে memory.get(addr, 0)-এর ডিফল্ট মান হিসেবে ০ পড়বে (যেভাবে এই পাঠের simulate() ইতিমধ্যে সব ক্ষেত্রে ডিফল্ট-শূন্য হ্যান্ডলিং ব্যবহার করে LOAD-এর সাধারণ প্যাটার্ন অনুসরণ করে)।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
রেজিস্টার ফাইল ও CPU রেজিস্টার