CPU Emulator
CPU Emulator
RISC-V RV32I-এর একটা সফটওয়্যার এমুলেটর — সত্যিকারের ৩২-বিট মেশিন কোড পড়ে, ফিল্ড ডিকোড করে, রেজিস্টার ফাইল ও ফ্ল্যাট মেমরির উপর fetch-decode-execute লুপ চালিয়ে — শেষে হাতে-অ্যাসেম্বল করা একটা লুপ-প্রোগ্রাম দিয়ে হাতে-গণনার সাথে মিলিয়ে verify করা।
কেন এই প্রজেক্ট
Fetch-decode-execute লেসনে আমরা দেখেছি একটা CPU কীভাবে মেমরি থেকে একটা ইন্সট্রাকশন তুলে, তার বিট প্যাটার্ন পড়ে বুঝে নেয় “কী করতে হবে”, তারপর সেটা করে। Instruction encoding লেসনে দেখেছি সেই বিট প্যাটার্নগুলো কীভাবে সাজানো — কোন বিট opcode, কোন বিট রেজিস্টার নম্বর, কোন বিট immediate।
এই প্রজেক্টে দুটোই একসাথে জোড়া লাগবে, আর এবার কোনো toy বা ঘরে-বানানো ISA না — সরাসরি RISC-V RV32I, একটা বাস্তব, ব্যাপকভাবে ব্যবহৃত ইন্সট্রাকশন সেট আর্কিটেকচার। আপনি যে মেশিন কোড ডিকোড করবেন, ঠিক একই মেশিন কোড একটা বাস্তব RISC-V চিপেও চলে। এটাই এই প্রজেক্টের আসল শিক্ষা — ISA স্পেসিফিকেশন একটা প্রোজে ছাপা কাগজ না, এটা একটা precise, বাস্তবায়নযোগ্য চুক্তি। আপনি সেই চুক্তিটা পড়ে হুবহু মেনে একটা সফটওয়্যার মেশিন বানাবেন, আর সেটা যাচাই করার একমাত্র উপায় হলো — এটা প্রত্যাশিত ফলাফল দেয় কি না।
RV32I ও এই এমুলেটরের সীমা
RV32I পূর্ণাঙ্গ ISA-তে ৪০+ ইন্সট্রাকশন আছে (multiply/divide extension বাদেই)। এই প্রজেক্টে আমরা একটা কার্যকরী core subset বানাব যেটা দিয়ে ইতিমধ্যে লুপ, শর্ত, ফাংশন-কল — সবকিছু লেখা সম্ভব:
| গ্রুপ | ইন্সট্রাকশন | ফরম্যাট |
|---|---|---|
| Arithmetic/logic (রেজিস্টার-রেজিস্টার) | ADD, SUB, AND, OR, XOR, SLT | R-type |
| Arithmetic (রেজিস্টার-immediate) | ADDI | I-type |
| Memory | LW, SW | I-type (load), S-type (store) |
| Branch | BEQ, BNE | B-type |
| Jump | JAL, JALR | J-type, I-type |
U-type (LUI/AUIPC) ফরম্যাটের বিট-লেআউটও নিচে দেখানো হলো, কারণ ডিকোডার-এর সম্পূর্ণ ছবি বুঝতে ওটাও দরকার — কিন্তু ন্যূনতম execute-তালিকায় কোনো U-type ইন্সট্রাকশন নেই, তাই LUI বাস্তবায়ন নিজেকে চ্যালেঞ্জ করুন-এ রাখা হয়েছে।
রেজিস্টার ফাইল — x0-এর কোয়ার্ক
RV32I-তে ৩২টা general-purpose রেজিস্টার (x0–x31), প্রতিটা ৩২-বিট। একটা নিয়ম মুখস্থ রাখার মতো গুরুত্বপূর্ণ:
x0সবসময় শূন্য। এতে লেখা যায়, কিন্তু লেখাটা কোনো প্রভাব ফেলে না — পড়লে সবসময়0ফেরত আসে।
এটা কোনো accident না, ইচ্ছাকৃত ডিজাইন। এর সুবিধা:
ADDI x5, x0, 7দিয়েx5-এ constant7বসানো যায় — আলাদা কোনো “load-immediate” ইন্সট্রাকশন লাগে না।BEQ x5, x0, targetদিয়ে সরাসরি “x5শূন্য কি না” পরীক্ষা করা যায়।- Compiler-এর জন্য একটা result-discard করার জায়গা পাওয়া যায় (
ADD x0, x5, x6মানে “যোগ করো, কিন্তু ফলাফল ফেলে দাও”)।
আমাদের এমুলেটরে এটা বাস্তবায়ন করার সবচেয়ে সহজ উপায় — রেজিস্টার পড়া/লেখার জন্য দুটো helper ফাংশন বানানো, রেজিস্টার অ্যারেতে সরাসরি ইনডেক্স না করে:
class CPU:
def __init__(self, mem_size=4096):
self.regs = [0] * 32
self.pc = 0
self.mem = bytearray(mem_size)
self.halted = False
def read_reg(self, i):
return self.regs[i] if i != 0 else 0
def write_reg(self, i, value):
if i != 0:
self.regs[i] = value & 0xFFFFFFFF # ৩২-বিটে wrap
write_reg নিজে x0-কে সুরক্ষা দেয় (i != 0 চেক), আর read_reg নিশ্চিত করে কেউ যদি ভুলবশত x0-তে কিছু লিখেও ফেলে (উপরের চেকের কারণে যেটা আসলে সম্ভবই না), তাও পড়ার সময় শূন্যই পাওয়া যাবে। দুটো চেক একসাথে থাকাটা defensive coding-এর ভালো অভ্যাস — 8-bit ALU প্রজেক্টে register file-এর R0-সুরক্ষাতেও একই প্যাটার্ন দেখেছিলাম।
ইন্সট্রাকশন ফরম্যাট — বিট-বাই-বিট
RV32I-এর প্রতিটা ইন্সট্রাকশন ৩২ বিট, আর সব ফরম্যাটেই opcode সবসময় বিট [6:0]-এ থাকে — এইটুকু জানলেই ডিকোডার প্রথম ধাপে opcode বের করে বাকি ফিল্ড কীভাবে পড়তে হবে সেটা ঠিক করতে পারে।
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: imm[31|30:25|... ছড়ানো, নিচে বিস্তারিত ...] opcode[6:0]
U-type: imm[31:12] rd[11:7] opcode[6:0]
J-type: imm[20|19:12|11|... ছড়ানো, নিচে বিস্তারিত ...] rd[11:7] opcode[6:0]
opcode ও funct3/funct7 মিলে ইন্সট্রাকশন বাছাই হয়:
| opcode | funct3 | funct7 | ইন্সট্রাকশন |
|---|---|---|---|
0110011 (R) | 000 | 0000000 | ADD |
0110011 (R) | 000 | 0100000 | SUB |
0110011 (R) | 111 | 0000000 | AND |
0110011 (R) | 110 | 0000000 | OR |
0110011 (R) | 100 | 0000000 | XOR |
0110011 (R) | 010 | 0000000 | SLT |
0010011 (I) | 000 | — | ADDI |
0000011 (I) | 010 | — | LW |
0100011 (S) | 010 | — | SW |
1100011 (B) | 000 | — | BEQ |
1100011 (B) | 001 | — | BNE |
1101111 (J) | — | — | JAL |
1100111 (I) | 000 | — | JALR |
খেয়াল করুন — ADD আর SUB-এর opcode আর funct3 একই; কেবল funct7 আলাদা। এটা RISC-V-এর একটা ইচ্ছাকৃত সিদ্ধান্ত: R-type-এর মধ্যেই একটা বাড়তি ৭-বিট ফিল্ড রাখা হয়েছে, যাতে যোগ আর বিয়োগের মতো “কাছাকাছি জাতের” অপারেশনগুলো একই ফরম্যাটের ভেতর বিট-ভাগাভাগি করে জায়গা বাঁচাতে পারে।
ধাপে ধাপে
১. মেমরি — flat byte array, little-endian
RV32I লিটল-এন্ডিয়ান — একটা ৩২-বিট শব্দের সবচেয়ে কম গুরুত্বপূর্ণ বাইট সবচেয়ে ছোট ঠিকানায় থাকে। Python-এর struct মডিউল দিয়ে এটা সরাসরি হ্যান্ডল করা যায়:
import struct
def fetch_word(mem, addr):
return struct.unpack_from('\<I', mem, addr)[0]
def load_word(mem, addr):
return struct.unpack_from('\<I', mem, addr)[0]
def store_word(mem, addr, value):
struct.pack_into('\<I', mem, addr, value & 0xFFFFFFFF)
'\<I' মানে “little-endian, unsigned ৩২-বিট ইন্টিজার” — fetch (ইন্সট্রাকশন পড়া) আর data load দুটোতেই একই ফরম্যাট।
২. Sign-extend — সব ফরম্যাটের ভিত্তি
Immediate ফিল্ডগুলো signed — ঋণাত্মক offset (পিছনের দিকে branch, নেগেটিভ constant) বোঝাতে হবে। একটা closed-form সূত্র দিয়ে এটা এক লাইনে করা যায়, কোনো if-branch ছাড়াই:
def sign_extend(value, bits):
sign_bit = 2 ** (bits - 1)
return (value & (sign_bit - 1)) - (value & sign_bit)
যুক্তি: sign_bit হলো সেই নির্দিষ্ট বিট-প্রস্থের MSB (sign বিট)-এর মাস্ক। যদি সেই বিট সেট থাকে, value & sign_bit অ-শূন্য (ঠিক sign_bit-এর সমান) হয়ে যায় আর ফলাফল থেকে পুরো sign_bit বিয়োগ হয়ে যায় — এটাই two’s complement-এর সংজ্ঞা। বিট সেট না থাকলে বিয়োগ হয় শূন্য, ফলাফল অপরিবর্তিত থাকে।
যাচাই — ১২-বিট 0xFFF (সব বিট সেট) sign-extend করলে -1 হওয়া উচিত:
sign_bit = 2**11 = 2048 (0x800)
value & (sign_bit-1) = 0xFFF & 0x7FF = 0x7FF = 2047
value & sign_bit = 0xFFF & 0x800 = 0x800 = 2048
ফলাফল = 2047 - 2048 = -1 ✓
৩. Decode — কমন ফিল্ড
সব ফরম্যাটেই opcode, rd, funct3, rs1, rs2, funct7-এর বিট-পজিশন একই — শুধু কোন ফরম্যাটে কোনটা “প্রযোজ্য” সেটা আলাদা। তাই একটা কমন এক্সট্র্যাক্টর সবার আগে লেখা যায়:
def decode_common(word):
opcode = word & 0x7F
rd = (word >> 7) & 0x1F
funct3 = (word >> 12) & 0x7
rs1 = (word >> 15) & 0x1F
rs2 = (word >> 20) & 0x1F
funct7 = (word >> 25) & 0x7F
return opcode, rd, funct3, rs1, rs2, funct7
৪. Decode — প্রতিটা ফরম্যাটের immediate
I-type সবচেয়ে সহজ — বিট [31:20] সরাসরি একটা ১২-বিট signed সংখ্যা:
def imm_i(word):
return sign_extend((word >> 20) & 0xFFF, 12)
S-type — immediate দুই টুকরায় ভাঙা ([31:25] আর [11:7]), কারণ rd-এর জায়গাটা store-এ লাগে না তাই সেই বিটগুলো immediate-এর নিচের অংশ ধরে রাখে:
def imm_s(word):
hi = (word >> 25) & 0x7F # imm[11:5]
lo = (word >> 7) & 0x1F # imm[4:0]
return sign_extend(hi * 32 + lo, 12) # hi 5 বিট বাম দিকে শিফট, lo দিয়ে OR
hi * 32 মানে hi-কে ৫ বিট বাম দিকে শিফট করা (2**5 = 32) — বিট-পজিশন [11:5]-এ বসানোর জন্য। যেহেতু hi আর lo কখনো ওভারল্যাপ করে না, + আর bitwise | এখানে সমান কাজ করে।
B-type সবচেয়ে জটিল — RISC-V ইচ্ছাকৃতভাবে immediate-এর বিটগুলো এমনভাবে সাজিয়েছে যাতে rs1/rs2/funct3-এর পজিশন R/I/S-type-এর সাথে অপরিবর্তিত থাকে (hardware-এ কম মাক্সিং লাগে), বিনিময়ে সফটওয়্যার ডিকোডারকে একটু বেশি কাজ করতে হয়:
def imm_b(word):
b12 = (word >> 31) & 1 # sign বিট
b11 = (word >> 7) & 1
b10_5 = (word >> 25) & 0x3F
b4_1 = (word >> 8) & 0xF
raw = b12 * 4096 + b11 * 2048 + b10_5 * 32 + b4_1 * 2
return sign_extend(raw, 13)
লক্ষ্য করুন branch offset-এর সবচেয়ে নিচের বিট (bit 0) কখনো এনকোড করা হয় না — ধরে নেওয়া হয় সবসময় 0, কারণ ইন্সট্রাকশন সবসময় ২-বাইট-অ্যালাইনড। এই জন্যই b4_1 * 2 (b4_1-কে ১ বিট বাম দিকে শিফট) — সবচেয়ে নিচের বিট হিসেবে স্বয়ংক্রিয়ভাবে 0 বসে যায়।
J-type একই কৌশলে, শুধু ২১-বিট প্রশস্ত (JAL অনেক দূরে লাফাতে পারে):
def imm_j(word):
b20 = (word >> 31) & 1
b19_12 = (word >> 12) & 0xFF
b11 = (word >> 20) & 1
b10_1 = (word >> 21) & 0x3FF
raw = b20 * 1048576 + b19_12 * 4096 + b11 * 2048 + b10_1 * 2
return sign_extend(raw, 21)
৫. Execute — R-type (ADD/SUB/AND/OR/XOR/SLT)
def exec_r(self, rd, funct3, rs1, rs2, funct7):
a, b = self.read_reg(rs1), self.read_reg(rs2)
if funct3 == 0b000:
result = (a - b) if funct7 == 0b0100000 else (a + b)
elif funct3 == 0b111:
result = a & b
elif funct3 == 0b110:
result = a | b
elif funct3 == 0b100:
result = a ^ b
elif funct3 == 0b010: # SLT — signed compare
result = 1 if sign_extend(b, 32) > sign_extend(a, 32) else 0
else:
raise NotImplementedError(f"R-type funct3={funct3:03b}")
self.write_reg(rd, result & 0xFFFFFFFF)
SLT-এর শর্তটা লক্ষ্য করুন: rd = (rs1 \< rs2) ? 1 : 0 লেখার বদলে sign_extend(b,32) > sign_extend(a,32) লেখা হয়েছে — গাণিতিকভাবে হুবহু একই জিনিস (a \< b ঠিক তখনই সত্যি যখন b > a), শুধু তুলনাটা উল্টো দিক থেকে লেখা।
৬. Execute — ADDI, LW
def exec_i_alu(self, rd, funct3, rs1, imm):
if funct3 == 0b000: # ADDI
self.write_reg(rd, (self.read_reg(rs1) + imm) & 0xFFFFFFFF)
else:
raise NotImplementedError(f"I-type-alu funct3={funct3:03b}")
def exec_load(self, rd, funct3, rs1, imm):
addr = (self.read_reg(rs1) + imm) & 0xFFFFFFFF
if funct3 == 0b010: # LW
self.write_reg(rd, fetch_word(self.mem, addr))
else:
raise NotImplementedError(f"LOAD funct3={funct3:03b}")
৭. Execute — SW
def exec_store(self, funct3, rs1, rs2, imm):
addr = (self.read_reg(rs1) + imm) & 0xFFFFFFFF
if funct3 == 0b010: # SW
store_word(self.mem, addr, self.read_reg(rs2))
else:
raise NotImplementedError(f"STORE funct3={funct3:03b}")
৮. Execute — BEQ/BNE
def exec_branch(self, funct3, rs1, rs2, imm):
a, b = self.read_reg(rs1), self.read_reg(rs2)
if funct3 == 0b000: taken = (a == b) # BEQ
elif funct3 == 0b001: taken = (a != b) # BNE
else: raise NotImplementedError(f"BRANCH funct3={funct3:03b}")
return (self.pc + imm) & 0xFFFFFFFF if taken else None
None ফেরত মানে “branch নেওয়া হয়নি” — caller সেটা দেখে বুঝবে pc + 4-এই এগোতে হবে, উপরে ফেরত-দেওয়া অ্যাড্রেসে না।
৯. Execute — JAL/JALR
def exec_jal(self, rd, imm):
self.write_reg(rd, self.pc + 4) # রিটার্ন-অ্যাড্রেস সংরক্ষণ
return (self.pc + imm) & 0xFFFFFFFF
def exec_jalr(self, rd, rs1, imm):
target = (self.read_reg(rs1) + imm) & 0xFFFFFFFE # সবচেয়ে নিচের বিট শূন্য করে দেওয়া হয়
self.write_reg(rd, self.pc + 4)
return target
JALR-এর টার্গেট-এর LSB জোর করে শূন্য করা হচ্ছে (& 0xFFFFFFFE) — এটা স্পেকের একটা সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ ডিটেইল, কারণ RISC-V compressed extension-এর সাথে সামঞ্জস্যের জন্য এই বিটটা ঠিকানার অংশ হিসেবে গণ্য হয় না।
১০. পুরো step() জোড়া লাগানো
def step(self):
word = fetch_word(self.mem, self.pc)
opcode, rd, funct3, rs1, rs2, funct7 = decode_common(word)
next_pc = self.pc + 4 # ডিফল্ট — sequential এগোনো
if opcode == 0b0110011:
self.exec_r(rd, funct3, rs1, rs2, funct7)
elif opcode == 0b0010011:
self.exec_i_alu(rd, funct3, rs1, imm_i(word))
elif opcode == 0b0000011:
self.exec_load(rd, funct3, rs1, imm_i(word))
elif opcode == 0b0100011:
self.exec_store(funct3, rs1, rs2, imm_s(word))
elif opcode == 0b1100011:
branch_target = self.exec_branch(funct3, rs1, rs2, imm_b(word))
if branch_target is not None:
next_pc = branch_target
elif opcode == 0b1101111: # JAL
target = imm_j(word)
next_pc = self.exec_jal(rd, target)
if target == 0: # অসীম self-jump = halt কনভেনশন
self.halted = True
elif opcode == 0b1100111: # JALR
next_pc = self.exec_jalr(rd, rs1, imm_i(word))
else:
raise NotImplementedError(f"opcode={opcode:07b} সাপোর্ট করা হয়নি")
self.pc = next_pc
def run(self, max_steps=10_000):
steps = 0
for _ in range(max_steps):
if self.halted:
break
self.step()
steps += 1
return steps
JAL x0, 0 (অসীম self-jump, নিজেই নিজেকে টার্গেট করা) কে “halt” কনভেনশন হিসেবে ব্যবহার করা হয়েছে — ঠিক যেভাবে single-cycle CPU প্রজেক্টে JMP 8 (নিজের ঠিকানায় jump) দিয়ে প্রোগ্রাম থামানো হয়েছিল। বাস্তব RV32I-তে সাধারণত ecall-এর মাধ্যমে OS-কে জানানো হয়, কিন্তু আমাদের ন্যূনতম ইন্সট্রাকশন সেটে ecall নেই, তাই এই সহজ কনভেনশনটাই যথেষ্ট।
হাতে-অ্যাসেম্বল করা প্রোগ্রাম — লুপ দিয়ে যোগফল
লক্ষ্য: 1 + 2 + 3 + 4 + 5 = 15 গণনা করা, একটা BNE-ভিত্তিক লুপে, তারপর ফলাফল মেমরিতে স্টোর করে আবার লোড করে যাচাই করা।
assembly ঠিকানা উদ্দেশ্য
────────────────────────────────────────────────────────────
addi x5, x0, 0 0x00 sum = 0
addi x6, x0, 1 0x04 i = 1
addi x7, x0, 6 0x08 limit = 6 (লুপ চলবে যতক্ষণ i != 6)
add x5, x5, x6 0x0C sum += i ← loop:
addi x6, x6, 1 0x10 i += 1
bne x6, x7, loop 0x14 if (i != 6) goto loop (offset −8)
sw x5, 100(x0) 0x18 mem[100] = sum
lw x28, 100(x0) 0x1C x28 = mem[100] (স্মৃতি রাউন্ড-ট্রিপ যাচাই)
jal x0, 0 0x20 অসীম self-jump — halt
bne x6, x7, loop-এর offset কীভাবে এলো: এই ইন্সট্রাকশনের ঠিকানা 0x14, লক্ষ্য (loop:) 0x0C — তাই imm = 0x0C − 0x14 = −8।
হাতে-এনকোড করা মেশিন কোড
| ঠিকানা | ইন্সট্রাকশন | opcode/ফিল্ড | hex |
|---|---|---|---|
0x00 | addi x5, x0, 0 | I-type, funct3=000, imm=0 | 0x00000293 |
0x04 | addi x6, x0, 1 | I-type, funct3=000, imm=1 | 0x00100313 |
0x08 | addi x7, x0, 6 | I-type, funct3=000, imm=6 | 0x00600393 |
0x0C | add x5, x5, x6 | R-type, funct3=000, funct7=0 | 0x006282B3 |
0x10 | addi x6, x6, 1 | I-type, funct3=000, imm=1 | 0x00130313 |
0x14 | bne x6, x7, −8 | B-type, funct3=001, imm=−8 | 0xFE731CE3 |
0x18 | sw x5, 100(x0) | S-type, funct3=010, imm=100 | 0x06502223 |
0x1C | lw x28, 100(x0) | I-type, funct3=010, imm=100 | 0x06402E03 |
0x20 | jal x0, 0 | J-type, imm=0 | 0x0000006F |
program.bin-এ এই ৯টা শব্দ ক্রমানুসারে little-endian বাইট হিসেবে লিখে cpu.mem[0:36]-এ লোড করলেই এমুলেটর চালানোর জন্য প্রস্তুত।
ট্রেস — ভেরিফায়েড রান
প্রথম কয়েকটা ধাপ (setup + প্রথম লুপ-পাস):
| ধাপ | pc | ইন্সট্রাকশন | x5(sum) | x6(i) | x7 |
|---|---|---|---|---|---|
| 1 | 0x00 | addi x5,x0,0 | 0 | 0 | 0 |
| 2 | 0x04 | addi x6,x0,1 | 0 | 1 | 0 |
| 3 | 0x08 | addi x7,x0,6 | 0 | 1 | 6 |
| 4 | 0x0C | add x5,x5,x6 | 1 | 1 | 6 |
| 5 | 0x10 | addi x6,x6,1 | 1 | 2 | 6 |
| 6 | 0x14 | bne x6,x7,−8 (2≠6, নেওয়া হলো, pc→0x0C) | 1 | 2 | 6 |
লুপ এভাবেই আরও ৪ বার চলে (ধাপ ৭–১৭) — প্রতি পাসে add দিয়ে sum-এ i যোগ হয়, addi দিয়ে i বাড়ে, bne আবার পিছনে লাফ দেয় — যতক্ষণ না i == 6:
| ধাপ | pc | ইন্সট্রাকশন | x5(sum) | x6(i) | x7 |
|---|---|---|---|---|---|
| 16 | 0x0C | add x5,x5,x6 (শেষ যোগ, i=5) | 15 | 5 | 6 |
| 17 | 0x10 | addi x6,x6,1 | 15 | 6 | 6 |
| 18 | 0x14 | bne x6,x7,−8 (6==6, নেওয়া হয়নি, pc→0x18) | 15 | 6 | 6 |
| 19 | 0x18 | sw x5,100(x0) → mem[100]=15 | 15 | 6 | 6 |
| 20 | 0x1C | lw x28,100(x0) → x28=15 | 15 | 6 | 6 |
| 21 | 0x20 | jal x0,0 → halted | 15 | 6 | 6 |
চূড়ান্ত অবস্থা: ২১টা step() কল-এর পর x5 = 15, x6 = 6, x7 = 6, x28 = 15, mem[100] = 15 — হাতে-গণনা করা 1+2+3+4+5 = 15-এর সাথে হুবহু মেলে, আর x28-এর মান প্রমাণ করে memory round-trip (store করে আবার load করা)-ও সঠিক।
বাস্তব টুলচেইনের সাথে মেলানো
এই হাতে-এনকোডিং ভুল হলে ধরার সবচেয়ে ভালো উপায় — একটা আসল RISC-V টুলচেইন দিয়ে একই অ্যাসেম্বলি কম্পাইল করে তুলনা করা:
$ riscv64-unknown-elf-as -march=rv32i -mabi=ilp32 -o sum.o sum.s
$ riscv64-unknown-elf-objdump -d sum.o
objdump-এর আউটপুটে প্রতিটা ইন্সট্রাকশনের পাশে ঠিক যে hex encoding দেখাবে, সেটা উপরের টেবিলের সাথে বিট-বাই-বিট মিলে যাওয়া উচিত। টুলচেইন ইনস্টল করা এই লেখার জন্য জরুরি না, কিন্তু বাস্তব প্রজেক্টে এটাই সবচেয়ে নির্ভরযোগ্য “গ্রাউন্ড ট্রুথ” — নিজের ডিকোডার লজিকের বিরুদ্ধে আরেকটা স্বাধীন সোর্স।
নিজেকে চ্যালেঞ্জ করুন
- LUI/AUIPC (U-type) — বিট
[31:12]-কে সরাসরিrd-তে বসিয়ে (LUI) বাpc-র সাথে যোগ করে (AUIPC) implement করুন — এটাই একমাত্র বাকি-থাকা ফরম্যাট - SLTU, SLTIU — unsigned compare যোগ করুন,
sign_extendনা ব্যবহার করে সরাসরি তুলনা করে - Byte/halfword মেমরি অ্যাক্সেস — LB/LH/LBU/LHU/SB/SH যোগ করুন,
struct-এর'\<b'/'\<h'/'\<B'/'\<H'ফরম্যাট কোড ব্যবহার করে - ইন্সট্রাকশন কাউন্টার ও ট্রেস মোড — প্রতিটা
step()-এ pc, decoded mnemonic, আর যা বদলালো সেটা প্রিন্ট করে একটাobjdump -d-এর মতো execution trace বানান - ELF লোডার — একটা সত্যিকারের
.elfফাইল থেকে.textসেকশন বের করে সরাসরি লোড করুন, হাতে-এনকোড করা hex-এর বদলে - M extension (MUL/DIV) — funct7
0000001দিয়ে চেনা যায়, RV32I-এর সবচেয়ে সহজ এক্সটেনশন
এটা যেখানে গিয়ে মিশবে
| এখানে যা শিখলেন | পরে কোথায় লাগবে |
|---|---|
| Fetch-decode-execute লুপ, সফটওয়্যারে বাস্তবায়িত | এই মডিউলেরই Pipeline Visualizer প্রজেক্ট — একই লজিক, কিন্তু এবার স্টেজে ভাগ করা |
| Bit-field ডিকোডিং (opcode/funct3/funct7/immediate) | Assembly module — Toy Assembler প্রজেক্ট, উল্টো দিকের কাজ (টেক্সট → বিট) |
| Sign extension, two’s complement | Level 1-এর floating-point ও integer representation লেসনের সরাসরি প্রয়োগ |
| রেজিস্টার ফাইল, memory-mapped store/load | Level 4 — OS-এর process memory model, ও calling convention |
| এই এমুলেটরের সীমাবদ্ধতা (কোনো cache, pipeline, বা branch prediction নেই) | Level 3-এরই Cache Simulator ও Pipeline Visualizer প্রজেক্ট — এই একই CPU-কে ধাপে ধাপে বাস্তবসম্মত করা |
| ISA-কে “hardware/software contract” হিসেবে দেখা | Level 11 — Advanced Architecture-এর microarchitecture বনাম architecture আলোচনা |