সরাসরি পর্বে যান
পর্ব ২৬ · ৬২-এর মধ্যে · মডিউল ৫

RISC-V-এর instruction — register, arithmetic, load আর store

RV32I for Programmers — the 32 Registers, Assembly Syntax, Arithmetic and Constants (lui/addi), Logic, Shifts, Comparisons, Loads and Stores

Taught in Bangla, this lesson teaches RV32I from a programmer's view: the 32 registers and ABI names, assembly syntax, add, sub and addi, building any 32-bit constant with lui and addi including the +0x800 fix, auipc and li, logic, shifts and comparisons, loads and stores with sign and zero extension and alignment, pseudo-instructions, the M extension and RV64I, and a[i] += 5 compiled, with a runnable RV32I interpreter.

এই পর্বে যা শিখবেন

  • RV32I-এর ৩২টি register, তাদের ABI নাম (zero, ra, sp, t0–t6, s0–s11, a0–a7…) আর কোনটির কী কাজ
  • RISC-V assembly-র লেখার নিয়ম, আর arithmetic, logical, shift ও তুলনার instruction — প্রতিটির সীমা আর ফাঁদসহ
  • lui, auipc আর addi দিয়ে যেকোনো ৩২-বিট ধ্রুবক বানানো, +0x800 সংশোধনের প্রমাণসহ
  • Load-store: base + offset ঠিকানা, little-endian byte, sign বনাম zero extension আর alignment
  • Pseudo-instruction-এর আসল রূপ, M extension আর RV64I-এর পার্থক্য, আর C-র a[i] += 5 কোন পাঁচটি instruction হয় — সাথে ঘরে বসে চর্চার টুল

১ · Programmer-এর চোখে RISC-V — register, মেমরি আর load-store-এর নিয়ম

পর্ব ২৫-এ (ISA কী) দেখেছেন, ISA হলো hardware আর software-এর চুক্তি। এবার সেই চুক্তির একটি আসল উদাহরণ খুলে পড়ার পালা। এই কোর্সের প্রধান ISA RISC-V, আর তার সবচেয়ে ছোট, বাধ্যতামূলক অংশটির নাম RV32I — “RV” মানে RISC-V, “32” মানে register ৩২ বিটের, আর “I” মানে base integer instruction set. গুণ-ভাগ (M), floating point (F, D), atomic (A), ছোট ১৬-বিটের instruction (C) — এসব আলাদা extension; RV32I-এর ওপর জোড়া লাগে। কেন এই মডিউলার নকশা, তার আলোচনা পর্ব ২৫-এ। এই পর্বে আমরা RV32I-কে দেখব একজন programmer-এর চোখ দিয়ে: কী কী আছে, কীভাবে লিখতে হয়, কোথায় ফাঁদ।

একজন assembly programmer RISC-V-এর ভেতরে মাত্র তিনটি জিনিস দেখেন:

  • ৩২টি integer register — x0 থেকে x31, প্রতিটি ৩২ বিটের (RV32-এ)। এর মধ্যে x0 সবসময় ০।
  • Program counter (pc) — এখন যে instruction চলছে তার ঠিকানা। প্রতিটি instruction ৪ byte, তাই সাধারণত pc প্রতিবার ৪ করে বাড়ে। pc সরাসরি লেখা যায় না; শাখা আর jump দিয়ে বদলায়।
  • মেমরি — byte-এর একটি লম্বা সারি, প্রতিটি byte-এর নিজস্ব ঠিকানা। RV32-এ ঠিকানা ৩২ বিটের, অর্থাৎ তাত্ত্বিকভাবে ৪ GiB. একাধিক byte-এর মান RISC-V little-endian ক্রমে রাখে — কম গুরুত্বের byte ছোট ঠিকানায় (পর্ব ০৬-এর endianness).

আর সবচেয়ে গুরুত্বপূর্ণ নিয়মটি হলো: হিসাব হয় শুধু register-এ. add, sub, and, sll — কোনোটিই মেমরির কোনো ঘর সরাসরি পড়তে বা লিখতে পারে না। মেমরি থেকে মান আনতে হয় load দিয়ে, ফেরত রাখতে হয় store দিয়ে। এমন নকশাকে বলে load-store architecture. ARM, MIPS, SPARC, PowerPC — সব RISC ISA এই নিয়ম মানে। x86-এর মতো CISC ISA-তে আবার add [rbx], 5-এর মতো instruction আছে, যা এক instruction-এই মেমরি পড়ে, যোগ করে, আবার লেখে।

একজন হিসাবরক্ষকের কথা ভাবুন। তাঁর ডেস্কে ৩২টি খাতা খোলা রাখার জায়গা — এগুলো register. অফিসের পেছনের ঘরে বিশাল আলমারি — সেটি মেমরি। নিয়ম হলো, হিসাব করা যায় শুধু ডেস্কের খাতায়। আলমারির কোনো খাতার অঙ্ক বদলাতে হলে আগে সেটি ডেস্কে আনতে হবে (load), ডেস্কে যোগ-বিয়োগ করতে হবে, তারপর আলমারিতে রেখে আসতে হবে (store). আর ডেস্কের প্রথম খাতাটি (x0) আসলে খাতাই নয় — একটি ছাপানো “০”, তাতে যা-ই লেখেন, মুছে যায়।

কেন এই কড়া নিয়ম? কারণ এতে প্রতিটি instruction-এর কাজ ছোট আর অনুমানযোগ্য থাকে: একটি arithmetic instruction কখনো মেমরির জন্য অপেক্ষা করবে না, আর একটি load শুধু মেমরি পড়বে। Pipeline-এ (পর্ব ৩৫) এই নিয়মিততাই বড় সুবিধা দেয়। দাম: একই কাজে instruction সংখ্যা বেশি। অংশ ৯-এ দেখবেন, C-র ছোট্ট a[i] += 5 RISC-V-তে পাঁচটি instruction, x86-এ একটি বা দুটি।

মূল ধারণা

RV32I = ৩২টি ৩২-বিট register + pc + byte-ঠিকানার little-endian মেমরি, আর প্রায় ৪০টি instruction. Arithmetic আর logic শুধু register-এ; মেমরির সাথে যোগাযোগ শুধু load আর store দিয়ে। পুরো C ভাষা এই ছোট সেটের ওপর চালানো যায়।

২ · ৩২টি register আর তাদের ABI নাম

Hardware-এর চোখে register-গুলোর নাম শুধু x0, x1 … x31. কিন্তু assembly-তে প্রায় কেউ x10 লেখেন না, লেখেন a0. এই দ্বিতীয় নামগুলো এসেছে ABI (Application Binary Interface) থেকে — compiler, library আর operating system-এর মধ্যে একটি চুক্তি, কোন register কোন কাজে ব্যবহার হবে। ফলে আপনার লেখা function আর অন্য কারও লেখা library একসাথে চলতে পারে। পুরো টেবিল:

RegisterABI নামভূমিকাFunction call-এর পরে মান টিকে থাকে?
x0zeroসবসময় ০; লেখা উপেক্ষিত হয়— (বদলায়ই না)
x1raReturn address — function শেষে কোথায় ফিরতে হবেনা
x2spStack pointer — stack-এর বর্তমান শীর্ষহ্যাঁ
x3gpGlobal pointer — global ডেটার কাছাকাছি একটি স্থির ঠিকানা— (পুরো প্রোগ্রামে স্থির)
x4tpThread pointer — প্রতিটি thread-এর নিজস্ব ডেটার ঠিকানা— (thread-এ স্থির)
x5–x7t0–t2Temporary — অস্থায়ী হিসাবের জন্যনা
x8s0 / fpSaved register, দরকারে frame pointerহ্যাঁ
x9s1Saved registerহ্যাঁ
x10–x11a0–a1Function-এর argument, আর ফেরত মান (return value)না
x12–x17a2–a7আরও argument; ecall-এ a7 প্রায়ই service নম্বরনা
x18–x27s2–s11Saved register — দীর্ঘ সময় রাখার মানহ্যাঁ
x28–x31t3–t6আরও temporaryনা

শেষ কলামটি calling convention-এর মূল কথা। “হ্যাঁ” মানে callee-saved: যে function ডাকা হলো (callee), সে এই register বদলাতে চাইলে আগে পুরোনো মান stack-এ রেখে ফেরার আগে ফিরিয়ে দেবে — তাই ডাকনেওয়ালা (caller) নিশ্চিন্তে s-register-এ মান রাখতে পারে। “না” মানে caller-saved: call-এর পরে t আর a register-এ কী থাকবে তার নিশ্চয়তা নেই; দরকার হলে caller নিজে আগে বাঁচিয়ে রাখবে। এই নিয়মের পুরো ব্যবহার, stack pointer দিয়ে stack frame বানানো আর recursion — সব পর্ব ৩০-এ (Function call আর stack). এখানে শুধু নামগুলো চিনে রাখুন।

রেস্তোরাঁর রান্নাঘরের তাকের মতো ভাবুন। কিছু তাক “নিজের” — বাবুর্চি সেখানে মশলা রাখেন, সহকারী ব্যবহার করলে কাজ শেষে আগের মতো সাজিয়ে দিতে বাধ্য (s-register). কিছু তাক “সবার” — যে কেউ যেকোনো সময় খালি করে নিজের জিনিস রাখতে পারে (t-register). আর অর্ডারের কাগজ আসে একটি নির্দিষ্ট জানালা দিয়ে, রান্না করা খাবারও যায় সেই জানালা দিয়ে (a0, a1 — argument আর ফেরত মান)।

তিনটি বিশেষ কথা মনে রাখুন। প্রথমত, x0-এর নাম zero register: এটি পড়লে সবসময় ০ আসে, আর এতে লিখলে কিছুই হয় না। এই একটি ছোট সুবিধা থেকে অনেক কিছু বিনা খরচে পাওয়া যায় — mv, neg, nop, j সব আসলে x0 ব্যবহার করা সাধারণ instruction (অংশ ৮)। দ্বিতীয়ত, hardware-এর কাছে x1–x31 প্রায় পুরোপুরি সমান; “a0 মানে argument” নিয়মটি software-এর, hardware-এর নয়। (সামান্য ব্যতিক্রম: কিছু instruction, যেমন C extension-এর ছোট c.jal, ra-কে নির্দিষ্টভাবে ব্যবহার করে, আর বড় CPU-র return-address predictor ra দেখে অনুমান করে।) তৃতীয়ত, return address, gp আর tp-এ সাধারণ প্রোগ্রামে হাত দেবেন না — linker আর operating system এগুলো ঠিক করে রাখে।

ছোট chip-এর জন্য RV32E: খুব ছোট microcontroller-এ ৩২টি register-ও বেশি মনে হতে পারে — register file chip-এর বড় একটি অংশ। তাই RISC-V-তে RV32E নামে একটি রূপ আছে, যেখানে শুধু x0–x15, ১৬টি register. বাকি instruction প্রায় হুবহু এক। এই কোর্সে আমরা সবসময় পূর্ণ ৩২টি register ধরে নেব।

৩ · Assembly লেখার নিয়ম — operand-এর ক্রম, comment, label আর directive

Assembly language হলো machine code-এর মানুষ-পড়ার রূপ। একটি প্রোগ্রাম, যার নাম assembler, প্রতিটি লাইনকে ৩২-বিটের একটি instruction-এ বদলে দেয় (কোন বিট কোথায় বসে, তা পর্ব ২৭-এর instruction encoding-এ)। RISC-V-এর লেখার ধরন খুবই নিয়মিত — চারটি ছাঁচ চিনলেই প্রায় সব পড়তে পারবেন:

# উদাহরণ — RV32I-এর চার ছাঁচ (GNU assembler-এর লেখার ধরন)
add   rd, rs1, rs2        # register-register:  rd = rs1 op rs2
addi  rd, rs1, imm        # register-immediate: rd = rs1 op imm (12-bit signed)
lw    rd, offset(rs1)     # load:   rd = memory[rs1 + offset]
sw    rs2, offset(rs1)    # store:  memory[rs1 + offset] = rs2   (no rd!)
  • গন্তব্য আগে: প্রথম operand সাধারণত গন্তব্য register (rd), তারপর উৎস (rs1, rs2). sub t0, t1, t2 মানে t0 = t1 − t2, উল্টো নয়।
  • Store ব্যতিক্রম: sw t0, 8(sp)-এ t0 উৎস — store কোনো register লেখে না, তাই rd নেই। প্রথম operand এখানে যে মান মেমরিতে যাবে। নতুনদের সবচেয়ে প্রচলিত ভুলগুলোর একটি এটি।
  • Immediate: instruction-এর ভেতরেই লেখা ধ্রুবক — দশমিকে (-5) বা hex-এ (0xFF). আলাদা মেমরি পড়তে হয় না।
  • Comment: # থেকে লাইনের শেষ পর্যন্ত। (x86-এর NASM বা MASM-এর মতো কিছু assembler-এ comment শুরু হয় ; দিয়ে; কিন্তু RISC-V-এর GNU assembler-এ ; একই লাইনে দুটি instruction আলাদা করে, তাই comment-এর জন্য # লিখুন।)
  • Label: লাইনের শুরুতে loop: বা arr: — একটি ঠিকানার নাম। Assembler নিজে হিসাব করে নেয় label-টি কোন ঠিকানায় পড়ল, আর শাখা বা la-তে সেই হিসাব বসায়।
  • Directive: বিন্দু দিয়ে শুরু, CPU-র instruction নয় — assembler-এর জন্য নির্দেশ: .text (কোডের অংশ), .data (ডেটার অংশ), .word 5, 7 (৩২-বিটের মান বসানো), .byte, .asciz "hi", .globl main.

