Foundationপ্রথম নীতি থেকে
LEVEL 3অ্যাডভান্সড~১৪ ঘণ্টাPython

টয় অ্যাসেম্বলার

Toy Assembler

RV32I-র জন্য একটা two-pass অ্যাসেম্বলার — টেক্সট মনেমনিক পড়ে সিম্বল টেবিল বানানো, তারপর প্রতিটা ইন্সট্রাকশনকে বিট-বাই-বিট এনকোড করে বাইনারি মেশিন কোড বানানো, আর CPU Emulator প্রজেক্টের এমুলেটরেই সেই কোড চালিয়ে verify করা।

মাইলস্টোন

আগে যা পড়া দরকার

কেন এই প্রজেক্ট

Assembler/linker/loader লেসনে আমরা দেখেছি একটা .s ফাইল কীভাবে .o ফাইলে রূপান্তরিত হয় — টেক্সট মনেমনিক (add, beq) কীভাবে বাইনারি বিট-প্যাটার্নে পরিণত হয়। Instruction encoding লেসনে দেখেছি সেই বিট-প্যাটার্নগুলোর গঠন — কোন বিট opcode, কোন বিট রেজিস্টার নম্বর।

CPU Emulator প্রজেক্টে আমরা এই দিকটা উল্টো দিক থেকে করেছিলাম — বাইনারি মেশিন কোড পড়ে টেক্সট অর্থ (কোন রেজিস্টার, কোন অপারেশন) বের করা, তারপর সেটা চালানো। এই প্রজেক্টে ঠিক তার আয়না-প্রতিবিম্ব — টেক্সট অ্যাসেম্বলি পড়ে বাইনারি মেশিন কোড বানানো

দুটো দিকই একসাথে বানালে একটা জিনিস স্পষ্ট হয়ে যায় যেটা যেকোনো একটা দিক থেকে বোঝা কঠিন — অ্যাসেম্বলার আর এমুলেটর আসলে একই স্পেসিফিকেশনের (opcode টেবিল, বিট-লেআউট) দুটো স্বাধীন বাস্তবায়ন, আর তারা একে অপরকে verify করে: অ্যাসেম্বলারের আউটপুট যদি এমুলেটরে সঠিক আচরণ দেয়, তাহলে দুটো বাস্তবায়নই স্পেসিফিকেশন সঠিকভাবে মেনেছে বলে জোরালো প্রমাণ পাওয়া যায়।

কোন ISA ব্যবহার করব, আর কেন

এই প্ল্যাটফর্মে দুটো instruction encoding ইতিমধ্যে সংজ্ঞায়িত আছে — Digital Logic module-এর Single-cycle CPU-র নিজের-বানানো mini-ISA, আর Computer Architecture module-এর CPU Emulator প্রজেক্টের RISC-V RV32I। এই প্রজেক্টে RV32I-ই ব্যবহার করব, নতুন কোনো ISA ডিজাইন না করে — তিনটা কারণে:

  1. একটা matched, verifiable pair পাওয়া যায় — এই অ্যাসেম্বলারের আউটপুট সরাসরি CPU Emulator প্রজেক্টের এমুলেটরে চালিয়ে যাচাই করা যায়, কারণ দুটোই একই বিট-লেআউট মেনে চলছে। নতুন ISA বানালে নিজের-লেখা এমুলেটরও নতুন করে বানাতে হতো — যাচাইয়ের দ্বিতীয় স্বাধীন উৎস হারিয়ে যেত।
  2. বিট-লেআউট ইতিমধ্যে স্পেসিফাই করা, প্রমাণিত — R/I/S/B/J ফরম্যাট, sign-extension সূত্র, opcode/funct3/funct7 টেবিল — সব CPU Emulator প্রজেক্টে বিস্তারিত আছে, এখানে পুনরাবৃত্তি না করে সরাসরি রেফার করা যায়।
  3. বাস্তব টুলচেইনের সাথে তুলনা করা যায়riscv64-unknown-elf-as/objdump দিয়ে একই সোর্স অ্যাসেম্বল করে output হুবহু মেলানো সম্ভব, যা কাল্পনিক ISA দিয়ে করা যেত না।

