এই পর্বে যা শিখবেন
- যেকোনো RV32I instruction হাতে encode করে hex বের করা, আর hex দেখে আবার assembly লেখা
- ছয়টি format-এর বিট-নকশা, আর register field কেন এক জায়গায় থাকে
- S, B, J-এর immediate কেন টুকরো টুকরো, আর কীভাবে জোড়া লাগাতে হয়
- opcode → funct3 → funct7 দিয়ে decoder কীভাবে
addআরsubআলাদা করে - Fixed বনাম variable length — RISC-V, x86 আর ARM-এর বাস্তব trade-off
১ · Instruction-ও একটি সংখ্যা — encoding কেন জরুরি
পর্ব ০১-এ একটি চমক দেখেছিলেন: add x5, x6, x7 লেখাটি মেমরিতে আসলে একটি সংখ্যা, 0x007302B3. CPU কোনো লেখা পড়ে না; সে মেমরি থেকে ৩২টি বিট আনে আর সেই বিটগুলো দেখে বোঝে কী করতে হবে। কোন বিট কী বোঝাবে, তার নিয়মের নাম instruction encoding, আর একটি নির্দিষ্ট বিট-সাজানোর ছাঁচকে বলে instruction format.
পর্ব ২৫-এ দেখেছেন ISA হলো hardware আর software-এর চুক্তি, আর পর্ব ২৬-এ RV32I-এর instruction-গুলো চিনেছেন। এই পর্বে সেই চুক্তির সবচেয়ে নিচের স্তর: প্রতিটি instruction কীভাবে বিটে লেখা হয়। এখান থেকেই পর্ব ৩২-এর datapath আর পর্ব ৩৩-এর control unit তাদের তার টানবে।
ব্যাংকের জমা-স্লিপের কথা ভাবুন। প্রতিটি স্লিপে অ্যাকাউন্ট নম্বরের ঘর, টাকার অঙ্কের ঘর আর তারিখের ঘর একই জায়গায় ছাপা। তাই ক্যাশিয়ার পুরো স্লিপ না পড়েই চোখ ফেলে সোজা অ্যাকাউন্ট নম্বরের ঘরে যান, আর একই সময়ে কম্পিউটারে সেই অ্যাকাউন্ট খুলতে শুরু করেন। কিন্তু কেউ যদি হাতে লেখা চিঠিতে টাকা জমা দিতে চান, তখন পুরো চিঠি পড়ে বুঝতে হবে কোথায় কী লেখা। Fixed format-এর instruction হলো ছাপানো স্লিপ, আর মুক্ত-ধাঁচের লম্বা-ছোট instruction হলো হাতে লেখা চিঠি।
Encoding-এর নকশা তিনটি জিনিসের ওপর সরাসরি প্রভাব ফেলে:
মেমরির জায়গা
একই প্রোগ্রাম যত কম byte-এ লেখা যায়, instruction cache-এ তত বেশি কোড ধরে, ফ্ল্যাশ মেমরিও কম লাগে। একে বলে code density. ছোট microcontroller-এ এটি টাকার প্রশ্ন।
Decode-এর গতি
Instruction আনার পর CPU-কে বুঝতে হয় এটি কী। বিটের জায়গা নির্দিষ্ট থাকলে এই কাজ কয়েকটি তারেই হয়ে যায়; না থাকলে লাগে লম্বা logic আর বেশি সময়।
Pipeline-এর সুবিধা
গার্মেন্টস কারখানার assembly line-এ প্রতিটি টুকরো একই মাপের হলে লাইন সমান তালে চলে। Instruction-এর মাপ আর ছাঁচ নিয়মিত হলে pipelining (পর্ব ৩৫) সহজ হয় — প্রতি clock-এ একটি করে instruction আনা যায়।
Encoding হলো একটি বাজেট। ৩২টি বিটের মধ্যে জায়গা দিতে হবে কাজের নাম (opcode), register-এর নম্বর আর সরাসরি লেখা সংখ্যা (immediate)-কে। একটিকে বেশি জায়গা দিলে অন্যটি কমে। ISA নকশার বড় সিদ্ধান্তগুলো আসলে এই বাজেটের ভাগাভাগি।
২ · Fixed-length বনাম variable-length
সব ISA একভাবে বিট খরচ করে না। দুটি প্রধান দর্শন আছে:
- Fixed-length: সব instruction একই মাপের। RISC-V-এর মূল instruction সবই ৩২ বিট (৪ byte); ARM-এর ৬৪-বিট মোড A64-ও নির্দিষ্ট ৩২ বিট। PC থেকে পরের instruction সবসময় ৪ byte দূরে।
- Variable-length: instruction-এর মাপ কাজ অনুযায়ী বদলায়। x86-64-এ একটি instruction ১ byte থেকে সর্বোচ্চ ১৫ byte পর্যন্ত হতে পারে। ছোট কাজ ছোট, বড় কাজ বড় — জায়গা বাঁচে, কিন্তু instruction কোথায় শেষ, তা বুঝতে হলে আগে সেটি পড়তে হয়।
- মাঝামাঝি: RISC-V-এর ঐচ্ছিক C extension ১৬-বিটের ছোট রূপ যোগ করে, তাই প্রোগ্রামে ১৬ আর ৩২ বিট মিশে থাকে। ARM-এর ৩২-বিট জগতের Thumb/Thumb-2-ও ১৬ আর ৩২ বিট মেশায়। এখানে মাপ মাত্র দুই রকম, আর প্রথম কয়েকটি বিট দেখেই মাপ বোঝা যায়।
| ISA | instruction-এর মাপ | একটি উদাহরণ (hex) | মাপ বোঝার উপায় |
|---|---|---|---|
| RISC-V RV32I | ৩২ বিট | add x5, x6, x7 = 007302B3 | সর্বনিম্ন ২ বিট 11 |
| RISC-V + C | ১৬ বা ৩২ বিট | c.add x5, x7 = 929E | সর্বনিম্ন ২ বিট 11 না হলে ১৬ বিট |
| ARM A64 | ৩২ বিট | add x0, x1, x2 = 8B020020 | সবসময় ৪ byte |
| ARM Thumb | ১৬ বা ৩২ বিট | adds r0, r1, r2 = 1888 | প্রথম halfword-এর ওপরের ৫ বিট |
| x86-64 | ১ থেকে ১৫ byte | ret = C3; add rax, [rbx+rcx*4+8] = 48 03 44 8B 08 | prefix, opcode, ModR/M একে একে পড়ে |
টেবিলের ARM আর x86-এর byte-গুলো তাদের manual-এর নিয়মে হাতে হিসাব করা; কোনো assembler একই instruction-এর জন্য অন্য একটি বৈধ encoding-ও বেছে নিতে পারে (x86-এ এমন বিকল্প বেশ সাধারণ)। x86-এর উদাহরণটি অংশ ৯-এ byte ধরে ভেঙে দেখব।
Intel ১৯৭৮ সালে 8086 বাজারে আনে, যার instruction ১ থেকে ৬ byte. তখন মেমরি ছিল খুব দামি, তাই ঘন encoding ছিল বুদ্ধিমানের কাজ। পরের চার দশকে প্রতিটি নতুন সুবিধা — ৩২-বিট, ৬৪-বিট, SSE, AVX — পুরোনো encoding না ভেঙে নতুন prefix বা escape byte হিসেবে যোগ হয়েছে। আর RISC-V শুরু হয় ২০১০ সালে UC Berkeley-তে, একেবারে শূন্য থেকে; তাই তারা অতীতের বোঝা ছাড়াই encoding সাজাতে পেরেছে। দুই নকশার পার্থক্য বোঝা মানে ইতিহাসের দামও বোঝা।
৩ · RISC-V-এর ছয়টি format — বিটের মানচিত্র
RV32I-এর প্রতিটি instruction নিচের ছয়টি ছাঁচের কোনো একটিতে বসে। বিটের নম্বর গোনা হয় ডান দিক থেকে: বিট ০ সবচেয়ে ডানে, বিট ৩১ সবচেয়ে বাঁয়ে। প্রতিটি field-এর নাম:
- opcode [6:0] — ৭ বিট, কোন শ্রেণির কাজ (ALU, load, store, branch…)। একে বলা হয় major opcode.
- rd [11:7] — destination register, যেখানে ফল লেখা হবে।
- funct3 [14:12] — ৩ বিট, একই শ্রেণির ভেতরে কোন কাজ।
- rs1 [19:15] আর rs2 [24:20] — দুটি source register. প্রতিটি ৫ বিট, কারণ ২৫ = ৩২টি register (x0..x31).
- funct7 [31:25] — আরও ৭ বিট বাড়তি পরিচয় (যেমন
addবনামsub). - imm — immediate, instruction-এর ভেতরেই লেখা সংখ্যা। Format ভেদে এর জায়গা বদলায়।
| format | 31–25 | 24–20 | 19–15 | 14–12 | 11–7 | 6–0 | কারা ব্যবহার করে |
|---|---|---|---|---|---|---|---|
| R | funct7 | rs2 | rs1 | funct3 | rd | opcode | add, sub, sll, and |
| I | imm[11:0] | rs1 | funct3 | rd | opcode | addi, lw, jalr, slli | |
| S | imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode | sw, sh, sb |
| B | imm[12│10:5] | rs2 | rs1 | funct3 | imm[4:1│11] | opcode | beq, bne, blt |
| U | imm[31:12] | rd | opcode | lui, auipc | |||
| J | imm[20│10:1│11│19:12] | rd | opcode | jal | |||
টেবিলে “imm[12│10:5]” মানে ওই ৭ বিটের প্রথমটিতে immediate-এর বিট ১২, তারপর বিট ১০ থেকে ৫. অনেক বই S আর B-কে একসাথে, U আর J-কে একসাথে দেখায়, কারণ B হলো S-এর একটি রূপ আর J হলো U-এর একটি রূপ — শুধু immediate-এর বিট সাজানোয় পার্থক্য।
Register field কেন কখনো জায়গা বদলায় না
টেবিলে কলাম ধরে তাকান। rs1 যেখানে আছে, সবসময় বিট ১৯..১৫-তে। rs2 সবসময় ২৪..২০-তে, rd সবসময় ১১..৭-এ। এটি দুর্ঘটনা নয়, সবচেয়ে গুরুত্বপূর্ণ নকশা-সিদ্ধান্তগুলোর একটি।
কারণটা সময়ের। Instruction আসার পর CPU-কে দুটি কাজ করতে হয়: (১) বোঝা এটি কোন instruction, আর (২) register file থেকে operand পড়া। Register-এর জায়গা নির্দিষ্ট থাকলে দ্বিতীয় কাজের জন্য প্রথমটির অপেক্ষা লাগে না। বিট ১৯..১৫ সরাসরি register file-এর প্রথম read port-এ, ২৪..২০ দ্বিতীয় port-এ তার দিয়ে জোড়া থাকে। opcode decode আর register পড়া চলে একসাথে। কোনো instruction-এ rs2 না থাকলে (যেমন addi) ওই বিটগুলো আসলে immediate-এর অংশ; register file তবুও একটি “register” পড়ে ফেলে, আর control unit পরে সেই মান উপেক্ষা করে। অকারণে একটি পড়া হলো, কিন্তু সময় বাঁচল।
তুলনার জন্য MIPS (Patterson–Hennessy-এর পুরোনো সংস্করণের ISA): R-type-এ ফলের register থাকে rd field-এ (বিট ১৫..১১), কিন্তু I-type-এ ফল যায় rt field-এ (বিট ২০..১৬)। তাই MIPS datapath-এ “কোন field-টি লেখার register” বাছতে একটি বাড়তি multiplexer আর RegDst নামের control signal লাগে। RISC-V-এ rd সব format-এ একই জায়গায়, তাই এই mux-টিই নেই।
৪ · Immediate-এর বিট কেন ছড়ানো
প্রথমবার B আর J format দেখলে প্রায় সবাই অবাক হন: “immediate-এর বিট এমন এলোমেলো কেন? ১২ বিট পাশাপাশি রাখলেই তো হতো!” উত্তর বুঝতে হলে মানুষের চোখে নয়, hardware-এর চোখে দেখতে হবে। নিচের টেবিলটি RISC-V manual-এর একটি বিখ্যাত ছবির রূপ: প্রতিটি format-এর ৩২-বিট immediate (sign-extend করার পর) তৈরি হয় instruction-এর কোন বিট থেকে।
| format | imm[31] | imm[30:20] | imm[19:12] | imm[11] | imm[10:5] | imm[4:1] | imm[0] |
|---|---|---|---|---|---|---|---|
| I | inst[31] | inst[31]… | inst[31]… | inst[31] | inst[30:25] | inst[24:21] | inst[20] |
| S | inst[31] | inst[31]… | inst[31]… | inst[31] | inst[30:25] | inst[11:8] | inst[7] |
| B | inst[31] | inst[31]… | inst[31]… | inst[7] | inst[30:25] | inst[11:8] | 0 |
| U | inst[31] | inst[30:20] | inst[19:12] | 0 | 0 | 0 | 0 |
| J | inst[31] | inst[31]… | inst[19:12] | inst[20] | inst[30:25] | inst[24:21] | 0 |
“inst[31]…” মানে ওই সব বিট inst[31]-এর কপি — অর্থাৎ sign extension. এবার টেবিলটি কলাম ধরে পড়ুন, আর চারটি নিয়ম খেয়াল করুন:
- Sign bit সবসময় inst[31]. পাঁচটি format-এই immediate-এর চিহ্ন বিট ৩১-এ। তাই instruction কোন format, তা বোঝার আগেই hardware ওপরের ২০টি বিট ভরাট করার তার টেনে রাখতে পারে। Sign extension সাধারণত immediate তৈরির সবচেয়ে ধীর অংশ (একটি বিট অনেক তারে ছড়াতে হয়), আর সেটিই এখানে decode-এর জন্য অপেক্ষা করে না।
- imm[10:5] সবসময় inst[30:25]. I, S, B, J — চারটিতেই। এই ৬ বিটের জন্য কোনো বাছাই লাগে না, শুধু তার।
- বাকি প্রতিটি বিটের উৎস মাত্র ২–৩টি (ধ্রুবক 0 বসানোর বিকল্প বাদে)। imm[4:1] আসে inst[24:21] (I, J) বা inst[11:8] (S, B) থেকে; imm[0] আসে inst[20], inst[7] বা 0 থেকে; imm[11] আসে inst[31], inst[7], inst[20] বা 0 থেকে। প্রতিটির জন্য ছোট একটি mux যথেষ্ট।
- B = S-এর বিট, একটু সরানো; J = I আর U-এর বিট, একটু সরানো। S-এর inst[7] (যা S-এ imm[0]) B-তে হয়ে যায় imm[11], কারণ branch offset সবসময় জোড় — imm[0] লেখারই দরকার নেই। J-তে I-এর inst[20] (I-এ imm[0]) হয়ে যায় imm[11], আর imm[19:12] বসে U-এর মতো একই জায়গায়।
RISC-V immediate-এর বিট ছড়িয়েছে যাতে প্রতিটি immediate-বিটের জন্য format জুড়ে যত কম সম্ভব আলাদা উৎস থাকে, আর sign bit সবসময় এক জায়গায় থাকে। মানুষের জন্য encode করা একটু কঠিন হলো, কিন্তু একটি chip-এ প্রতি সেকেন্ডে কোটি কোটি বার যে কাজ হয়, সেটি সহজ হলো। এই বিনিময় একবার মানুষ করে assembler লেখার সময়, তারপর আর কখনো নয়।
ছোট একটি যুক্তি দিয়ে দেখুন কেন B-তে imm[0] বাদ দেওয়া নিরাপদ। RISC-V-এ সব instruction-এর ঠিকানা অন্তত ২-এর গুণিতক (C extension থাকলে ১৬-বিটের instruction ২ byte-এর সীমানায় বসে, না থাকলে সবই ৪-এর সীমানায়)। তাই যেকোনো দুই instruction-এর ঠিকানার পার্থক্য ২-এর গুণিতক, অর্থাৎ বিট ০ সবসময় 0. যে বিট সবসময় 0, তা লিখে রাখা মানে একটি বিট নষ্ট — সেই বিট দিয়ে বরং পরিসর দ্বিগুণ করা যায়। এজন্য B-তে ১২টি বিট রাখা হলেও offset আসলে ১৩ বিটের।
৫ · ধাপে ধাপে encoding — assembly থেকে hex
Encode করার পদ্ধতি সবসময় একই: (১) instruction-টির format আর opcode-funct3-funct7 খুঁজুন, (২) register-এর নম্বরকে ৫ বিটে লিখুন, (৩) immediate-কে two's complement-এ লিখে format-এর নিয়মে টুকরো করুন, (৪) বাঁ থেকে ডানে জুড়ে ৪ বিট করে ভাগ করে hex লিখুন। কাজটি যে প্রোগ্রাম করে, তার নাম assembler.
R-type: add x5, x6, x7 (স্মরণ) আর sub
opcode = 0110011 (OP), funct3 = 000, funct7 = 0000000; rd = ৫ = 00101, rs1 = ৬ = 00110, rs2 = ৭ = 00111. বাঁ থেকে জুড়ে:
funct7 rs2 rs1 f3 rd opcode
0000000 00111 00110 000 00101 0110011
= 0000 0000 0111 0011 0000 0010 1011 0011 = 0x007302B3
sub: funct7 = 0100000 → 0x407302B3
sub-এ শুধু বিট ৩০ বদলায়, তাই hex-এর প্রথম অঙ্ক 0 থেকে 4 হয়। এই একটি বিটের গল্প অংশ ৭-এ।
I-type: addi x5, x5, 10 আর lw x6, 8(x10)
addi: opcode 0010011 (OP-IMM), funct3 000. immediate ১০ = 0000 0000 1010 পুরোটা বিট ৩১..২০-তে বসে।
imm[11:0] rs1 f3 rd opcode
000000001010 00101 000 00101 0010011
= 0000 0000 1010 0010 1000 0010 1001 0011 = 0x00A28293
lw x6, 8(x10): load-ও I-type, opcode 0000011 (LOAD), funct3 010 (word). Assembly-তে লেখা হয় “rd, offset(rs1)”, তাই rd = x6, rs1 = x10, imm = ৮:
000000001000 01010 010 00110 0000011 = 0x00852303
ঋণাত্মক immediate-এ শুধু two's complement লাগে: addi x5, x0, -1-এ imm = 1111 1111 1111, ফল 0xFFF00293. Shift instruction (slli, srli, srai) I-type হলেও ৩২-বিট register-এ ৩১-এর বেশি shift অর্থহীন, তাই immediate-এর নিচের ৫ বিট হলো shift-এর পরিমাণ (shamt), আর ওপরের ৭ বিট কাজ করে funct7-এর মতো: slli x5, x6, 3 = 0x00331293, srai x5, x6, 3 = 0x40335293.
S-type: sw x9, -4(x18) — immediate দুই টুকরো
Store-এ কোনো ফল register-এ লেখা হয় না, তাই rd লাগে না। কিন্তু দুটি register লাগে: কোথায় রাখবেন (ঠিকানার ভিত্তি, rs1) আর কী রাখবেন (rs2). rs1 আর rs2-কে তাদের নির্দিষ্ট জায়গায় রাখতে হলে immediate-কে rd-এর ফাঁকা জায়গা আর funct7-এর জায়গায় ভাগ করে বসাতে হয়। নিচের ডায়াগ্রামে ধাপে ধাপে দেখুন:
sw rs2, imm(rs1) → S-type opcode = 0100011 (STORE) funct3 = 010 (sw = word, ৪ byte) sb = 000, sh = 001
rs2 = x9 = 9 = 01001 (যে মান রাখা হবে) rs1 = x18 = 18 = 10010 (ঠিকানার ভিত্তি) rd = নেই (store কিছু ফেরত দেয় না)
+4 = 0000 0000 0100
উল্টে = 1111 1111 1011
+1 = 1111 1111 1100 = 0xFFC
imm = 1111111 11100
└ imm[11:5]┘└imm[4:0]┘
imm[11:5] = 1111111 → বিট 31..25
imm[4:0] = 11100 → বিট 11..7 (rd-এর জায়গা)
imm[11:5] rs2 rs1 f3 imm[4:0] opcode
1111111 01001 10010 010 11100 0100011
= 1111 1110 1001 1001 0010 1110 0010 0011
= 0xFE992E23
ঠিকানা: A A+1 A+2 A+3
byte : 23 2E 99 FE
objdump দেখায়: fe992e23 sw s1,-4(s2)
- Format চেনা।
swএকটি store, তাই S-type. Opcode0100011সব store-এর জন্য এক; কোন মাপের store, তা বলে funct3 — word-এর জন্য010. - Register নম্বর। Assembly-তে প্রথমে আসে যে মান রাখা হবে (x9), সেটি rs2 =
01001. বন্ধনীর ভেতরেরটি ঠিকানার ভিত্তি (x18), সেটি rs1 =10010. ক্রম গুলিয়ে ফেলা এখানে সবচেয়ে সাধারণ ভুল। - Offset-কে ১২ বিটে। −৪-এর two's complement: ৪-কে ১২ বিটে লিখে সব বিট উল্টে ১ যোগ করুন —
1111 1111 1100, অর্থাৎ0xFFC. - দুই টুকরো। ওপরের ৭ বিট (imm[11:5] =
1111111) যায় funct7-এর জায়গায়, নিচের ৫ বিট (imm[4:0] =11100) যায় rd-এর জায়গায়। এভাবেই rs1 আর rs2 তাদের নির্দিষ্ট ঘরে থাকতে পারে। - জুড়ে hex. ছয়টি অংশ বাঁ থেকে ডানে জুড়ে ৪ বিট করে ভাগ করলে
F E 9 9 2 E 2 3, অর্থাৎ0xFE992E23. শুরুরFEদেখেই অভিজ্ঞ চোখ বোঝে immediate ঋণাত্মক — বিট ৩১ = 1. - মেমরিতে। RISC-V little-endian (পর্ব ০৬), তাই সবচেয়ে ছোট ঠিকানায় সবচেয়ে নিচের byte
23. Disassembler (objdump) register-কে ABI নামে দেখায়: x9 = s1, x18 = s2 (নামগুলোর বিস্তারিত পর্ব ৩০-এ)।
B-type: beq x5, x6, 12 — offset ২-এর গুণিতকে
Branch-এর immediate হলো বর্তমান PC থেকে লক্ষ্যের দূরত্ব, byte-এ (PC-relative). এর অর্থ আর ব্যবহারের পুরো আলোচনা পর্ব ২৯-এ; এখানে শুধু বিট বসানো। offset = +১২, ১৩ বিটে 0 0 000000 0110 0 — অর্থাৎ imm[12] = 0, imm[11] = 0, imm[10:5] = 000000, imm[4:1] = 0110, আর imm[0] = 0 (লেখা হয় না)।
imm[12│10:5] rs2 rs1 f3 imm[4:1│11] opcode
0 000000 00110 00101 000 0110 0 1100011
= 0000 0000 0110 0010 1000 0110 0110 0011 = 0x00628663
U-type: lui x5, 0x12345
lui (load upper immediate) ২০ বিটের সংখ্যাকে register-এর ওপরের ২০ বিটে বসায়, নিচের ১২ বিট 0. এর encoding সবচেয়ে সহজ: ২০ বিট সোজা বিট ৩১..১২-তে।
imm[31:12] rd opcode
0001 0010 0011 0100 0101 00101 0110111 = 0x123452B7
ফলে x5 = 0x12345000
J-type: jal x1, 2048
jal লাফ দেয় PC + offset-এ, আর ফেরার ঠিকানা (PC + ৪) রাখে rd-তে। offset ২০৪৮ = ২১১, অর্থাৎ শুধু imm[11] = 1. J-এর নিয়মে imm[11] বসে inst[20]-এ:
imm[20] imm[10:1] imm[11] imm[19:12] rd opcode
0 0000000000 1 00000000 00001 1101111
= 0000 0000 0001 0000 0000 0000 1110 1111 = 0x001000EF
এবার নিজে চালিয়ে দেখুন। নিচের ছোট assembler এগারোটি instruction-কে field-এ ভাঙে, field-গুলো আলাদা করে ছাপে, আর hex বের করে একটি জানা উত্তরের টেবিলের সাথে মেলায়। জানা উত্তরগুলো আলাদা একটি Python encoder দিয়ে বের করা, আর gcc -O0-এর পরিচিত RISC-V আউটপুটের (যেমন addi sp,sp,-16 = ff010113, ret = 00008067) সাথে মিলিয়ে দেখা।
// Tiny RV32I assembler: one line of assembly -> fields -> 32-bit word.
const T = { // mnemonic: [format, opcode, funct3, funct7]
add: ["R", 0x33, 0, 0x00], sub: ["R", 0x33, 0, 0x20], addi: ["I", 0x13, 0],
slli: ["I", 0x13, 1, 0x00], srai: ["I", 0x13, 5, 0x20], lw: ["I", 0x03, 2],
jalr: ["I", 0x67, 0], sw: ["S", 0x23, 2], beq: ["B", 0x63, 0],
lui: ["U", 0x37], jal: ["J", 0x6F] };
const b = (v, hi, lo) => (v >> lo) & ((1 << (hi - lo + 1)) - 1); // bits hi..lo
function asm(line) {
const [m, ...a] = line.replace(/[(),]/g, " ").trim().split(/\s+/);
const [fmt, op, f3, f7] = T[m], r = s => s ? +s.slice(1) : 0; // "x18" -> 18
const mem = m === "lw" || m === "jalr" || m === "sw"; // rd, imm(rs1)
const imm = Number(mem || fmt === "U" || fmt === "J" ? a[1] : a[2]);
const rs1 = r(mem ? a[2] : a[1]);
const f = { // fields from bit 31 down to bit 7, as [value, width]
R: [[f7, 7], [r(a[2]), 5], [rs1, 5], [f3, 3], [r(a[0]), 5]],
I: [[f7 === undefined ? imm : (f7 << 5) | imm, 12], [rs1, 5], [f3, 3], [r(a[0]), 5]],
S: [[b(imm, 11, 5), 7], [r(a[0]), 5], [rs1, 5], [f3, 3], [b(imm, 4, 0), 5]],
B: [[b(imm, 12, 12) << 6 | b(imm, 10, 5), 7], [r(a[1]), 5], [r(a[0]), 5], [f3, 3],
[b(imm, 4, 1) << 1 | b(imm, 11, 11), 5]],
U: [[imm, 20], [r(a[0]), 5]],
J: [[b(imm, 20, 20) << 19 | b(imm, 10, 1) << 9 | b(imm, 11, 11) << 8 | b(imm, 19, 12), 20],
[r(a[0]), 5]] }[fmt].concat([[op, 7]]);
let w = 0; for (const [v, k] of f) w = (w << k) | (v & ((1 << k) - 1));
return { fmt, word: w >>> 0, bits: f.map(([v, k]) => (v & ((1 << k) - 1)).toString(2).padStart(k, "0")) };
}
const known = [["add x5, x6, x7", 0x007302B3], ["sub x5, x6, x7", 0x407302B3],
["addi x5, x5, 10", 0x00A28293], ["lw x6, 8(x10)", 0x00852303], ["sw x9, -4(x18)", 0xFE992E23],
["beq x5, x6, 12", 0x00628663], ["lui x5, 0x12345", 0x123452B7], ["jal x1, 2048", 0x001000EF],
["jalr x0, 0(x1)", 0x00008067], ["slli x5, x6, 3", 0x00331293], ["srai x5, x6, 3", 0x40335293]];
for (const [line, want] of known) {
const { fmt, word, bits } = asm(line), hex = "0x" + word.toString(16).toUpperCase().padStart(8, "0");
console.log(`${line.padEnd(16)} ${fmt} ${bits.join(" ").padEnd(38)} ${hex} ${word === want ? "ok" : "MISMATCH"}`);
}আউটপুট (নির্ধারিত — প্রতিবার একই):
add x5, x6, x7 R 0000000 00111 00110 000 00101 0110011 0x007302B3 ok
sub x5, x6, x7 R 0100000 00111 00110 000 00101 0110011 0x407302B3 ok
addi x5, x5, 10 I 000000001010 00101 000 00101 0010011 0x00A28293 ok
lw x6, 8(x10) I 000000001000 01010 010 00110 0000011 0x00852303 ok
sw x9, -4(x18) S 1111111 01001 10010 010 11100 0100011 0xFE992E23 ok
beq x5, x6, 12 B 0000000 00110 00101 000 01100 1100011 0x00628663 ok
lui x5, 0x12345 U 00010010001101000101 00101 0110111 0x123452B7 ok
jal x1, 2048 J 00000000000100000000 00001 1101111 0x001000EF ok
jalr x0, 0(x1) I 000000000000 00001 000 00000 1100111 0x00008067 ok
slli x5, x6, 3 I 000000000011 00110 001 00101 0010011 0x00331293 ok
srai x5, x6, 3 I 010000000011 00110 101 00101 0010011 0x40335293 ok
এগারোটিই ok. খেয়াল করুন, প্রতিটি লাইনের শেষ ৭ বিট opcode, আর R, S, B-তে মাঝের দুটি ৫-বিটের দল (rs2, rs1) একই কলামে। sw লাইনে প্রথম দল 1111111 আর পঞ্চম দল 11100 — উপরের ডায়াগ্রামের দুই টুকরো। beq-এর পঞ্চম দল 01100 = imm[4:1] (0110) আর তার পরে imm[11] (0). srai-এর ১২-বিট immediate 010000000011: ওপরের ৭ বিট 0100000 (funct7-এর কাজ), নিচের ৫ বিট shamt = ৩. কোডে b(imm, hi, lo) একটি bit field বের করে (পর্ব ০৬-এর shift আর mask), আর w = (w << k) | v লুপে বাঁ থেকে ডানে field জোড়ে; শেষে >>> 0 ফলকে unsigned ৩২-বিট হিসেবে দেখায়।
৬ · উল্টো পথে — hex থেকে assembly
Debugger, crash dump বা firmware-এর hex দেখে instruction বোঝা প্রায়ই দরকার হয়। এই কাজ করে disassembler. পদ্ধতি: hex-কে binary-তে লিখুন, ৪ বিট করে দল রেখে ডান থেকে বিট গুনুন, আগে opcode পড়ুন — opcode-ই বলে দেবে বাকি বিটগুলো কোন ছাঁচে পড়তে হবে।
উদাহরণ ১: 0x00A28293
0000 0000 1010 0010 1000 0010 1001 0011
opcode [6:0] = 0010011 → OP-IMM (I-type)
rd [11:7] = 00101 → x5
funct3 [14:12] = 000 → addi
rs1 [19:15] = 00101 → x5
imm [31:20] = 0000 0000 1010 = +10
→ addi x5, x5, 10
উদাহরণ ২: 0xFE912E23
1111 1110 1001 0001 0010 1110 0010 0011
opcode [6:0] = 0100011 → STORE (S-type)
imm[4:0] [11:7]= 11100
funct3 [14:12] = 010 → sw
rs1 [19:15] = 00010 → x2 (sp)
rs2 [24:20] = 01001 → x9 (s1)
imm[11:5][31:25]= 1111111
imm = 1111111 11100 = 0xFFC = −4
→ sw x9, -4(x2) (= sw s1, -4(sp))
অংশ ৫-এর 0xFE992E23-এর সাথে তুলনা করুন — শুধু rs1 বদলেছে (x18 → x2), তাই মাত্র একটি hex অঙ্ক (9 → 1). Stack pointer-এর সাপেক্ষে register বাঁচানো এমন sw প্রায় প্রতিটি function-এর শুরুতে দেখা যায় (পর্ব ৩০)।
B-type decode — immediate জোড়া লাগানো
সবচেয়ে মনোযোগ লাগে B আর J-তে, কারণ immediate-এর টুকরোগুলো ঠিক ক্রমে জুড়ে তারপর sign extend করতে হয়। একটি লুপের শেষে পেছনে লাফানো branch দেখুন:
hex : F E B 5 0 C E 3
binary: 1111 1110 1011 0101 0000 1100 1110 0011
bit : 31 0
opcode [6:0] = 1100011 → BRANCH (B-type)
[1:0] = 11 → ৩২-বিটের instruction
funct3 [14:12] = 000 → beq rs1 [19:15] = 01010 → x10 rs2 [24:20] = 01011 → x11
inst[31] = 1 → imm[12] inst[7] = 1 → imm[11] inst[30:25] = 111111 → imm[10:5] inst[11:8] = 1100 → imm[4:1] imm[0] = 0 (লেখা থাকে না)
imm[12:0] = 1 1 111111 1100 0
= 1 1111 1111 1000 (১৩ বিট) = 0x1FF8
বিট 12 = 1 → ঋণাত্মক
মান = 0x1FF8 − 0x2000 = 8184 − 8192 = −8
beq x10, x11, -8
x10 == x11 হলে PC ← PC − 8
অর্থাৎ ২টি instruction পেছনে (প্রতিটি ৪ byte)
objdump সাধারণত লেবেল বা পরম ঠিকানা দেখায়
- Opcode আগে। শেষ hex অঙ্ক 3 =
0011, তার আগে E =1110; বিট ৬..০ =1100011, অর্থাৎ BRANCH. সর্বনিম্ন দুই বিট11, তাই এটি ১৬-বিটের compressed instruction নয়। - নির্দিষ্ট field-গুলো। বিট ১৪..১২ =
000, BRANCH-এ এর মানেbeq. বিট ১৯..১৫ =01010= ১০, বিট ২৪..২০ =01011= ১১. Register field-গুলো অন্য format-এর মতো একই জায়গায় — তাই format জানার আগেই এগুলো পড়া যেত। - টুকরো সংগ্রহ। B-এর নিয়মে: inst[31] = 1 হলো imm[12], inst[7] = 1 হলো imm[11], inst[30:25] =
111111হলো imm[10:5], inst[11:8] =1100হলো imm[4:1]. imm[0] সবসময় 0. - জুড়ে চিহ্ন বোঝা। ১৩ বিট:
1 1111 1111 1000=0x1FF8= ৮১৮৪. বিট ১২ = 1, তাই এটি ঋণাত্মক; মান = ৮১৮৪ − ২১৩ = ৮১৮৪ − ৮১৯২ = −৮. - ফল।
beq x10, x11, -8: দুই register সমান হলে PC ৮ byte পেছনে যায় — ২টি ৪-byte instruction আগে, লুপের শুরুতে। Disassembler সাধারণত offset না দেখিয়ে লক্ষ্যের ঠিকানা বা লেবেল দেখায়; অর্থ একই।
RV32I-এর opcode মানচিত্র
বিট ৬..০-এর এই দশটি মান চিনলেই RV32I-এর প্রায় যেকোনো word-এর format বলে দেওয়া যায়:
| নাম | opcode [6:0] | hex | format | instruction |
|---|---|---|---|---|
| OP | 0110011 | 0x33 | R | add sub sll slt sltu xor srl sra or and |
| OP-IMM | 0010011 | 0x13 | I | addi slti sltiu xori ori andi slli srli srai |
| LOAD | 0000011 | 0x03 | I | lb lh lw lbu lhu |
| STORE | 0100011 | 0x23 | S | sb sh sw |
| BRANCH | 1100011 | 0x63 | B | beq bne blt bge bltu bgeu |
| LUI | 0110111 | 0x37 | U | lui |
| AUIPC | 0010111 | 0x17 | U | auipc |
| JAL | 1101111 | 0x6F | J | jal |
| JALR | 1100111 | 0x67 | I | jalr |
| SYSTEM | 1110011 | 0x73 | I | ecall ebreak (আর CSR instruction) |
এর বাইরে RV32I-এ আছে MISC-MEM (0001111, fence). লক্ষ করুন, সব opcode-এর শেষ দুই বিট 11 — কারণটা অংশ ৯-এ। আর nop নামের কোনো আলাদা instruction নেই: এটি addi x0, x0, 0 = 0x00000013, একটি pseudo-instruction (পর্ব ২৬)।
এবার disassembler চালান। এটি প্রতিটি word ভেঙে field বের করে, B আর J-এর immediate জোড়া লাগিয়ে sign extend করে, assembly লেখা বানায়; তারপর শুধু পাওয়া field-গুলো থেকে word আবার তৈরি করে (assembler-এর মতো immediate ছড়িয়ে) দেখে মূলের সাথে মেলে কি না। তালিকার word-গুলো আগের assembler আর Python encoder থেকে নেওয়া:
// Tiny RV32I disassembler + round trip: word -> fields -> text, and fields -> word again.
const b = (w, hi, lo) => (w >>> lo) & ((1 << (hi - lo + 1)) - 1); // bits hi..lo
const sx = (v, n) => (v << (32 - n)) >> (32 - n); // sign-extend n bits
const IMM = { // unscramble: collect immediate bits from their places in the word
I: w => sx(b(w, 31, 20), 12), L: w => sx(b(w, 31, 20), 12), U: w => b(w, 31, 12) << 12,
S: w => sx(b(w, 31, 25) << 5 | b(w, 11, 7), 12),
B: w => sx(b(w, 31, 31) << 12 | b(w, 7, 7) << 11 | b(w, 30, 25) << 5 | b(w, 11, 8) << 1, 13),
J: w => sx(b(w, 31, 31) << 20 | b(w, 19, 12) << 12 | b(w, 20, 20) << 11 | b(w, 30, 21) << 1, 21) };
const PUT = { // scramble again (the assembler's job): immediate -> its bit places
I: i => b(i, 11, 0) << 20, L: i => b(i, 11, 0) << 20, U: i => b(i, 31, 12) << 12,
S: i => b(i, 11, 5) << 25 | b(i, 4, 0) << 7,
B: i => b(i, 12, 12) << 31 | b(i, 10, 5) << 25 | b(i, 4, 1) << 8 | b(i, 11, 11) << 7,
J: i => b(i, 20, 20) << 31 | b(i, 10, 1) << 21 | b(i, 11, 11) << 20 | b(i, 19, 12) << 12 };
const KIND = { 0x33: "R", 0x13: "I", 0x03: "L", 0x67: "L", 0x23: "S", 0x63: "B", 0x37: "U", 0x6F: "J" };
const HAS = { R: 7, I: 1, L: 1, S: 2, B: 2, U: 1, J: 1 }; // 1 = rd field, 2 = rs2, 4 = funct7
const NAME = { R0_0: "add", R0_32: "sub", R5_32: "sra", I0: "addi", I1_0: "slli", I5_0: "srli",
I5_32: "srai", L2: "lw", L0: "jalr", S2: "sw", B0: "beq", B1: "bne", U: "lui", J: "jal" };
function dis(w) {
const op = b(w, 6, 0), k = KIND[op], d = b(w, 11, 7), f3 = b(w, 14, 12), s1 = b(w, 19, 15),
s2 = b(w, 24, 20), f7 = b(w, 31, 25), sh = k === "I" && (f3 & 3) === 1, uj = k === "U" || k === "J";
const m = NAME[k + (uj ? "" : f3) + (k === "R" || sh ? "_" + f7 : "")], rd = "x" + d;
const imm = sh ? s2 : k === "R" ? 0 : IMM[k](w); // shift: 5-bit shamt, imm[11:5] acts as funct7
const rs1 = "x" + s1, rs2 = "x" + s2;
const text = { R: `${rd}, ${rs1}, ${rs2}`, I: `${rd}, ${rs1}, ${imm}`, L: `${rd}, ${imm}(${rs1})`,
S: `${rs2}, ${imm}(${rs1})`, B: `${rs1}, ${rs2}, ${imm}`, U: `${rd}, 0x${(imm >>> 12).toString(16)}`,
J: `${rd}, ${imm}` }[k], h = HAS[k] | (sh ? 6 : 0); // rebuild from the field values alone
const again = ((k === "R" ? 0 : sh ? 0 : PUT[k](imm)) | (h & 4 ? f7 << 25 : 0) | (h & 2 ? s2 << 20 : 0)
| (uj ? 0 : s1 << 15 | f3 << 12) | (h & 1 ? d << 7 : 0) | op) >>> 0;
return [m + " " + text, again];
}
const hex = v => "0x" + v.toString(16).toUpperCase().padStart(8, "0");
for (const w of [0x007302B3, 0x407302B3, 0x00A28293, 0xFE912E23, 0x00852303, 0xFE0516E3, 0xFEB50CE3,
0x123452B7, 0xFF1FF0EF, 0x00008067, 0x00331293, 0x40335293, 0x407352B3]) {
const [text, again] = dis(w); console.log(`${hex(w)} ${text.padEnd(20)} -> ${hex(again)} ${again === w ? "ok" : "FAIL"}`);
}আউটপুট (নির্ধারিত — প্রতিবার একই):
0x007302B3 add x5, x6, x7 -> 0x007302B3 ok
0x407302B3 sub x5, x6, x7 -> 0x407302B3 ok
0x00A28293 addi x5, x5, 10 -> 0x00A28293 ok
0xFE912E23 sw x9, -4(x2) -> 0xFE912E23 ok
0x00852303 lw x6, 8(x10) -> 0x00852303 ok
0xFE0516E3 bne x10, x0, -20 -> 0xFE0516E3 ok
0xFEB50CE3 beq x10, x11, -8 -> 0xFEB50CE3 ok
0x123452B7 lui x5, 0x12345 -> 0x123452B7 ok
0xFF1FF0EF jal x1, -16 -> 0xFF1FF0EF ok
0x00008067 jalr x0, 0(x1) -> 0x00008067 ok
0x00331293 slli x5, x6, 3 -> 0x00331293 ok
0x40335293 srai x5, x6, 3 -> 0x40335293 ok
0x407352B3 sra x5, x6, x7 -> 0x407352B3 ok
তেরোটি word-ই ঠিক assembly-তে ফিরল, আর field থেকে আবার বানানো word মূলের সাথে হুবহু মিলল (ok). 0xFE912E23 → sw x9, -4(x2) আর 0xFEB50CE3 → beq x10, x11, -8 — উপরের হাতের হিসাব আর ডায়াগ্রামের সাথে এক। 0xFF1FF0EF → jal x1, -16: J-এর ২১-বিট immediate জুড়ে sign extend না করলে এখানে একটি বিশাল ধনাত্মক সংখ্যা আসত। কোডের sx(v, n) একটি পুরোনো কৌশল: n-বিটের মানকে বাঁয়ে ঠেলে বিট ৩১-এ চিহ্ন আনে, তারপর arithmetic shift >> চিহ্নটি ছড়িয়ে ফিরিয়ে আনে। IMM আর PUT একে অপরের উল্টো — প্রথমটি decoder-এর, দ্বিতীয়টি assembler-এর কাজ।
একই কাজ C-তে শুধু shift আর mask দিয়ে। নিচের প্রোগ্রাম 0xFE912E23 ভাঙে, immediate জোড়ে, assembly ছাপে, আর আবার pack করে মেলায়। Sign extension এমনভাবে লেখা যাতে signed shift-এর implementation-defined আচরণ এড়ানো যায়:
#include <stdio.h>
#include <stdint.h>
/* bits hi..lo of w, moved down to bit 0 */
static uint32_t field(uint32_t w, unsigned hi, unsigned lo) {
return (w >> lo) & ((1u << (hi - lo + 1)) - 1u);
}
/* 12-bit two's complement -> int32_t without implementation-defined shifts */
static int32_t sext12(uint32_t v) {
return (int32_t)(v & 0x7FFu) - (int32_t)(v & 0x800u);
}
static uint32_t pack_s(int32_t imm, unsigned rs2, unsigned rs1, unsigned f3, unsigned op) {
uint32_t u = (uint32_t)imm & 0xFFFu; /* low 12 bits of imm */
return (field(u, 11, 5) << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12)
| (field(u, 4, 0) << 7) | op;
}
int main(void) {
uint32_t w = 0xFE912E23u;
unsigned op = field(w, 6, 0), f3 = field(w, 14, 12);
unsigned rs1 = field(w, 19, 15), rs2 = field(w, 24, 20);
int32_t imm = sext12((field(w, 31, 25) << 5) | field(w, 11, 7));
printf("word = 0x%08X\n", (unsigned)w);
printf("opcode = 0x%02X (%s), funct3 = %u (%s)\n", op,
op == 0x23 ? "STORE" : "?", f3, f3 == 2 ? "sw" : "?");
printf("imm[11:5] = 0x%02X, imm[4:0] = 0x%02X -> imm = %d\n",
(unsigned)field(w, 31, 25), (unsigned)field(w, 11, 7), (int)imm);
printf("assembly: sw x%u, %d(x%u)\n", rs2, (int)imm, rs1);
uint32_t again = pack_s(imm, rs2, rs1, f3, op);
printf("re-packed = 0x%08X -> %s\n", (unsigned)again, again == w ? "same" : "DIFFERENT");
return 0;
}আউটপুট (Wandbox, gcc, x86-64 Linux — warning ছাড়া compile হয়):
word = 0xFE912E23
opcode = 0x23 (STORE), funct3 = 2 (sw)
imm[11:5] = 0x7F, imm[4:0] = 0x1C -> imm = -4
assembly: sw x9, -4(x2)
re-packed = 0xFE912E23 -> same
JavaScript-এর disassembler যা বলেছিল, C-ও তাই বলছে: STORE, funct3 = ২ (sw), immediate-এর দুই টুকরো 0x7F আর 0x1C, জুড়ে −৪. sext12()-এর কৌশল: ১২-বিটের two's complement মানে বিট ১১-এর ওজন −২০৪৮, বাকিগুলোর ধনাত্মক; তাই (v & 0x7FF) − (v & 0x800) = ২০৪৪ − ২০৪৮ = −৪. সব mask u-যুক্ত unsigned, তাই কোনো signed overflow নেই।
৭ · funct3 আর funct7 — একই opcode-এ অনেক কাজ
৭ বিটের opcode দিয়ে ১২৮টি শ্রেণি বানানো যায়, কিন্তু RV32I-এর প্রায় ৪০টি instruction-এর প্রতিটিকে আলাদা opcode দিলে ভবিষ্যতের extension-এর জায়গা ফুরিয়ে যেত। তাই opcode শুধু শ্রেণি বলে, আর ভেতরের পার্থক্য করে funct3 (৮টি পর্যন্ত) ও দরকার হলে funct7.
| funct3 | OP (R) | OP-IMM (I) | LOAD | STORE | BRANCH |
|---|---|---|---|---|---|
000 | add / sub | addi | lb | sb | beq |
001 | sll | slli | lh | sh | bne |
010 | slt | slti | lw | sw | — |
011 | sltu | sltiu | — | — | — |
100 | xor | xori | lbu | — | blt |
101 | srl / sra | srli / srai | lhu | — | bge |
110 | or | ori | — | — | bltu |
111 | and | andi | — | — | bgeu |
দুটি ঘরে দুটি করে নাম — add/sub আর srl/sra. এদের আলাদা করে funct7-এর বিট ৩০. একটু ভাবলে নকশাটি সুন্দর: দুই ক্ষেত্রেই বিট ৩০ = 1 মানে “একটু ভিন্ন রূপ” — যোগের বদলে বিয়োগ, logical shift-এর বদলে arithmetic shift. srai-তেও একই বিট ৩০ immediate-এর ভেতরে বসে। নিচে দেখুন decoder কীভাবে ধাপে ধাপে বাছাই করে:
A = 0x007302B3 = 0000 0000 0111 0011 0000 0010 1011 0011 B = 0x407302B3 = 0100 0000 0111 0011 0000 0010 1011 0011 ↑ বিট 30 — একমাত্র পার্থক্য
A[6:0] = B[6:0] = 0110011 → OP (R-type)
→ ALU-তে দুই register-এর মান নিয়ে কাজ
একই সময়ে: rs1 = x6, rs2 = x7 register file-এ পড়া শুরু
A[14:12] = B[14:12] = 000
OP + 000 → add বা sub — এখনো ঠিক হয়নি
A[31:25] = 0000000 → add x5 = x6 + x7 B[31:25] = 0100000 → sub x5 = x6 − x7
ALU control unit-এর ইনপুট ← {inst[30], funct3}
0 000 → ADD 1 000 → SUB
0 101 → SRL 1 101 → SRA
SUB = A + ~B + 1 (B-র বিট উল্টে, carry-in = 1)
- দুটি word, একটি বিটের পার্থক্য।
0x007302B3(add) আর0x407302B3(sub) — বাকি ৩১টি বিট হুবহু এক। প্রথম hex অঙ্ক 0 আর 4-এর পার্থক্য শুধু বিট ৩০. - Opcode দেখে শ্রেণি। দুটিরই বিট ৬..০ =
0110011, অর্থাৎ R-type ALU কাজ। Register field নির্দিষ্ট জায়গায় থাকায় এই মুহূর্তেই x6 আর x7 পড়া শুরু হয়ে গেছে — decoder-এর সিদ্ধান্তের অপেক্ষা ছাড়াই। - funct3 দিয়ে সংকীর্ণ করা। funct3 =
000, OP শ্রেণিতে এর মানে “add বা sub”. আরও একটি তথ্য লাগবে। - funct7 দিয়ে চূড়ান্ত।
0000000হলে add,0100000হলে sub. বাস্তবে hardware পুরো ৭ বিট নয়, শুধু বিট ৩০ দেখলেই চলে (বাকি বিটগুলো বৈধতা যাচাইয়ে কাজে লাগে, আর M extension-এরmulচেনাতে funct7 =0000001). - ALU পর্যন্ত। Patterson–Hennessy-এর RISC-V datapath-এ ALU control unit-এর কাছে যায় inst[30] আর funct3 — মোট ৪ বিট; সেখান থেকে সে ALU-র ৪-বিট control code বানায় (এই কোর্সে add =
0010, sub =0110).subহলে ALU দ্বিতীয় operand-এর বিট উল্টে carry-in = 1 দিয়ে যোগ করে (পর্ব ১৩-এর ALU). পুরো control টেবিল আসবে পর্ব ৩৩-এ।
একটি প্রশ্ন স্বাভাবিকভাবে আসে: subi নেই কেন? কারণ addi-এর immediate signed — addi x5, x5, -10 লিখলেই বিয়োগ হয়। একটি opcode-ঘর বাঁচল। একইভাবে sll-এর পাশে কোনো “sla” নেই, কারণ বাঁয়ে shift-এ logical আর arithmetic-এর পার্থক্যই নেই।
৮ · Immediate-এর সীমা — field-এর চওড়া থেকে হিসাব
n বিটের two's complement সংখ্যার পরিসর −২n−১ থেকে ২n−১ − ১ (পর্ব ০৫)। এর সাথে লুকানো শূন্য-বিট যোগ করলেই প্রতিটি format-এর সীমা বেরিয়ে আসে:
- I আর S: ১২ বিট → −২০৪৮ থেকে +২০৪৭. তাই
lw/swভিত্তি-ঠিকানা থেকে প্রায় ±২ KiB পর্যন্ত পৌঁছায়। - B: ১২টি বিট লেখা থাকে, সাথে লুকানো imm[0] = 0, মোট ১৩ বিট → −৪০৯৬ থেকে +৪০৯৪ byte, শুধু জোড় সংখ্যা। অর্থাৎ ±৪ KiB, বা ৪-byte instruction-এ পেছনে ১০২৪টি আর সামনে ১০২৩টি instruction পর্যন্ত।
- J: ২০টি বিট + লুকানো 0 = ২১ বিট → −১০৪৮৫৭৬ থেকে +১০৪৮৫৭৪ byte, অর্থাৎ ±১ MiB.
- U: ২০ বিট সোজা ওপরের ২০ বিটে; নিচের ১২ বিট সবসময় 0. RV32-তে এটি পুরো ৩২-বিটের একটি bit pattern (
0x00000000থেকে0xFFFFF000, ৪০৯৬-এর ধাপে)।
সীমার বাইরের মান এলে assembler কী করে? সোজা ভাষায় — error দেয়, নয়তো (pseudo-instruction হলে) একাধিক instruction বানায়। বড় ধ্রুবক যেমন 0x12345ABC লোড করতে li pseudo-instruction ভেঙে যায় lui + addi-তে। কিন্তু addi-এর ১২ বিট signed, তাই নিচের ১২ বিট 0xABC (≥ 0x800) আসলে −১৩৪৮ হিসেবে যোগ হবে; তাই ওপরের অংশে ১ বাড়িয়ে নিতে হয় — বিস্তারিত পর্ব ২৬-এ। দূরের function ডাকতে auipc + jalr জোড়া ব্যবহার হয় (±২ GiB). আর linker পরে দেখে যে লক্ষ্য কাছেই, তবে এই জোড়াকে একটি jal-এ ছোট করে ফেলতে পারে — একে বলে linker relaxation.
নিচের ক্যালকুলেটর প্রতিটি format-এর সীমা field-এর চওড়া থেকে হিসাব করে, কিছু মান যাচাই করে assembler-এর মতো error দেখায়, B আর J-এর offset-বিট কোথায় বসে তা ছাপে, আর বড় ধ্রুবককে lui + addi-তে ভাঙে:
// Immediate ranges from field widths + what an assembler must do when a value does not fit.
const F = { // format: [stored bits, implicit zero bits at the bottom, signed?]
I: [12, 0, true], S: [12, 0, true], B: [12, 1, true], J: [20, 1, true], U: [20, 12, false] };
function range(fmt) {
const [n, z, signed] = F[fmt], w = n + z; // total immediate width incl. implicit 0s
const lo = signed ? -(2 ** (w - 1)) : 0, hi = (signed ? 2 ** (w - 1) : 2 ** w) - 2 ** z;
return { n, z, w, lo, hi, step: 2 ** z };
}
function check(fmt, v) { // what the assembler does with value v
const r = range(fmt);
if (v % r.step) return `error: ${v} is not a multiple of ${r.step}`;
if (v < r.lo || v > r.hi) return `error: ${v} out of range [${r.lo}, ${r.hi}]`;
return "fits";
}
console.log("fmt stored implicit range step");
for (const f of Object.keys(F)) {
const r = range(f);
console.log(`${f} ${String(r.n).padEnd(6)} ${String(r.z).padEnd(9)} [${r.lo}, ${r.hi}]`.padEnd(48) + r.step);
}
for (const [f, v] of [["I", 2047], ["I", 2048], ["I", -2048], ["S", -2049],
["B", 4094], ["B", 4096], ["B", 13], ["J", -1048576], ["J", 1048576]])
console.log(`${f} ${String(v).padStart(8)} : ${check(f, v)}`);
const bitsOf = (v, w) => [...Array(w)].map((_, i) => (v >> (w - 1 - i)) & 1).join("");
function layout(fmt, v) { // where each offset bit sits in the word
const order = fmt === "B" ? [[12], [10, 5], [4, 1], [11]] : [[20], [10, 1], [11], [19, 12]];
const where = fmt === "B" ? ["inst[31]", "inst[30:25]", "inst[11:8]", "inst[7]"]
: ["inst[31]", "inst[30:21]", "inst[20]", "inst[19:12]"];
return order.map(([hi, lo = hi], i) =>
`imm[${hi}${lo !== hi ? ":" + lo : ""}]=${bitsOf(v >> lo, hi - lo + 1)} -> ${where[i]}`).join(" ");
}
console.log("B -20 :", layout("B", -20));
console.log("J 2048 :", layout("J", 2048));
for (const v of [0x12345678, 0x12345ABC]) { // 32-bit constant = U part + sign-extended 12-bit part
const hi = (v + 0x800) >>> 12, lo = v - hi * 4096;
console.log(`0x${v.toString(16)} = lui 0x${hi.toString(16)} + addi ${lo} -> 0x${(hi * 4096 + lo).toString(16)}`); }আউটপুট (নির্ধারিত — প্রতিবার একই):
fmt stored implicit range step
I 12 0 [-2048, 2047] 1
S 12 0 [-2048, 2047] 1
B 12 1 [-4096, 4094] 2
J 20 1 [-1048576, 1048574] 2
U 20 12 [0, 4294963200] 4096
I 2047 : fits
I 2048 : error: 2048 out of range [-2048, 2047]
I -2048 : fits
S -2049 : error: -2049 out of range [-2048, 2047]
B 4094 : fits
B 4096 : error: 4096 out of range [-4096, 4094]
B 13 : error: 13 is not a multiple of 2
J -1048576 : fits
J 1048576 : error: 1048576 out of range [-1048576, 1048574]
B -20 : imm[12]=1 -> inst[31] imm[10:5]=111111 -> inst[30:25] imm[4:1]=0110 -> inst[11:8] imm[11]=1 -> inst[7]
J 2048 : imm[20]=0 -> inst[31] imm[10:1]=0000000000 -> inst[30:21] imm[11]=1 -> inst[20] imm[19:12]=00000000 -> inst[19:12]
0x12345678 = lui 0x12345 + addi 1656 -> 0x12345678
0x12345abc = lui 0x12346 + addi -1348 -> 0x12345abc
টেবিলটি উপরের তালিকার সাথে মেলে: I/S [−২০৪৮, ২০৪৭], B [−৪০৯৬, ৪০৯৪] ধাপ ২, J [−১০৪৮৫৭৬, ১০৪৮৫৭৪] ধাপ ২, আর U-তে শুধু ৪০৯৬-এর গুণিতক (৪২৯৪৯৬৩২০০ = 0xFFFFF000). addi-তে ২০৪৮ ধরে না, অথচ −২০৪৮ ধরে — two's complement-এর অসমতা। B-তে ১৩ বিজোড়, তাই “multiple of 2” error. B −২০-এর বিন্যাস অংশ ৬-এর disassembler-এ পাওয়া bne x10, x0, -20 = 0xFE0516E3-এর সাথে মেলে: inst[31] = 1, inst[11:8] = 0110, inst[7] = 1. J ২০৪৮-এ একমাত্র 1-বিট inst[20]-এ, ঠিক 0x001000EF-এর মতো। শেষ দুই লাইনে 0x12345ABC-এর জন্য lui 0x12346 আর addi -1348 — ওপরের অংশ ১ বেড়েছে, কারণ নিচের অংশ ঋণাত্মক হিসেবে যোগ হবে।
৯ · ঘনত্বের লড়াই — C extension, encoding-বাজেট, x86 আর ARM
১৬-বিটের C extension
৩২-বিটের নিয়মিত encoding decode-এর জন্য চমৎকার, কিন্তু জায়গা কিছুটা নষ্ট করে: addi x5, x5, 10-এ rd আর rs1 একই, immediate ছোট — তবুও পুরো ৩২ বিট। RISC-V-এর ঐচ্ছিক C extension (RVC) সবচেয়ে বেশি ব্যবহৃত instruction-গুলোর ১৬-বিটের রূপ দেয়, কয়েকটি শর্তে:
- দুই-operand রূপ:
c.add x5, x7মানেadd x5, x5, x7(rd = rs1) =0x929E. - ছোট immediate:
c.addi x5, 10=0x02A9, যাaddi x5, x5, 10-এর সমান; এখানে immediate মাত্র ৬ বিট (−৩২..৩১)। - কম register:
c.lw,c.sw-এর মতো অনেক instruction-এ register field ৩ বিট — শুধু x8..x15, যেগুলো compiler সবচেয়ে বেশি ব্যবহার করে।
প্রতিটি ১৬-বিটের instruction ঠিক একটি ৩২-বিটের instruction-এর সমান। তাই hardware decode-এর শুরুতেই একে ৩২-বিটের রূপে “খুলে” নিতে পারে, পেছনের pipeline কিছুই টের পায় না। RISC-V manual-এর হিসাবে সাধারণ প্রোগ্রামে প্রায় ৫০–৬০% instruction এভাবে ছোট করা যায়, ফলে কোডের মাপ মোটামুটি ২৫–৩০% কমে — প্রোগ্রাম আর compiler ভেদে সংখ্যাটি বদলায়।
মাপ চেনার নিয়ম — সর্বনিম্ন দুই বিট
CPU কীভাবে জানবে পরের ২ byte একটি পুরো instruction, না একটি ৩২-বিটের প্রথম অর্ধেক? উত্তর: সর্বনিম্ন দুই বিট। 11 হলে ৩২-বিট (বা তার বেশি), আর 00, 01, 10 হলে ১৬-বিট। 0x929E-এর শেষ hex অঙ্ক E = 1110, শেষ দুই বিট 10 → ১৬-বিট; 0x02A9-এ 01 → ১৬-বিট; আর 0x007302B3-এ 11 → ৩২-বিট। এজন্যই RV32I-এর সব opcode 11-এ শেষ। x86-এর মতো পুরো instruction পড়ে মাপ বুঝতে হয় না — প্রথম দুটি বিটই যথেষ্ট। (Manual-এ ৪৮ আর ৬৪ বিটের instruction-এর জন্যও বিট-ছাঁচ সংরক্ষিত আছে, যদিও এখনো খুব কম ব্যবহৃত।)
বাজেটের হিসাব — register বাড়ালে কী হয়
R-type-এ তিনটি register field = ৩ × ৫ = ১৫ বিট, opcode ৭, funct3 ৩, funct7 ৭ — মোট ৩২. যদি ৬৪টি register থাকত, প্রতিটি field ৬ বিট, তিনটিতে ১৮; তখন funct7 থেকে ৩ বিট কাটতে হতো, আর I-type-এর immediate ১২ থেকে ১০ বিটে নামত (পরিসর ±৫১২)। ১৬টি register হলে উল্টো — বিট বাঁচে, কিন্তু compiler-কে বারবার মান মেমরিতে রেখে আবার আনতে হয় (spill). ৩২ একটি পরীক্ষিত মাঝামাঝি; MIPS, ARM A64 আর RISC-V সবাই প্রায় এখানেই থেমেছে। ছোট embedded chip-এর জন্য RISC-V-এর RV32E রূপে register মাত্র ১৬টি — encoding একই, শুধু x16..x31 নিষিদ্ধ।
আরেকটি বাজেট হলো opcode space — ভবিষ্যতের জন্য খালি রাখা ঘর। শেষ দুই বিট 11 ধরে নিলে ৩২-বিটের major opcode বড়জোর ৩২টি। RV32I এর মধ্যে মাত্র ১১টি নেয়; বাকিগুলো যায় M (গুণ-ভাগ), A (atomic), F/D (floating point), V (vector)-এর মতো standard extension-এ, আর কয়েকটি ঘর (custom-0, custom-1…) ইচ্ছা করেই কোম্পানিগুলোর নিজস্ব instruction-এর জন্য খালি রাখা।
x86-64 — prefix, opcode, ModR/M, SIB
x86-64-এর একটি instruction কয়েকটি ঐচ্ছিক অংশ দিয়ে তৈরি, ক্রম মেনে:
[prefix…] [REX] opcode(1–3) [ModR/M] [SIB] [displacement 1/2/4] [immediate 1/2/4/8]
সর্বোচ্চ মোট ১৫ byte
- Prefix: যেমন
66(operand-এর মাপ ১৬ বিট),F0(lock). ৬৪-বিট মোডে REX prefix (40–4F) ৬৪-বিট operand আর r8–r15 register চেনায়। AVX-এর জন্য আরও আছে VEX/EVEX. - Opcode: ১ থেকে ৩ byte;
0Fদিয়ে শুরু হলে “আরও opcode আসছে”। - ModR/M: ১ byte —
mod(২ বিট),reg(৩),r/m(৩); বলে operand register না মেমরি। - SIB: scale-index-base,
base + index × scaleধরনের ঠিকানার জন্য। - Displacement আর immediate: সরাসরি সংখ্যা, little-endian ক্রমে।
অংশ ২-এর উদাহরণটি byte ধরে ভাঙা যাক — add rax, [rbx+rcx*4+8] (Intel syntax) = 48 03 44 8B 08 (Intel manual-এর নিয়মে হাতে বের করা; assembler-ভেদে অন্য বৈধ রূপ সম্ভব):
| byte | অংশ | বিট | অর্থ |
|---|---|---|---|
48 | REX | 0100 1000 | W = 1 → ৬৪-বিট operand (rax) |
03 | opcode | 0000 0011 | ADD r64, r/m64 — ফল register-এ |
44 | ModR/M | 01 000 100 | mod = 01 (৮-বিট displacement), reg = 000 (rax), r/m = 100 (SIB আসছে) |
8B | SIB | 10 001 011 | scale = 10 (×৪), index = 001 (rcx), base = 011 (rbx) |
08 | disp8 | 0000 1000 | +৮ |
একটি RISC-V instruction যেখানে একটি কাজ করে, এই ৫ byte-এর x86 instruction সেখানে একটি ঠিকানা হিসাব, মেমরি থেকে পড়া আর যোগ — তিনটি কাজ করে। RISC-V-এ এজন্য লাগত slli, add, lw, add — চারটি instruction, ১৬ byte (C extension-এ কিছুটা কম)। এটাই CISC ঘনত্বের সুবিধা।
x86 decode কেন কঠিন
সমস্যাটা হলো দৈর্ঘ্য। একটি ১৬ byte-এর টুকরো আনলে তার ভেতরে কয়টি instruction, কোথায় কোনটি শুরু — প্রথমটির prefix, opcode আর ModR/M না পড়ে বলা যায় না; আর দ্বিতীয়টির শুরু জানতে প্রথমটির দৈর্ঘ্য লাগে। এক cycle-এ চার-পাঁচটি instruction decode করতে চাইলে hardware-কে অনেক byte-অবস্থান থেকে একসাথে “এখান থেকে শুরু হলে দৈর্ঘ্য কত” অনুমান করতে হয়। এই length decoding-এ অনেক transistor আর শক্তি খরচ হয়। আধুনিক Intel আর AMD CPU তাই আগে decode করা micro-op একটি micro-op cache-এ রেখে দেয়, যাতে লুপের কোড বারবার decode করতে না হয় — superscalar-এর front end-এর প্রসঙ্গে পর্ব ৩৯-এ এটি আবার আসবে।
ARM A64 — নির্দিষ্ট ৩২ বিট, ভিন্ন সাজানো
২০১১ সালে ঘোষিত ARMv8-A-এর A64 নতুন করে সাজানো হয়: সব instruction ৩২ বিট, ৩১টি সাধারণ register (৫-বিট field), আর ১৬-বিটের Thumb রাখা হয়নি। কিন্তু field-এর জায়গা RISC-V-এর মতো নয়: A64-এ Rd থাকে সবচেয়ে নিচে [4:0], Rn [9:5]-এ, আর opcode-এর বিট ছড়ানো ওপরের দিকে। add x0, x1, x2 = 0x8B020020: ওপরের 8B বলে “৬৪-বিট ADD (shifted register)”, Rm = ২ বসে [20:16]-এ, Rn = ১ বসে [9:5]-এ, Rd = ০. দুই ISA-ই একই নীতি মানে — register field নির্দিষ্ট জায়গায় — শুধু জায়গাগুলো আলাদা।
আরও গভীরে: উচ্চ গতির RISC-V core-এ C extension একটি নতুন জটিলতা আনে: ৩২-বিটের instruction ২ byte-এর সীমানায় শুরু হতে পারে, তাই cache line বা page-এর সীমানা পেরিয়ে অর্ধেক-অর্ধেক থাকতে পারে, আর fetch unit-কে টুকরো জোড়া লাগাতে হয়। এ নিয়ে RISC-V সমাজে বিতর্কও হয়েছে — কেউ কেউ বড় application processor-এর profile-এ C-কে ঐচ্ছিক করার প্রস্তাব দিয়েছেন। অন্যদিকে Intel ২০২৩ সালে APX নামের একটি প্রস্তাবে x86-64-এ ৩২টি সাধারণ register আনার জন্য নতুন prefix (REX2) বর্ণনা করেছে — অর্থাৎ পুরোনো ISA-ও encoding-বাজেটের সেই একই প্রশ্নের মুখোমুখি।
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
addi x5, x5, 10-এর hex কত? —0x00A28293(imm00A, rs1 = rd = ৫, opcode0x13).- RISC-V-এ rs1, rs2, rd সব format-এ এক জায়গায় কেন? — decode-এর সাথে একই সময়ে register file পড়া যায়, আর লেখার register বাছতে mux লাগে না।
- B-type-এর পরিসর কত, কেন? — ১২টি বিট + লুকানো imm[0] = 0, তাই ১৩-বিট signed: −৪০৯৬..+৪০৯৪ byte.
addআরsubকীভাবে আলাদা হয়? — opcode আর funct3 এক; funct7-এর বিট ৩০ (0 = add, 1 = sub).- একটি RISC-V word ১৬ না ৩২ বিট, কীভাবে বুঝবেন? — সর্বনিম্ন দুই বিট
11হলে ৩২-বিট, নইলে ১৬-বিট (C extension).
১০ · AI-এর যুগে — encoding-এর দাবি যাচাই
Instruction encoding-এ AI-এর ভুল ধরা কঠিন, কারণ উত্তর দেখতে নিখুঁত লাগে — একটি সুন্দর বিট-টেবিল আর একটি hex. কিন্তু একটি বিট এদিক-ওদিক হলেই সম্পূর্ণ অন্য instruction. সুখবর হলো, এখানে সবকিছু যন্ত্রে যাচাই করা যায়: একটি assembler, একটি disassembler আর manual-এর টেবিল।
| AI প্রায়ই যা করে | সত্যিটা | যাচাইয়ের উপায় |
|---|---|---|
| MIPS-এর format (rs, rt, rd, shamt, ১৬-বিট imm) দিয়ে RISC-V বোঝায় | RISC-V-এ funct3/funct7, ১২-বিট imm, ভিন্ন বিট-অবস্থান | RISC-V unprivileged manual-এর format ছবি |
sw-তে rs1 আর rs2 উল্টে ফেলে | sw rs2, imm(rs1); rs2 = যে মান রাখা হবে | objdump -d দিয়ে disassemble |
| B-type-এ imm[0] বসিয়ে দেয় বা offset-কে instruction-সংখ্যা ধরে | offset byte-এ, imm[0] লেখা হয় না | নিজের disassembler বা llvm-mc |
| ঋণাত্মক immediate-এর hex ভুল, sign extension বাদ | ১২-বিট two's complement, বিট ৩১ = চিহ্ন | হাতে two's complement, স্ক্রিপ্টে মিলিয়ে দেখা |
| “C extension কোড অর্ধেক করে দেয়” | সাধারণত ২৫–৩০% কম, প্রোগ্রাম-ভেদে | একই কোড -march=rv32i আর rv32ic-এ compile করে মাপ তুলনা |
| x86 instruction-এর দৈর্ঘ্য বা byte বানিয়ে বলে | ১–১৫ byte; একাধিক বৈধ encoding থাকতে পারে | assembler-এর listing (as -al) বা objdump |
AI-কে encoding-এর প্রশ্ন করার সময় কাজটি ধাপে ভাগ করে নিন, আর যাচাইয়ের উপায়ও চেয়ে নিন:
AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১RISC-V immediate-এর বিট ছড়িয়ে দিল, অথচ MIPS ১৬-বিট immediate পাশাপাশি রেখেছিল। কোনটি ভালো নকশা?
দুটি নকশা দুই যুগের দুই অগ্রাধিকার থেকে এসেছে। MIPS (১৯৮০-এর দশক) ৩ ধরনের format রেখেছিল: R, I আর J. I-type-এ ১৬-বিটের immediate একটানা বিট ১৫..০-তে — পড়া সহজ, পরিসরও বড় (load-store-এর offset ±৩২ KiB). কিন্তু এর দাম ছিল অন্য জায়গায়। I-type-এ ফলের register বসে rt field-এ (বিট ২০..১৬), আর R-type-এ rd field-এ (বিট ১৫..১১)। ফলে datapath-এ লেখার register বাছতে একটি mux আর RegDst control signal লাগে — Patterson–Hennessy-এর MIPS সংস্করণে যা প্রায় প্রতিটি পাঠককে মুখস্থ করতে হতো।
RISC-V-এর নকশাকারীরা (Berkeley-র দল, ২০১০ থেকে) কয়েক দশকের pipeline নকশার অভিজ্ঞতা নিয়ে উল্টো দিক থেকে ভাবলেন: কোন বিট কোথায় থাকলে hardware সবচেয়ে সরল ও দ্রুত হয়? তাঁদের সিদ্ধান্ত: register field কখনো নড়বে না, sign bit সবসময় বিট ৩১-এ, আর immediate-এর প্রতিটি বিটের উৎস format জুড়ে যত কম সম্ভব। এর ফলে S, B, J-এর immediate দেখতে এলোমেলো হলো। কিন্তু immediate-generator circuit-এ প্রতিটি বিটের জন্য মাত্র ২–৪ ইনপুটের mux লাগে, আর sign extension decode-এর অপেক্ষা ছাড়াই শুরু হয়।
মানুষের অসুবিধা একবারই — যে assembler বা disassembler লেখে, সে একবার টেবিলটি কোডে তোলে। Chip-এ সুবিধা প্রতিটি instruction-এ, প্রতিটি cycle-এ। দামটিও আছে: RISC-V-এর ১২-বিট immediate MIPS-এর ১৬-বিটের চেয়ে ছোট, তাই কিছু ধ্রুবকের জন্য বাড়তি lui লাগে। নকশাকারীরা মেপে দেখেছিলেন বাস্তব প্রোগ্রামের বেশিরভাগ immediate ছোট, তাই দামটি সামান্য।
মূল উপলব্ধি: ISA নকশায় “সুন্দর” মানে মানুষের চোখে পরিপাটি নয়, hardware-এ সরল। যে কাজ কোটি কোটিবার হয় তাকে সস্তা করুন, যে কাজ একবার হয় তার জটিলতা মেনে নিন।
প্র ০২x86-এর encoding এত জটিল হলেও চার দশক ধরে টিকে আছে কেন? Variable length কি শুধুই বোঝা?
১৯৮০ আর ’৯০-এর দশকে অনেকেই ভেবেছিলেন RISC-এর নিয়মিত encoding-এর কাছে x86 হেরে যাবে। হয়নি, আর এর পেছনে প্রযুক্তি আর ব্যবসা দুটোই আছে।
প্রথম কারণ — পুরোনো software: ১৯৮১ সালের IBM PC থেকে শুরু করে বিপুল পরিমাণ সফটওয়্যার x86 machine code-এ লেখা। Intel আর AMD প্রতিটি নতুন সুবিধা যোগ করেছে পুরোনো byte-এর অর্থ না বদলে: ৩২-বিটে operand-size prefix, ৬৪-বিটে REX prefix (AMD, ২০০৩-এর কাছাকাছি সময়ে বাজারে), SIMD-এর জন্য 0F escape আর পরে VEX/EVEX. এই backward compatibility x86-এর সবচেয়ে বড় সম্পদ।
দ্বিতীয় কারণ — ঘনত্ব: অংশ ৯-এর উদাহরণে একটি ৫-byte instruction RISC-V-এর চারটি instruction-এর কাজ করে। ছোট instruction মানে instruction cache-এ বেশি কোড, fetch-এ কম bandwidth.
তৃতীয় কারণ — microarchitecture-এর কৌশল: ১৯৯৫ সালের Pentium Pro থেকে Intel ভেতরে x86 instruction-কে সরল RISC-ধাঁচের micro-op-এ ভেঙে চালায়। Decode কঠিন, কিন্তু সেটি শুধু সামনের অংশে; পেছনের execution engine RISC-এর মতোই কাজ করে। পরে micro-op cache এসে লুপের কোডে decode-এর খরচ আরও কমিয়েছে।
তবে দামটা বাস্তব: decoder-এ বাড়তি transistor, শক্তি আর নকশার জটিলতা; আর একসাথে অনেক instruction decode করা কঠিন। মোবাইলে, যেখানে প্রতিটি মিলিওয়াট গুরুত্বপূর্ণ, ARM-এর আধিপত্য এর একটি কারণ বলে অনেকে মনে করেন (যদিও শক্তির পার্থক্যের বড় অংশ ISA নয়, microarchitecture আর নকশার লক্ষ্য থেকে আসে — গবেষকদের মধ্যে এ নিয়ে মতভেদ আছে)।
মূল উপলব্ধি: একটি ISA-র encoding একবার জনপ্রিয় হলে তা বদলানো প্রায় অসম্ভব; ইঞ্জিনিয়াররা তখন তার দুর্বলতা ঢাকতে microarchitecture-এ কৌশল খোঁজেন। শুরুর নকশার সিদ্ধান্ত তাই দশকের পর দশক টিকে থাকে।
প্র ০৩১৬-বিটের compressed instruction কি “RISC”-এর নীতির বিরুদ্ধে যায়? ARM A64 Thumb বাদ দিল কেন, অথচ RISC-V C রাখল?
RISC-এর মূল কথা “সব instruction ঠিক ৩২ বিট” নয়; মূল কথা হলো decode সরল রাখা আর hardware-এর ওপর জটিল কাজ না চাপানো। প্রশ্ন হলো ১৬-বিট যোগ করলে সেই সরলতা কতটা ভাঙে।
ARM ১৯৯০-এর দশকের মাঝামাঝি Thumb আনে মূলত মোবাইল ফোন আর embedded যন্ত্রের জন্য, যেখানে ROM আর মেমরি দামি ছিল। প্রথম দিকে Thumb ছিল আলাদা একটি “মোড” — প্রসেসরকে বিশেষ branch দিয়ে মোড বদলাতে হতো। পরে Thumb-2 একই স্রোতে ১৬ আর ৩২ বিট মেশায়, আর আজকের Cortex-M microcontroller শুধু এটিই চালায়। কিন্তু ARMv8-A-এর A64 (২০১১) ডিজাইনের সময় লক্ষ্য ছিল উচ্চ গতির phone আর server core. সেখানে বড় cache আছে, কোডের মাপ তুলনামূলক কম গুরুত্বপূর্ণ, আর এক cycle-এ অনেক instruction decode করতে নির্দিষ্ট ৪ byte অনেক সুবিধা দেয়। তাই A64-এ কোনো ১৬-বিট রূপ নেই।
RISC-V একই ISA দিয়ে ছোট microcontroller থেকে server পর্যন্ত ঢাকতে চায়, তাই C-কে ঐচ্ছিক extension করেছে। এর নকশায় দুটি সতর্কতা আছে: প্রতিটি ১৬-বিটের instruction ঠিক একটি ৩২-বিটের instruction-এ “খোলা” যায়, আর মাপ বোঝা যায় শুধু দুটি বিট দেখে। কোনো মোড বদল নেই। ফলে ছোট core-এ decode-এর শুরুতে একটি ছোট expander বসালেই চলে।
তবু বড় core-এ এর একটি দাম আছে (আরও গভীরে বাক্স দেখুন): ৩২-বিটের instruction ২ byte-এর সীমানায় শুরু হতে পারে, তাই একসাথে অনেক instruction খুঁজে বের করা কিছুটা কঠিন হয় — x86-এর মতো ভয়াবহ নয়, কিন্তু শূন্যও নয়।
মূল উপলব্ধি: নীতির চেয়ে লক্ষ্য বড়। একই প্রশ্নের উত্তর microcontroller-এর জন্য এক, server-এর জন্য আরেক — ভালো নকশাকার আগে জিজ্ঞেস করেন “কার জন্য বানাচ্ছি?”
প্র ০৪RISC-V-এ register ঠিক ৩২টি কেন — ১৬ বা ৬৪ নয় কেন? Encoding এখানে কী বলে?
Register-এর সংখ্যা সরাসরি encoding-এর বাজেটে বসে। n টি register চিনতে লাগে log2n বিট, আর R-type-এ তিনটি register field. ৩২টিতে ১৫ বিট, ৬৪টিতে ১৮ বিট, ১৬টিতে ১২ বিট।
৬৪ হলে: বাড়তি ৩ বিট কোথাও থেকে কাটতে হতো। I-type-এ immediate ১২ থেকে ১০ বিটে নামত, পরিসর ±৫১২ — অনেক বেশি ধ্রুবকে বাড়তি instruction লাগত। আবার বেশি register মানে বড় register file: বেশি port-এর বড় মেমরি ধীর হয়, শক্তি বেশি খায়, আর function call-এ বেশি register বাঁচাতে হয়। Compiler-এর অভিজ্ঞতায় সাধারণ কোডে ৩২-এর ওপরে লাভ দ্রুত কমে আসে।
১৬ হলে: বিট বাঁচে, কিন্তু compiler-এর হাতে কম জায়গা; বেশি মান মেমরিতে spill হয়, বেশি load-store. পুরোনো x86-এ ৮টি সাধারণ register ছিল, x86-64 করল ১৬, আর অংশ ৯-এর APX প্রস্তাব ৩২-এ নিতে চায় — বোঝা যায় শিল্পও ৩২-কে একটি ভালো জায়গা মনে করে। ARM-এর ৩২-বিট জগতে ছিল ১৬টি, A64-এ তা বেড়ে ৩১টি সাধারণ register.
একটি সূক্ষ্মতা: ISA-র ৩২টি register মানেই chip-এ ৩২টি register নয়। আধুনিক out-of-order core-এ register renaming (পর্ব ৪০) ভেতরে শতাধিক physical register রাখে। ISA-র সংখ্যা শুধু compiler কতগুলো “নাম” দেখতে পায়, তা ঠিক করে — আর সেই নামের জন্যই বিট খরচ হয়। তাই ছোট embedded chip-এর জন্য RV32E ১৬টি register রাখে (register file-এর জায়গা আর শক্তি বাঁচে), অথচ encoding-এর ছাঁচ বদলায় না — field এখনো ৫ বিট, শুধু ওপরের অর্ধেক নিষিদ্ধ।
মূল উপলব্ধি: ৩২ কোনো জাদুর সংখ্যা নয়; এটি register-চাহিদা, immediate-এর পরিসর আর register file-এর গতি — এই তিনটির মাঝে মাপা একটি সমঝোতা।
প্র ০৫একটি ISA ভবিষ্যতের জন্য opcode-এর জায়গা কীভাবে বাঁচায়? জায়গা ফুরিয়ে গেলে কী হয়?
একটি ISA ২০–৩০ বছর বাঁচে, অথচ নকশার দিন কেউ জানে না ১০ বছর পরে কোন instruction লাগবে — vector, cryptography, AI-এর জন্য matrix. তাই opcode space হলো শহরের খালি জমির মতো: আজ ফেলে রাখলে কাল কাজে লাগে।
x86-এর অভিজ্ঞতা: 8086-এর এক-byte opcode প্রায় সব ভরে গিয়েছিল। নতুন instruction-এর জন্য তাই “escape” কৌশল: 0F দিয়ে শুরু মানে দুই-byte opcode, পরে 0F 38 আর 0F 3A দিয়ে তিন-byte. AVX-এর জন্য VEX, AVX-512-এর জন্য EVEX — প্রতিবার instruction আরও লম্বা হলো। জায়গা ফুরালে encoding বাড়ে, decode আরও জটিল হয়।
RISC-V-এর পরিকল্পনা: ৩২-বিটের ৩২টি major opcode-এর মধ্যে RV32I অল্প কয়েকটি নেয়। M, A, F, D, V-এর মতো standard extension কিছু ঘর পায়, আর কয়েকটি ঘর (custom-0..3) আনুষ্ঠানিকভাবে কোম্পানির নিজস্ব instruction-এর জন্য সংরক্ষিত — যেমন একটি AI accelerator তার নিজের matrix instruction সেখানে বসাতে পারে, standard software না ভেঙে। এ ছাড়া ৪৮ বা ৬৪ বিটের লম্বা instruction-এর জন্য বিট-ছাঁচও রাখা আছে।
ঝুঁকিটাও আছে: প্রত্যেকে নিজের extension বানালে একটি RISC-V chip-এর প্রোগ্রাম আরেকটিতে না-ও চলতে পারে — fragmentation. এর সমাধানে RISC-V International “profile” সংজ্ঞায়িত করে (যেমন application processor-এর জন্য RVA-সিরিজ), যেখানে বলা থাকে কোন extension-গুলো অবশ্যই থাকবে, যাতে Linux distribution-এর মতো software একটি নির্দিষ্ট ভিত্তির ওপর ভরসা করতে পারে। বাংলাদেশসহ বিভিন্ন দেশের বিশ্ববিদ্যালয় আর ছোট নকশা-দলের জন্য এই খোলা ISA (open ISA) একটি সুযোগ — লাইসেন্স ছাড়াই নিজের core বানিয়ে শেখা ও পরীক্ষা করা যায়।
মূল উপলব্ধি: ভালো encoding শুধু আজকের instruction-কে জায়গা দেয় না, কালকের জন্য খালি জমি রাখে। আর খালি জমির ব্যবহারের নিয়ম না থাকলে স্বাধীনতাই বিশৃঙ্খলা হয়ে যায়।
অনুশীলন
খাতা-কলম নিন; প্রতিটি উত্তর ৪ বিট করে দল বেঁধে লিখুন, তারপর উপরের assembler বা disassembler-এ মিলিয়ে নিন।
-
Encode করুন: (ক)
sub x10, x11, x12(খ)addi x10, x0, -5. প্রতিটির field, binary আর hex লিখুন।(ক) R-type, opcode
0110011, funct3000, funct70100000(sub); rd = ১০ =01010, rs1 = ১১ =01011, rs2 = ১২ =01100.0100000 01100 01011 000 01010 0110011 = 0100 0000 1100 0101 1000 0101 0011 0011 = 0x40C58533(খ) I-type, opcode
0010011, funct3000; rd = ১০, rs1 = ০. −৫: ৫ =0000 0000 0101, উল্টে1111 1111 1010, +১ =1111 1111 1011=0xFFB.111111111011 00000 000 01010 0010011 = 1111 1111 1011 0000 0000 0101 0001 0011 = 0xFFB00513এটিই
li a0, -5pseudo-instruction-এর রূপ (ছোট ধ্রুবকে একটিমাত্রaddi). -
হাতে decode করুন: (ক)
0xFFC4A303(খ)0xFE031AE3. দ্বিতীয়টিতে immediate-এর প্রতিটি টুকরো কোথা থেকে এল, তা দেখান।(ক)
1111 1111 1100 0100 1010 0011 0000 0011. opcode0000011= LOAD; rd [11:7] =00110= x6; funct3 =010= lw; rs1 [19:15] =01001= x9; imm [31:20] =1111 1111 1100= −৪. ফল:lw x6, -4(x9).(খ)
1111 1110 0000 0011 0001 1010 1110 0011. opcode1100011= BRANCH; funct3 =001= bne; rs1 =00110= x6; rs2 =00000= x0. Immediate: inst[31] = 1 → imm[12]; inst[7] = 1 → imm[11]; inst[30:25] =111111→ imm[10:5]; inst[11:8] =1010→ imm[4:1]; imm[0] = 0. জুড়ে1 1111 1111 0100=0x1FF4= ৮১৮০; বিট ১২ = 1, তাই ৮১৮০ − ৮১৯২ = −১২. ফল:bne x6, x0, -12— x6 শূন্য না হলে PC ৩টি instruction পেছনে যায় — একটি সাধারণ countdown লুপ। -
সীমার হিসাব: (ক)
addi x5, x5, 3000কি একটি instruction-এ লেখা যায়? (খ) একটিbeqথেকে লক্ষ্য ৫০০০ byte সামনে — কী হবে? (গ)jalসর্বোচ্চ কয়টি ৪-byte instruction সামনে লাফাতে পারে?(ক) না। I-type-এর পরিসর −২০৪৮..২০৪৭, ৩০০০ বাইরে। Assembler error দেবে। বিকল্প: ধ্রুবকটি আগে একটি register-এ আনুন (
li x6, 3000, যাlui+addi-তে ভাঙে: ৩০০০ =0xBB8, নিচের ১২ বিট ≥0x800, তাইlui x6, 1আরaddi x6, x6, -1096; ৪০৯৬ − ১০৯৬ = ৩০০০), তারপরadd x5, x5, x6. অথবা দুটিaddi(২০০০ + ১০০০)।(খ) B-type-এর পরিসর +৪০৯৪ পর্যন্ত, ৫০০০ বাইরে। Assembler সাধারণত branch-এর শর্ত উল্টে একটি ছোট লাফ আর একটি
jalবানায়:bne x5, x6, skipতারপরjal x0, target(±১ MiB),skip:— কিছু assembler নিজে থেকেই এটি করে, কিছু error দেয়।(গ) সর্বোচ্চ offset +১০৪৮৫৭৪ byte. ৪-এর গুণিতক হতে হবে, তাই সবচেয়ে বড় ব্যবহারযোগ্য offset ১০৪৮৫৭২ = ৪ × ২৬২১৪৩, অর্থাৎ সামনে ২৬২১৪৩টি instruction (পেছনে ২৬২১৪৪টি)।
-
কোড লিখুন: একটি C ফাংশন
int32_t b_imm(uint32_t w)লিখুন যা একটি B-type word থেকে branch offset (sign extend করা) ফেরত দেয়।0xFEB50CE3-এ এটি কত দেবে, আগে অনুমান করুন।অনুমান: −৮ (উপরের ডায়াগ্রাম)। একটি সমাধান — প্রতিটি টুকরো তার জায়গায় সরিয়ে OR, তারপর ১৩-বিট sign extension, signed shift ছাড়াই:
#include <stdint.h> int32_t b_imm(uint32_t w) { uint32_t v = ((w >> 31) & 1u) << 12 /* inst[31] -> imm[12] */ | ((w >> 7) & 1u) << 11 /* inst[7] -> imm[11] */ | ((w >> 25) & 0x3Fu) << 5 /* inst[30:25] -> imm[10:5] */ | ((w >> 8) & 0xFu) << 1; /* inst[11:8] -> imm[4:1] */ return (int32_t)(v & 0xFFFu) - (int32_t)(v & 0x1000u); /* 13-bit sign */ } /* b_imm(0xFEB50CE3) == -8, b_imm(0x00628663) == 12, b_imm(0xFE0516E3) == -20 */শেষ লাইনের কৌশল অংশ ৬-এর
sext12()-এর মতো: ১৩-বিটের two's complement-এ বিট ১২-এর ওজন −৪০৯৬.0xFEB50CE3-এ v =0x1FF8: ৪০৮৮ − ৪০৯৬ = −৮.
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি RISC-V word হাতে decode করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- hex-কে binary-তে লিখে ডান থেকে বিট গোনা
- সর্বনিম্ন দুই বিট দেখে ১৬ না ৩২ বিট, তা ঠিক করা
- opcode [6:0] পড়ে format (R/I/S/B/U/J) চেনা
- funct3 (আর দরকারে funct7) দেখে নির্দিষ্ট instruction বাছা
- format-এর নিয়মে immediate-এর টুকরো জুড়ে sign extend করা
RISC-V RV32I-এ যেসব format-এ rs1 থাকে, সেখানে rs1 field কোন বিটে থাকে?
- বিট ১১..৭
- বিট ১৯..১৫
- বিট ২৪..২০
- format ভেদে বদলায়
0x007302B3 হলো add x5, x6, x7. 0x407302B3 কোন instruction?
- lw x5, 7(x6)
- xor x5, x6, x7
- sub x5, x6, x7
- sra x5, x6, x7
RISC-V-এর B-type branch-এ offset-এর জন্য ১২টি বিট লেখা থাকে। Offset-এর পরিসর কত?
- −4096 থেকে +4094 byte (শুধু জোড়)
- −2048 থেকে +2047 byte
- −1048576 থেকে +1048574 byte
- −4096 থেকে +4094টি instruction
RISC-V-এ C extension থাকলে CPU কীভাবে বোঝে একটি instruction ১৬ বিটের?
- একটি বিশেষ branch দিয়ে “compressed মোডে” যেতে হয়
- বিট ১৫ = 1 হলে
- আগে একটি prefix byte আসে
- সর্বনিম্ন দুই বিট 11 না হলে
RISC-V word 0x00A28293 decode করলে কী পাওয়া যায়?
- add x5, x5, x10
- addi x5, x5, 10
- lw x5, 10(x5)
- addi x10, x10, 5
RISC-V-এর I, S, B, U, J — সব format-এ immediate-এর sign bit কেন instruction-এর বিট ৩১-এ রাখা হয়েছে?
- Little-endian মেমরিতে এটি প্রথম byte হয় বলে
- বিট ৩১ আসলে opcode-এর অংশ বলে
- Format decode হওয়ার আগেই sign extension শুরু করা যায় বলে
- মানুষের পক্ষে hex পড়া সহজ হয় বলে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- ছয় format: R, I, S, B, U, J; opcode [6:0], rd [11:7], funct3 [14:12], rs1 [19:15], rs2 [24:20], funct7 [31:25] — register field কখনো জায়গা বদলায় না।
- Immediate: sign সবসময় inst[31]; S-এ দুই টুকরো, B আর J-তে ছড়ানো আর imm[0] = 0 লুকানো — জোড়া লাগিয়ে তারপর sign extend.
- সীমা: I/S −২০৪৮..২০৪৭, B ±৪ KiB, J ±১ MiB, U ওপরের ২০ বিট; বাইরে গেলে
lui/auipcবা উল্টানো branch +jal. - বাছাই: opcode → funct3 → funct7;
add/subআরsrl/sra-এর পার্থক্য শুধু বিট ৩০. জানা উদাহরণ:0x007302B3,0x00A28293. - অন্যরা: C extension-এ ১৬ বিট (নিচের দুই বিট ≠ 11); x86 ১–১৫ byte, decode কঠিন, micro-op cache; ARM A64 নির্দিষ্ট ৩২ বিট।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৮ · Addressing mode — immediate, register, base+offset আর PC-relativeপরবর্তী পর্বoperand কোথায় থাকে, আর ঠিকানা কীভাবে হিসাব হয়।
- কম্পিউটার আর্কিটেকচার ও ডিজিটাল লজিক · কোর্স হোম৬২টি পর্বtransistor থেকে multicore পর্যন্ত পুরো রোডম্যাপ।
- পর্ব ৩২ · Single-cycle datapathসামনেএই পর্বের field-গুলো কোন তারে কোথায় যায়।
- C · C আর assemblyপাশের ট্র্যাকcompiler-এর তৈরি assembly পড়া আর বোঝা।
- C · বিটওয়াইজ অপারেশনভিত্তিshift, mask আর bit field — encoder-decoder লেখার হাতিয়ার।