সব একসাথে একটি ছোট প্রোগ্রামে — একটি array-র প্রথম দুটি সংখ্যার যোগফল:

# উদাহরণ — RARS বা GNU as-এ লেখা যায় এমন একটি ছোট RV32I প্রোগ্রাম
        .data
arr:    .word   40, 2, 7          # three 32-bit ints, 4 bytes each
result: .word   0

        .text
        .globl  main
main:   la      t0, arr           # t0 = address of arr   (pseudo: auipc + addi)
        lw      t1, 0(t0)         # t1 = arr[0] = 40
        lw      t2, 4(t0)         # t2 = arr[1] = 2   (next word is 4 bytes later)
        add     t3, t1, t2        # t3 = 42
        la      t4, result
        sw      t3, 0(t4)         # result = 42

লক্ষ করুন 4(t0): arr[1]-এর ঠিকানা arr-এর ঠিকানা থেকে ১ নয়, ৪ byte দূরে, কারণ প্রতিটি .word ৪ byte. C-তে arr[i] লিখলে compiler এই গুণ নিজে করে; assembly-তে আপনাকে করতে হয়। আর la (load address) একটি pseudo-instruction — আসল instruction নয়, assembler এটিকে দুটি আসল instruction-এ বদলায় (অংশ ৪ আর ৮)।

MIPS আর RISC-V-এর লেখা গুলিয়ে ফেলবেন না। পুরোনো অনেক বই আর অনলাইন উদাহরণ MIPS-এর: সেখানে register লেখা হয় $t0, $a0, $zero (ডলার চিহ্নসহ), আর argument register মাত্র চারটি ($a0–$a3). RISC-V-এ ডলার নেই, argument register আটটি (a0–a7), আর t ও s register-এর সংখ্যা ও নম্বরও আলাদা। দুটি ISA দেখতে কাছাকাছি, কিন্তু একটির কোড অন্যটির assembler-এ চলে না।

৪ · Arithmetic আর ধ্রুবক — add, sub, addi, lui, auipc আর li

add, sub আর addi

add rd, rs1, rs2 দুটি register যোগ করে, sub rd, rs1, rs2 বিয়োগ করে। ফল সবসময় mod 232 — সীমা ছাড়ালে চুপচাপ wrap-around; কোনো flag নেই, কোনো trap নেই। Overflow দরকার হলে কীভাবে ধরতে হয়, তা পর্ব ২০-এর sltu আর slt-এর কৌশল-এ বিস্তারিত আছে; এখানে আর পুনরাবৃত্তি করছি না। একই বিটের যোগফল signed আর unsigned দুই অর্থেই সঠিক, কারণ two's complement (পর্ব ০৫)।

addi rd, rs1, imm একটি register-এর সাথে একটি ধ্রুবক যোগ করে। ধ্রুবকটি instruction-এর ভেতরে মাত্র ১২ বিটে রাখা হয়, আর ব্যবহারের আগে ৩২ বিটে sign-extend করা হয়। তাই সীমা:

−2048 ≤ imm ≤ 2047  (−211 থেকে 211 − 1)

কেন ১২ বিট? প্রতিটি instruction ঠিক ৩২ বিটের; তার মধ্যে opcode (৭ বিট), গন্তব্য register (৫ বিট), একটি উৎস register (৫ বিট) আর ছোট একটি function field (৩ বিট) বসাতেই ২০ বিট চলে যায়; বাকি থাকে ১২. এর বিস্তারিত পর্ব ২৭-এ। এখান থেকে তিনটি ব্যবহারিক কথা:

  • subi নেই। দরকারই নেই: t0 − 100 = addi t0, t0, -100. ঋণাত্মক immediate যোগ করাই বিয়োগ। একটি instruction কম মানে decoder আর encoding দুটিই সরল।
  • সীমা অপ্রতিসম। addi t0, t0, -2048 বৈধ, কিন্তু addi t0, t0, 2048 নয় — assembler ভুল (error) দেখাবে। 2048 যোগ করতে দুটি addi (যেমন 2047 আর 1) বা আগে register-এ ধ্রুবক বানিয়ে add.
  • বড় ধ্রুবক সরাসরি নয়। 100000 বা 0x12345FFF-এর মতো মান এক instruction-এ register-এ আনা যায় না। এর জন্য আছে lui.

lui আর auipc — উপরের ২০ বিট

lui rd, imm20 (load upper immediate) একটি ২০-বিটের ধ্রুবক নিয়ে register-এর উপরের ২০ বিটে বসায়, আর নিচের ১২ বিট শূন্য করে: rd = imm20 × 212 = imm20 << 12. যেমন lui t0, 0x12345 দেয় t0 = 0x12345000. এর সঙ্গী auipc rd, imm20 (add upper immediate to pc): rd = pc + (imm20 << 12). এটি দিয়ে বর্তমান instruction-এর আশেপাশের ঠিকানা বের করা যায় — প্রোগ্রাম মেমরির যেখানেই লোড হোক, ঠিকানা ঠিক থাকে। একে বলে PC-relative ঠিকানা; la আর call pseudo-instruction এর ওপর দাঁড়ানো (পর্ব ২৮-এ addressing mode হিসেবে আবার দেখবেন)।

৩২-বিট ধ্রুবক = lui + addi, আর +0x800-এর ফাঁদ

পরিকল্পনা সহজ শোনায়: উপরের ২০ বিট lui দিয়ে, নিচের ১২ বিট addi দিয়ে। কিন্তু একটি সূক্ষ্ম ফাঁদ আছে। addi-র immediate sign-extend হয়: নিচের ১২ বিটের সর্বোচ্চ বিট (bit 11) ১ হলে, অর্থাৎ নিচের অংশ 0x800 বা তার বেশি হলে, addi সেটিকে ঋণাত্মক সংখ্যা ধরে — 0x800 থেকে 0xFFF মানে −2048 থেকে −1. তখন যোগ নয়, আসলে বিয়োগ হয়, আর ফল ঠিক 0x1000 কম আসে। প্রতিকার: সেই ক্ষেত্রে lui-র মান ১ বাড়িয়ে দেওয়া। নিচে 0x12345FFF লোড করার চেষ্টা, ধাপে ধাপে — “পরে” চাপুন:

লক্ষ্যt0 = 0x12345FFF
0x12345FFF  =  0x12345 │ FFF
               upper 20 bits │ lower 12 bits
lower 12 = 0xFFF = 1111 1111 1111   (bit 11 = 1)
প্রথম চেষ্টাlui t0, 0x12345
t0 = 0x12345 << 12 = 0x12345000
(lower 12 bits are now 0)
addi-র চোখে 0xFFFsign extension
12-bit 0xFFF = 1111 1111 1111
sign-extended to 32 bits = 0xFFFFFFFF = -1
so "addi t0, t0, 0xFFF" really means  addi t0, t0, -1
(GNU as rejects 0xFFF here: 4095 is out of range)
ভুল ফলaddi t0, t0, -1
0x12345000 + (-1) = 0x12344FFF
wanted              0x12345FFF
difference          0x00001000   (one step of lui too low)
সংশোধনhi = (c + 0x800) >> 12
bit 11 of c is 1  ->  add 1 to the upper part
(0x12345FFF + 0x800) >> 12 = 0x123467FF >> 12 = 0x12346
lui t0, 0x12346      t0 = 0x12346000
সঠিক ফলaddi t0, t0, -1
0x12346000 + (-1) = 0x12345FFF   ok
li t0, 0x12345FFF   ==  lui t0, 0x12346 ; addi t0, t0, -1
  1. লক্ষ্য ভাগ করা। 0x12345FFF-কে দুই টুকরো করি: উপরের ২০ বিট 0x12345 আর নিচের ১২ বিট 0xFFF. নিচের অংশের সর্বোচ্চ বিট (bit 11) এখানে ১ — এটিই পরের ঝামেলার উৎস।
  2. সরল চেষ্টা। lui t0, 0x12345 দিয়ে t0 = 0x12345000. উপরের অংশ বসে গেল, নিচের ১২ বিট শূন্য। এবার শুধু 0xFFF যোগ করা বাকি — অন্তত তাই মনে হয়।
  3. addi 0xFFF-কে কী ভাবে। ১২ বিটের 0xFFF-এর সর্বোচ্চ বিট ১, তাই sign extension-এর পর এটি 0xFFFFFFFF, অর্থাৎ −1. 4095 যোগ করার কোনো উপায় addi-তে নেই; সর্বোচ্চ 2047. তাই নিচের ১২ বিটকে −1 হিসেবেই লিখতে হবে।
  4. ফল ভুল। 0x12345000 − 1 = 0x12344FFF — চাওয়া মানের চেয়ে ঠিক 0x1000 (৪০৯৬) কম। addi যোগ না করে বিয়োগ করেছে, আর বিয়োগটি উপরের অংশ থেকে একটি “ধার” নিয়ে গেছে।
  5. সংশোধন। নিয়ম: hi = (c + 0x800) >> 12. 0x800 যোগ করলে bit 11 যখন ১, তখন ঠিক একটি carry উপরের অংশে যায়: 0x12345 হয়ে যায় 0x12346. এবার lui t0, 0x12346 দেয় 0x12346000.
  6. সঠিক ফল। 0x12346000 + (−1) = 0x12345FFF. বাড়তি ১ (0x1000) আর −1-এর বিয়োগ মিলে ঠিক চাওয়া মান। li t0, 0x12345FFF লিখলে assembler নিজেই এই দুটি instruction বানায়।

সংশোধনটি কেন সবসময় ঠিক — ছোট প্রমাণ

ধরুন ধ্রুবক c-কে লিখি c = H · 212 + L, যেখানে H উপরের ২০ বিট আর L নিচের ১২ বিট (0 ≤ L ≤ 4095). addi L-কে sign-extend করে পড়ে, অর্থাৎ যোগ করে ℓ = L − 4096 · b, যেখানে b = bit 11 (L ≥ 2048 হলে b = 1, নাহলে 0). ফলে −2048 ≤ ℓ ≤ 2047. আমরা চাই lui-র মান h এমন হোক যাতে h · 212 + ℓ = c, অর্থাৎ h = H + b. এখন দেখুন, (c + 2048) >> 12 = H + ⌊(L + 2048) / 4096⌋, আর L + 2048 ≥ 4096 ঠিক তখনই, যখন L ≥ 2048, অর্থাৎ b = 1. সুতরাং (c + 0x800) >> 12 = H + b — ঠিক যা লাগবে। সব হিসাব mod 232, তাই H = 0xFFFFF-এর মতো সীমানায় h “উপচে” ০ হয়ে গেলেও ফল সঠিক থাকে। GNU assembler-এর %hi(c) আর %lo(c) ঠিক এই নিয়মে হিসাব করে: lui t0, %hi(c) আর addi t0, t0, %lo(c).

নিয়মটি এবার অনেক ধ্রুবকে চালিয়ে দেখা যাক। নিচের simulator প্রতিটি ধ্রুবকের জন্য সঠিক lui/addi জোড়া বের করে, ৩২-বিট wrap মেনে আবার জোড়া লাগায়, আর পাশে দেখায় +0x800 ভুলে গেলে কী হতো; শেষে ১০ লক্ষ এলোমেলো ধ্রুবকে যাচাই করে। Run চাপুন:

// Split any 32-bit constant into lui (upper 20 bits) + addi (signed low 12 bits)
const hx = (v, w = 8) => "0x" + (v >>> 0).toString(16).toUpperCase().padStart(w, "0");
function split(c) {
  const lo = (c << 20) >> 20;                    // low 12 bits, sign-extended: -2048..2047
  const hi = ((c + 0x800) >>> 12) & 0xFFFFF;     // +0x800 adds 1 exactly when bit 11 is set
  return { hi, lo };
}
const rebuild = (hi, lo) => ((hi << 12) + lo) | 0;   // what lui + addi computes (mod 2^32)
const naiveHi = c => (c >>> 12) & 0xFFFFF;            // forgetting the correction
console.log("constant     lui imm   addi imm  rebuilt      | naive lui  naive result");
for (const c of [0x12345FFF, 0x12345678, 0xDEADBEEF, 0x00000800, 0x000007FF,
                 0xFFFFF800, 0x7FFFF800, 0xFFFFFFFF, 0x80000000]) {
  const { hi, lo } = split(c), r = rebuild(hi, lo), n = rebuild(naiveHi(c), lo);
  console.log(`${hx(c)}   ${hx(hi, 5)}  ${String(lo).padStart(6)}    ${hx(r)} ${r === (c | 0) ? "ok " : "BAD"}` +
              ` | ${hx(naiveHi(c), 5)}    ${hx(n)} ${n === (c | 0) ? "ok" : "off by " + hx(((c | 0) - n) >>> 0, 4)}`);
}
const { hi, lo } = split(0x12345FFF);
console.log(`\nli t0, 0x12345FFF  =>  lui t0, ${hx(hi, 5)}   then   addi t0, t0, ${lo}`);
let seed = 2026, bad = 0;                            // deterministic sweep over 1,000,000 constants
for (let i = 0; i < 1e6; i++) {
  seed = (Math.imul(seed, 1664525) + 1013904223) | 0;
  const s = split(seed);
  if (rebuild(s.hi, s.lo) !== seed || s.lo < -2048 || s.lo > 2047) bad++;
}
console.log(`random constants: 1000000, wrong rebuilds: ${bad}`);

আউটপুট (নির্ধারিত — প্রতিবার একই):