নিচে যে চারটা instruction format দেখানো হচ্ছে (R, I, S, B) সেগুলোর বিট-লেআউট হুবহু CPU Emulator প্রজেক্টের টেবিলের সাথে মেলে — এখানে সংক্ষেপে আবার লেখা হলো যাতে এই প্রজেক্ট স্বয়ংসম্পূর্ণভাবে পড়া যায়:

R-type:  funct7[31:25] rs2[24:20] rs1[19:15] funct3[14:12] rd[11:7]  opcode[6:0]
I-type:  imm[31:20]               rs1[19:15] funct3[14:12] rd[11:7]  opcode[6:0]
S-type:  imm[31:25]    rs2[24:20] rs1[19:15] funct3[14:12] imm[11:7] opcode[6:0]
B-type:  b12,b10-5[31:25] rs2[24:20] rs1[19:15] funct3[14:12] b4-1,b11[11:7] opcode[6:0]
opcodefunct3funct7ইন্সট্রাকশন
01100110000000000ADD
01100110000100000SUB
0010011000ADDI
0000011010LW
0100011010SW
1100011001BNE

আমাদের অ্যাসেম্বলার এই ৬টা ইন্সট্রাকশন সাপোর্ট করলেই R-type, I-type, লোড/স্টোর, আর label-ভিত্তিক branch — চারটা category-ই কভার হয়ে যায়।

উদাহরণ — প্রত্যাশিত ব্যবহার

$ cat sum.asm
        addi x5, x0, 0        # sum = 0
        addi x6, x0, 1        # i = 1
        addi x7, x0, 6        # limit = 6
loop:   add  x5, x5, x6       # sum += i
        addi x6, x6, 1        # i += 1
        bne  x6, x7, loop     # লেবেলের ঠিকানা সিম্বল টেবিল থেকে আসছে
        sw   x5, 100(x0)

$ python3 toy_assembler.py sum.asm sum.bin
Pass 1: 7টা ইন্সট্রাকশন, সিম্বল টেবিল -- loop -> 12
Pass 2: 7টা ইন্সট্রাকশন এনকোড হলো
লেখা হলো sum.bin (28 বাইট)

প্রতিটা শব্দ (little-endian) হাতে-গণনা করে যাচাই করা যায় — নিচে ধাপ ৯-এ দেখানো পদ্ধতিতেই:

ঠিকানাইন্সট্রাকশনএনকোডেড শব্দ (hex)ফাইলে বাইট (little-endian)
0x00addi x5, x0, 00x0000029393 02 00 00
0x04addi x6, x0, 10x0010031313 03 10 00
0x08addi x7, x0, 60x0060039393 03 60 00
0x0Cadd x5, x5, x60x006282B3B3 82 62 00
0x10addi x6, x6, 10x0013031313 03 13 00
0x14bne x6, x7, loop (offset −8)0xFE731CE3E3 1C 73 FE
0x18sw x5, 100(x0)0x0650222323 22 50 06

লক্ষ্য করুন bne x6, x7, loop-এ loop লেবেলটা আগে সংজ্ঞায়িত (উপরের দিকে) — এটা একটা backward branch, যেটা CPU Emulator প্রজেক্টের “sum of 1..5” প্রোগ্রামের সাথে সরাসরি তুলনা করা যায় (উপরের টেবিলের প্রথম ৭টা শব্দ ঐ প্রজেক্টের হাতে-এনকোড করা টেবিলের সাথে বিট-বাই-বিট মেলে)। নিচে ধাপ ৬-এ একটা forward reference (এখনো-সংজ্ঞায়িত-হয়নি এমন লেবেলের দিকে jump) দেখানো হবে, যেখানে two-pass পদ্ধতির আসল প্রয়োজনীয়তা স্পষ্ট হয়।

ধাপে ধাপে

১. কেন দুই-পাস — সমস্যাটা প্রথমে দেখা

এক-পাসে অ্যাসেম্বল করার চেষ্টা করলে কী ভাঙে সেটা দেখা যাক। ধরা যাক প্রোগ্রামটা এরকম:

        beq  x0, x0, skip     # সবসময় লাফ দাও (x0 == x0 সবসময় সত্য)
        addi x5, x0, 99       # এই লাইন কখনো চলবে না
skip:   addi x6, x0, 1

beq x0, x0, skip এনকোড করার সময় দরকার skip-এর ঠিকানা থেকে branch instruction-এর নিজের ঠিকানা বিয়োগ করে offset বের করা (imm = addr(skip) - addr(beq))। কিন্তু যখন অ্যাসেম্বলার প্রথম লাইনটা পড়ছে, তখনো skip লেবেলটা দেখাই হয়নি — সেটা তৃতীয় লাইনে আসবে। একটা মাত্র পাসে সিদ্ধান্ত নেওয়ার সময় skip-এর ঠিকানা অজানা।

সমাধান — কাজটা দুই ভাগে ভাগ করা:

  • Pass ১ (সিম্বল টেবিল বানানো): পুরো ফাইল একবার পড়ে, কোনো এনকোডিং না করে, শুধু প্রতিটা লেবেলের ঠিকানা রেকর্ড করা (ইন্সট্রাকশন গুনে ঠিকানা এগিয়ে)।
  • Pass ২ (এনকোড করা): ফাইল আবার পড়ে, এবার প্রতিটা ইন্সট্রাকশন এনকোড করা — কোনো লেবেল রেফারেন্স পেলে Pass ১-এ বানানো সিম্বল টেবিল থেকে ঠিকানা খুঁজে নেওয়া, যেটা এতক্ষণে সম্পূর্ণ

Pass ১ শেষ হওয়ার আগ পর্যন্ত forward reference resolve করা অসম্ভব — এটাই দুই-পাস পদ্ধতির মূল যুক্তি।

২. Tokenizer — একটা লাইনকে টুকরো করা

def tokenize_line(line):
    line = line.split('#', 1)[0].strip()   # কমেন্ট বাদ
    if not line:
        return None

    label = None
    if ':' in line:
        label, line = line.split(':', 1)
        label = label.strip()
        line = line.strip()

    if not line:
        return {'label': label, 'mnemonic': None, 'operands': []}

    parts = line.replace(',', ' ').split()
    mnemonic = parts[0].lower()
    operands = parts[1:]
    return {'label': label, 'mnemonic': mnemonic, 'operands': operands}