constant     lui imm   addi imm  rebuilt      | naive lui  naive result
0x12345FFF   0x12346      -1    0x12345FFF ok  | 0x12345    0x12344FFF off by 0x1000
0x12345678   0x12345    1656    0x12345678 ok  | 0x12345    0x12345678 ok
0xDEADBEEF   0xDEADC    -273    0xDEADBEEF ok  | 0xDEADB    0xDEADAEEF off by 0x1000
0x00000800   0x00001   -2048    0x00000800 ok  | 0x00000    0xFFFFF800 off by 0x1000
0x000007FF   0x00000    2047    0x000007FF ok  | 0x00000    0x000007FF ok
0xFFFFF800   0x00000   -2048    0xFFFFF800 ok  | 0xFFFFF    0xFFFFE800 off by 0x1000
0x7FFFF800   0x80000   -2048    0x7FFFF800 ok  | 0x7FFFF    0x7FFFE800 off by 0x1000
0xFFFFFFFF   0x00000      -1    0xFFFFFFFF ok  | 0xFFFFF    0xFFFFEFFF off by 0x1000
0x80000000   0x80000       0    0x80000000 ok  | 0x80000    0x80000000 ok

li t0, 0x12345FFF  =>  lui t0, 0x12346   then   addi t0, t0, -1
random constants: 1000000, wrong rebuilds: 0

প্রথম সারিই stepper-এর উদাহরণ: সঠিক জোড়া lui 0x12346 আর addi -1, আর সংশোধন ছাড়া ফল 0x12344FFF — ঠিক 0x1000 কম। 0x12345678-এ নিচের অংশ 0x678 < 0x800, তাই সংশোধন লাগে না, দুই কলামই ok. 0x00000800 মজার: নিচের অংশ ঠিক 2048, যা addi-তে রাখা যায় না, তাই lui 1 (= 4096) থেকে 2048 বাদ। 0x000007FF, 0xFFFFF800 আর 0xFFFFFFFF-এ lui-র মান 0 — অর্থাৎ ধ্রুবকটি −2048…2047-এর মধ্যে, আর assembler শুধু একটি addi বসাবে (যেমন li t0, -1 = addi t0, zero, -1). 0x7FFFF800-এ সংশোধিত lui-র মান 0x80000, যা নিজে 0x80000000 — তার থেকে 2048 বাদ দিলে wrap করে সঠিক মান। 0x80000000-এ নিচের অংশ ০, তাই শুধু lui. শেষ লাইন: ১০ লক্ষ এলোমেলো ধ্রুবকে একটিও ভুল নেই — প্রমাণ আর পরীক্ষা একমত।

li — যা আপনি লেখেন, আর যা assembler বানায়

বাস্তবে আপনি প্রায় কখনো নিজে lui-addi ভাঙবেন না; লিখবেন li rd, constant (load immediate). এটি pseudo-instruction: assembler ধ্রুবক দেখে ঠিক করে — −2048…2047 হলে একটি addi rd, zero, c; নিচের ১২ বিট ০ হলে শুধু lui; নাহলে সংশোধিতসহ lui + addi. তাহলে এই হিসাব শেখার দরকার কী? তিনটি কারণে: compiler-এর বানানো assembly বা disassembly পড়তে গেলে lui a5, 0x12346 দেখে বুঝতে হবে আসল ধ্রুবক কত; পরীক্ষা আর ইন্টারভিউতে ঠিক এই প্রশ্নটি আসে; আর নিজের assembler, JIT বা emulator লিখলে এই নিয়ম আপনাকেই প্রয়োগ করতে হবে।

৫ · Logical, shift আর তুলনা — বিট নিয়ে কাজ

and, or, xor — আর NOT কোথায়?

and, or, xor দুটি register-এর প্রতিটি বিট জোড়ায় জোড়ায় মেলায়; andi, ori, xori একই কাজ করে একটি ১২-বিটের immediate দিয়ে — এবং এখানেও immediate sign-extend হয়। এটি MIPS থেকে আলাদা: MIPS-এর andi/ori immediate zero-extend করত। RISC-V-এর নিয়মে একটি সুবিধা আছে: andi t0, t0, -16 (−16 = 0xFFFFFFF0) এক instruction-এ নিচের চারটি বিট মুছে দেয় — ঠিকানাকে ১৬-এর গুণিতকে নামানোর প্রচলিত কৌশল।

RV32I-এ আলাদা NOT instruction নেই। কারণ x ⊕ 1 = x′ (বিটের উল্টো; এই পর্বে A′ মানে A-র সব বিট উল্টানো, কোডে ~A), আর −1-এর সব ৩২টি বিট ১. তাই xori rd, rs, -1-ই NOT — not rd, rs pseudo-instruction ঠিক এটাই বানায়। একটি bitmask দিয়ে প্রচলিত চারটি কাজ, পর্ব ০৬-এর bit manipulation-এর RISC-V রূপ:

# উদাহরণ — t0-এর বিট নিয়ে কাজ (RV32I)
andi  t1, t0, 0xFF        # keep only the low byte          (extract)
ori   t0, t0, 0x20        # set bit 5                       (set)
andi  t0, t0, -33         # clear bit 5: -33 = 0xFFFFFFDF   (clear)
xori  t0, t0, 0x1         # flip bit 0                      (toggle)
not   t2, t0              # = xori t2, t0, -1               (all bits)
# bit 20 সেট করতে mask 0x100000 লাগে — ১২ বিটে ধরে না, তাই আগে register-এ:
lui   t3, 0x100           # t3 = 0x00100000 = 1 << 20
or    t0, t0, t3

Shift — sll, srl, sra আর তাদের immediate রূপ

sll (shift left logical) বিট বাঁয়ে সরায়, ডানে ০ ঢোকায় — k ঘর সরানো মানে 2k দিয়ে গুণ (mod 232). ডানে সরানোর দুই রকম: srl (logical) বাঁয়ে ০ ঢোকায় — unsigned সংখ্যার 2k দিয়ে ভাগ; sra (arithmetic) বাঁয়ে sign-বিটের কপি ঢোকায় — signed সংখ্যার ভাগ, তবে floor-এর দিকে গোল করে (−7 sra 1 = −4; C-র −7/2 = −3 — এর সংশোধন পর্ব ২০-এর অংশ ৭-এ)। Hardware-এ এগুলো কীভাবে এক cycle-এ হয়, তা পর্ব ১৩-এর barrel shifter-এ।

Register রূপে (sll rd, rs1, rs2) কত ঘর সরাবে তা আসে rs2-এর শুধু নিচের ৫ বিট থেকে — অর্থাৎ ০ থেকে ৩১. rs2 = 33 হলে আসলে ১ ঘর সরে (33 mod 32). Immediate রূপে (slli, srli, srai) এই shift amount instruction-এই লেখা, ০ থেকে ৩১ — এর বাইরে assembler মানবে না। লক্ষ করুন, srai আছে কিন্তু slai নেই: বাঁয়ে arithmetic আর logical shift একই কাজ।

C-তে x << 32 undefined behaviour. RISC-V hardware-এ sll 32 ঘরকে ০ ঘর ধরে (মান বদলায় না), x86-এ ৩২-বিট shift-ও count-এর নিচের ৫ বিট নেয়, কিন্তু অন্য কিছু CPU পুরো count মানে। C standard তাই width-এর সমান বা বেশি shift-কে undefined রেখেছে — “আমার যন্ত্রে চলে” এখানে কোনো নিশ্চয়তা নয়।

তুলনা — slt, sltu, slti, sltiu

RV32I-এ কোনো flag নেই; তুলনার ফল রাখা হয় সাধারণ register-এ, ১ বা ০ হিসেবে। slt rd, rs1, rs2 (set on less than): rs1 < rs2 (signed) হলে rd = 1, নাহলে 0. sltu একই প্রশ্ন unsigned অর্থে। slti আর sltiu immediate-এর সাথে তুলনা করে — sltiu-তেও immediate প্রথমে sign-extend হয়, তারপর unsigned তুলনা। এই দুটি দিয়ে কয়েকটি দরকারি pseudo-instruction তৈরি:

Pseudoঅর্থআসল instructionকেন কাজ করে
seqz rd, rsrd = (rs == 0)sltiu rd, rs, 1unsigned-এ ১-এর চেয়ে ছোট শুধু ০
snez rd, rsrd = (rs != 0)sltu rd, zero, rs০ <u rs সত্য, যদি rs ০ না হয়
sltz rd, rsrd = (rs < 0)slt rd, rs, zerosigned তুলনা শূন্যের সাথে
sgtz rd, rsrd = (rs > 0)slt rd, zero, rsoperand-এর ক্রম উল্টে

দুটি register সমান কি না জানতে? sub t0, a0, a1 (বা xor) তারপর seqz t0, t0 — দুটি instruction. আর তুলনার পর শাখা দরকার হলে আলাদা slt লাগেই না: blt, bltu-র মতো শাখা নিজেই তুলনা করে (অংশ ৮)। Signed আর unsigned তুলনা গুলিয়ে ফেলার ক্লাসিক বাগ পর্ব ২০-এর blt বনাম bltu-তে আছে।

দলRegister রূপImmediate রূপমনে রাখার কথা
Arithmeticadd, subaddi (subi নেই)mod 232, flag নেই; imm −2048…2047
Upper immediate—lui, auipcimm20 << 12; auipc pc-এর সাথে যোগ করে
Logicaland, or, xorandi, ori, xoriimm sign-extend; NOT = xori −1
Shiftsll, srl, sraslli, srli, sraiপরিমাণ শুধু নিচের ৫ বিট (০–৩১)
তুলনাslt, sltuslti, sltiuফল ১ বা ০; sltiu-র imm-ও sign-extend

৬ · Load আর store — base + offset, sign বনাম zero extension, alignment

RV32I-এ মেমরির সাথে কাজের instruction মাত্র আটটি: পাঁচটি load আর তিনটি store. সবগুলোর ঠিকানা একই নিয়মে: ঠিকানা = rs1 + sign-extend(offset), যেখানে offset ১২ বিটের (−2048…2047). এই যোগফলকে বলে effective address. মহল্লার গলির কথা ভাবুন: rs1 হলো গলির মুখের ঠিকানা (base), offset হলো “মুখ থেকে কয়েক বাড়ি পরে”। Stack-এর জিনিস (8(sp)), struct-এর field (12(a0)), array-র উপাদান — সব এই একটি ছাঁচে। ঠিকানা বানানোর অন্য পদ্ধতিগুলোর তুলনা পর্ব ২৮-এ (Addressing mode).

Instructionকত byteRegister-এর বাকি বিটেC-তে সাধারণত
lb rd, off(rs1)১bit 7-এর কপি (sign extension)int8_t, signed char
lbu rd, off(rs1)১০ (zero extension)uint8_t, unsigned char
lh rd, off(rs1)২bit 15-এর কপিint16_t, short
lhu rd, off(rs1)২০uint16_t
lw rd, off(rs1)৪— (পুরো register)int, pointer
sb rs2, off(rs1)১rs2-এর নিচের ৮ বিট লেখেchar-এ রাখা
sh rs2, off(rs1)২rs2-এর নিচের ১৬ বিট লেখেshort-এ রাখা
sw rs2, off(rs1)৪পুরো ৩২ বিট লেখেint-এ রাখা

Store-এর কোনো “u” রূপ নেই — কেন? কারণ store শুধু নিচের বিটগুলো কেটে রাখে; বাঁয়ের বিট কী ছিল তাতে কিছু যায় আসে না। কিন্তু load-এ ছোট মানকে ৩২-বিটের register-এ ভরতে হয়, আর বাঁয়ের ফাঁকা ২৪ বা ১৬ বিটে কী বসবে, সেটি নির্ভর করে মানটি signed না unsigned তার ওপর — ঠিক পর্ব ০৫-এর sign আর zero extension. মেমরির একটি byte 0xF0 দিয়ে দেখুন:

মেমরিa0 = 0x100
address : 0x100  0x101  0x102  0x103
byte    :  F0     7F     80     00
0xF0 = 1111 0000   (signed -16, unsigned 240)
lb t0, 0(a0)sign extension
byte       :                               1111 0000
bit 7 = 1  ->  fill 24 bits with 1
t0         : 1111 1111 1111 1111 1111 1111 1111 0000
           = 0xFFFFFFF0 = -16
lbu t1, 0(a0)zero extension
byte       :                               1111 0000
"u"        ->  fill 24 bits with 0
t1         : 0000 0000 0000 0000 0000 0000 1111 0000
           = 0x000000F0 = 240
অন্য byte0x7F আর 0x80
lb  from 0x101 (7F): bit 7 = 0 -> 0x0000007F = 127
lbu from 0x101 (7F):             0x0000007F = 127   (same!)
lb  from 0x102 (80): bit 7 = 1 -> 0xFFFFFF80 = -128
lbu from 0x102 (80):             0x00000080 =  128
পুরো wordlw t2, 0(a0)
little-endian: lowest address = least significant byte
t2 = 0x00 80 7F F0 = 0x00807FF0 = 8421360
the byte F0 is now just the low 8 bits -- no extension needed
  1. মেমরির ছবি। a0-তে ঠিকানা 0x100. সেখান থেকে চারটি byte: F0, 7F, 80, 00. প্রথম byte 0xF0-এর বিট 1111 0000 — signed অর্থে −16, unsigned অর্থে 240. byte নিজে জানে না সে কোনটি।
  2. lb: চিহ্ন রক্ষা। lb byte-টিকে signed ধরে। bit 7 = 1, তাই বাঁয়ের ২৪টি বিটে ১ বসে: 0xFFFFFFF0. ৩২-বিট signed অর্থে এটি −16 — মান হুবহু রক্ষা পেল।
  3. lbu: শূন্য দিয়ে ভরা। lbu byte-টিকে unsigned ধরে, বাঁয়ে সবসময় ০: 0x000000F0 = 240. একই মেমরি, একই বিট, অথচ register-এ ভিন্ন সংখ্যা।
  4. পার্থক্য শুধু bit 7 = 1 হলে। 0x7F-এর bit 7 = 0, তাই lb আর lbu দুটিই 127 দেয়। 0x80-এ bit 7 = 1: lb দেয় −128, lbu দেয় 128. অর্থাৎ 0x80–0xFF-এর byte-এই বাগ লুকিয়ে থাকে।
  5. lw-এ extension নেই। lw চারটি byte একসাথে আনে; little-endian ক্রমে ছোট ঠিকানার byte (F0) সবচেয়ে কম গুরুত্বের, তাই t2 = 0x00807FF0. পুরো register ভরে গেছে, ফাঁকা বিট নেই — তাই RV32-এ lwu লাগে না।

এবার একই মেমরিতে সব রকম load একসাথে, half-word আর word-সহ। নিচের simulator byte-গুলো little-endian নিয়মে জোড়া লাগায়, তারপর load-এর নাম দেখে sign বা zero extension করে; শেষ কলামে বলে ঠিকানাটি aligned কি না:

// lb/lbu/lh/lhu/lw on little-endian memory: which bytes, and how the rest of the register is filled
const mem = [0xF0, 0x7F, 0x80, 0x00, 0xEF, 0xBE, 0xAD, 0xDE];   // addresses 0..7
const hx = (v, w = 8) => "0x" + (v >>> 0).toString(16).toUpperCase().padStart(w, "0");
function load(op, addr) {
  const n = { b: 1, h: 2, w: 4 }[op[1]];                        // bytes read
  let v = 0;
  for (let k = n - 1; k >= 0; k--) v = v * 256 + mem[addr + k]; // little-endian: LSB at lowest address
  const bits = 8 * n, signed = !op.endsWith("u");
  if (bits < 32) v = signed ? (v << (32 - bits)) >> (32 - bits)  // sign-extend: copy the top bit
                            : v;                                // zero-extend: upper bits stay 0
  return { v: v | 0, aligned: addr % n === 0, raw: mem.slice(addr, addr + n) };
}
console.log("memory: " + mem.map((b, i) => `[${i}]=${b.toString(16).toUpperCase().padStart(2, "0")}`).join(" "));
console.log("\ninstr         bytes read     register         signed  aligned?");
const tests = [["lb", 0], ["lbu", 0], ["lb", 1], ["lbu", 1], ["lb", 2], ["lbu", 2],
  ["lh", 0], ["lhu", 0], ["lh", 2], ["lhu", 2], ["lh", 6], ["lhu", 6], ["lw", 0], ["lw", 4], ["lh", 1]];
for (const [op, a] of tests) {
  const r = load(op, a), bytes = r.raw.map(b => b.toString(16).toUpperCase().padStart(2, "0")).join(" ");
  console.log(`${(op + " x5, " + a + "(x0)").padEnd(14)}${bytes.padEnd(15)}${hx(r.v)}  ${String(r.v).padStart(11)}  ${r.aligned ? "yes" : "NO - may be slow or trap"}`);
}

আউটপুট:

memory: [0]=F0 [1]=7F [2]=80 [3]=00 [4]=EF [5]=BE [6]=AD [7]=DE

instr         bytes read     register         signed  aligned?
lb x5, 0(x0)  F0             0xFFFFFFF0          -16  yes
lbu x5, 0(x0) F0             0x000000F0          240  yes
lb x5, 1(x0)  7F             0x0000007F          127  yes
lbu x5, 1(x0) 7F             0x0000007F          127  yes
lb x5, 2(x0)  80             0xFFFFFF80         -128  yes
lbu x5, 2(x0) 80             0x00000080          128  yes
lh x5, 0(x0)  F0 7F          0x00007FF0        32752  yes
lhu x5, 0(x0) F0 7F          0x00007FF0        32752  yes
lh x5, 2(x0)  80 00          0x00000080          128  yes
lhu x5, 2(x0) 80 00          0x00000080          128  yes
lh x5, 6(x0)  AD DE          0xFFFFDEAD        -8531  yes
lhu x5, 6(x0) AD DE          0x0000DEAD        57005  yes
lw x5, 0(x0)  F0 7F 80 00    0x00807FF0      8421360  yes
lw x5, 4(x0)  EF BE AD DE    0xDEADBEEF   -559038737  yes
lh x5, 1(x0)  7F 80          0xFFFF807F       -32641  NO - may be slow or trap

প্রথম ছয় সারি stepper-এর সাথে মেলে: F0 থেকে −16 বনাম 240, 7F থেকে দুটিই 127, 80 থেকে −128 বনাম 128. Half-word-এ মজার একটি জিনিস: lh x5, 2(x0) পড়ে byte 80 আর 00 — ফল 0x0080 = 128, ধনাত্মক! কারণ little-endian-এ চিহ্ন ঠিক করে উঁচু ঠিকানার byte (00), আর তার bit 7 শূন্য। উল্টো দিকে ঠিকানা ৬-এ AD DE: উঁচু byte 0xDE-এর bit 7 = 1, তাই lh দেয় −8531 আর lhu দেয় 57005 (= 0xDEAD). lw x5, 4(x0) চারটি byte EF BE AD DE উল্টো ক্রমে জুড়ে 0xDEADBEEF. শেষ সারিতে lh ঠিকানা ১ থেকে — ২-byte-এর মান বিজোড় ঠিকানায়, তাই misaligned (নিচে দেখুন)।

C-তে এই একই ব্যাপার ঘটে cast-এ। int8_t-এ রূপান্তর মানে lb-এর মতো sign extension, uint8_t মানে lbu-এর মতো zero extension. নিচের প্রোগ্রাম Wandbox-এ (gcc, x86-64 Linux — সেটিও little-endian) সেটি দেখায়, আর সাথে lui-addi-র ভাগের হিসাবটি unsigned গণিতে আবার করে — সব হিসাব unsigned-এ, তাই কোনো undefined behaviour নেই:

#include <stdio.h>
#include <stdint.h>
#include <string.h>
int main(void) {
    uint8_t mem[4] = {0xF0, 0x7F, 0x80, 0x00};
    for (int i = 0; i < 3; i++) {
        int32_t  lb  = (int8_t)mem[i];   /* like lb : copy bit 7 upward (gcc: modulo 256) */
        uint32_t lbu = mem[i];           /* like lbu: upper 24 bits are 0 */
        printf("byte %02X: lb -> %4d = 0x%08X   lbu -> %3u = 0x%08X\n",
               (unsigned)mem[i], (int)lb, (unsigned)lb, (unsigned)lbu, (unsigned)lbu);
    }
    uint32_t w; memcpy(&w, mem, 4);       /* lw on a little-endian machine */
    printf("lw  -> 0x%08X\n", (unsigned)w);
    uint32_t consts[3] = {0x12345FFFu, 0xDEADBEEFu, 0x00000800u};
    for (int i = 0; i < 3; i++) {
        uint32_t c = consts[i], low = c & 0xFFFu;
        int32_t lo = (low >= 0x800u) ? (int32_t)low - 4096 : (int32_t)low;   /* signed 12-bit */
        uint32_t hi = ((c + 0x800u) >> 12) & 0xFFFFFu;
        uint32_t back = (hi << 12) + (uint32_t)lo;                           /* lui + addi, mod 2^32 */
        uint32_t naive = (((c >> 12) & 0xFFFFFu) << 12) + (uint32_t)lo;
        printf("0x%08X: lui 0x%05X, addi %5d -> 0x%08X   naive lui -> 0x%08X\n",
               (unsigned)c, (unsigned)hi, (int)lo, (unsigned)back, (unsigned)naive);
    }
    return 0;
}

Wandbox-এ (gcc, warning চালু) কোনো warning ছাড়া compile হয়ে আউটপুট:

byte F0: lb ->  -16 = 0xFFFFFFF0   lbu -> 240 = 0x000000F0
byte 7F: lb ->  127 = 0x0000007F   lbu -> 127 = 0x0000007F
byte 80: lb -> -128 = 0xFFFFFF80   lbu -> 128 = 0x00000080
lw  -> 0x00807FF0
0x12345FFF: lui 0x12346, addi    -1 -> 0x12345FFF   naive lui -> 0x12344FFF
0xDEADBEEF: lui 0xDEADC, addi  -273 -> 0xDEADBEEF   naive lui -> 0xDEADAEEF
0x00000800: lui 0x00001, addi -2048 -> 0x00000800   naive lui -> 0xFFFFF800

প্রথম তিন লাইন stepper আর JS simulator-এর সাথে হুবহু মেলে: F0 → −16 বনাম 240, 7F → 127 দুটিতেই, 80 → −128 বনাম 128. memcpy দিয়ে চারটি byte একটি uint32_t-এ আনলে 0x00807FF0 — x86-64-ও little-endian বলে RISC-V-এর lw-এর সাথে একই ফল। শেষ তিন লাইনে প্রতিটি ধ্রুবকের সঠিক lui-addi জোড়া মূল মান ফিরিয়ে দেয়, আর সংশোধন ছাড়া (“naive”) ফল প্রতিবার 0x1000 কম। একটি সূক্ষ্ম কথা: 127-এর বেশি মানকে (int8_t)-এ রূপান্তর C standard-এ implementation-defined; gcc সেটিকে নিয়মিতভাবে mod 256 ধরে, তাই এখানে ফল নির্ধারিত।

Alignment — ঠিকানাটি কি মাপের গুণিতক?

একটি N-byte-এর মান যদি N-এর গুণিতক ঠিকানায় থাকে (lw-এর জন্য ৪-এর গুণিতক, lh-এর জন্য জোড়), তবে সেটি naturally aligned (পর্ব ০৬-এর alignment আর padding). এর বাইরে গেলে misaligned access. RISC-V-এর unprivileged spec এখানে ইচ্ছা করেই নমনীয়: misaligned load-store hardware নিজে সামলাতে পারে, অথবা একটি exception (trap) তুলতে পারে, আর তখন firmware বা operating system চাইলে software-এ টুকরো টুকরো করে কাজটি সেরে দিতে পারে (trap-and-emulate). ফলে একই কোড এক chip-এ সামান্য ধীর, আরেক chip-এ শত গুণ ধীর হতে পারে, কোথাও আবার প্রোগ্রাম থেমেও যেতে পারে — কোন chip কী করে তা নির্দিষ্ট chip-এর নথিতে দেখতে হয়। x86 সাধারণত misaligned access hardware-এ সামলায়; পুরোনো ARM অনেক ক্ষেত্রে করত না। নিরাপদ নিয়ম: ডেটা aligned রাখুন — compiler নিজেই struct-এ padding দিয়ে তা করে।

৭ · সব একসাথে — একটি ছোট RV32I interpreter

এ পর্যন্ত যা শিখলেন, তা একটি কাজ করা যন্ত্রে বসানোর সময় হয়েছে। নিচের JavaScript প্রোগ্রামটি RV32I-এর একটি ছোট instruction set simulator: ৩২টি register (Int32Array, তাই প্রতিটি লেখায় আপনাআপনি ৩২-বিট wrap), ৬৪ byte-এর little-endian মেমরি, আর text assembly পড়ার একটি ছোট parser. এটি বোঝে add sub and or xor sll srl sra slt sltu, তাদের immediate রূপ, lui auipc lw lb lbu sw sb, আর pseudo-instruction li mv not neg-কে আসল instruction-এ ভেঙে নেয়। প্রতিটি instruction-এর পর যে register বা মেমরি বদলাল, তা ছাপে। (জায়গা বাঁচাতে প্রোগ্রামের instruction-গুলো | দিয়ে আলাদা করা; আসল assembler-এ প্রতি লাইনে একটি।)

// Tiny RV32I interpreter (text subset): 32 registers x 32 bits, little-endian byte memory
const ABI = "zero ra sp gp tp t0 t1 t2 s0 s1 a0 a1 a2 a3 a4 a5 a6 a7 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 t3 t4 t5 t6".split(" ");
const R = new Int32Array(32), mem = new Uint8Array(64);  R[2] = 64;            // sp = 64
const rn = n => (n === "fp" ? 8 : ABI.includes(n) ? ABI.indexOf(n) : +n.slice(1)), num = s => Number(s) | 0;
const hx = v => "0x" + (v >>> 0).toString(16).toUpperCase().padStart(8, "0");
const set = (d, v) => { if (rn(d)) R[rn(d)] = v; return `${d} = ${hx(R[rn(d)])} (${R[rn(d)]})`; };  // x0 stays 0
const ALU = { add: (a, b) => a + b, sub: (a, b) => a - b, and: (a, b) => a & b, or: (a, b) => a | b,
  xor: (a, b) => a ^ b, sll: (a, b) => a << (b & 31), srl: (a, b) => a >>> (b & 31), sra: (a, b) => a >> (b & 31), slt: (a, b) => +(a < b), sltu: (a, b) => +(a >>> 0 < b >>> 0) };