এই ফাংশন একটা ডিকশনারি ফেরত দেয় — label (থাকলে), mnemonic, আর operands-এর লিস্ট। loop: add x5, x5, x6 লাইনটা tokenize করলে পাওয়া যায় label 'loop', mnemonic 'add', আর operands ['x5', 'x5', 'x6']। কমেন্ট (#-এর পরের অংশ) আর খালি লাইন — দুটোই এখানে সাফ হয়ে যায়, বাকি পুরো অ্যাসেম্বলারকে সেসব নিয়ে ভাবতে হয় না।

৩. Pass ১ — সিম্বল টেবিল

def pass_one(lines):
    symtab = {}
    addr = 0
    instructions = []   # (addr, label, mnemonic, operands)

    for raw in lines:
        tok = tokenize_line(raw)
        if tok is None:
            continue
        if tok['label'] is not None:
            if tok['label'] in symtab:
                raise ValueError(f"লেবেল '{tok['label']}' দুইবার সংজ্ঞায়িত")
            symtab[tok['label']] = addr
        if tok['mnemonic'] is not None:
            instructions.append((addr, tok['mnemonic'], tok['operands']))
            addr += 4   # প্রতিটা RV32I ইন্সট্রাকশন ৪ বাইট, fixed-width
    return symtab, instructions

addr শুধু তখনই এগোয় যখন সত্যিকারের একটা ইন্সট্রাকশন থাকে (শুধু-লেবেল-সহ লাইনে না, যেমন skip: একা একটা লাইনে থাকলে)। RV32I-র প্রতিটা ইন্সট্রাকশন ঠিক ৪ বাইট — এই fixed-width ধর্মটাই ঠিকানা গণনা এত সহজ করে দেয়; variable-length ISA-তে (যেমন x86) Pass ১-এ প্রতিটা ইন্সট্রাকশনের দৈর্ঘ্যও এনকোড করে বের করতে হতো।

৪. রেজিস্টার পার্সিং আর এনকোডার হেল্পার

def parse_reg(s):
    if not s.startswith('x'):
        raise ValueError(f"রেজিস্টার প্রত্যাশিত, পেলাম '{s}'")
    n = int(s[1:])
    if not (0 <= n <= 31):
        raise ValueError(f"রেজিস্টার x{n} রেঞ্জের বাইরে (0-31)")
    return n

def encode_r(opcode, funct3, funct7, rd, rs1, rs2):
    return (funct7 \<\< 25) | (rs2 \<\< 20) | (rs1 \<\< 15) | (funct3 \<\< 12) | (rd \<\< 7) | opcode

def encode_i(opcode, funct3, rd, rs1, imm):
    imm12 = imm & 0xFFF
    return (imm12 \<\< 20) | (rs1 \<\< 15) | (funct3 \<\< 12) | (rd \<\< 7) | opcode

def encode_s(opcode, funct3, rs1, rs2, imm):
    imm12 = imm & 0xFFF
    hi = (imm12 \>\> 5) & 0x7F     # imm[11:5]
    lo = imm12 & 0x1F             # imm[4:0]
    return (hi \<\< 25) | (rs2 \<\< 20) | (rs1 \<\< 15) | (funct3 \<\< 12) | (lo \<\< 7) | opcode

def encode_b(opcode, funct3, rs1, rs2, imm):
    imm13 = imm & 0x1FFF
    b12   = (imm13 \>\> 12) & 1
    b11   = (imm13 \>\> 11) & 1
    b10_5 = (imm13 \>\> 5)  & 0x3F
    b4_1  = (imm13 \>\> 1)  & 0xF
    return (b12 \<\< 31) | (b10_5 \<\< 25) | (rs2 \<\< 20) | (rs1 \<\< 15) | \
           (funct3 \<\< 12) | (b4_1 \<\< 8) | (b11 \<\< 7) | opcode

এই এনকোডারগুলো CPU Emulator প্রজেক্টের imm_i/imm_s/imm_b ডিকোডারগুলোর ঠিক বিপরীত কাজ করে — ডিকোডার বিট থেকে সংখ্যা বের করে, এনকোডার সংখ্যা থেকে বিট বসায়। encode_b-তে imm13 & 0x1FFF করে negative offset-কেও ঠিকভাবে ধরা হচ্ছে, কারণ Python-এ negative integer-এর bitwise AND করলে two’s complement-এর মতোই আচরণ করে (-8 & 0x1FFF স্বয়ংক্রিয়ভাবে সঠিক ১৩-বিট প্যাটার্ন দেয়)।

৫. Pass ২ — এনকোড করা, লেবেল রেজলভ করা

def pass_two(symtab, instructions):
    machine_code = []
    for addr, mnemonic, ops in instructions:
        if mnemonic == 'add':
            rd, rs1, rs2 = parse_reg(ops[0]), parse_reg(ops[1]), parse_reg(ops[2])
            word = encode_r(0b0110011, 0b000, 0b0000000, rd, rs1, rs2)
        elif mnemonic == 'sub':
            rd, rs1, rs2 = parse_reg(ops[0]), parse_reg(ops[1]), parse_reg(ops[2])
            word = encode_r(0b0110011, 0b000, 0b0100000, rd, rs1, rs2)
        elif mnemonic == 'addi':
            rd, rs1, imm = parse_reg(ops[0]), parse_reg(ops[1]), int(ops[2])
            word = encode_i(0b0010011, 0b000, rd, rs1, imm)
        elif mnemonic == 'lw':
            rd, offset, rs1 = parse_reg(ops[0]), *parse_mem_operand(ops[1])
            word = encode_i(0b0000011, 0b010, rd, rs1, offset)
        elif mnemonic == 'sw':
            rs2, offset, rs1 = parse_reg(ops[0]), *parse_mem_operand(ops[1])
            word = encode_s(0b0100011, 0b010, rs1, rs2, offset)
        elif mnemonic == 'bne':
            rs1, rs2 = parse_reg(ops[0]), parse_reg(ops[1])
            target_label = ops[2]
            if target_label not in symtab:
                raise ValueError(f"অচেনা লেবেল '{target_label}'")
            imm = symtab[target_label] - addr        # ← Pass ১-এর সিম্বল টেবিল ব্যবহার হচ্ছে এখানে
            word = encode_b(0b1100011, 0b001, rs1, rs2, imm)
        else:
            raise ValueError(f"অচেনা mnemonic '{mnemonic}'")
        machine_code.append(word)
    return machine_code


def parse_mem_operand(s):
    # "100(x0)" ফরম্যাট পার্স করা → (offset, base_register)
    offset_str, reg_str = s.split('(')
    reg_str = reg_str.rstrip(')')
    return int(offset_str), parse_reg(reg_str)

bne-র শাখাতেই দুই-পাস পদ্ধতির পুরো লাভটা দেখা যায়: symtab[target_label] - addr — যদি target_label ফাইলে পরে সংজ্ঞায়িত হয় (forward reference), তাও এই লাইনটা কাজ করবে, কারণ symtab ইতিমধ্যে Pass ১-এ সম্পূর্ণ বানানো হয়ে গেছে। Pass ২ শুরুর আগেই পুরো ফাইলের সব লেবেল জানা।

৬. Forward reference-এর concrete উদাহরণ

এটাই সেই কেস যেখানে one-pass অ্যাসেম্বলার ভেঙে পড়ত। এই অ্যাসেম্বলারের সাপোর্টেড instruction set-এ conditional branch হিসেবে শুধু bne আছে (beq নেই, “নিজেকে চ্যালেঞ্জ করুন”-এ যোগ করার জন্য রাখা হয়েছে), তাই forward reference দেখানো হচ্ছে একটা bne দিয়ে — লুপে ঢোকার আগেই লুপের লেবেলের দিকে (যেটা এখনো নিচে সংজ্ঞায়িতই হয়নি) branch করে একটা “guard” ইন্সট্রাকশন এড়িয়ে যাওয়া:

        addi x5, x0, 0        # 0x00: i = 0
        addi x6, x0, 5        # 0x04: n = 5
        bne  x6, x0, loop     # 0x08: forward reference — 'loop' এখনো নিচে সংজ্ঞায়িতই হয়নি!
        addi x5, x0, 999      # 0x0C: n == 0 হলেই শুধু এই sentinel লাইনটা চলে (fallthrough)
loop:   addi x5, x5, 1        # 0x10
        addi x6, x6, -1       # 0x14
        bne  x6, x0, loop     # 0x18: backward reference — 'loop' ইতিমধ্যে জানা
done:   sw   x5, 0(x0)        # 0x1C

Pass ১ শেষে সিম্বল টেবিলে loop16 আর done28 জমা থাকে। Pass ২ যখন ঠিকানা 0x08-এর bne x6, x0, loop এনকোড করছে, তখন symtab['loop'] থেকে সরাসরি 16 পেয়ে যাচ্ছে — যদিও ফাইলে loop: লাইনটা এখনো নিচে, এখনো “পড়া হয়নি” এই ইন্সট্রাকশনের দৃষ্টিকোণ থেকে। imm = 16 - 8 = 8 (দুই ইন্সট্রাকশন সামনে, ঠিক sentinel লাইনটা টপকে)। দ্বিতীয় bne-টা (ঠিকানা 0x18) একই লেবেলের দিকেই যাচ্ছে কিন্তু এবার পিছনে — imm = 16 - 24 = -8। দুটো ক্ষেত্রেই resolve হচ্ছে একই পদ্ধতিতে, symtab থেকে সরাসরি লুকআপ করে — এটাই প্রমাণ করে সিম্বল টেবিল একবার সম্পূর্ণ হয়ে গেলে forward আর backward reference-এর মধ্যে অ্যাসেম্বলারের কাছে আর কোনো পার্থক্যই থাকে না।

৭. পুরোটা জোড়া লাগানো

def assemble(source_lines):
    symtab, instructions = pass_one(source_lines)
    print(f"Pass 1: {len(instructions)}টা ইন্সট্রাকশন, সিম্বল টেবিল = {symtab}")
    machine_code = pass_two(symtab, instructions)
    print(f"Pass 2: {len(machine_code)}টা ইন্সট্রাকশন এনকোড হলো")
    return machine_code


def write_binary(machine_code, path):
    with open(path, 'wb') as f:
        for word in machine_code:
            f.write(word.to_bytes(4, byteorder='little', signed=False))


if __name__ == '__main__':
    import sys
    with open(sys.argv[1]) as f:
        lines = f.readlines()
    code = assemble(lines)
    write_binary(code, sys.argv[2])
    print(f"লেখা হলো {sys.argv[2]} ({len(code) * 4} বাইট)")

word.to_bytes(4, byteorder='little', ...) — RV32I little-endian, তাই প্রতিটা ৩২-বিট শব্দের সবচেয়ে কম গুরুত্বপূর্ণ বাইট প্রথমে লেখা হচ্ছে, ঠিক CPU Emulator প্রজেক্টের fetch_word/store_word-এর প্রত্যাশা অনুযায়ী।

৮. হাতে-গণনা করে যাচাই

addi x5, x0, 0 এনকোড করলে কী আসা উচিত সেটা হাতে গুনে দেখা যাক:

opcode = 0b0010011 (I-type ADDI)
funct3 = 0b000
rd     = 5   (x5)
rs1    = 0   (x0)
imm    = 0

word = (0 \<\< 20) | (0 \<\< 15) | (0b000 \<\< 12) | (5 \<\< 7) | 0b0010011
     = 0x00000000 | 0x00000000 | 0x00000000 | 0x00000280 | 0x00000013
     = 0x00000293

toy_assembler.py-র আউটপুট বাইনারিতে প্রথম ৪ বাইট little-endian-এ 93 02 00 00 হওয়া উচিত — যা little-endian থেকে ফেরত পড়লে 0x00000293। এই একই মান CPU Emulator প্রজেক্টের হাতে-এনকোড করা টেবিলেও আছে (addi x5, x0, 00x00000293) — দুটো স্বাধীন সোর্স (এই প্রজেক্টের এনকোডার, ঐ প্রজেক্টের হাতে-গণনা) একমত হওয়াটাই প্রমাণ যে এনকোডিং সঠিক।

৯. এমুলেটরে চালিয়ে end-to-end verify

CPU Emulator প্রজেক্টের CPU ক্লাস পুনর্ব্যবহার করে:

with open('sum.bin', 'rb') as f:
    program = f.read()

cpu = CPU(mem_size=4096)
cpu.mem[0:len(program)] = program

num_instructions = len(program) // 4
for _ in range(num_instructions):
    cpu.step()

print(f"x5 (sum) = {cpu.regs[5]}")   # প্রত্যাশিত: 15
print(f"mem[100] = {int.from_bytes(cpu.mem[100:104], 'little')}")

লক্ষ্য করুন এখানে cpu.run() না ডেকে cpu.step() ঠিক num_instructions বার কল করা হয়েছে — cpu.run() থামে শুধু self.halted সত্যি হলে, আর সেটা সত্যি হয় শুধু jal x0, 0 (self-jump) ইন্সট্রাকশনে (CPU Emulator প্রজেক্টের halt কনভেনশন)। আমাদের toy assembler-এ jal এখনো নেই (এটা “নিজেকে চ্যালেঞ্জ করুন”-এর একটা আইটেম), তাই আমাদের sum.bin-এ কোনো halt ইন্সট্রাকশন নেই — cpu.run() ডাকলে ৭টা বৈধ ইন্সট্রাকশনের পর প্রোগ্রামের বাইরের (শূন্যে-ভরা) মেমরি থেকে fetch করে একটা অচেনা opcode=0000000 পেয়ে emulator ব্যতিক্রম (exception) ছুঁড়ে দেবে। ঠিক কতগুলো ইন্সট্রাকশন চালাতে হবে সেটা জেনে-বুঝে সীমিত সংখ্যক step() কল করাই এখানে নিরাপদ উপায়।

যদি x5 == 15 আসে (1+2+3+4+5), তাহলে এই প্রজেক্টের অ্যাসেম্বলার আর CPU Emulator প্রজেক্টের এমুলেটর — দুটোই একে অপরকে সফলভাবে verify করেছে।

নিজেকে চ্যালেঞ্জ করুন

  1. আরও ইন্সট্রাকশন যোগ করুনand, or, xor, slt, jal — CPU Emulator প্রজেক্টের opcode টেবিল থেকে সরাসরি এনকোডিং নিয়ে
  2. Pseudo-instruction সাপোর্ট করুনli rd, imm (load immediate) একটা addi rd, x0, imm-এ expand করে দিন — বাস্তব অ্যাসেম্বলারগুলো ঠিক এভাবেই সুবিধাজনক শর্টকাট দেয়
  3. এরর মেসেজে লাইন নম্বর যোগ করুন — এখন এরর হলে কোন লাইনে সমস্যা তা বলা হয় না; tokenizer-এ লাইন নম্বর ট্র্যাক করে এরর মেসেজে যোগ করুন
  4. .data সেকশন সাপোর্ট করুন — সংখ্যা/স্ট্রিং লিটারেল সরাসরি মেমরিতে রাখার একটা directive বানান, আর সিম্বল টেবিলে সেই ডেটার ঠিকানাও রাখুন
  5. আসল টুলচেইনের সাথে তুলনা করুন — একই .asm ফাইল riscv64-unknown-elf-as দিয়ে অ্যাসেম্বল করে objdump -d দিয়ে দেখুন, আপনার এনকোডার একই hex দিচ্ছে কি না

এটা যেখানে গিয়ে মিশবে

এখানে যা শিখলেনপরে কোথায় লাগবে
Two-pass সিম্বল রেজোলিউশনLevel 5 — Compilers module-এর symbol table ও linking পর্ব
Text → বিট এনকোডিং (ডিকোডিংয়ের উল্টো)এই মডিউলেরই বাকি লেসন, ও Compiler Output Autopsy প্রজেক্টে compiler-generated encoding পড়া
Forward reference resolve করাLevel 5 — multi-pass compiler design, forward-declared function/type
Fixed-width ইন্সট্রাকশন ও ঠিকানা গণনাLevel 4 — OS module-এর ELF loader, program loading
Matched encoder/decoder pair দিয়ে verificationLevel 10 — Security module-এর disassembler/exploit-encoding কাজ