function expand(op, a) {                                 // pseudo-instructions -> real RV32I
  const P = { mv: [["addi", a[0], a[1], "0"]], not: [["xori", a[0], a[1], "-1"]], neg: [["sub", a[0], "zero", a[1]]] };
  if (op !== "li") return P[op] || [[op, ...a]];
  const v = num(a[1]), lo = (v << 20) >> 20, hi = ((v - lo) >>> 12) & 0xFFFFF;  // lo = signed low 12 bits
  return hi === 0 ? [["addi", a[0], "zero", String(lo)]] : [["lui", a[0], "0x" + hi.toString(16)], ["addi", a[0], a[0], String(lo)]];
}
function exec([op, d, x, y], pc) { const m = /^(-?\w+)\((\w+)\)$/.exec(x || "");  // m: "off(base)"
  if (op === "lui" || op === "auipc") return set(d, (op === "auipc" ? pc : 0) + (num(x) << 12));
  if (m) {                                               // loads/stores: base register + offset
    const ad = (R[rn(m[2])] + num(m[1])) >>> 0, v = R[rn(d)];
    if (op === "sw") for (let k = 0; k < 4; k++) mem[ad + k] = v >>> (8 * k);
    if (op === "sb") mem[ad] = v;                        // only the low 8 bits fit
    if (op[0] === "s") return `mem[${ad}..${ad + 3}] = ` + [...mem.slice(ad, ad + 4)].map(b => b.toString(16).padStart(2, "0")).join(" ");
    if (op === "lw") return set(d, mem[ad] | mem[ad + 1] << 8 | mem[ad + 2] << 16 | mem[ad + 3] << 24);
    return set(d, op === "lb" ? (mem[ad] << 24) >> 24 : mem[ad]);   // lb sign-, lbu zero-extends
  }
  const base = op === "sltiu" ? "sltu" : op.replace(/i$/, ""), imm = base !== op;
  const [lo, hi] = /^s(ll|rl|ra)i$/.test(op) ? [0, 31] : [-2048, 2047];         // shamt or 12-bit imm
  if (imm && (num(y) < lo || num(y) > hi)) throw Error(`${op}: immediate ${y} out of range`);
  return set(d, ALU[base](R[rn(x)], imm ? num(y) : R[rn(y)]));
}
const prog = `lui t0, 0x12346 | addi t0, t0, -1 | li t1, 0x12345FFF | xor t2, t0, t1 | andi t3, t0, 0xFF
  srli t4, t0, 16 | not t5, t3 | srai t6, t5, 4 | srli a0, t5, 4 | slt a1, t5, zero | sltu a2, t5, zero
  neg a3, t3 | sw t0, -4(sp) | lb a4, -4(sp) | lbu a5, -4(sp) | sb t4, -4(sp) | lw a6, -4(sp) | auipc a7, 1`;
let pc = 0;                                  // "|" separates instructions here only to save space
for (const [op, ...args] of prog.split(/\s*[|\n]\s*/).map(l => l.split(/[\s,]+/)))
  for (const ins of expand(op, args)) { console.log(`${pc.toString(16).padStart(2, "0")}  ${(ins[0] + " " + ins.slice(1).join(", ")).padEnd(22)} ${exec(ins, pc)}`); pc += 4; }

আউটপুট (বাঁয়ে pc hex-এ, তারপর আসলে চলা instruction, তারপর বদলানো মান):

00  lui t0, 0x12346        t0 = 0x12346000 (305422336)
04  addi t0, t0, -1        t0 = 0x12345FFF (305422335)
08  lui t1, 0x12346        t1 = 0x12346000 (305422336)
0c  addi t1, t1, -1        t1 = 0x12345FFF (305422335)
10  xor t2, t0, t1         t2 = 0x00000000 (0)
14  andi t3, t0, 0xFF      t3 = 0x000000FF (255)
18  srli t4, t0, 16        t4 = 0x00001234 (4660)
1c  xori t5, t3, -1        t5 = 0xFFFFFF00 (-256)
20  srai t6, t5, 4         t6 = 0xFFFFFFF0 (-16)
24  srli a0, t5, 4         a0 = 0x0FFFFFF0 (268435440)
28  slt a1, t5, zero       a1 = 0x00000001 (1)
2c  sltu a2, t5, zero      a2 = 0x00000000 (0)
30  sub a3, zero, t3       a3 = 0xFFFFFF01 (-255)
34  sw t0, -4(sp)          mem[60..63] = ff 5f 34 12
38  lb a4, -4(sp)          a4 = 0xFFFFFFFF (-1)
3c  lbu a5, -4(sp)         a5 = 0x000000FF (255)
40  sb t4, -4(sp)          mem[60..63] = 34 5f 34 12
44  lw a6, -4(sp)          a6 = 0x12345F34 (305422132)
48  auipc a7, 1            a7 = 0x00001048 (4168)

লাইন ধরে পড়ুন। 00–04: stepper-এর মতো lui 0x12346 + addi -1 = 0x12345FFF. 08–0c: li t1, 0x12345FFF লেখা হয়েছিল একটি লাইনে, কিন্তু চলেছে দুটি instruction — pseudo-instruction ভেঙে গেছে, আর pc ৮ বেড়েছে। 10: দুটি সমান, তাই xor = 0. 14–18: mask দিয়ে নিচের byte (0xFF) আর shift দিয়ে উপরের অর্ধেক (0x1234). 1c: not আসলে xori t5, t3, -1: 0xFF-এর উল্টো 0xFFFFFF00. 20–24: একই মান ৪ ঘর ডানে — srai বাঁয়ে ১ ঢুকিয়ে −16 রাখে, srli ০ ঢুকিয়ে বিশাল ধনাত্মক 0x0FFFFFF0 বানায়। 28–2c: 0xFFFFFF00 signed অর্থে শূন্যের চেয়ে ছোট (a1 = 1), unsigned অর্থে কোনো কিছুই শূন্যের চেয়ে ছোট নয় (a2 = 0). 30: neg = sub a3, zero, t3 = −255. 34: sw 0x12345FFF লিখেছে ff 5f 34 12 — little-endian, সবচেয়ে কম গুরুত্বের byte আগে। 38–3c: সেই প্রথম byte 0xFF থেকে lb = −1, lbu = 255. 40: sb t4 t4-এর (0x1234) শুধু নিচের byte 0x34 লিখেছে — বাকিটা কেটে গেছে। 44: তাই এখন lw পায় 0x12345F34. 48: auipc a7, 1 = pc (0x48) + 0x1000 = 0x1048. চাইলে কোডে addi t0, t0, 4095 যোগ করে দেখুন — simulator assembler-এর মতোই “immediate out of range” বলে থেমে যাবে।

৮ · বাকি instruction-এর মানচিত্র — শাখা, jump, ecall, pseudo-instruction, M extension আর RV64I

শাখা আর jump — শুধু তালিকা

প্রোগ্রাম সবসময় সোজা লাইনে চলে না; if, লুপ আর function call-এর জন্য pc বদলাতে হয়। RV32I-এ এর জন্য ছয়টি শর্তসাপেক্ষ শাখা আর দুটি jump. কীভাবে এগুলো দিয়ে if-else, while, for আর switch বানানো হয়, তা পর্ব ২৯-এ (শাখা আর লুপ assembly-তে); function call আর ফেরা পর্ব ৩০-এ। এখানে শুধু মানচিত্র:

Instructionকাজটীকা
beq rs1, rs2, labelসমান হলে লাফsigned/unsigned-এর প্রশ্ন নেই
bne rs1, rs2, labelঅসমান হলে লাফ
blt / bgers1 < rs2 / rs1 ≥ rs2 (signed)bgt, ble pseudo — operand উল্টে
bltu / bgeuএকই, unsignedarray-র সীমা যাচাইয়ে প্রায়ই
jal rd, labelrd = pc + 4, তারপর label-এ লাফfunction call (rd = ra) বা শুধু লাফ (rd = zero)
jalr rd, off(rs1)rd = pc + 4, তারপর rs1 + off ঠিকানায়ফেরা (ret), pointer দিয়ে call

ecall আর ebreak

ecall (environment call) হলো প্রোগ্রামের পক্ষ থেকে “উপরের স্তরকে” ডাকা — সাধারণত operating system-কে, অর্থাৎ একটি system call. কোন সেবা চাই আর তার argument কী, তা register-এ রেখে ecall; CPU তখন একটি exception তুলে উঁচু privilege-এ OS-এর handler-এ চলে যায় (বিস্তারিত পর্ব ৩৮ আর OS কোর্সের system call পর্বে)। Linux-এ RISC-V-এর প্রচলিত নিয়ম: a7-এ system call নম্বর, a0–a5-এ argument, ফল a0-তে। RARS-এর মতো শিক্ষামূলক simulator-এর নিজস্ব সেবা-নম্বর থাকে (সংখ্যা ছাপা, string ছাপা, প্রোগ্রাম শেষ) — সেগুলো Linux-এর নম্বর নয়, simulator-এর Help-এ দেখে নিন। ebreak debugger-কে ডাকে: breakpoint বসানোর জন্য debugger কোডের ওই জায়গায় সাময়িকভাবে ebreak বসায়।

প্রচলিত pseudo-instruction আর তাদের আসল রূপ

PseudoকাজAssembler যা বানায়
nopকিছুই নাaddi x0, x0, 0
mv rd, rsকপিaddi rd, rs, 0
not rd, rsসব বিট উল্টানোxori rd, rs, -1
neg rd, rs−rssub rd, x0, rs
li rd, immধ্রুবকaddi rd, x0, imm, বা lui + addi (+0x800 সংশোধনসহ)
la rd, symbollabel-এর ঠিকানাauipc rd, %pcrel_hi(symbol) + addi rd, rd, %pcrel_lo(…)
j labelশর্তহীন লাফjal x0, label
jr rsregister-এর ঠিকানায় লাফjalr x0, 0(rs)
retfunction থেকে ফেরাjalr x0, 0(ra)
call funcদূরের function ডাকাauipc ra, %pcrel_hi(func) + jalr ra, %pcrel_lo(…)(ra)
beqz rs, labelশূন্য হলে লাফbeq rs, x0, label

লক্ষ করুন, প্রায় প্রতিটি সারিতে x0 আছে। শূন্য register না থাকলে এদের প্রতিটির জন্য আলাদা opcode লাগত। আর %pcrel_hi-তে সেই একই +0x800 সংশোধন কাজ করে, শুধু ধ্রুবকের বদলে “label আর বর্তমান pc-এর দূরত্ব”-এর ওপর। la-র রূপ একটু ভিন্ন হতে পারে: position-independent কোডে (যেমন Linux-এর shared library) assembler auipc + lw দিয়ে একটি টেবিল (GOT) থেকে ঠিকানা পড়ে।

M extension — গুণ আর ভাগ

RV32I-এ গুণ বা ভাগের instruction নেই! খুব ছোট chip-এ multiplier বাদ দিয়ে জায়গা বাঁচানো যায়; তখন compiler গুণ-ভাগকে library function (যেমন gcc-র __mulsi3) দিয়ে করে। প্রায় সব বাস্তব chip-এ অবশ্য M extension থাকে: mul (৬৪-বিট গুণফলের নিচের ৩২ বিট), mulh, mulhu, mulhsu (উপরের ৩২ বিট — signed×signed, unsigned×unsigned, signed×unsigned), div, divu, rem, remu. এদের ব্যবহার আর hardware পর্ব ২১-এর গুণের instruction-এ, আর শূন্য দিয়ে ভাগ বা INT_MIN ÷ −1-এ কী হয় (RISC-V trap করে না, নির্দিষ্ট মান দেয়), তা পর্ব ২২-এর ভাগের সীমানা-য়।

RV64I — ৬৪-বিটে কী বদলায়

আপনার ফোন বা ল্যাপটপের মতো ৬৪-বিট RISC-V সিস্টেম RV64I ব্যবহার করে। মূল পার্থক্যগুলো:

  • Register ৬৪ বিটের। নাম আর ভূমিকা একই (x0–x31, a0–a7…)। add, sub, and এখন ৬৪-বিটে কাজ করে।
  • নতুন load-store: ld/sd (৮ byte), আর lwu — এখন ৩২-বিট load-এর পরেও ৩২টি বাঁয়ের বিট ফাঁকা থাকে, তাই lw sign-extend করে আর lwu zero-extend.
  • “w” পরিবার: addw, subw, addiw, sllw, srlw, sraw আর তাদের immediate রূপ — ৩২-বিটের হিসাব করে, তারপর ফলকে ৬৪ বিটে sign-extend করে। C-র int (৬৪-বিট সিস্টেমেও সাধারণত ৩২ বিট) এগুলো দিয়েই চলে। M extension-এও mulw, divw, remw ইত্যাদি যোগ হয়।
  • Shift-এর পরিমাণ ৬ বিট (০–৬৩), আর বড় ধ্রুবক বানাতে বেশি instruction লাগতে পারে — যেকোনো ৬৪-বিট ধ্রুবকের জন্য compiler কখনো কখনো ছয়-আটটি instruction বানায়, বা মেমরির একটি ধ্রুবক-টেবিল থেকে ld করে।

৯ · C থেকে RV32I — a[i] += 5, আর ঘরে বসে চর্চার টুল

C-র একটি লাইন, a[i] += 5; (a একটি int array, যার শুরুর ঠিকানা a0-তে, আর i আছে a1-এ), RV32I-এ কী হয়? Load-store নিয়ম মানলে চারটি কাজ লাগবে: ঠিকানা হিসাব, মান আনা, যোগ, মান ফেরত রাখা। ঠিকানা হিসাবেই দুটি instruction, কারণ int ৪ byte — i-কে ৪ দিয়ে গুণ (২ ঘর বাঁয়ে shift) করে base-এর সাথে যোগ। ধরুন a = 0x100, i = 3, আর a[3] = 37. “পরে” চাপুন আর register-এর মান দেখুন:

1# a[i] += 5   (a0 = &a[0], a1 = i)2slli t0, a1, 2      # t0 = i * 43add  t0, a0, t0     # t0 = &a[i]4lw   t1, 0(t0)      # t1 = a[i]5addi t1, t1, 5      # t1 = a[i] + 56sw   t1, 0(t0)      # a[i] = t1
Register আর মেমরি
a0
a1
t0
t1
mem[0x10C]
  1. শুরুর অবস্থা। a0-তে array-র শুরুর ঠিকানা 0x100, a1-এ i = 3. মেমরির 0x10C ঠিকানায় a[3] = 37. t0, t1-এ এখনো অর্থপূর্ণ কিছু নেই (?)।
  2. slli t0, a1, 2. i-কে ২ ঘর বাঁয়ে সরানো মানে ৪ দিয়ে গুণ: t0 = 12 (0x0000000C). এটি byte-এ দূরত্ব — a[3] শুরু থেকে ১২ byte পরে।
  3. add t0, a0, t0. base + দূরত্ব: t0 = 0x100 + 12 = 0x10C — a[3]-এর ঠিকানা। lw-এর offset একটি ধ্রুবক, i চলক বলে এই যোগ আলাদা instruction-এ করতে হলো।
  4. lw t1, 0(t0). আলমারি থেকে খাতা ডেস্কে: মেমরির 0x10C থেকে চার byte এনে t1 = 37. মেমরি অপরিবর্তিত।
  5. addi t1, t1, 5. হিসাব শুধু register-এ: t1 = 42. খেয়াল করুন, মেমরিতে এখনো 37 — এই মুহূর্তে অন্য কোনো thread a[3] পড়লে পুরোনো মান পাবে।
  6. sw t1, 0(t0). খাতা আলমারিতে ফেরত: 0x10C-এ এখন 42. পাঁচটি instruction, যার দুটি মেমরি ছোঁয়। x86-এ একই কাজ add dword ptr [rdi+rsi*4], 5 — এক instruction, কারণ x86-এ memory operand আর scaled index আছে।

Compiler-ও প্রায় ঠিক এটাই বানায়। নিচে void f(int *a, int i) { a[i] += 5; }-এর জন্য RV32-লক্ষ্যের gcc-র -O2-এর মতো আউটপুট (হাতে সাজানো; register-এর নাম আর ক্রম সংস্করণভেদে একটু ভিন্ন হতে পারে):

# উদাহরণ — riscv64-unknown-elf-gcc -march=rv32i -mabi=ilp32 -O2 -S f.c এর মতো আউটপুট
f:
        slli    a1,a1,2
        add     a1,a0,a1
        lw      a5,0(a1)
        addi    a5,a5,5
        sw      a5,0(a1)
        ret

এখানে compiler t0-এর বদলে a1 আর a5 ব্যবহার করেছে — function-এর ভেতরে argument register-ও temporary-র মতো ব্যবহার করা যায়, কারণ calling convention-এ এগুলো caller-saved. পুরো C → assembly-র পথ, optimization আর x86-64 ও ARM-এর সাথে তুলনা পর্ব ৩১-এ (C থেকে assembly), আর C-এর দিক থেকে C প্রোগ্রামিং কোর্সের C আর assembly পর্বে।

আরও গভীরে: বাস্তবের assembler আর CPU এই ছোট instruction-গুলোকে আরও দক্ষ করে। RISC-V-এর GNU linker “relaxation” করে: call-এর auipc + jalr জোড়ার লক্ষ্য কাছে হলে সেটিকে একটি jal-এ ছোট করে দেয়, আর gp-এর কাছের global-কে gp-ভিত্তিক এক instruction-এ আনে (linker relaxation). বড় out-of-order core lui + addi বা slli + add-এর মতো জোড়াকে ভেতরে একটি কাজ হিসেবে চালাতে পারে (macro-op fusion) — কোন core কোন জোড়া মেলায়, তা নকশাভেদে আলাদা। আর নতুন Zba extension-এর sh2add a1, a1, a0 (a0 + (a1 << 2)) একাই প্রথম দুটি instruction-এর কাজ করে; সাম্প্রতিক application-প্রোফাইলগুলোতে Zba অন্তর্ভুক্ত।

ঘরে বসে চর্চা — simulator আর toolchain

এই পাতার simulator শেখার জন্য যথেষ্ট, কিন্তু পূর্ণ RISC-V assembly চালাতে কয়েকটি বিনামূল্যের টুল আছে (নাম আর সংস্করণ সময়ের সাথে বদলাতে পারে):

  • RARS (RISC-V Assembler and Runtime Simulator) — Java-তে লেখা desktop simulator, MIPS-এর পরিচিত MARS থেকে এসেছে। Register আর মেমরি টেবিল দেখায়, ধাপে ধাপে চালানো যায়, ecall দিয়ে সংখ্যা ছাপা যায়।
  • Venus — ব্রাউজারে চলে এমন RISC-V simulator; কিছু বিশ্ববিদ্যালয়ের কোর্সে ব্যবহার হয়। কিছু ইনস্টল করতে হয় না।
  • Ripes — গ্রাফিক্যাল simulator, যা instruction-কে pipeline-এর ধাপে ধাপে চলতে দেখায় — পর্ব ৩২–৩৬ পড়ার সময় খুব কাজে লাগবে।
  • আসল toolchain — একটি cross-compiler (আপনার x86 কম্পিউটারে চলে, কিন্তু RISC-V-এর কোড বানায়) আর QEMU emulator. Ubuntu-তে সাধারণত package থেকেই পাওয়া যায়:
# উদাহরণ — এই পাতার runner-এ চলে না; নিজের Linux-এ (Ubuntu/Debian ধরে নিয়ে, package-এর নাম ভিন্ন হতে পারে)
sudo apt install gcc-riscv64-linux-gnu qemu-user
riscv64-linux-gnu-gcc -O2 -S f.c -o f.s          # see the RV64 assembly
riscv64-linux-gnu-gcc -O2 -static hello.c -o hello
qemu-riscv64 ./hello                              # run a RISC-V binary on x86

# bare-metal toolchain (riscv64-unknown-elf-gcc) থাকলে ৩২-বিট কোডও দেখা যায়:
riscv64-unknown-elf-gcc -march=rv32i -mabi=ilp32 -O2 -S f.c   # f.s: assembly
riscv64-unknown-elf-gcc -march=rv32i -mabi=ilp32 -O2 -c f.c   # f.o: object file
riscv64-unknown-elf-objdump -d f.o                # machine code + disassembly

সবচেয়ে সহজ পথ অবশ্য ব্রাউজারের Compiler Explorer (godbolt.org): compiler তালিকা থেকে একটি RISC-V gcc বা clang বেছে -O2 দিলেই C-র পাশে assembly দেখা যায়। শেখার ভালো অভ্যাস: প্রথমে নিজে অনুমান করুন compiler কী বানাবে, তারপর মিলিয়ে দেখুন।

১০ · AI-এর যুগে RISC-V assembly

AI টুল RISC-V-এর সাধারণ কোড ভালোই লেখে, কিন্তু ঠিক এই পর্বের খুঁটিনাটিতে প্রায়ই ভুল করে — বিশেষ করে যেহেতু তার শেখার উপাদানে MIPS, ARM আর RISC-V-এর কোড মিশে আছে। প্রচলিত ভুলগুলো:

AI-এর প্রচলিত ভুলঠিক কথাকীভাবে যাচাই
lui t0, 0x12345 + addi t0, t0, 0xFFF লেখা0xFFF সীমার বাইরে; −1 লিখলে lui-তে +1 লাগে: lui 0x12346অংশ ৪-এর simulator, বা assembler চালিয়ে
subi, not-এর আলাদা opcode, বা flag register আছে ধরে নেওয়াaddi −k; not = xori −1; flag নেইRISC-V unprivileged spec-এর RV32I তালিকা
MIPS-এর $t0, lui + ori বা চারটি argument register-এর নিয়মRISC-V-এ ডলার নেই, a0–a7 আটটি, ori-র imm sign-extendRARS-এ চালালেই assembler আপত্তি করবে
lb দিয়ে unsigned char পড়া0x80–0xFF-এর byte-এ ভুল ঋণাত্মক মান; lbu লাগবে0xF0 byte দিয়ে চালিয়ে দেখা
sw-এ operand উল্টো, বা lw t0, t1(4)-এর মতো ভুল ছাঁচsw rs2, off(rs1); offset বাইরে, base বন্ধনীতেdisassembly (objdump -d) দেখে
“RISC-V misaligned access সবসময় চলে” বা “সবসময় trap করে”Spec দুটিই অনুমতি দেয়; chip ও firmware-ভেদে আলাদানির্দিষ্ট chip-এর নথি

AI-কে RISC-V assembly-র প্রশ্ন করলে ISA, register-এর প্রাথমিক মান আর প্রতিটি ধাপের মান চেয়ে নিন, যাতে নিজে মিলিয়ে দেখতে পারেন:

আমি RV32I (RISC-V, ৩২ বিট, শুধু base integer instruction) শিখছি। MIPS-এর নিয়ম বা ডলার-চিহ্নের register নাম ব্যবহার কোরো না। ১. t0-তে 0xCAFEBABE লোড করার জন্য lui আর addi লেখো। নিচের ১২ বিট কত, sign-extend করলে কত, আর lui-র মান কেন সংশোধন করতে হলো — দেখাও। ২. মেমরিতে 0x200 ঠিকানা থেকে byte F0 7F 80 00 আছে। lb, lbu, lh, lhu, lw দিয়ে 0x200 আর 0x202 থেকে পড়লে register-এ কী আসবে, hex-এ লেখো। ৩. C-র a[i] += 5 (int a[], a0 = &a[0], a1 = i) RV32I-এ লেখো, প্রতিটি instruction-এর পর register-এর মান দেখিয়ে (a0 = 0x100, i = 3, a[3] = 37). ৪. শেষে বলো কোন উত্তরগুলো আমি RARS বা Ripes-এ চালিয়ে যাচাই করতে পারি, আর কোথায় তুমি নিশ্চিত নও।

AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • RISC-V-এ subi নেই কেন? — addi-র immediate signed, তাই addi rd, rs, -k-ই বিয়োগ; আলাদা opcode অপ্রয়োজনীয়।
  • 0x12345FFF কীভাবে লোড করবেন? — lui t0, 0x12346 তারপর addi t0, t0, -1; নিচের ১২ বিট ≥ 0x800, তাই lui-তে +1.
  • lb আর lbu-এর পার্থক্য? — lb bit 7 কপি করে (sign extension), lbu ০ বসায়; byte 0xF0 থেকে −16 বনাম 240.
  • x0 register কেন? — সবসময় ০; mv, neg, nop, j, beqz-এর মতো অনেক pseudo-instruction বিনা খরচে তৈরি হয়।
  • Load-store architecture কী? — হিসাব শুধু register-এ; মেমরি শুধু load আর store দিয়ে — RISC-V, ARM, MIPS; x86 নয়।

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

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

প্র ০১৩২টি register-এর একটি, x0, স্থায়ীভাবে শূন্য — এটি কি একটি register নষ্ট করা নয়? অন্য ISA এই সমস্যা কীভাবে সামলেছে?

শূন্য-register-এর ধারণা RISC-V-এর আবিষ্কার নয়। ১৯৮০-র দশকের প্রথম দিকের RISC নকশাগুলোতেই এটি দেখা যায়: Stanford-এর MIPS-এ $zero, Berkeley-র RISC প্রকল্প থেকে আসা SPARC-এ %g0. পরে ARM-এর ৬৪-বিট ISA (AArch64) একটি চতুর রূপ নেয়: register নম্বর ৩১ কোনো instruction-এ শূন্য-register (XZR/WZR) আর কোনো instruction-এ stack pointer — কোনটি, তা instruction-ভেদে নির্দিষ্ট। x86-এ শূন্য-register নেই; সেখানে register শূন্য করতে xor eax, eax লেখা হয়, যাকে আধুনিক Intel ও AMD CPU বিশেষভাবে চিনে প্রায় বিনা খরচে চালায়।

লাভটা কোথায়? প্রথমত, অনেক instruction “বিনা পয়সায়” পাওয়া যায়। কপির জন্য আলাদা mv লাগে না (addi rd, rs, 0), negation-এর জন্য আলাদা neg নয় (sub rd, x0, rs), শূন্যের সাথে তুলনার শাখা আলাদা নয় (beq rs, x0), শর্তহীন লাফ আলাদা নয় (jal x0), আর কিছু না করার nop-ও নয় (addi x0, x0, 0). ফলে decoder ছোট থাকে, আর encoding-এর জায়গা ভবিষ্যৎ extension-এর জন্য খালি থাকে। দ্বিতীয়ত, ফল ফেলে দেওয়া সহজ — গন্তব্য x0 দিলে ফল কোথাও লেখা হয় না। jal x0 ঠিক এভাবে return address ফেলে দেয়।

খরচ: ৩২টির মধ্যে একটি register সাধারণ কাজে পাওয়া যায় না — প্রায় ৩ শতাংশ। কিন্তু compiler-এর গবেষণায় দীর্ঘদিন ধরে দেখা গেছে, ১৬ থেকে ৩২টি register থাকলে বেশিরভাগ function-এর চলকের জন্য যথেষ্ট; ৩১ আর ৩২-এর পার্থক্য সামান্য। Hardware-এ খরচ আরও কম: x0 পড়লে register file থেকে না পড়ে সরাসরি ০ পাঠানো, আর লেখার সময় x0-কে উপেক্ষা করা — কয়েকটি gate. Pipeline-এ (পর্ব ৩৬) forwarding-এর যুক্তিতে শুধু খেয়াল রাখতে হয়, x0-তে “লেখা” কোনো instruction থেকে ভুল করে মান forward না হয়।

একটি সূক্ষ্ম সুবিধাও আছে: RISC-V-এ কিছু encoding-এ “rd = x0” ব্যবহার করে ইঙ্গিত (hint) রাখার জায়গা সংরক্ষিত — যেমন ফল ফেলে দেওয়া কোনো instruction ভবিষ্যতে বিশেষ অর্থ পেতে পারে, অথচ পুরোনো CPU-তে সেটি নিরাপদে nop-এর মতো চলবে।

মূল উপলব্ধি: একটি register “নষ্ট” করে ISA পায় ছোট decoder, কম opcode আর অনেক pseudo-instruction. এটি RISC দর্শনের সারকথা — কয়েকটি সাধারণ, নিয়মিত উপাদান জুড়ে বাকিটা software দিয়ে বানানো।

প্র ০২৩২-বিট instruction-এ ৩২-বিট ধ্রুবক ধরে না। MIPS, ARM, x86 আর RISC-V এই সমস্যা কীভাবে আলাদাভাবে সমাধান করেছে?

Fixed-length instruction-এর ISA-তে মৌলিক সমস্যা একটাই: ৩২ বিটের instruction-এ opcode আর register-এর জায়গা দেওয়ার পর পুরো ৩২-বিট ধ্রুবকের জায়গা থাকে না। চার ISA চার রকম উত্তর দিয়েছে, আর প্রতিটি উত্তর সেই ISA-র দর্শন দেখায়।

MIPS বেছেছিল lui + ori. MIPS-এর immediate ১৬ বিটের, আর ori-র immediate zero-extend হয়। তাই উপরের ১৬ বিট lui-তে, নিচের ১৬ বিট ori-তে — কোনো সংশোধন লাগে না, কারণ OR কখনো উপরের বিটে ধার নেয় না। RISC-V-এর logical immediate-ও sign-extend হয় (নকশাকে নিয়মিত রাখতে সব immediate-এ এক নিয়ম, যাতে hardware-এ একটিই sign-extension পথ লাগে), তাই ori দিয়েও একই ফাঁদ থাকত; RISC-V তাই addi ব্যবহার করে আর +0x800 সংশোধন assembler-এর (%hi) দায়িত্বে দেয়।

ARM-এর পুরোনো ৩২-বিট ISA ছিল সবচেয়ে চতুর: ৮-বিটের একটি মান আর ৪-বিটের “ঘোরানোর পরিমাণ” দিয়ে immediate বানানো, ফলে 0xFF000000-এর মতো অনেক মান এক instruction-এ আসত, কিন্তু যেকোনো মান নয়। বাকিগুলো কোডের কাছেই মেমরিতে রাখা হতো (literal pool) আর pc-relative load দিয়ে আনা হতো। AArch64 আনে movz আর movk: ১৬ বিটের টুকরো নির্দিষ্ট অবস্থানে বসানো, অন্য বিট অপরিবর্তিত রেখে — ৬৪-বিট ধ্রুবক সর্বোচ্চ চারটি instruction-এ, কোনো সংশোধন ছাড়া।

x86-এর instruction দৈর্ঘ্য পরিবর্তনশীল (১ থেকে ১৫ byte), তাই সমস্যাটিই নেই: mov eax, 0x12345FFF একটি ৫-byte instruction, আর ৬৪-বিট মোডে movabs পুরো ৮ byte-এর ধ্রুবক নিতে পারে। দাম অন্য জায়গায়: decoder জটিল, কারণ পরের instruction কোথায় শুরু তা জানতে আগেরটির দৈর্ঘ্য বুঝতে হয়।

RISC-V-এর ৬৪-বিট রূপে যেকোনো ৬৪-বিট ধ্রুবকের জন্য compiler কখনো কখনো lui, addiw, slli, addi-র লম্বা শিকল বানায়, বা মেমরির ধ্রুবক-টেবিল থেকে ld করে। বাস্তবে বেশিরভাগ ধ্রুবক ছোট (০, ১, −১, ছোট offset), তাই এক instruction-এই হয়ে যায় — RISC-V-এর নকশাকারেরা ঠিক এই পরিসংখ্যানের ওপর ভরসা করেছিলেন।

মূল উপলব্ধি: বড় ধ্রুবক হলো fixed-length ISA-র একটি স্বাভাবিক দাম। কেউ সমাধান দেয় hardware-এ (x86-এর লম্বা instruction), কেউ instruction-জোড়ায় (MIPS, RISC-V), কেউ মেমরিতে (ARM-এর literal pool) — আর RISC-V-এর +0x800 সংশোধন হলো “সব immediate sign-extend” নিয়মের ছোট্ট মূল্য।

প্র ০৩x86-এ a[i] += 5 এক instruction, RISC-V-এ পাঁচটি। তাহলে load-store নকশা কি ধীর? আধুনিক CPU এই প্রশ্নের উত্তর কীভাবে দেয়?

১৯৮০-র দশকের শুরুতে Berkeley-তে David Patterson-এর RISC প্রকল্প আর Stanford-এ John Hennessy-র MIPS প্রকল্প একটি বিতর্কিত দাবি করেছিল: জটিল instruction-এর বদলে সরল instruction বেশি করে চালালে মোট সময় কম লাগতে পারে। যুক্তি ছিল CPU time-এর সূত্রে (পর্ব ০৩): সময় = instruction সংখ্যা × CPI × clock-এর সময়। Load-store নকশায় instruction সংখ্যা বাড়ে, কিন্তু প্রতিটি instruction সরল ও একই ছাঁচের বলে pipeline-এ সহজে চলে (CPI কম), আর clock দ্রুত করা যায়। এই কাজের জন্য Patterson আর Hennessy ২০১৭ সালের Turing Award পান।

আজকের x86 CPU-ও আসলে ভেতরে এই দর্শনই মানে। add dword ptr [rdi+rsi*4], 5-কে decoder ভেঙে কয়েকটি micro-operation (μop) বানায় — ঠিকানা হিসাব আর load, যোগ, store — মোটামুটি RISC-V-এর পাঁচটি instruction-এর কাজই, শুধু ভেতরে। অর্থাৎ কাজের পরিমাণ একই থাকে; পার্থক্য হলো ভাঙার কাজটি কে করে — compiler (RISC-V) নাকি hardware-এর decoder (x86).

তবু x86-এর দুটি বাস্তব সুবিধা আছে। প্রথমত, code density: এক instruction কম byte-এ লেখা যায়, তাই instruction cache-এ বেশি কোড ধরে। RISC-V এর জবাবে C (compressed) extension এনেছে, যেখানে প্রচলিত instruction-এর ১৬-বিট রূপ থাকে; ফলে RISC-V কোডের আকার x86 আর ARM-এর কাছাকাছি আসে বলে নানা পরিমাপে দেখা গেছে। দ্বিতীয়ত, decoder-এর ভেতরে জোড়া লাগানো: বড় RISC-V core পাশাপাশি দুটি instruction (যেমন slli + add) মিলিয়ে একটি কাজ হিসেবে চালাতে পারে — macro-op fusion — আর Zba-র sh2add সেই জোড়াকে ISA-তেই একটি instruction বানিয়েছে।

আরেকটি বিষয় প্রায়ই ভুলে যাওয়া হয়: concurrency. x86-এর add [mem], 5-ও এক instruction হলেও atomic নয় (lock prefix ছাড়া)। অর্থাৎ অন্য core মাঝখানে একই ঘর বদলাতে পারে, ঠিক RISC-V-এর lw-addi-sw-এর মতো। Atomic হিসাবের জন্য RISC-V-এর A extension-এ amoadd.w-এর মতো আলাদা instruction আছে (OS কোর্সের atomics পর্ব দেখুন)।

মূল উপলব্ধি: Instruction সংখ্যা একা গতি ঠিক করে না। Load-store নকশা জটিলতাকে compiler-এ সরিয়ে hardware-কে দ্রুত আর সরল রাখে; CISC সেই একই ভাঙার কাজ decoder-এ লুকায়। আধুনিক CPU-তে দুই পথ মাঝামাঝি এসে মিলেছে।

প্র ০৪Misaligned load-এ hardware-এর সমস্যা ঠিক কোথায়? RISC-V কেন spec-এ এটি নির্দিষ্ট না করে chip-এর ওপর ছেড়ে দিল?

মেমরি আর cache আসলে byte ধরে কাজ করে না, কাজ করে বড় টুকরোয় — cache line সাধারণত ৬৪ byte (পর্ব ৪৫), আর ঠিকানা অনুবাদ হয় page ধরে, সাধারণত ৪ KiB (পর্ব ৫০)। একটি aligned ৪-byte word সবসময় একটি line আর একটি page-এর ভেতরে থাকে, তাই একবার পড়লেই হয়। কিন্তু misaligned word দুটি cache line-এর সীমানা পেরোতে পারে — তখন দুবার পড়ে byte-গুলো জোড়া লাগাতে হয়; এমনকি দুটি page পেরোলে দুটি আলাদা ঠিকানা-অনুবাদ লাগে, আর দ্বিতীয়টিতে page fault হলে প্রথম অর্ধেকের কী হবে, সেটিও সামলাতে হয়। এই সব ক্ষেত্রের জন্য hardware-এ বাড়তি পথ লাগে, যা ছোট chip-এ অপ্রয়োজনীয় খরচ।

ইতিহাসে দুই রকম পথ দেখা যায়। x86 শুরু থেকেই misaligned access অনুমতি দিয়েছে, আর আধুনিক x86 CPU সাধারণত সেটি দ্রুতই করে — cache line পেরোলে সামান্য বেশি সময়। পুরোনো ARM (ARMv5 পর্যন্ত) misaligned ldr-এ অদ্ভুত আচরণ করত — byte-গুলো ঘুরিয়ে দিত — আর অনেক সিস্টেমে এটি exception দিত; ARMv6 থেকে সাধারণ load-এ hardware সমর্থন আসে। MIPS-এ misaligned load exception দিত, আর পাশে lwl/lwr-এর মতো বিশেষ instruction ছিল জোড়া লাগানোর জন্য।

RISC-V দুটিই খোলা রেখেছে, কারণ এর লক্ষ্য খুব ছোট microcontroller থেকে বড় server পর্যন্ত। ছোট chip বাড়তি hardware বাদ দিয়ে exception তুলতে পারে; তখন machine-mode firmware (যেমন OpenSBI-এর মতো একটি স্তর) বা operating system চাইলে byte-load দিয়ে কাজটি নকল করে দিতে পারে — প্রোগ্রাম সঠিক চলে, কিন্তু প্রতিটি misaligned access-এ শত শত cycle খরচ হতে পারে। বড় chip সরাসরি hardware-এ করতে পারে। Linux-এর মতো সিস্টেমে software এমন ধীর পথ ধরা পড়লে সতর্কবার্তাও দিতে পারে। বাস্তবের নির্দিষ্ট আচরণ chip আর firmware-ভেদে আলাদা, তাই নথি দেখে নিশ্চিত হওয়াই নিয়ম।

Programmer-এর জন্য শিক্ষা পরিষ্কার: network packet বা file format পড়ার সময় (যেখানে field যেকোনো ঠিকানায় থাকতে পারে) pointer cast করে *(uint32_t *)p না লিখে memcpy ব্যবহার করুন — compiler লক্ষ্য-chip জেনে নিরাপদ আর দ্রুত কোড বানাবে। __attribute__((packed)) struct-ও misaligned access আনতে পারে; সেটি জেনে ব্যবহার করুন।

মূল উপলব্ধি: Alignment-এর নিয়ম আসলে cache line আর page-এর সীমানা থেকে আসে। RISC-V নমনীয়তা রেখে ছোট chip-কে সস্তা রেখেছে, কিন্তু তার দাম দেয় বহনযোগ্যতা — একই কোড chip-ভেদে খুব ভিন্ন গতিতে চলতে পারে।

প্র ০৫RV32I-এ মাত্র প্রায় ৪০টি instruction, গুণ-ভাগও নেই। এত ছোট সেট দিয়ে কি সত্যিই পুরো C, এমনকি Linux চালানো যায়? বাস্তব chip-এ কী থাকে?

তাত্ত্বিকভাবে C চালাতে খুব কম জিনিস লাগে: যোগ-বিয়োগ, বিট-কাজ, তুলনা, শাখা, load আর store — RV32I-এ এর সবই আছে। গুণ না থাকলে compiler shift আর add দিয়ে বানানো library function ডাকে (gcc-র libgcc-তে __mulsi3, __divsi3 ইত্যাদি); floating point না থাকলে soft-float library integer instruction দিয়ে IEEE 754-এর হিসাব নকল করে (পর্ব ২৩)। ধীর, কিন্তু সঠিক। তাই খুব ছোট sensor বা নিয়ন্ত্রক chip-এ শুধু RV32I বা RV32E রাখা অর্থবহ — সিলিকনের জায়গা আর শক্তি দুটোই বাঁচে।

Linux-এর মতো operating system-এর দরকার আরও বেশি: privilege-এর স্তর আর virtual memory-র hardware (privileged spec), atomic instruction (A extension — একাধিক core-এ lock-এর জন্য), control register পড়া-লেখার instruction (Zicsr), আর ব্যবহারিক গতির জন্য M, F, D আর C. এই কারণে বহু বছর ধরে Linux-চালানো RISC-V সিস্টেমের ন্যূনতম মান ছিল “RV64GC” — G মানে IMAFD সঙ্গে Zicsr ও Zifencei. পরে RISC-V International “profile” নামে প্যাকেজ সংজ্ঞায়িত করেছে (যেমন RVA22, RVA23), যাতে software নির্মাতারা জানেন একটি application processor-এ ঠিক কোন কোন extension নিশ্চিত থাকবে — সাম্প্রতিক profile-এ vector extension আর Zba-র মতো বিট-কাজের extension-ও আছে।

বাস্তব chip-এ এই মডিউলারিটি স্পষ্ট দেখা যায়। Espressif-এর ESP32-C3-এর মতো সস্তা Wi-Fi microcontroller-এ একটি ৩২-বিট RISC-V core আছে (মোটামুটি RV32IMC); Raspberry Pi-এর RP2350 chip-এ ARM core-এর পাশাপাশি বিকল্প হিসেবে দুটি RISC-V core (Hazard3) রাখা হয়েছে; আর বড় দিকে ৬৪-বিট বহু-core chip Linux চালায়। একই base ISA, একই compiler, শুধু extension-এর তালিকা আলাদা — gcc-তে -march=rv32imc বা -march=rv64gc দিয়ে ঠিক সেটিই বলা হয়।

এই নকশার একটি শিক্ষাগত সুবিধাও আছে, যা এই কোর্স ব্যবহার করছে: base ISA এত ছোট যে একজন শিক্ষার্থী কয়েক সপ্তাহে পুরোটা বুঝে নিজে একটি simulator বা এমনকি একটি CPU (পর্ব ৩২ আর ক্যাপস্টোন পর্ব ৫৭) বানাতে পারেন। x86-এর হাজারখানেক instruction-এ সেটি প্রায় অসম্ভব।

মূল উপলব্ধি: RV32I হলো ন্যূনতম, কিন্তু সম্পূর্ণ ভিত্তি; বাকি সব extension দিয়ে প্রয়োজন অনুযায়ী যোগ হয়। কোন chip-এ কী আছে, তা extension-এর নামের তালিকাই বলে দেয় — আর compiler সেই তালিকা মেনে কোড বানায়।

অনুশীলন

পরীক্ষার ধাঁচে লেখা — আগে খাতায় সমাধান করুন, তারপর উত্তর মিলিয়ে নিন। সব উত্তর এই পর্বের simulator-এর মতো script দিয়ে যাচাই করা।

  1. RV32I লিখুন (শুধু base instruction, mul নয়): (ক) a0 = a1 − 100; (খ) a0 = 0x00ABC800; (গ) a0 = a1 mod 16 (a1 unsigned); (ঘ) a0 = (a1 == a2) ? 1 : 0; (ঙ) a0 = a1 × 10; (চ) a0 = a1-কে নিচের দিকে ৮-এর গুণিতকে নামানো (যেমন 1234 → 1232).
    # (ক)
    addi  a0, a1, -100            # no subi: add a negative immediate
    # (খ) low 12 bits = 0x800 (bit 11 = 1) -> lo = -2048, hi = 0xABC + 1
    lui   a0, 0xABD               # a0 = 0x00ABD000
    addi  a0, a0, -2048           # a0 = 0x00ABC800
    # (গ)
    andi  a0, a1, 15              # x mod 2^k = x AND (2^k - 1), unsigned
    # (ঘ)
    sub   t0, a1, a2              # 0 exactly when equal  (xor also works)
    seqz  a0, t0                  # = sltiu a0, t0, 1
    # (ঙ) 10x = 8x + 2x
    slli  t0, a1, 3
    slli  t1, a1, 1
    add   a0, t0, t1
    # (চ) -8 = 0xFFFFFFF8 clears the low 3 bits
    andi  a0, a1, -8
    • (খ) যাচাই: 0x00ABD000 − 2048 = 0x00ABD000 − 0x800 = 0x00ABC800. সংশোধন ছাড়া lui 0xABC দিলে 0x00ABB800 আসত।
    • (ঙ) a1 = 1234 হলে 9872 + 2468 = 12340. (চ) 1234 AND −8 = 1232. (গ) 1234 mod 16 = 2.
    • (গ)-র কৌশল শুধু unsigned বা অঋণাত্মক মানে C-র %-এর সমান; ঋণাত্মক signed মানে C-র ভাগশেষ ঋণাত্মক হয় (পর্ব ২০)।
  2. Register-এর মান অনুমান করুন: RV32I-এ নিচের কোড চলার পর t1–t6, a0, a5 আর a1–a4-এর মান hex-এ (আর দরকারে দশমিকে) লিখুন। sp একটি ৪-এর গুণিতক ঠিকানা, মেমরি little-endian.
    li    t0, -1
    srli  t1, t0, 28
    srai  t2, t0, 28
    slli  t3, t1, 31
    slt   t4, t3, zero
    sltu  t5, t3, t1
    xori  t6, t1, -1
    addi  a0, zero, 2047
    addi  a0, a0, 1
    li    a6, 33
    sll   a5, t1, a6
    sw    t3, 0(sp)
    lbu   a1, 3(sp)
    lb    a2, 3(sp)
    lhu   a3, 2(sp)
    lh    a4, 0(sp)
    t0 = 0xFFFFFFFF   (li -1 = addi t0, zero, -1)
    t1 = 0x0000000F   (logical: zeros enter from the left)
    t2 = 0xFFFFFFFF   (arithmetic: copies of the sign bit enter)
    t3 = 0x80000000   (only bit 0 of 0xF lands on bit 31; the rest fall off)
    t4 = 1            (0x80000000 is negative as signed)
    t5 = 0            (2147483648 <u 15 ?  no)
    t6 = 0xFFFFFFF0   (NOT 0xF)
    a0 = 0x00000800 = 2048   (2047 + 1: 2048 itself is not a legal addi immediate)
    a5 = 0x0000001E   (shift amount 33 -> low 5 bits = 1, so 0xF << 1)
    memory at sp: 00 00 00 80
    a1 = 0x00000080 = 128      a2 = 0xFFFFFF80 = -128
    a3 = 0x00008000 = 32768    a4 = 0x00000000 = 0
    • t3: 0xF << 31-এ গাণিতিক ফল 0x7_8000_0000, কিন্তু register রাখে নিচের ৩২ বিট: 0x80000000.
    • sw-এর পর little-endian-এ sp+3-এ সবচেয়ে বেশি গুরুত্বের byte 0x80 বসে; তাই lbu/lb 3(sp) থেকে 128/−128, lhu 2(sp) থেকে byte 00 80 → 0x8000, আর lh 0(sp) থেকে 00 00 → 0.
  3. ভুল ধ্রুবক সংশোধন করুন: একজন বন্ধু t0-তে 0xCAFEBABE রাখতে লিখেছেন lui t0, 0xCAFEB আর addi t0, t0, -1346. (ক) এই দুটি চলার পর t0-তে কী থাকবে? (খ) ভুলটি কোথায়, সঠিক কোড কী? (গ) আরেকজন লিখেছেন addi t0, t0, 0xABE — assembler কী বলবে, কেন?
    0xCAFEBABE: upper 20 = 0xCAFEB, lower 12 = 0xABE = 1010 1011 1110 (bit 11 = 1)
    0xABE as signed 12-bit = 2750 - 4096 = -1346        (this part was right)
    (a) 0xCAFEB000 + (-1346) = 0xCAFEB000 - 0x542 = 0xCAFEAABE   (0x1000 too small)
    (b) hi = (0xCAFEBABE + 0x800) >> 12 = 0xCAFEC
        lui  t0, 0xCAFEC        # t0 = 0xCAFEC000
        addi t0, t0, -1346      # t0 = 0xCAFEBABE   ok
    (c) 0xABE = 2750 > 2047: out of the 12-bit signed range -> assembler error
    • সহজে মনে রাখার নিয়ম: নিচের তিন hex অঙ্কের প্রথমটি 8–F হলে (bit 11 = 1) lui-র মান ১ বাড়ান। এখানে প্রথম অঙ্ক A, তাই 0xCAFEB → 0xCAFEC.
    • সবচেয়ে নিরাপদ: li t0, 0xCAFEBABE লিখে assembler-কে হিসাব করতে দিন, বা lui t0, %hi(0xCAFEBABE) / addi t0, t0, %lo(0xCAFEBABE).
  4. হাতে trace আর কোড লেখা: C-তে unsigned char s[] একটি byte-array, যার শুরুর ঠিকানা a0 = 0x200, আর a1 = i = 5. s[i] = s[i] + 1;-এর জন্য RV32I লিখুন। s[5] = 0xFF হলে প্রতিটি instruction-এর পর register আর মেমরির মান লিখুন। (খ) কেউ lbu-র বদলে lb লিখলে এখানে কী বদলাবে, আর if (s[i] > 100)-এর মতো কোডে কী বিপদ হবে? (গ) s যদি int-এর array হতো, কী কী বদলাত?
    add   t0, a0, a1      # t0 = 0x200 + 5 = 0x205   (1 byte per element: no shift)
    lbu   t1, 0(t0)       # t1 = 0x000000FF = 255
    addi  t1, t1, 1       # t1 = 0x00000100 = 256    (register has 32 bits: no wrap yet)
    sb    t1, 0(t0)       # stores only the low byte 0x00 -> s[5] = 0
    • চারটি instruction. ৮-বিট wrap (255 + 1 = 0) ঘটে sb-এ, যখন নিচের ৮ বিট কেটে রাখা হয় — ঠিক C-র unsigned char-এর নিয়ম।
    • (খ) lb দিলে t1 = 0xFFFFFFFF = −1, তারপর +1 = 0, আর sb-ও 0 রাখে — এই লাইনের ফল একই! কিন্তু তুলনায় বিপদ: 0xFF-কে lb −1 পড়ে, তাই 255 > 100 সত্য হওয়ার কথা হলেও blt/bge-ভিত্তিক তুলনা মিথ্যা দেবে। unsigned ডেটায় তাই সবসময় lbu.
    • (গ) int হলে প্রতিটি উপাদান ৪ byte: আগে slli t0, a1, 2, তারপর add, আর lbu/sb-এর বদলে lw/sw — ঠিক অংশ ৯-এর পাঁচ instruction. ঠিকানা তখন 0x200 + 20 = 0x214.

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমRV32I-এ a[i] += 5 (int a[], a0 = &a[0], a1 = i) করার instruction-গুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. slli t0, a1, 2 — i-কে ৪ দিয়ে গুণ
  2. add t0, a0, t0 — a[i]-এর ঠিকানা
  3. lw t1, 0(t0) — মেমরি থেকে a[i] আনা
  4. addi t1, t1, 5 — register-এ যোগ
  5. sw t1, 0(t0) — মেমরিতে ফেরত রাখা

RV32I-এর addi instruction-এর immediate কোন সীমার মধ্যে হতে পারে?

  • 0 থেকে 4095
  • −32768 থেকে 32767
  • −2048 থেকে 2047
  • যেকোনো ৩২-বিট মান

RV32I-এ 0x12345FFF ধ্রুবকটি t0-তে রাখার সঠিক instruction-জোড়া কোনটি?

  • lui t0, 0x12345 তারপর addi t0, t0, -1
  • lui t0, 0x12346 তারপর addi t0, t0, -1
  • lui t0, 0x12345 তারপর addi t0, t0, 4095
  • lui t0, 0x12345 তারপর ori t0, t0, 0xFFF

মেমরির একটি byte-এর মান 0xF0. RV32I-এ lb t0, 0(a0) আর lbu t1, 0(a0) (a0 সেই byte-এর ঠিকানা) চলার পর t0 আর t1-এ কী থাকে?

  • t0 = 0x000000F0, t1 = 0x000000F0
  • t0 = 0x000000F0, t1 = 0xFFFFFFF0
  • t0 = 0xFFFFFFF0, t1 = 0xFFFFFFF0
  • t0 = 0xFFFFFFF0, t1 = 0x000000F0

RISC-V-এর pseudo-instruction not t0, t1 assembler কোন আসল instruction-এ বদলায়?

  • xori t0, t1, -1
  • sub t0, zero, t1
  • xori t0, t1, 0
  • andi t0, t1, -1

RV32I-এ t0 = 0x80000000 হলে srai t1, t0, 4 আর srli t2, t0, 4-এর পর t1 আর t2 কত?

  • t1 = 0x08000000, t2 = 0x08000000
  • t1 = 0xF8000000, t2 = 0x08000000
  • t1 = 0x08000000, t2 = 0xF8000000
  • t1 = 0x00000008, t2 = 0x00000008

RV32I-এ sw t0, 8(sp) instruction-টি কী করে?

  • sp + 8 ঠিকানা থেকে ৪ byte পড়ে t0-তে রাখে
  • sp-কে ৮ বাড়িয়ে t0-কে নতুন sp-তে রাখে
  • t0-এর মান মেমরির sp + 8 ঠিকানায় লেখে
  • t0-এর মান register x8-এ কপি করে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Programmer-এর মডেল: ৩২টি register (x0 = 0, ra, sp, gp, tp, t0–t6, s0–s11, a0–a7), pc, আর byte-ঠিকানার little-endian মেমরি। হিসাব শুধু register-এ, মেমরি শুধু load আর store দিয়ে।
  2. Immediate ১২ বিট, signed: −2048…2047; subi নেই (addi −k); logical immediate-ও sign-extend; NOT = xori −1; shift-এর পরিমাণ শুধু নিচের ৫ বিট।
  3. ৩২-বিট ধ্রুবক: hi = (c + 0x800) >> 12, lo = sign-extend(c-এর নিচের ১২ বিট); lui hi + addi lo. 0x12345FFF = lui 0x12346 + addi −1. auipc pc-এর সাথে যোগ করে — la, call-এর ভিত্তি।
  4. Load-store: ঠিকানা = rs1 + offset; lb/lh sign-extend, lbu/lhu zero-extend, store শুধু নিচের বিট কেটে রাখে; misaligned access chip-ভেদে ধীর বা trap.
  5. Pseudo-instruction আর বাকিরা: nop, mv, not, neg, li, la, j, ret, call — x0, auipc আর jalr-এর ওপর গড়া; শাখা পর্ব ২৯-এ, M extension-এ গুণ-ভাগ, RV64I-এ ৬৪-বিট register আর ld/sd/addw.

পরের পর্বে: এই পর্বের প্রতিটি instruction আসলে ৩২টি বিট। R, I, S, B, U, J — ছয়টি ছাঁচে কোন বিট কোথায় বসে, কেন immediate-এর বিট এলোমেলো দেখায়, আর একটি instruction হাতে encode ও decode করবেন কীভাবে।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান