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

Addressing mode — immediate, register, base+offset আর PC-relative

Addressing Modes — Immediate, Register, Base+Offset & PC-Relative

Taught in Bangla, this lesson defines the effective address and each classic addressing mode, from implied and immediate to indirect, displacement, indexed, relative, auto-increment and stack, with formulas and memory-access counts. It solves the classic exam numerical of one instruction under many modes, shows RISC-V's four modes and how the rest are built, x86-64 SIB, RIP-relative and lea, ARM pre- and post-index, C constructs, PIC and ASLR.

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

  • effective address-এর ধারণা, আর প্রতিটি ক্লাসিক addressing mode-এর সংজ্ঞা, সূত্র, উদাহরণ ও খরচ
  • “register আর মেমরির মান দেওয়া আছে, operand কত?” — এই পরীক্ষার numerical নির্ভুলভাবে সমাধান
  • RISC-V-র চারটি mode, আর lui/auipc, slli+add দিয়ে বাকি mode বানানোর কৌশল
  • x86-64-এর SIB ঠিকানা, RIP-relative আর lea; ARM-এর pre-index আর post-index
  • C-র local, global, array, struct আর pointer কোন mode-এ যায়; PIC, ASLR আর pipeline-এর সাথে সম্পর্ক

১ · Addressing mode কী — instruction কীভাবে তার operand খুঁজে পায়

কুরিয়ারের একজন লোক তিনটি পার্সেল হাতে নিয়েছেন। প্রথমটির গায়ে লেখা “২০০ টাকা” — টাকাটা পার্সেলের সাথেই আছে, কোথাও খুঁজতে হবে না। দ্বিতীয়টিতে লেখা “বাড়ি নং ৫০০” — সেই বাড়িতে গেলেই প্রাপককে পাওয়া যাবে। তৃতীয়টিতে লেখা “বাড়ি নং ৫০০-এ গিয়ে আসল ঠিকানা জেনে নিন” — অর্থাৎ ৫০০ নম্বর বাড়িতে আরেকটি ঠিকানা লেখা কাগজ আছে, তারপর সেখানে যেতে হবে। একই রকম একটি সংখ্যা, কিন্তু তিনটি আলাদা পড়ার নিয়ম, আর প্রতিটির জন্য হাঁটাহাঁটির পরিমাণও আলাদা।

CPU-র প্রতিটি instruction-এর একই সমস্যা। পর্ব ২৫-এ (ISA কী) দেখেছেন, instruction-এ থাকে কী করতে হবে (opcode) আর কার ওপর করতে হবে, অর্থাৎ operand কোথায় আছে। operand থাকতে পারে instruction-এর ভেতরেই, কোনো register-এ, বা মেমরির কোনো ঘরে। Instruction-এর বিটগুলো থেকে operand-এর আসল অবস্থান কীভাবে বের করতে হবে, সেই নিয়মের নাম addressing mode.

মূল ধারণা

Operand যদি মেমরিতে থাকে, তার চূড়ান্ত ঠিকানাকে বলে effective address (EA). Addressing mode হলো একটি সূত্র: EA = f(instruction-এর address field, register-এর মান, PC, কখনো মেমরির মান)। EA হাতে পেলে operand = M[EA], অর্থাৎ মেমরির সেই ঘরের মান।

এই পর্বে চিহ্নগুলো এভাবে লিখব: A = instruction-এর address field (বা offset/displacement) — instruction-এর ভেতরে লেখা সংখ্যাটি; R = কোনো register-এর মান; M[x] = মেমরির x ঠিকানার মান; PC = program counter. তাহলে “direct” মানে EA = A, “indirect” মানে EA = M[A], “register indirect” মানে EA = R. বাকি সবকিছু এই তিনটি মৌলিক ভাবনার মিশ্রণ: সংখ্যা নিজেই, সংখ্যা যোগ, আর মেমরি থেকে আরেকবার পড়া।

ISA-ডিজাইনারের কাছে এটি একটি দর-কষাকষি। বেশি mode মানে প্রতিটি instruction বেশি কাজ করতে পারে, program ছোট হয়; কিন্তু instruction-এ mode বোঝাতে বাড়তি বিট লাগে, hardware জটিল হয়, আর pipeline চালানো কঠিন হয়। ১৯৭০-এর দশকের VAX-এর মতো CISC মেশিন প্রায় প্রতিটি operand-এ আলাদা mode বেছে নিতে দিত; অন্যদিকে RISC-V ইচ্ছা করে মাত্র কয়েকটি রেখেছে। কেন — সেটিই এই পর্বের মূল গল্প।

২ · ক্লাসিক addressing mode — সংজ্ঞা, EA সূত্র আর তুলনা

বিশ্ববিদ্যালয়ের Computer Organization বই (যেমন Mano বা Stallings-এর ধাঁচের বই) আর চাকরির লিখিত পরীক্ষায় যে তালিকাটি বারবার আসে, সেটি এক এক করে দেখি। প্রতিটিতে আমরা জানতে চাই: operand কোথায়, EA-র সূত্র কী, instruction fetch করার পরে operand আনতে মেমরিতে আর কতবার যেতে হয়, আর কোথায় কাজে লাগে। উদাহরণগুলো একটি কাল্পনিক accumulator মেশিনের LOAD instruction দিয়ে, যা operand-টি accumulator (AC) register-এ আনে।

যাদের operand মেমরিতে নেই

  • Implied (inherent): operand-এর কথা instruction-এ আলাদা করে লেখাই নেই, opcode-ই বলে দেয়। যেমন CMA (accumulator-এর complement), CLC (carry flag মুছে দেওয়া), x86-এর cbw, বা stack মেশিনের ADD. মেমরিতে বাড়তি যাওয়া: ০. সুবিধা: সবচেয়ে ছোট instruction. অসুবিধা: কেবল নির্দিষ্ট কয়েকটি কাজের জন্য।
  • Immediate: address field-এই মানটি আছে — operand = A. যেমন LOAD #500 মানে AC ← ৫০০; RISC-V-তে addi x5, x5, 1. মেমরিতে বাড়তি যাওয়া: ০. সুবিধা: দ্রুত, ধ্রুবক রাখার সেরা উপায়। অসুবিধা: মানের পরিসর address field-এর বিট-সংখ্যায় সীমিত (RISC-V-তে ১২ বিট, −২০৪৮ থেকে ২০৪৭)। কিছু বই বলে immediate-এর কোনো EA নেই; Mano-র ধাঁচে বলা হয় EA = address field-টি যে মেমরি-ঘরে বসে আছে, অর্থাৎ instruction-এর দ্বিতীয় word-এর ঠিকানা। পরীক্ষায় যে বই অনুসরণ করা হয়, তার রীতি মানুন।
  • Register: operand একটি register-এ — operand = R. যেমন add x5, x6, x7. মেমরিতে বাড়তি যাওয়া: ০. সুবিধা: সবচেয়ে দ্রুত, আর register-এর নাম মাত্র ৩–৫ বিটে লেখা যায়। অসুবিধা: register সংখ্যায় কম — RISC-V-তে ৩২টি, x86-64-এ ১৬টি general-purpose register থাকে।

যাদের operand মেমরিতে

  • Direct (absolute): EA = A. LOAD 500 মানে AC ← M[৫০০]. মেমরিতে বাড়তি: ১. ব্যবহার: নির্দিষ্ট ঠিকানার global variable, পুরোনো ৮-বিট মেশিনের hardware register. অসুবিধা: পুরো ঠিকানা instruction-এ ধরাতে হয় — ৩২ বা ৬৪ বিটের ঠিকানা একটি ৩২-বিট instruction-এ আঁটে না; আর program অন্য ঠিকানায় সরালে ঠিকানাটি বদলাতে হয়।
  • Indirect (memory indirect): EA = M[A]. ঠিকানা ৫০০-তে আসল ঠিকানা লেখা আছে। মেমরিতে বাড়তি: ২ (প্রথমে ঠিকানা, তারপর মান)। ব্যবহার: pointer-এর মাধ্যমে ডেটা, ছোট address field দিয়ে বড় মেমরিতে পৌঁছানো, jump table. অসুবিধা: ধীর; আধুনিক RISC-এ নেই।
  • Register indirect: EA = R. register-এ ঠিকানা রাখা আছে। LOAD (R1); RISC-V-তে lw x5, 0(x10). মেমরিতে বাড়তি: ১. ব্যবহার: C-র *p. সুবিধা: indirect-এর সুবিধা, কিন্তু এক মেমরি-যাত্রা কম, আর instruction ছোট।
  • Displacement / base + offset: EA = Rbase + A. RISC-V-র lw x5, 8(x10) মানে EA = x10 + ৮. মেমরিতে বাড়তি: ১. ব্যবহার: struct-এর field, stack-এর local variable (sp + offset), array-র নির্দিষ্ট উপাদান। অসুবিধা: offset-এর পরিসর সীমিত। এটিই সবচেয়ে বেশি ব্যবহৃত mode.
  • Indexed: EA = A + Rindex. গঠন displacement-এর মতোই, ভাবনা উল্টো: এখানে A হলো array-র শুরুর (বড়) ঠিকানা, আর index register (XR)-এ থাকে কত দূরে যেতে হবে। ব্যবহার: a[i], যখন a-র ঠিকানা compile-এর সময় জানা। মেমরিতে বাড়তি: ১.
  • Base + index (+ scale): EA = Rbase + Rindex × s (+ A). দুটি register যোগ, index-কে ১, ২, ৪ বা ৮ দিয়ে গুণ — x86-এর [rbx + rcx*4 + 8]. ব্যবহার: a[i] যখন a নিজেই একটি pointer, বা struct-এর array-র ভেতরের field. মেমরিতে বাড়তি: ১. অসুবিধা: hardware-এ একটি বাড়তি adder আর shifter লাগে, encoding জটিল।
  • Relative (PC-relative): EA = PC + A, A সাধারণত signed. ব্যবহার: branch, jump, function call, আর code-এর কাছাকাছি থাকা ডেটা। সুবিধা: offset ছোট রাখা যায়, আর program পুরোটা সরালেও কাজ করে। সতর্কতা: কোন PC? Mano-র বইয়ে fetch-এর পরের PC (পরের instruction-এর ঠিকানা), RISC-V-তে instruction-এর নিজের ঠিকানা, x86-64-এর RIP-relative-এ পরের instruction-এর ঠিকানা।
  • Auto-increment / auto-decrement: register indirect, সাথে register-এর মান নিজে থেকে বদলায়। Post-increment (R)+: EA = R, তারপর R ← R + d. Pre-decrement -(R): আগে R ← R − d, তারপর EA = R. d = operand-এর মাপ (byte-addressed মেমরিতে ১, ২, ৪ বা ৮; word-addressed পরীক্ষার মেশিনে ১)। PDP-11 আর Motorola 68000-এর বিখ্যাত বৈশিষ্ট্য, ARM-এ writeback নামে আছে (অংশ ৬)। ব্যবহার: array ধরে এগোনো, stack-এ push/pop. অসুবিধা: একটি instruction দুটি register লেখে (operand আর ঠিকানা) — pipeline-এর জন্য জটিল।
  • Stack: operand stack-এর মাথায়, EA = SP; push-pop-এ SP বদলায়, অর্থাৎ আসলে SP-এর ওপর auto-increment/decrement. Stack মেশিনে (যেমন Java bytecode-এর নকশা) ADD instruction stack-এর ওপরের দুটি মান তুলে যোগফল রাখে — operand-এর নাম লিখতেই হয় না।

লাইব্রেরির কথা ভাবুন। Direct: “তাক নং ৫০০-তে বইটি আছে।” Indirect: “ক্যাটালগের ৫০০ নম্বর কার্ডে বইয়ের তাক-নম্বর লেখা আছে” — দুবার হাঁটা। Register indirect: তাক-নম্বরটি আপনার হাতের চিরকুটে (register) লেখা — একবার হাঁটা। Displacement: “চিরকুটের তাক থেকে আরও ৮টি তাক পরে।” Relative: “আপনি এখন যেখানে দাঁড়িয়ে, সেখান থেকে ২০০ তাক সামনে” — পুরো লাইব্রেরি অন্য ভবনে সরালেও নির্দেশটি সত্য থাকে। Auto-increment: বই নিয়ে চিরকুটের নম্বর এক বাড়িয়ে লিখে রাখা, যাতে পরের বার পাশের বইটি নেন।

এক টেবিলে সব mode

Modeলেখার ধরনEA / operandবাড়তি মেমরি-যাত্রাসাধারণ ব্যবহারসুবিধাঅসুবিধা
ImpliedCMAopcode-ই বলে০flag, accumulatorছোট instructionকাজ সীমিত
Immediate#Aoperand = A০ধ্রুবকদ্রুতপরিসর ছোট
RegisterR1operand = R০হিসাবের মাঝের মানসবচেয়ে দ্রুতregister কম
DirectAEA = A১নির্দিষ্ট globalসরলবড় ঠিকানা আঁটে না, সরানো যায় না
Indirect@AEA = M[A]২pointer, jump tableছোট A দিয়ে বড় মেমরিধীর
Register indirect(R1)EA = R১*pদ্রুত pointerঠিকানা আগে register-এ আনতে হয়
DisplacementA(R1)EA = R + A১struct field, stackবহুমুখীoffset-এর পরিসর সীমিত
IndexedA(XR)EA = A + XR১স্থির ঠিকানার arrayলুপে সুবিধাA বড় হলে instruction বড়
Base + index × s[B + I*s]EA = B + I × s১pointer-এর arrayএক instruction-এ a[i]জটিল hardware
RelativeA(PC)EA = PC + A১ (branch-এ ০)branch, PIC ডেটাস্থানান্তরযোগ্যদূরত্ব সীমিত
Auto-inc/dec(R)+, -(R)EA = R (আগে বা পরে R ± d)১array ধরে চলা, stackকম instructionদুটি register লেখা
StackPUSH/POPEA = SP১function call, expressionoperand লিখতে হয় নাstack-এর মাথায় সীমিত

“বাড়তি মেমরি-যাত্রা” মানে instruction fetch হয়ে যাওয়ার পরে শুধু operand আনতে কতবার মেমরিতে যেতে হয়। Mano-র ধাঁচের মেশিনে address field প্রায়ই instruction-এর দ্বিতীয় word-এ থাকে; তখন সেটি পড়তেও একবার মেমরিতে যেতে হয় — প্রশ্নে কোনটি গুনতে বলা হয়েছে, সাবধানে পড়ুন। Branch-এর ক্ষেত্রে relative mode ডেটা আনে না, শুধু নতুন PC হিসাব করে, তাই সেখানে বাড়তি যাত্রা ০.

৩ · পরীক্ষার ক্লাসিক numerical — একই instruction, ভিন্ন mode

এবার সেই প্রশ্ন, যা Computer Organization-এর পরীক্ষায় প্রায়ই কোনো না কোনো রূপে আসে। অবস্থাটি এমন:

  • একটি দুই-word-এর LOAD instruction মেমরির ঠিকানা ২০০ আর ২০১-এ। ২০০-তে opcode আর mode, ২০১-এ address field A = ৫০০.
  • instruction fetch শেষে PC = ২০২ (পরের instruction-এর ঠিকানা)।
  • Register: R1 = ৪০০, index register XR = ১০০, stack pointer SP = ১০০০, AC = ৭৫. মেমরি word-addressed, তাই auto-increment-এ d = ১.
ঠিকানা২০১৩৯৯৪০০৫০০৬০০৭০২৮০০৯০০১০০০
মান৫০০৪৫০৭০০৮০০৯০০৩২৫৩০০২৫০৬০০

প্রশ্ন: প্রতিটি mode-এ EA কত, আর AC-তে কোন মান আসবে? প্রথমে ছয়টি মূল mode ধাপে ধাপে দেখুন — প্রতিটি ধাপে কোন ঘরটি ছোঁয়া হলো, খেয়াল করুন:

Immediateoperand = A
instruction at 200-201, A = 500, PC = 202
LOAD #500
EA      = 201  (A sits in the 2nd word)
operand = 500      extra memory reads: 0
DirectEA = A
LOAD 500
EA      = 500
operand = M[500] = 800    extra memory reads: 1
IndirectEA = M[A]
LOAD @500
read 1 : M[500] = 800  -> this is the address
EA      = 800
read 2 : operand = M[800] = 300   extra reads: 2
Register indirectEA = R1
LOAD (R1)        R1 = 400
EA      = 400
operand = M[400] = 700    extra memory reads: 1
(A = 500 is not used at all)
IndexedEA = A + XR
LOAD 500(XR)     XR = 100
EA      = 500 + 100 = 600
operand = M[600] = 900    extra memory reads: 1
RelativeEA = PC + A
LOAD 500(PC)     PC = 202 (already past the instruction)
EA      = 202 + 500 = 702
operand = M[702] = 325    extra memory reads: 1
wrong-PC trap: 200 + 500 = 700 -> not in table
  1. Immediate. address field-এর ৫০০ নিজেই operand, তাই AC = ৫০০. মেমরিতে বাড়তি যেতে হয় না। Mano-র রীতিতে EA লেখা হয় ২০১ — যে ঘরে সংখ্যাটি বসে আছে।
  2. Direct. ৫০০ একটি ঠিকানা: EA = ৫০০, আর সেই ঘরে আছে ৮০০. তাই AC = ৮০০. operand আনতে মেমরিতে একবার যাওয়া।
  3. Indirect. প্রথমবার M[৫০০] পড়ে পাওয়া গেল ৮০০ — এটি ঠিকানা, মান নয়। EA = ৮০০, দ্বিতীয়বার M[৮০০] পড়ে AC = ৩০০. দুবার মেমরিতে যাওয়া — সবচেয়ে ধীর mode.
  4. Register indirect. ঠিকানা আছে R1-এ: EA = ৪০০, M[৪০০] = ৭০০, তাই AC = ৭০০. এখানে address field ব্যবহারই হয় না, তাই এমন instruction সাধারণত এক word-এর হয়।
  5. Indexed. EA = A + XR = ৫০০ + ১০০ = ৬০০, M[৬০০] = ৯০০, তাই AC = ৯০০. XR বাড়ালে একই instruction পরের ঘরগুলো পড়বে — array-র জন্য আদর্শ।
  6. Relative. PC এখন ২০২, কারণ দুই word-এর instruction fetch শেষ। EA = ২০২ + ৫০০ = ৭০২, M[৭০২] = ৩২৫, AC = ৩২৫. সবচেয়ে সাধারণ ভুল হলো ২০০ বা ২০১ ধরে হিসাব করা — তখন ৭০০ বা ৭০১ আসে, যা ভুল।

বাকি mode-গুলোও একই নিয়মে। নিচের JavaScript calculator-টি পুরো প্রশ্নের তেরোটি সারি একসাথে হিসাব করে। প্রতিটি সারি একই register দিয়ে শুরু হয় (auto-increment-এর পার্শ্বপ্রতিক্রিয়া পরের সারিতে যায় না), আর “mem-reads” কলামে instruction fetch-এর পরের মেমরি-যাত্রা গোনা হয়েছে। “চালান” চেপে দেখুন, তারপর নিজের হাতের হিসাবের সাথে মেলান:

// Classic exam problem: one LOAD instruction, address field A = 500, 13 modes.
const M = {399: 450, 400: 700, 500: 800, 600: 900, 702: 325,
           800: 300, 900: 250, 1000: 600};
const A = 500, PC = 202;                 // PC already points past the 2-word instruction
let R1 = 400, XR = 100, SP = 1000, AC = 75;
const modes = [
  //  name                  EA                operand            extra mem reads
  ["Implied    (AC)",      () => null,       () => AC,           0],
  ["Immediate  #A",        () => 201,        () => A,            0],
  ["Register   R1",        () => null,       () => R1,           0],
  ["Direct     A",         () => A,          ea => M[ea],        1],
  ["Indirect   @A",        () => M[A],       ea => M[ea],        2],
  ["Reg indir  (R1)",      () => R1,         ea => M[ea],        1],
  ["Relative   PC+A",      () => PC + A,     ea => M[ea],        1],
  ["Indexed    XR+A",      () => XR + A,     ea => M[ea],        1],
  ["Base+disp  R1+A",      () => R1 + A,     ea => M[ea],        1],
  ["Base+index R1+XR*4",   () => R1 + XR * 4, ea => M[ea],       1],
  ["Autoinc    (R1)+",     () => R1,         ea => M[ea],        1, () => R1++],
  ["Autodec    -(R1)",     () => --R1,       ea => M[ea],        1],
  ["Stack pop  (SP)+",     () => SP,         ea => M[ea],        1, () => SP++],
];
console.log("mode                  EA     operand  mem-reads  side effect");
for (const [name, eaFn, opFn, reads, after] of modes) {
  const saveR1 = R1, saveSP = SP;        // every row starts from the same registers
  const ea = eaFn();
  const op = opFn(ea);
  if (after) after();
  const side = R1 !== saveR1 ? `R1 = ${R1}` : SP !== saveSP ? `SP = ${SP}` : "";
  console.log(name.padEnd(22) + String(ea ?? "-").padEnd(7) +
              String(op).padEnd(9) + String(reads).padEnd(11) + side);
  R1 = saveR1; SP = saveSP;
}

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

mode                  EA     operand  mem-reads  side effect
Implied    (AC)       -      75       0
Immediate  #A         201    500      0
Register   R1         -      400      0
Direct     A          500    800      1
Indirect   @A         800    300      2
Reg indir  (R1)       400    700      1
Relative   PC+A       702    325      1
Indexed    XR+A       600    900      1
Base+disp  R1+A       900    250      1
Base+index R1+XR*4    800    300      1
Autoinc    (R1)+      400    700      1          R1 = 401
Autodec    -(R1)      399    450      1          R1 = 399
Stack pop  (SP)+      1000   600      1          SP = 1001

হাতের হিসাবের সাথে প্রতিটি সারি মেলে। কয়েকটি বিশেষ লক্ষণীয় বিষয়: Indirect আর base + index × ৪ দুটিই EA ৮০০-তে পৌঁছায় — ভিন্ন পথে একই ঘর, তাই উত্তর ৩০০; কিন্তু indirect-এ মেমরিতে যেতে হয় দুবার। Base + displacement-এ R1 + A = ৪০০ + ৫০০ = ৯০০, operand ২৫০. Auto-increment আর register indirect-এর EA আর operand একই (৪০০, ৭০০), পার্থক্য শুধু পরে R1 = ৪০১. Auto-decrement-এ আগে R1 কমে ৩৯৯ হয়, তাই operand M[৩৯৯] = ৪৫০. Stack pop-এ SP-এর ঘর (১০০০) থেকে ৬০০ ওঠে, তারপর SP = ১০০১ (এই মেশিনে stack নিচের দিকে বাড়ে, তাই pop-এ SP বাড়ে)। Immediate আর register-এ মেমরি থেকে কিছুই পড়া হয় না।

পরীক্ষার তিনটি ফাঁদ। (১) Relative-এ কোন PC — প্রশ্নে “PC-এর মান fetch-এর পরে” বলা থাকলে সেটিই; না থাকলে instruction-এর দৈর্ঘ্য যোগ করে পরের ঠিকানা নিন, আর উত্তরে আপনার ধারণাটি লিখে দিন। (২) Indexed আর displacement-এর সূত্র একই রকম দেখায় (A + register); পার্থক্য ভূমিকায় — কোনটি বড় স্থির ঠিকানা আর কোনটি পরিবর্তনশীল দূরত্ব। (৩) Auto-increment-এ EA পুরোনো R, auto-decrement-এ EA নতুন R.

৪ · RISC-V-এর ইচ্ছাকৃত ছোট তালিকা — immediate, register, base+offset আর PC-relative

এত mode দেখার পরে RISC-V-এর তালিকা দেখে অবাক লাগতে পারে। RISC-V-এর মূল integer অংশ RV32I-তে আছে মাত্র চারটি:

Modeউদাহরণমানেপরিসর
Immediateaddi x5, x5, -1x5 ← x5 + (−১)১২-বিট signed: −২০৪৮ … ২০৪৭; ওপরের ২০ বিট lui দিয়ে
Registeradd x5, x6, x7x5 ← x6 + x7৩২টি register (x0 সবসময় ০)
Base + offsetlw x5, 8(x10)x5 ← M[x10 + ৮]offset ১২-বিট signed; load-store-এর একমাত্র mode
PC-relativebeq, jal, auipcলক্ষ্য = PC + offset (PC = instruction-এর নিজের ঠিকানা)branch ±৪ KiB, jal ±১ MiB, auipc + পরের instruction ±২ GiB

পর্ব ২৬-এ (RISC-V-এর instruction) load আর store-এর পরিচয় পেয়েছেন, আর পর্ব ২৭-এ (instruction encoding) দেখেছেন এই offset আর immediate-গুলো ৩২ বিটের কোন ঘরে বসে। এখানে মূল কথাটি হলো: RISC-V একটি load-store architecture. Arithmetic instruction কেবল register আর immediate নিয়ে কাজ করে; মেমরি ছোঁয় শুধু load আর store, আর তাদের ঠিকানা সবসময় EA = rs1 + sign-extended imm12.

এই একটি mode আসলে আরও দুটি ক্লাসিক mode-কে বিনা খরচে ঢেকে দেয়:

  • Register indirect = offset ০: lw x5, 0(x10) মানে EA = x10, অর্থাৎ C-র *p.
  • ছোট ঠিকানায় direct = base x0: x0 সবসময় ০, তাই lw x5, 100(x0) মানে EA = ১০০. শুধু −২০৪৮ থেকে ২০৪৭-এর মধ্যের ঠিকানায় কাজ করে — ছোট embedded সিস্টেমে মাঝে মাঝে কাজে লাগে।

PC-relative-এর দিকে তিনটি instruction গুরুত্বপূর্ণ। Branch (beq, bne, blt…) PC + offset-এ লাফ দেয় — শর্ত, লুপ আর তাদের offset-এর বিস্তারিত হিসাব পর্ব ২৯-এ (শাখা আর লুপ assembly-তে)। jal function call-এর জন্য, আর jalr হলো register + offset-এ লাফ — function pointer আর ret-এর জন্য register indirect jump. আর auipc (add upper immediate to PC) কোথাও লাফ দেয় না: rd ← PC + (imm20 << ১২) — শুধু একটি PC-ভিত্তিক ঠিকানা register-এ রাখে, যাতে পরের instruction সেখান থেকে ডেটা পড়তে পারে। এর জুড়ি lui (load upper immediate): rd ← imm20 << ১২, PC ছাড়া।

MIPS-এর সাথে তুলনা। অনেক পুরোনো পাঠ্যবই MIPS ব্যবহার করে, যার load-store mode-ও একটিই — base + ১৬-বিট offset দিয়ে ঠিকানা। কিন্তু MIPS-এর branch offset গোনা হয় পরের instruction (PC + ৪) থেকে আর word-এ (৪ byte-এ গুণ করে), RISC-V-তে branch-এর নিজের ঠিকানা থেকে আর ২ byte-এর এককে। AI আর অনলাইন নোট প্রায়ই দুটি মিশিয়ে ফেলে।

৫ · বাকি mode RISC-V-তে বানানো — indexed, direct আর post-increment

যে mode RISC-V-তে নেই, compiler সেটি দুই-তিনটি সরল instruction জুড়ে বানায়। প্রশ্ন একটাই: কত instruction লাগে, আর তাতে কী হারাই বা পাই।

Indexed: a[i] = shift + add + load

C-তে int a[]-এর a[i]-এর ঠিকানা = a + i × ৪, কারণ প্রতিটি int ৪ byte জায়গা নেয়। x86-এ এটি একটি operand-এই হয়; RISC-V-তে তিন ধাপ: i-কে ২ ঘর বাঁয়ে সরানো (× ৪), base-এর সাথে যোগ, তারপর offset ০ দিয়ে load. নিচে a[i] += 1 — a-র ঠিকানা x10 = 0x1000, i = x11 = ৩, আর a[3]-এ আছে ৪০:

1slli x5, x11, 2    # x5 = i * 42add  x5, x10, x5   # x5 = &a[i]3lw   x6, 0(x5)     # EA = x5 + 04addi x6, x6, 1     # x6 = a[i] + 15sw   x6, 0(x5)     # EA = x5 + 0
Register আর মেমরি (x10 = 0x1000, x11 = 3)
x5
x6
EA
M[0x100C]
instruction / মেমরি-যাত্রা
  1. শুরুর অবস্থা। x10-এ array-র শুরুর ঠিকানা 0x1000, x11-এ i = ৩. a[3]-এর ঠিকানা হবে 0x1000 + ৩ × ৪ = 0x100C, সেখানে মান ৪০. এখনো কোনো instruction চলেনি।
  2. Index-কে byte-এ রূপান্তর। slli x5, x11, 2 — ২ ঘর বাঁয়ে shift মানে ৪ দিয়ে গুণ: x5 = ১২. x86 এই কাজটি ঠিকানার ভেতরের “scale” দিয়ে করে; RISC-V-তে এটি একটি আলাদা instruction.
  3. Base যোগ। add x5, x10, x5 — x5 = 0x1000 + ১২ = 0x100C. এখন x5-এ &a[i], অর্থাৎ indexed mode-এর EA আমরা হাতে বানিয়ে ফেললাম। এখনো মেমরি ছোঁয়া হয়নি।
  4. Load. lw x6, 0(x5) — RISC-V-র একমাত্র data mode, base + offset, offset ০: EA = 0x100C. মেমরি থেকে ৪০ এল x6-এ। এটিই প্রথম মেমরি-যাত্রা (instruction fetch বাদে)।
  5. যোগ। addi x6, x6, 1 — immediate mode, x6 = ৪১. RISC-V-র arithmetic কখনো মেমরিতে কাজ করে না, তাই মান আগে register-এ আনতে হয়েছে।
  6. Store. sw x6, 0(x5) — একই EA 0x100C-এ ৪১ লেখা। মোট ৫টি instruction আর ২টি মেমরি-যাত্রা। x86-64 একই কাজ করে একটি instruction-এ: add dword ptr [rdi+rsi*4], 1 — কিন্তু ভেতরে সেটিও মেমরি পড়ে, যোগ করে, আবার লেখে।

RISC-V-র ঐচ্ছিক Zba extension (bit manipulation-এর অংশ, ২০২১ সালের দিকে অনুমোদিত) ঠিক এই ধাঁচটির জন্য sh1add, sh2add, sh3add দিয়েছে: sh2add x5, x11, x10 মানে x5 = (x11 << ২) + x10. তখন a[i] পড়তে লাগে দুটি instruction — পুরো scaled-index mode না এনেও খরচ কমল।

Direct বা global variable: lui + lw

একটি global int g ধরুন 0x00012F40 ঠিকানায়। ৩২-বিটের ঠিকানা ১২-বিটের offset-এ আঁটে না, তাই দুই ভাগ করা হয়: ওপরের ২০ বিট lui দিয়ে register-এ, নিচের ১২ বিট load-এর offset হিসেবে। Assembly-তে লেখা হয় lui x5, %hi(g) আর lw x6, %lo(g)(x5). একটি সূক্ষ্ম ব্যাপার: offset sign-extend হয়। g-এর নিচের ১২ বিট 0xF40 — এর সর্বোচ্চ বিট ১, তাই load সেটিকে −১৯২ হিসেবে পড়বে। ক্ষতিপূরণ দিতে %hi-কে এক বাড়াতে হয়: %hi = (g + 0x800) >> ১২ = 0x13, %lo = −১৯২. যাচাই: 0x13000 − ১৯২ = 0x12F40 ✓.

PC-relative ঠিকানা: auipc + addi (la)

Program কোন ঠিকানায় load হবে জানা না থাকলে (অংশ ৮) পরম ঠিকানা লেখা যায় না, কিন্তু “এই instruction থেকে কত দূরে” লেখা যায়। la x10, msg (load address) একটি pseudo-instruction: assembler এটিকে auipc + addi-তে ভাঙে (PIC-এ global symbol হলে auipc + lw দিয়ে GOT থেকে — assembler-এর option-ভেদে; শুধু PC-relative রূপের জন্য lla নামে আরেকটি pseudo-instruction আছে)। ধরুন auipc আছে 0x00010080-এ আর msg আছে 0x00012F40-এ:

লক্ষ্যদূরত্ব বের করা
PC of auipc   = 0x00010080
msg           = 0x00012F40
offset        = msg - PC = 0x2EC0  (11968 bytes)
সরল ভাগওপরের ২০ আর নিচের ১২ বিট
0x2EC0 = 0x00002 _ EC0
naive hi20 = 0x2,  lo12 = 0xEC0
lo12 bit 11 = 1   -> sign problem
Sign-extensionaddi যা দেখে
addi reads 12-bit 0xEC0 as signed:
0xEC0 - 0x1000 = -320
naive result = PC + 0x2000 - 320 = 0x11F40  (wrong)
সংশোধন%pcrel_hi-তে +0x800
hi20 = (0x2EC0 + 0x800) >> 12 = 0x36C0 >> 12 = 3
lo12 = -320
check: 3 * 4096 - 320 = 12288 - 320 = 11968 = 0x2EC0
auipc x10, 3x10 = PC + (3 << 12)
x10 = 0x00010080 + 0x00003000
    = 0x00013080   (no memory access, no jump)
addi x10, x10, -320শেষ ঠিকানা
x10 = 0x00013080 - 0x140 = 0x00012F40 = msg
loaded 0x40000 higher?  PC and msg both move:
offset still 0x2EC0 -> same two instructions work
  1. দূরত্বই আসল তথ্য। auipc-এর ঠিকানা 0x00010080, msg-এর 0x00012F40; দূরত্ব 0x2EC0 = ১১৯৬৮ byte. Linker এই দূরত্বটিই instruction-এ বসায়, পরম ঠিকানা নয়।
  2. সরলভাবে ভাগ। ওপরের ২০ বিট 0x2, নিচের ১২ বিট 0xEC0. কিন্তু 0xEC0-এর বিট ১১ = ১ — ১২-বিটের signed সংখ্যা হিসেবে এটি ঋণাত্মক।
  3. Sign-extension-এর ফাঁদ। addi তার immediate-কে signed পড়ে: 0xEC0 − 0x1000 = −৩২০. সরল ভাগ ব্যবহার করলে ফল হতো PC + 0x2000 − ৩২০ = 0x11F40 — ঠিক ৪০৯৬ byte কম, ভুল ঠিকানা।
  4. সংশোধন: +0x800. ওপরের অংশ হিসাব করা হয় (offset + 0x800) >> ১২ দিয়ে, তাই hi = ৩. নিচের অংশ −৩২০ থাকে। যাচাই: ৩ × ৪০৯৬ − ৩২০ = ১১৯৬৮ = 0x2EC0. Assembler আর linker এটি %pcrel_hi/%pcrel_lo দিয়ে নিজেই করে।
  5. auipc x10, 3. x10 = PC + (৩ << ১২) = 0x00013080. খেয়াল করুন, auipc মেমরি পড়ে না, লাফও দেয় না — শুধু PC-কে ভিত্তি করে একটি সংখ্যা বানায়।
  6. addi x10, x10, -320. x10 = 0x13080 − 0x140 = 0x00012F40 — ঠিক msg. পুরো program যদি 0x40000 ওপরে load হয়, PC আর msg দুটিই সমান সরে, দূরত্ব একই থাকে — একই দুটি instruction তখনো ঠিক ঠিকানা দেয়। একেই বলে position-independent.

Post-increment: lw + addi

Array-র সব উপাদান যোগ করার লুপে PDP-11 বা ARM-এর একটি instruction “পড়া আর pointer এগিয়ে দেওয়া” দুটোই করে। RISC-V-তে সেটি lw x6, 0(x10) আর addi x10, x10, 4 — দুটি instruction. নিচের ছোট interpreter চারটি কাজের RV32I sequence চালায়, প্রতিটি load/store-এর EA ছাপে, আর শেষে মোট instruction সংখ্যার পাশে CISC বা ARM-এর একক-instruction রূপটি দেখায়:

// RV32I-lite: run short sequences, print every effective address (EA).
const h = v => "0x" + (v >>> 0).toString(16);
const mem = {0x100c: 40, 0x12f40: 7, 0x2008: 99, 0x3000: 1, 0x3004: 2, 0x3008: 3};
function run(title, prog, regs, cisc, pc0 = 0x10080) {
  const x = new Array(32).fill(0);
  for (const k in regs) x[k] = regs[k];
  const r = s => x[+s.slice(1)], set = (s, v) => { if (s !== "x0") x[+s.slice(1)] = v | 0; };
  let i = 0, n = 0;
  console.log("== " + title);
  while (i < prog.length) {
    const [op, ...a] = prog[i].replace(/[(),]/g, " ").trim().split(/\s+/);
    const pc = pc0 + 4 * i, ea = () => r(a[2]) + Number(a[1]);
    let note = "", next = i + 1;
    if (op === "addi") set(a[0], r(a[1]) + Number(a[2]));
    else if (op === "add") set(a[0], r(a[1]) + r(a[2]));
    else if (op === "slli") set(a[0], r(a[1]) << Number(a[2]));
    else if (op === "lui") set(a[0], Number(a[1]) << 12);
    else if (op === "auipc") set(a[0], pc + (Number(a[1]) << 12));
    else if (op === "lw") { set(a[0], mem[ea()]); note = `EA ${h(ea())} -> ${mem[ea()]}`; }
    else if (op === "sw") { mem[ea()] = r(a[0]); note = `EA ${h(ea())} <- ${r(a[0])}`; }
    else if (op === "bne" && r(a[0]) !== r(a[1])) next = i + Number(a[2]) / 4;
    console.log(`  ${h(pc)}  ${prog[i].padEnd(18)}${note}`);
    i = next; n++;
  }
  console.log(`  -> ${n} instructions, x6 = ${x[6]}, x7 = ${x[7]} | CISC: ${cisc}`);
}
run("indexed: a[i] += 1, a = 0x1000, i = 3", ["slli x5, x11, 2", "add x5, x10, x5",
  "lw x6, 0(x5)", "addi x6, x6, 1", "sw x6, 0(x5)"], {10: 0x1000, 11: 3},
  "add dword ptr [rdi+rsi*4], 1 (1)");
run("direct: global g at 0x12f40", ["lui x5, 0x13", "lw x6, -192(x5)"], {}, "mov eax, [g] (1)");
run("PC-relative: same g", ["auipc x5, 3", "lw x6, -320(x5)"], {}, "mov eax, g[rip] (1)");
run("struct field: p->y, p = 0x2000", ["lw x6, 8(x10)"], {10: 0x2000}, "mov eax, [rdi+8] (1)");
run("post-increment: sum 3 ints", ["lw x6, 0(x10)", "addi x10, x10, 4",
  "add x7, x7, x6", "bne x10, x11, -12"], {10: 0x3000, 11: 0x300c},
  "ARM ldr r6, [r0], #4 = lw+addi (1)");

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

== indexed: a[i] += 1, a = 0x1000, i = 3
  0x10080  slli x5, x11, 2
  0x10084  add x5, x10, x5
  0x10088  lw x6, 0(x5)      EA 0x100c -> 40
  0x1008c  addi x6, x6, 1
  0x10090  sw x6, 0(x5)      EA 0x100c <- 41
  -> 5 instructions, x6 = 41, x7 = 0 | CISC: add dword ptr [rdi+rsi*4], 1 (1)
== direct: global g at 0x12f40
  0x10080  lui x5, 0x13
  0x10084  lw x6, -192(x5)   EA 0x12f40 -> 7
  -> 2 instructions, x6 = 7, x7 = 0 | CISC: mov eax, [g] (1)
== PC-relative: same g
  0x10080  auipc x5, 3
  0x10084  lw x6, -320(x5)   EA 0x12f40 -> 7
  -> 2 instructions, x6 = 7, x7 = 0 | CISC: mov eax, g[rip] (1)
== struct field: p->y, p = 0x2000
  0x10080  lw x6, 8(x10)     EA 0x2008 -> 99
  -> 1 instructions, x6 = 99, x7 = 0 | CISC: mov eax, [rdi+8] (1)
== post-increment: sum 3 ints
  0x10080  lw x6, 0(x10)     EA 0x3000 -> 1
  0x10084  addi x10, x10, 4
  0x10088  add x7, x7, x6
  0x1008c  bne x10, x11, -12
  0x10080  lw x6, 0(x10)     EA 0x3004 -> 2
  0x10084  addi x10, x10, 4
  0x10088  add x7, x7, x6
  0x1008c  bne x10, x11, -12
  0x10080  lw x6, 0(x10)     EA 0x3008 -> 3
  0x10084  addi x10, x10, 4
  0x10088  add x7, x7, x6
  0x1008c  bne x10, x11, -12
  -> 12 instructions, x6 = 3, x7 = 6 | CISC: ARM ldr r6, [r0], #4 = lw+addi (1)

Stepper-এর সাথে প্রথম অংশ হুবহু মেলে: EA 0x100c, ৪০ থেকে ৪১, পাঁচটি instruction. Global g-এ দুটি পথ — lui + lw -192 আর auipc + lw -320 — একই EA 0x12f40-এ পৌঁছায়, কিন্তু দ্বিতীয়টি program-এর ঠিকানার ওপর নির্ভর করে না। Struct field-এ RISC-V আর x86-এর instruction সংখ্যা সমান (১টি) — base + offset ঠিক এই কাজের জন্যই। শেষ লুপে প্রতি উপাদানে চারটি instruction চলে (মোট ১২), যোগফল x7 = ৬; প্রতিবার addi pointer এগিয়ে দেয়, তাই EA 0x3000, 0x3004, 0x3008. ARM-এর post-index এর মধ্যে lw + addi জোড়াকে একটিতে নামায় — প্রতি উপাদানে এক instruction বাঁচে।

সংক্ষেপে instruction-এর হিসাব:

কাজRV32IRV32I + Zbax86-64AArch64
x = a[i] (a register-এ)৩ (slli, add, lw)২১ (mov eax, [rdi+rsi*4])১ (ldr w0, [x0, x1, lsl #2])
a[i] += 1৫৪১ (মেমরিতে read-modify-write)৩ (ldr, add, str)
global পড়া২ (lui/auipc + lw)২১ (RIP-relative)২ (adrp + ldr)
p->y১১১১
x = *p++২২২ (mov, add)১ (post-index)

Instruction কম মানেই দ্রুত নয়। পর্ব ০৩-এর iron law মনে করুন: সময় = instruction সংখ্যা × CPI × clock period. RISC-V বেশি instruction চালায়, কিন্তু প্রতিটি সরল — তাই CPI কম আর clock দ্রুত রাখা সহজ। আধুনিক x86 আবার ভেতরে জটিল instruction-কে ছোট micro-op-এ ভাঙে। শেষ পর্যন্ত দুই পথ প্রায় একই জায়গায় মেলে।

৬ · x86-64 আর ARM — এক instruction-এ জটিল ঠিকানা

x86-64: [base + index*scale + disp]

x86-64-এর প্রায় যেকোনো instruction-এর একটি operand মেমরিতে থাকতে পারে, আর তার ঠিকানার সাধারণ রূপ (এই পর্বে x86 লেখা Intel syntax-এ; gcc-র ডিফল্ট AT&T syntax-এ [rbx + rcx*4 + 8]-কে লেখা হয় 8(%rbx,%rcx,4)):

EA = base + index × scale + displacement
scale ∈ {১, ২, ৪, ৮}, displacement ৮ বা ৩২ বিট signed

Instruction encoding-এ ModRM আর SIB (scale-index-base) নামের byte এই অংশগুলো বলে দেয়; যেকোনো অংশ বাদ দেওয়া যায়। তাই mov eax, [rbx + rcx*4 + 8] একাই base + index × scale + displacement — struct-এর array থেকে একটি field পড়ার জন্য নিখুঁত। কিছু নিয়ম আছে: scale ৩ হয় না, আর rsp index হতে পারে না (তার encoding দিয়ে “index নেই” বোঝানো হয়)। x86-এ সাধারণ instruction-এ দুটি operand একসাথে মেমরিতে থাকতে পারে না — movs-এর মতো string instruction ব্যতিক্রম।

RIP-relative: ৬৪-বিট মোডে mov eax, DWORD PTR g[rip] মানে EA = পরের instruction-এর ঠিকানা + ৩২-বিট displacement, পরিসর প্রায় ±২ GiB. ৩২-বিট x86-এ data-র জন্য এমন কোনো mode ছিল না (অংশ ৮-এ এর পরিণাম)। AMD যখন x86-64 নকশা করে, ৩২-বিট মোডের একটি “শুধু displacement” encoding-কে ৬৪-বিট মোডে RIP-relative-এ বদলে দেয়; পরম ঠিকানা লাগলে SIB byte দিয়ে এখনো লেখা যায়।

LEA — ঠিকানা হিসাব, মেমরি নয়। lea ঠিকানার পুরো হিসাবটি করে, কিন্তু মেমরিতে যায় না — ফলটি register-এ রাখে। ফলে এটি একটি “বিনা পয়সার” যোগ-গুণের যন্ত্র: lea eax, [rdi + rdi*2] মানে eax = ৩ × edi, lea eax, [rdi + rsi + 7] মানে তিনটি সংখ্যার যোগ, flag না বদলিয়ে। gcc আর clang ছোট ধ্রুবক দিয়ে গুণ (৩, ৫, ৯) প্রায়ই এভাবেই করে। নিচের evaluator কয়েকটি operand-এর EA হিসাব করে, আর অবৈধ গঠন ধরে ফেলে:

// x86-64 memory operand: EA = base + index*scale + disp (any part may be missing).
const reg = {rbx: 0x7000n, rcx: 5n, rsi: 3n, rdi: 7n, rsp: 0x7ffe0n};
const RIP_NEXT = 0x401020n;        // RIP-relative uses the address of the NEXT instruction
function ea({ base, index, scale = 1, disp = 0 }) {
  if (![1, 2, 4, 8].includes(scale)) throw new Error("scale must be 1, 2, 4 or 8");
  if (index === "rsp") throw new Error("rsp cannot be an index register");
  const b = base === "rip" ? RIP_NEXT : base ? reg[base] : 0n;
  const i = index ? reg[index] * BigInt(scale) : 0n;
  return b + i + BigInt(disp);
}
const hex = v => "0x" + v.toString(16);
const tests = [
  ["mov eax, [rbx]",              { base: "rbx" }],
  ["mov eax, [rbx + 8]",          { base: "rbx", disp: 8 }],
  ["mov eax, [rbx + rcx*4 + 8]",  { base: "rbx", index: "rcx", scale: 4, disp: 8 }],
  ["mov rax, [rbx + rcx*8 - 16]", { base: "rbx", index: "rcx", scale: 8, disp: -16 }],
  ["mov eax, [rsi*4 + 0x601040]", { index: "rsi", scale: 4, disp: 0x601040 }],
  ["mov eax, [rip + 0x2fe0]",     { base: "rip", disp: 0x2fe0 }],
  ["lea eax, [rdi + rdi*2]",      { base: "rdi", index: "rdi", scale: 2 }],
  ["mov eax, [rsp + 12]",         { base: "rsp", disp: 12 }],
  ["mov eax, [rbx + rcx*3]",      { base: "rbx", index: "rcx", scale: 3 }],
  ["mov eax, [rbx + rsp*2]",      { base: "rbx", index: "rsp", scale: 2 }],
];
for (const [text, op] of tests) {
  let out;
  try { out = "EA = " + hex(ea(op)); } catch (e) { out = "invalid: " + e.message; }
  console.log(text.padEnd(30) + out + (text.startsWith("lea") ? "  (no memory access)" : ""));
}

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

mov eax, [rbx]                EA = 0x7000
mov eax, [rbx + 8]            EA = 0x7008
mov eax, [rbx + rcx*4 + 8]    EA = 0x701c
mov rax, [rbx + rcx*8 - 16]   EA = 0x7018
mov eax, [rsi*4 + 0x601040]   EA = 0x60104c
mov eax, [rip + 0x2fe0]       EA = 0x404000
lea eax, [rdi + rdi*2]        EA = 0x15  (no memory access)
mov eax, [rsp + 12]           EA = 0x7ffec
mov eax, [rbx + rcx*3]        invalid: scale must be 1, 2, 4 or 8
mov eax, [rbx + rsp*2]        invalid: rsp cannot be an index register

[rbx + rcx*4 + 8] = 0x7000 + ৫ × ৪ + ৮ = 0x701c. ঋণাত্মক displacement-ও চলে: 0x7000 + ৪০ − ১৬ = 0x7018. base ছাড়া [rsi*4 + 0x601040] হলো ক্লাসিক indexed mode — স্থির ঠিকানার global array, ৩ নম্বর উপাদান। RIP-relative-এ ভিত্তি পরের instruction-এর ঠিকানা 0x401020, তাই EA 0x404000. lea-এর ফল 0x15 = ২১ = ৩ × ৭ — এটি একটি সংখ্যা, কোনো মেমরি পড়া হয়নি। শেষ দুটি সারি assembler-ও প্রত্যাখ্যান করত: scale ৩ নেই, আর rsp index হয় না।

ARM: pre-index আর post-index writeback

ARM-এর load-store-ও base + offset ভিত্তিক, তবে offset-এর সাথে base register হালনাগাদ করার দুটি রূপ আছে। ৩২-বিট ARM (A32) syntax-এ, r1 = 0x100 ধরে:

রূপInstructionEAপরে r1ক্লাসিক নাম
Offsetldr r0, [r1, #4]0x1040x100 (অপরিবর্তিত)displacement
Pre-indexedldr r0, [r1, #4]!0x1040x104pre-increment (auto-increment-এর রূপ)
Post-indexedldr r0, [r1], #40x1000x104post-increment (R)+
Register offsetldr r0, [r1, r2, lsl #2]r1 + r2 × ৪অপরিবর্তিতbase + scaled index

৬৪-বিট ARM (AArch64)-এ একই ধারণা: ldr x0, [x1, #8]! আর ldr x0, [x1], #8; PC-relative-এর জন্য adr (±১ MiB) আর adrp + add (৪ KiB page ধরে, প্রায় ±৪ GiB) — RISC-V-র auipc + addi-এর কাছাকাছি ভাবনা। একটি ঐতিহাসিক খুঁটিনাটি: A32-এ instruction-এর ভেতরে PC পড়লে মান হয় instruction-এর ঠিকানা + ৮ (Thumb-এ + ৪) — পুরোনো pipeline-এর উত্তরাধিকার; AArch64-এ PC মানে instruction-এর নিজের ঠিকানা। বিস্তারিত ARM-এর reference manual-এ মিলিয়ে নেওয়াই নিরাপদ, কারণ সংস্করণভেদে নিয়মের খুঁটিনাটি আলাদা।

৭ · C-এর গঠন থেকে addressing mode

Compiler-এর চোখে C-র প্রতিটি variable বা expression একটি প্রশ্ন: “এর ঠিকানা কীভাবে বানাব?” উত্তরগুলো একটি টেবিলে আনা যায়:

C-তেঠিকানার সূত্রModeRISC-V (আনুমানিক)x86-64 (আনুমানিক)
local int xsp (বা frame pointer) + স্থির offsetdisplacementlw a0, 12(sp)mov eax, [rsp+12]
global int gস্থির ঠিকানা, বা PC + দূরত্বdirect / PC-relativelui/auipc + lwmov eax, g[rip]
a[i]a + i × sizeof(উপাদান)indexed / base + scaled indexslli + add + lwmov eax, [rdi+rsi*4]
p->fieldp + offsetof(field)displacementlw a0, 8(a0)mov eax, [rdi+8]
*ppregister indirectlw a0, 0(a0)mov eax, [rdi]
p->next->valM[p + offset] + offsetindirect-এর শিকলদুটি lw, প্রথমটির ফল দ্বিতীয়টির baseদুটি mov
s[i].ys + i × sizeof(s[0]) + offsetof(y)base + index × scale + dispshift/গুণ + add + lw y_off(..)এক operand (মাপ ১, ২, ৪, ৮ হলে)
f()PC + দূরত্বPC-relativejal ra, f (দূরে হলে auipc + jalr)call f
(*fp)()register-এর মানregister indirect (jump)jalr ra, 0(a5)call rax

Struct-এর field-এর offset কীভাবে ঠিক হয় — padding আর alignment-সহ — তা পর্ব ০৬-এ (alignment আর padding) offsetof দিয়ে দেখেছেন; সেই offset-টিই এখানে displacement হিসেবে instruction-এ বসে। আর পর্ব ০৬-এর endianness মনে রাখুন: EA বলে কোন byte থেকে শুরু, আর endianness বলে সেই byte-গুলো কোন ক্রমে মান গড়ে। C-র দিক থেকে pointer আর array-র সম্পর্ক বিস্তারিত আছে C কোর্সের pointer পর্বে আর pointer ও array পর্বে।

একটি ছোট ফাংশন দেখি, যেখানে চারটি ভিন্ন ঠিকানা-ধরন একসাথে আছে:

struct Node { int val; struct Node *next; };
int g;

int f(int *a, int i, struct Node *p) {
    return a[i] + g + p->next->val;
}
# RV32 (pointer = 4 byte)          ; x86-64 (pointer = 8 byte)
f:                                 f:
    slli  a1, a1, 2                    movsx  rsi, esi
    add   a0, a0, a1                   mov    rax, QWORD PTR [rdx+8]
    lw    a5, 0(a0)       # a[i]       mov    eax, DWORD PTR [rax]
    lui   a4, %hi(g)                   add    eax, DWORD PTR [rdi+rsi*4]
    lw    a4, %lo(g)(a4)  # g          add    eax, DWORD PTR g[rip]
    lw    a3, 4(a2)       # p->next    ret
    lw    a3, 0(a3)       # ->val
    add   a0, a5, a4
    add   a0, a0, a3
    ret

RISC-V-তে দশটি instruction, x86-64-এ ছয়টি। next-এর offset RV32-এ ৪ (pointer ৪ byte) কিন্তু x86-64-এ ৮ — int val-এর পরে ৪ byte padding, কারণ ৮-byte pointer-কে ৮-এর গুণিতকে বসতে হয়। x86-এর movsx দরকার, কারণ int i ৩২ বিটের, অথচ ঠিকানার হিসাব ৬৪ বিটে। আর p->next->val হলো দুটি নির্ভরশীল load: প্রথমটি শেষ না হলে দ্বিতীয়টির ঠিকানাই জানা নেই। Linked list ধরে হাঁটা (C কোর্সের linked list পর্ব) ঠিক এমন শিকল — একে বলে pointer chasing, আর প্রতিটি ধাপে cache miss হলে (পর্ব ৪৫) পুরো শিকলটি ধীর হয়ে যায়।

Local variable নিয়ে একটি সতর্কতা: -O0-এ প্রায় প্রতিটি local stack-এ থাকে (sw a0, -20(s0)), কিন্তু -O2-এ বেশিরভাগ local register-এই থেকে যায়, মেমরিতে যায়ই না। Stack frame কীভাবে সাজানো হয় আর sp-relative offset কীভাবে ঠিক হয়, সেটি পর্ব ৩০-এ (function call আর stack); পুরো C-থেকে-assembly পথ পর্ব ৩১-এ (C থেকে assembly) দেখবেন।

আসল মেমরিতে base + offset আর index × scale

নিচের C প্রোগ্রাম একটি local array, একটি global struct-এর array আর একটি global ২-মাত্রিক array-র উপাদানের ঠিকানা ছাপে — তবে পরম ঠিকানা নয়, শুধু শুরুর ঠিকানা থেকে দূরত্ব। কারণ ASLR (অংশ ৮) প্রতিবার পরম ঠিকানা বদলে দেয়, কিন্তু দূরত্ব বদলায় না — আর addressing mode ঠিক এই দূরত্ব দিয়েই কাজ করে:

#include <stdio.h>
#include <stddef.h>
#include <stdint.h>

struct Item { char tag; int qty; double price; };   /* offsets 0, 4, 8 */
int gtable[4][3];                                   /* global: static data area */
struct Item gitems[3];

static long off(const void *p, const void *base) {  /* distance in bytes */
    return (long)((uintptr_t)p - (uintptr_t)base);
}

int main(void) {
    int local[5];                                   /* local: on the stack */
    printf("int local[5]:      &local[i] - &local[0] =");
    for (int i = 0; i < 5; i++) printf(" %ld", off(&local[i], local));
    printf("   (i * %zu)\n", sizeof local[0]);

    printf("struct Item:       sizeof %zu, tag@%zu qty@%zu price@%zu\n",
           sizeof(struct Item), offsetof(struct Item, tag),
           offsetof(struct Item, qty), offsetof(struct Item, price));
    printf("&gitems[2].price - &gitems[0] = %ld  (2*%zu + %zu)\n",
           off(&gitems[2].price, gitems), sizeof(struct Item),
           offsetof(struct Item, price));
    printf("&gtable[2][1] - &gtable[0][0] = %ld  ((2*3 + 1) * 4)\n",
           off(&gtable[2][1], gtable));

    long far = off(&local[0], &gtable[0][0]);
    printf("stack vs global far apart (> 1 MiB)? %s\n",
           (far > (1L << 20) || far < -(1L << 20)) ? "yes" : "no");
    return 0;
}

Wandbox-এর gcc (Linux x86-64) আউটপুট:

int local[5]:      &local[i] - &local[0] = 0 4 8 12 16   (i * 4)
struct Item:       sizeof 16, tag@0 qty@4 price@8
&gitems[2].price - &gitems[0] = 40  (2*16 + 8)
&gtable[2][1] - &gtable[0][0] = 28  ((2*3 + 1) * 4)
stack vs global far apart (> 1 MiB)? yes

প্রথম লাইন: local array-র উপাদান ৪ byte পরপর — EA = base + i × ৪, অর্থাৎ index × scale. দ্বিতীয় লাইন: struct Item-এ qty ৪-এ (৩ byte padding), price ৮-এ, মোট ১৬ — পর্ব ০৬-এর নিয়ম। তৃতীয় লাইন: gitems[2].price = শুরু + ২ × ১৬ + ৮ = ৪০ — ঠিক x86-এর [base + index*16 + 8], যদিও scale ১৬ নেই বলে compiler index-কে আগে ১৬ দিয়ে গুণ (shift) করে নেবে। চতুর্থ লাইন: gtable[2][1] row-major ক্রমে (২ × ৩ + ১) × ৪ = ২৮. শেষ লাইন: stack আর global data মেমরির একেবারে আলাদা এলাকায় থাকে — ঠিক কত দূরে, সেটি প্রতিবার ASLR-এ বদলায়, তাই শুধু “হ্যাঁ/না” ছাপা হলো।

৮ · PC-relative-এর আসল কদর, আর জটিল mode-এর দাম

Position-independent code, shared library আর ASLR

আপনার ফোনে বা কম্পিউটারে একটি C library (Linux-এ libc.so) একই সময়ে শত শত program ব্যবহার করে। Library-র code-টি RAM-এ একবারই থাকে, সব program-এর মধ্যে ভাগ করে — একে বলে shared library. কিন্তু প্রতিটি program-এর virtual address space-এ library-টি ভিন্ন ঠিকানায় বসতে পারে। Code-এর ভেতরে যদি direct mode-এ পরম ঠিকানা লেখা থাকে, প্রতিটি ঠিকানায় বসানোর সময় সেই সংখ্যাগুলো বদলাতে হবে (relocation) — তখন code page আর ভাগ করা যায় না, প্রতিটি program নিজের কপি চায়।

সমাধান position-independent code (PIC): code এমনভাবে লেখা, যাতে কোনো পরম ঠিকানা না থাকে, শুধু PC থেকে দূরত্ব। Library-র code আর তার নিজের ডেটার দূরত্ব link-এর সময়েই স্থির, তাই PC-relative ঠিকানা যেকোনো জায়গায় সঠিক (অংশ ৫-এর stepper-এর শেষ ধাপ)। অন্য module-এর variable বা function-এর ঠিকানা আগে জানা নেই; সেগুলোর জন্য প্রতিটি module-এ একটি টেবিল থাকে — GOT (global offset table). Program load হওয়ার সময় loader GOT-এ আসল ঠিকানাগুলো লিখে দেয়, আর code PC-relative mode-এ GOT থেকে ঠিকানা পড়ে, তারপর register indirect দিয়ে ডেটা — অর্থাৎ ক্লাসিক indirect mode, দুটি instruction-এ।

ASLR এই ক্ষমতাকেই নিরাপত্তায় ব্যবহার করে: প্রতিবার program চালু হলে stack, heap, library আর (PIE হলে) program-এর নিজের code এলোমেলো ঠিকানায় বসে। আক্রমণকারী মেমরির কোন ঠিকানায় কী আছে আগে থেকে অনুমান করতে পারে না, ফলে অনেক ধরনের আক্রমণ কঠিন হয়। Program যদি PC-relative না হতো, ASLR-এর জন্য প্রতিবার পুরো code relocate করতে হতো। এ কারণেই অনেক Linux distribution-এ gcc এখন ডিফল্ট হিসেবে position-independent executable (PIE) বানায়। OS-এর দিক থেকে logical আর physical ঠিকানা দেখুন OS কোর্সের মেমরির ভিত্তি পর্বে, আর ASLR-সহ নিরাপত্তা-কৌশল OS নিরাপত্তা পর্বে।

জটিল mode আর pipeline

পর্ব ৩৫-এ (pipelining) পাঁচ ধাপের pipeline দেখবেন: fetch, decode, execute, memory, write-back. সেখানে প্রতিটি instruction-এর কাজ একটি নির্দিষ্ট ছাঁচে বসাতে হয়। RISC-V-র base + offset ছাঁচে নিখুঁত বসে: EA হিসাব execute ধাপে ALU-র একটি যোগ, memory ধাপে একটি মেমরি-যাত্রা, write-back-এ একটি register লেখা। এবার জটিল mode-গুলো ভাবুন:

  • Memory indirect: দুটি মেমরি-যাত্রা — একটি memory ধাপে আঁটে না; instruction-কে pipeline-এ দুবার ঘুরতে হয় বা আলাদা micro-op লাগে।
  • Auto-increment / writeback: একটি instruction দুটি register লেখে (load-এর ফল আর নতুন base), তাই register file-এ বাড়তি write port, আর hazard-এর হিসাবও জটিল (পর্ব ৩৬)।
  • Base + index × scale + disp: দুটি register পড়া, একটি shift আর তিনটি সংখ্যার যোগ — address-এর হিসাব একটি সরল যোগের চেয়ে দীর্ঘ, তাই অনেক x86 core-এ আলাদা address generation unit (AGU) থাকে।
  • Exception মাঝপথে: instruction-এর মাঝখানে page fault হলে (পর্ব ৩৮, ৫০), auto-increment ইতিমধ্যে register বদলে ফেলেছে কি না — সেটি মনে রেখে instruction আবার শুরু করতে হয়। পুরোনো কিছু CISC chip এ কারণে virtual memory ঠিকমতো সমর্থন করতে কষ্ট পেয়েছে (ভাবনার প্রশ্ন ০৫)।

এর মানে এই নয় যে জটিল mode “খারাপ”। ARM AArch64-এর মতো আধুনিক RISC-ও post-index রেখেছে, কারণ লুপে এটি সত্যিই instruction বাঁচায়, আর আধুনিক out-of-order core-এ বাড়তি write port-এর খরচ সামলানো যায়। মূল শিক্ষা হলো: প্রতিটি mode-এর একটি hardware-মূল্য আছে, আর ISA-ডিজাইনার মাপজোখ দেখে ঠিক করেন কোনটি দাম দেওয়ার যোগ্য।

আরও গভীরে: RISC-V-র GNU toolchain “linker relaxation” করে: global variable যদি global pointer (gp) register-এর ±২ KiB-এর মধ্যে থাকে, linker lui + lw জোড়াকে একটি lw off(gp)-এ নামিয়ে আনে — ছোট ডেটার জন্য কার্যত এক-instruction direct mode. কিছু উচ্চ-ক্ষমতার RISC-V core auipc + addi বা slli + add-এর মতো পাশাপাশি জোড়াকে ভেতরে একটি operation হিসেবে চালায় (macro-op fusion) বলে প্রকাশিত নকশায় জানা যায়। উল্টো দিকে, কিছু x86 microarchitecture-এ সরল [base + ছোট disp] ঠিকানার load জটিল ঠিকানার চেয়ে প্রায় এক cycle আগে ফল দেয় — অর্থাৎ “এক instruction” হলেও জটিল ঠিকানা পুরোপুরি বিনা মূল্যে নয়।

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

  • Effective address কী? — Addressing mode-এর নিয়ম মেনে হিসাব করা operand-এর আসল মেমরি-ঠিকানা; operand = M[EA].
  • Indirect আর register indirect-এ মেমরি-যাত্রা কতবার? — Indirect ২ (ঠিকানা, তারপর মান), register indirect ১; দুটিতেই instruction fetch আলাদা।
  • PC = ২০২, A = ৫০০ হলে relative mode-এ EA? — ৭০২; শর্ত হলো PC-এর মানটি fetch-এর পরের (পরের instruction-এর ঠিকানা)।
  • RISC-V-তে load-store-এর mode কয়টি? — একটি: rs1 + ১২-বিট signed offset; indexed বা direct দরকার হলে বাড়তি instruction দিয়ে।
  • lea আর mov-এর পার্থক্য? — mov EA থেকে মেমরি পড়ে; lea EA সংখ্যাটিই register-এ রাখে, মেমরি ছোঁয় না।

৯ · AI-এর যুগে — addressing-এর হিসাব যাচাই

Addressing mode-এর প্রশ্নে AI সাধারণত সংজ্ঞাগুলো ঠিক বলে, কিন্তু সংখ্যায় আর ISA-র রীতিতে ভুল করে — বিশেষ করে যেখানে “কোন PC” বা “কোন বই-এর রীতি” গুরুত্বপূর্ণ। নিচের টেবিলের ভুলগুলো খুব পরিচিত:

AI প্রায়ই যা বলেসত্যিটাযাচাইয়ের উপায়
Relative mode-এ instruction-এর নিজের ঠিকানা + Aবই/ISA-ভেদে: Mano-তে fetch-এর পরের PC, RISC-V-তে নিজের ঠিকানা, x86 RIP-এ পরের ঠিকানা, A32-এ + ৮প্রশ্নের শর্ত আর ISA manual মিলিয়ে নিন
Indirect mode-এ “একবার মেমরিতে যাওয়া”দুবার: M[A], তারপর M[M[A]]প্রতিটি পড়া আলাদা করে লিখুন
%hi = ঠিকানার ওপরের ২০ বিট, ব্যসনিচের ১২ বিটের বিট ১১ = ১ হলে +১ (অর্থাৎ +0x800 করে shift)hi × ৪০৯৬ + lo যোগ করে মূল সংখ্যা ফেরে কি না দেখুন
RISC-V-তে lw x5, x10(x11) বা scaled indexRV32I-তে নেই; শুধু imm(rs1)assembler চালিয়ে দেখুন — error দেবে
MIPS-এর branch নিয়ম (PC + ৪, word offset) RISC-V-তেRISC-V: branch-এর নিজের PC, ২-byte এককobjdump -d দিয়ে encoding মেলান
lea মেমরি পড়েশুধু ঠিকানা হিসাব করেঅবৈধ ঠিকানায় lea চালালেও crash হয় না

AI-কে numerical করাতে চাইলে তাকে রীতি স্পষ্ট বলুন আর ধাপগুলো দেখাতে বাধ্য করুন:

আমি একটি addressing mode-এর numerical সমাধান যাচাই করতে চাই। শর্ত: - দুই-word-এর LOAD instruction ঠিকানা 300-301-এ, address field A = 350 - fetch-এর পরে PC = 302; R2 = 150; index register XR = 20 - মেমরি: M[149]=77, M[150]=610, M[350]=500, M[370]=45, M[500]=91, M[652]=12 ১. immediate, direct, indirect, relative, indexed, register indirect, autoincrement আর autodecrement — প্রতিটির EA আর operand একটি টেবিলে দাও। ২. প্রতিটি সারিতে সূত্রটি (যেমন EA = PC + A) আর instruction fetch-এর পরে কতবার মেমরিতে যেতে হলো, লেখো। ৩. relative mode-এ তুমি কোন PC ধরেছ, স্পষ্ট বলো। ৪. শেষে একটি ছোট JavaScript বা Python স্ক্রিপ্ট দাও, যা এই টেবিল নিজে হিসাব করে — আমি চালিয়ে মেলাব।

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

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

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

প্র ০১VAX-এর মতো মেশিনে এত addressing mode থাকার পরেও RISC নকশাকারীরা প্রায় সব বাদ দিলেন কেন — এটি কি ক্ষমতা হারানো, নাকি বুদ্ধিমান বাছাই?

১৯৭০-এর দশকের শেষে DEC-এর VAX ছিল CISC চিন্তার শিখর। প্রায় প্রতিটি instruction-এর প্রতিটি operand আলাদা “operand specifier” দিয়ে লেখা হতো, আর সেখানে register, immediate, displacement, deferred (indirect), auto-increment, auto-decrement, indexed — নানা mode বেছে নেওয়া যেত। ফলে একটি instruction কয়েক byte থেকে অনেক byte পর্যন্ত লম্বা হতে পারত। ভাবনাটা ছিল যুক্তিসঙ্গত: মেমরি তখন দামি আর ধীর, তাই program যত ছোট, তত ভালো; আর assembly-তে হাতে লেখা প্রোগ্রামারের জন্য শক্তিশালী instruction আরামদায়ক।

কিন্তু ১৯৮০-র দশকের শুরুতে গবেষকেরা মাপতে শুরু করলেন, আসল program-এ কোন mode কতটা ব্যবহার হয়। IBM-এর 801 প্রকল্প, Berkeley-র RISC আর Stanford-এর MIPS দল প্রায় একই সিদ্ধান্তে পৌঁছায়। Hennessy আর Patterson-এর বইয়ে VAX-এ চালানো benchmark-এর মাপজোখ দেখানো হয়েছে: displacement, immediate আর register indirect মিলেই মেমরি-ঠিকানার বিশাল অংশ; memory indirect বা জটিল mode খুবই কম। কারণ compiler সেগুলো খুব কম কাজে লাগাতে পারত, আর একই কাজ সরল instruction-এ প্রায়ই সমান দ্রুত হতো।

অন্যদিকে প্রতিটি বিরল mode-এর একটি স্থায়ী দাম ছিল: decode করার circuit, পরিবর্তনশীল দৈর্ঘ্যের instruction, একটি instruction-এ অনেক মেমরি-যাত্রা, আর মাঝপথে exception হলে কী অবস্থা ফেরাতে হবে তার জটিল হিসাব। এই জটিলতা pipeline-কে ধীর করত — প্রতিটি সাধারণ instruction-ও সেই দাম দিত। RISC-এর যুক্তি তাই দাঁড়াল: সাধারণ ঘটনাকে দ্রুত করুন (“make the common case fast”), আর বিরল ঘটনা সফটওয়্যারে কয়েকটি instruction দিয়ে সারুন।

RISC-V এই ধারার সবচেয়ে কড়া উদাহরণ: load-store-এ একটিই mode. তবে এটি অন্ধ বিশ্বাস নয় — মাপজোখ বদলালে নকশাও বদলায়। Zba extension-এ sh2add যোগ হয়েছে, কারণ array indexing সত্যিই খুব সাধারণ; ARM AArch64 post-index রেখেছে; আর x86 তার জটিল mode-গুলো রেখেও ভেতরে micro-op-এ ভেঙে RISC-এর মতো pipeline চালায়।

মূল উপলব্ধি: addressing mode বাছাই হলো মাপজোখ-নির্ভর প্রকৌশল। যে mode কদাচিৎ কাজে লাগে কিন্তু প্রতিটি instruction-কে ধীর করে, সেটি বাদ দেওয়াই লাভ; যেটি বারবার লাগে, সেটি সস্তা করার উপায় খোঁজাই ভালো নকশা।

প্র ০২প্রচলিত কথা — C-র ++ আর -- নাকি PDP-11-এর auto-increment থেকে এসেছে। এটি কতটা সত্য, আর PDP-11-এর addressing নকশায় শেখার কী আছে?

DEC-এর PDP-11 (১৯৭০) ছিল সেই মেশিন, যেখানে Unix আর C বড় হয়েছে। এর addressing নকশা আজও সৌন্দর্যের উদাহরণ। আটটি register (R0–R7), আর প্রতিটি operand-এর জন্য ৩ বিটের mode: register, register deferred (R), auto-increment (R)+, auto-increment deferred @(R)+, auto-decrement -(R), তার deferred রূপ, index X(R) আর index deferred. চমকটা হলো R7 আসলে PC, আর R6 হলো stack pointer. PC-র ওপর auto-increment প্রয়োগ করলে (PC)+ পরের word-টি পড়ে PC এগিয়ে দেয় — এটিই immediate mode! @(PC)+ হলো absolute (direct) mode, আর X(PC) হলো PC-relative. অর্থাৎ আলাদা নিয়ম না বানিয়ে কয়েকটি সাধারণ নিয়ম মিলিয়েই সব mode পাওয়া গেল। আর -(SP) মানে push, (SP)+ মানে pop.

এমন মেশিনে C-র *p++ = *q++ একটি instruction-এ নেমে আসে: MOV (R1)+, (R2)+. তাই বহু মানুষ ধরে নেন, C-র ++ PDP-11-এর জন্যই বানানো। কিন্তু Dennis Ritchie নিজে তাঁর “The Development of the C Language” প্রবন্ধে এটিকে ঐতিহাসিকভাবে অসম্ভব বলেছেন: ++ আর -- Ken Thompson এনেছিলেন B ভাষায়, যা PDP-11 আসার আগে PDP-7-এ চলত। Ritchie উল্লেখ করেছেন, PDP-7-এর কিছু বিশেষ মেমরি-ঘর ছিল যেগুলোর মধ্য দিয়ে indirect ঠিকানা নিলে ঘরটির মান নিজে থেকে বেড়ে যেত — হয়তো সেখান থেকে ভাবনাটি এসেছে বলে তাঁর অনুমান। তবে PDP-11-এর auto-increment যে C-র এই operator-গুলোকে দক্ষ আর জনপ্রিয় করেছিল, তা নিয়ে সন্দেহ নেই।

পরে Motorola 68000 (১৯৭৯) এই ধারাকে এগিয়ে নেয় (An)+ আর -(An) দিয়ে, আর ARM-এর writeback রূপ ([r1], #4) একই ভাবনার RISC-সংস্করণ। আজকের compiler পুরোনো “*p++ দ্রুত, a[i] ধীর” ধারণা ভুল প্রমাণ করেছে: দুটিই সমান দক্ষ কোডে নামে, কারণ compiler নিজেই loop-এর pointer আর index একে অপরে রূপান্তর করে।

মূল উপলব্ধি: ভালো ISA কম কিন্তু মেলানো যায় এমন (orthogonal) নিয়ম দিয়ে অনেক ক্ষমতা দেয় — PDP-11-এ PC আর SP-কে সাধারণ register বানানোর ফলেই immediate, absolute, relative আর stack mode “বিনা খরচে” পাওয়া গেছে। আর ইতিহাসের প্রচলিত গল্প মূল উৎসে মিলিয়ে নেওয়া উচিত।

প্র ০৩৩২-বিট x86-এ shared library-র code ধীর ছিল, x86-64-এ সমস্যাটি প্রায় মিলিয়ে গেল — কারণ কী, আর এর সাথে ASLR-এর সম্পর্ক কোথায়?

৩২-বিট x86-এ ডেটার জন্য কোনো PC-relative (EIP-relative) mode ছিল না। Jump আর call PC-relative হলেও, একটি global variable পড়তে হলে হয় পরম ঠিকানা (direct mode) লিখতে হতো, নয়তো কোনো register-এ আগে থেকে একটি ঠিকানা রাখতে হতো। Position-independent code-এর জন্য compiler তাই একটি কৌশল নিত: একটি call instruction পরের ঠিকানাটি stack-এ রাখে, সেটি pop করে (বা gcc-র __x86.get_pc_thunk.bx-এর মতো ছোট ফাংশন দিয়ে) নিজের PC জানা, তারপর সেখান থেকে GOT-এর ঠিকানা বের করে একটি register-এ (প্রথাগতভাবে ebx) রেখে দেওয়া।

এর দাম দুই রকম। প্রথমত, প্রতিটি ফাংশনের শুরুতে বাড়তি কয়েকটি instruction. দ্বিতীয়ত, এবং বেশি গুরুত্বপূর্ণ — ৩২-বিট x86-এ general-purpose register মাত্র আটটি (তার একটি stack pointer), আর তার একটিকে GOT-এর জন্য আটকে রাখলে বাকি কোডে register-এর টান পড়ে, বেশি মান stack-এ যায়। বিভিন্ন মাপজোখে PIC কোড সাধারণ কোডের চেয়ে কয়েক শতাংশ ধীর পাওয়া যেত (benchmark-ভেদে পরিমাণ আলাদা)। ফলে অনেকে মূল program PIC ছাড়া বানাতেন — আর সেটি নির্দিষ্ট ঠিকানায় load হতো।

AMD যখন ২০০০-এর দশকের শুরুতে x86-64 নকশা করে, RIP-relative ঠিকানা যোগ করে। এখন mov eax, g[rip] একটি instruction-এই global পড়ে, কোনো register আটকে রাখতে হয় না, আর register-ও ১৬টি। PIC-এর খরচ এত কমে গেল যে পুরো program-কেই position-independent (PIE) বানানো লাভজনক হলো।

এখানেই ASLR-এর সাথে যোগ। ASLR-এর ধারণা ২০০০-এর দশকের শুরুতে PaX প্রকল্প, OpenBSD-সহ নানা জায়গায় আসে, পরে Linux, Windows আর macOS-এ মূলধারায় আসে। কিন্তু program যদি নির্দিষ্ট ঠিকানায় load হতেই হয়, তার code-এর অংশটি এলোমেলো করা যায় না — আক্রমণকারী সেখানে পরিচিত instruction-এর টুকরো খুঁজে পায়। PIE আর সস্তা PC-relative mode মিলে পুরো ঠিকানা-বিন্যাস এলোমেলো করা সম্ভব হলো। ARM AArch64-এর adrp আর RISC-V-র auipc একই প্রয়োজন থেকে নকশা করা।

মূল উপলব্ধি: একটি addressing mode-এর উপস্থিতি বা অনুপস্থিতি শুধু গতির প্রশ্ন নয় — এটি ঠিক করে দেয় কোন software কৌশল (shared library, PIE, ASLR) সস্তায় সম্ভব। নিরাপত্তাও শেষ পর্যন্ত ISA-র নকশার ওপর দাঁড়িয়ে।

প্র ০৪“Load effective address” নামের instruction মেমরি থেকে কিছুই load করে না — তবু compiler কেন lea-কে এত পছন্দ করে?

lea-এর জন্ম ১৯৭৮ সালের Intel 8086-এ, একটি বাস্তব প্রয়োজন থেকে। 8086-এর ঠিকানার হিসাব ছিল জটিল — base (BX বা BP) + index (SI বা DI) + displacement, তার ওপর segment register × ১৬ যোগ করে physical ঠিকানা। প্রোগ্রামার প্রায়ই একটি ঠিকানা হিসাব করে রেখে দিতে চাইতেন (যেমন একটি string-এর শুরুর ঠিকানা কোনো ফাংশনে পাঠাতে)। lea ঠিক এই কাজ করে: memory operand-এর EA (segment বাদে offset-টি) হিসাব করে register-এ রাখে, মেমরিতে যায় না। মজার তথ্য: 8086-এর manual-এ প্রতিটি addressing mode-এর জন্য EA হিসাবের সময় আলাদা করে দেওয়া থাকত, কয়েক থেকে ডজনখানেক clock পর্যন্ত — জটিল mode তখন সত্যিই দামি ছিল।

৩২-বিট আর ৬৪-বিট x86-এ ঠিকানার রূপ হলো base + index × scale + disp, যেকোনো general register দিয়ে। এখন লক্ষ করুন, এটি আসলে একটি ছোট গাণিতিক যন্ত্র: দুটি সংখ্যা যোগ, তার একটিকে ১, ২, ৪ বা ৮ দিয়ে গুণ, আর একটি ধ্রুবক যোগ — সব একটি instruction-এ। তার ওপর lea flag বদলায় না, আর ফল যেকোনো register-এ রাখা যায় (সাধারণ add দুই-operand-এর, ফলটি একটি উৎসের ওপর লিখে দেয়)। তাই compiler x * 5-কে lea eax, [rdi + rdi*4], x * 9-কে lea eax, [rdi + rdi*8], আর a + b + 10-কে lea eax, [rdi + rsi + 10] বানায়। x * 15-এর মতো সংখ্যা দুটি lea-তে হয়।

Hardware-এর দিক থেকে কাজটি যায় address generation-এর সার্কিটে বা সাধারণ ALU-তে — core-ভেদে আলাদা। কিছু Intel core-এ তিন অংশের (base + index + disp) lea-র latency সরল রূপের চেয়ে বেশি বলে Intel-এর optimization manual আর স্বাধীন মাপজোখে দেখা যায়, তাই compiler microarchitecture দেখে বাছাই করে।

RISC-V-তে আলাদা lea লাগে না, কারণ ঠিকানার হিসাব আর সাধারণ হিসাব একই instruction: addi আর add, সাথে Zba-র sh2add — সবই সাধারণ হিসাবের কাজ। পার্থক্যটা দর্শনের: x86-এ address hardware একটি বিশেষ যন্ত্র, যাকে compiler চালাকি করে সাধারণ হিসাবে খাটায়; RISC-V-তে ঠিকানা কেবলই আরেকটি সংখ্যা।

মূল উপলব্ধি: EA একটি সাধারণ সংখ্যা — মেমরিতে যাওয়া না-যাওয়া আলাদা সিদ্ধান্ত। lea সেই দুটিকে আলাদা করে দেখায়, আর ঠিকানার hardware-কে বিনা মূল্যের গণনাযন্ত্রে পরিণত করে।

প্র ০৫Auto-increment-এর মতো “পার্শ্বপ্রতিক্রিয়া”-ওয়ালা mode থাকলে instruction-এর মাঝপথে page fault সামলানো কেন কঠিন — ইতিহাসে এর কী প্রভাব পড়েছিল?

Virtual memory-তে (পর্ব ৫০) program যে page ছোঁয়, সেটি RAM-এ না থাকলে CPU একটি page fault তোলে। OS page-টি disk থেকে আনে, তারপর instruction-টি আবার চালানোর ব্যবস্থা করে। এই কৌশল কাজ করে কেবল তখনই, যখন instruction-টি এমন অবস্থায় থামে যে আবার শুরু করলে ফল ঠিক আসবে। RISC-V-র lw x5, 8(x10) সহজ: fault হলে কোনো register বদলায়নি, তাই হুবহু আবার চালালেই হয়।

এবার একটি CISC instruction ভাবুন: MOVE.L (A0)+, (A1)+ — উৎস থেকে পড়ে A0 বাড়ায়, তারপর গন্তব্যে লেখে আর A1 বাড়ায়। গন্তব্যের page না থাকলে fault হলো — কিন্তু A0 ইতিমধ্যে বেড়ে গেছে! আবার শুরু করলে ভুল উৎস পড়বে। তাই CPU-কে হয় সব পার্শ্বপ্রতিক্রিয়া উল্টে দিতে হয় (কোন register কতটা বদলেছে মনে রেখে), নয়তো ভেতরের অবস্থা সেভ করে ঠিক মাঝখান থেকে চালিয়ে যেতে হয়। দুটিই জটিল hardware.

Motorola 68000 (১৯৭৯) এমন অবস্থায় নির্ভরযোগ্যভাবে আবার শুরু করতে পারত না, তাই সেটি দিয়ে পূর্ণ demand-paged virtual memory বানানো কঠিন ছিল। ১৯৮২ সালের দিকে 68010 bus error-এর সময় ভেতরের অবস্থা stack-এ সেভ করে মাঝপথ থেকে চালিয়ে যাওয়ার ব্যবস্থা আনে। এর আগে কিছু workstation নির্মাতা দুটি 68000 ব্যবহার করে কাজ চালানোর চতুর পথ নিয়েছিল বলে জানা যায় — একটি program চালাত, অন্যটি fault সামলাত। VAX-ও auto-increment-এর পরিবর্তন মনে রাখার জন্য বিশেষ ব্যবস্থা রাখত। x86-এর rep movs-এর মতো string instruction-ও নিজের অবস্থা register-এ (rsi, rdi, rcx) রেখে মাঝপথে থামতে পারে — নকশা করেই।

এখান থেকে আসে precise exception-এর ধারণা (পর্ব ৩৮): exception-এর মুহূর্তে আগের সব instruction পুরো শেষ, পরের কোনোটির কোনো চিহ্ন নেই। প্রতিটি instruction যত কম অবস্থা বদলায়, এই নিশ্চয়তা দেওয়া তত সহজ। RISC নকশার “এক instruction, এক মেমরি-যাত্রা, এক register লেখা” নিয়মের একটি বড় কারণ এটাই। আধুনিক out-of-order CPU reorder buffer দিয়ে (পর্ব ৪০) সব পরিবর্তন commit-এর আগ পর্যন্ত আটকে রাখে, তাই জটিল instruction-ও নিরাপদে বাতিল করা যায় — কিন্তু সেই যন্ত্রের নিজেরই বড় দাম।

মূল উপলব্ধি: addressing mode-এর পার্শ্বপ্রতিক্রিয়া শুধু সুবিধা নয়, exception আর virtual memory-র জন্য একটি দায়। একটি ISA-কে ভবিষ্যতের OS আর hardware কৌশলের কথা ভেবে নকশা করতে হয়।

অনুশীলন

কাগজ-কলম নিন — প্রথম তিনটি হাতে হিসাবের, শেষটি আউটপুট অনুমানের।

  1. পরীক্ষার numerical: একটি দুই-word-এর LOAD instruction ঠিকানা ৩০০–৩০১-এ, address field A = ৩৫০. fetch-এর পরে PC = ৩০২, R2 = ১৫০, index register XR = ২০. মেমরি: M[১৪৯] = ৭৭, M[১৫০] = ৬১০, M[৩৫০] = ৫০০, M[৩৭০] = ৪৫, M[৫০০] = ৯১, M[৬৫২] = ১২. Immediate, direct, indirect, relative, indexed, register indirect (R2), autoincrement (R2) আর autodecrement (R2)-এ EA আর AC-তে আসা মান কত? (word-addressed, d = ১)
    ModeEA-র হিসাবEAAC
    Immediateoperand = A (A বসে আছে ৩০১-এ)৩০১৩৫০
    DirectEA = A৩৫০৫০০
    IndirectEA = M[৩৫০]৫০০৯১
    RelativeEA = PC + A = ৩০২ + ৩৫০৬৫২১২
    IndexedEA = A + XR = ৩৫০ + ২০৩৭০৪৫
    Register indirectEA = R2১৫০৬১০
    AutoincrementEA = R2, পরে R2 = ১৫১১৫০৬১০
    Autodecrementআগে R2 = ১৪৯, EA = R2১৪৯৭৭

    Indirect-এ মেমরিতে দুবার যেতে হয় (৩৫০, তারপর ৫০০); immediate-এ একবারও নয়। Relative-এ ৩০০ বা ৩০১ ধরে হিসাব করলে ৬৫০ বা ৬৫১ আসত — যার মান টেবিলে নেই, এটিই ইঙ্গিত যে প্রশ্নকর্তা fetch-এর পরের PC চেয়েছেন। অংশ ৩-এর calculator-এ এই মানগুলো বসিয়ে যাচাই করে নিন।

  2. C থেকে RISC-V: struct S { int n; int arr[8]; }; — এখানে int-এর মাপ ৪ byte ধরুন। s->arr[i] = *q; লাইনটির RV32I রূপ লিখুন, যেখানে s আছে a0 = 0x3000, i আছে a1 = ২, q আছে a2 = 0x4010. প্রতিটি load/store-এর EA কত, আর x86-64-এ কয়টি instruction লাগবে?
    lw    t0, 0(a2)      # *q          EA = 0x4010
    slli  t1, a1, 2      # i * 4       t1 = 8
    add   t1, a0, t1     # s + i*4     t1 = 0x3008
    sw    t0, 4(t1)      # arr at +4   EA = 0x300C

    arr-এর offset ৪ (n-এর পরে, padding নেই), তাই EA = 0x3000 + ৪ + ২ × ৪ = 0x300C. মোট ৪টি instruction. লক্ষ করুন, struct-এর offset ৪-কে আলাদা addi না করে store-এর displacement-এ বসানো হয়েছে — base + offset mode-এর সেরা ব্যবহার। x86-64-এ লাগে ২টি: mov eax, [rdx] আর mov [rdi + rsi*4 + 4], eax (i-কে ৬৪ বিটে sign-extend করতে আরেকটি movsx লাগতে পারে)। দুটি মেমরি operand একসাথে x86-এর এক mov-এ চলে না, তাই এক instruction-এ হয় না।

  3. auipc + addi হাতে: একটি auipc আছে 0x00400010-এ, আর লক্ষ্য symbol buf আছে 0x00401A20-এ। %pcrel_hi আর %pcrel_lo কত হবে? দুটি instruction-এর পরে register-এ কোন মান আসবে, ধাপে ধাপে দেখান। hi = ১ নিলে কী ভুল হতো?

    দূরত্ব = 0x00401A20 − 0x00400010 = 0x1A10. নিচের ১২ বিট 0xA10, বিট ১১ = ১ (0xA = 1010), তাই signed হিসেবে 0xA10 − 0x1000 = −0x5F0 = −১৫২০. hi = (0x1A10 + 0x800) >> ১২ = 0x2210 >> ১২ = ২.

    auipc a0, 2 → a0 = 0x00400010 + 0x2000 = 0x00402010; addi a0, a0, -1520 → 0x00402010 − 0x5F0 = 0x00401A20 ✓. hi = ১ নিলে ফল হতো 0x00401010 − 0x5F0 = 0x00400A20 — ঠিক ৪০৯৬ byte কম। কারণ addi তার immediate-কে সবসময় sign-extend করে।

  4. আউটপুট অনুমান (ARM A32): শুরুতে r1 = 0x100, আর মেমরিতে (৪-byte word) M[0x100] = ১০, M[0x104] = ২০, M[0x108] = ৩০, M[0x10C] = ৪০. পরপর তিনটি instruction চলে: ldr r0, [r1, #4]!, তারপর ldr r2, [r1], #8, তারপর ldr r3, [r1, #-4]. প্রতিটির EA, আর শেষে r0, r2, r3, r1 কত?

    (১) Pre-index: EA = 0x100 + ৪ = 0x104, r0 = ২০, আর writeback-এ r1 = 0x104. (২) Post-index: EA = পুরোনো r1 = 0x104, r2 = ২০, তারপর r1 = 0x104 + ৮ = 0x10C. (৩) সাধারণ offset, writeback নেই: EA = 0x10C − ৪ = 0x108, r3 = ৩০, r1 অপরিবর্তিত।

    শেষে r0 = ২০, r2 = ২০, r3 = ৩০, r1 = 0x10C. মূল ফাঁদ দ্বিতীয় instruction-এ: post-index-এ EA পুরোনো base, তাই r2-ও ২০ পায়, ৩০ নয়। RISC-V-তে একই কাজে প্রতিটি writeback-এর জন্য একটি বাড়তি addi লাগত।

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

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

ক্রমRV32I-তে a[i] += 1 (int array, a আর i register-এ) করার instruction-গুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. slli — i-কে ৪ দিয়ে গুণ করে byte offset বানানো
  2. add — array-র শুরুর ঠিকানার সাথে offset যোগ
  3. lw — offset ০ দিয়ে a[i] register-এ আনা
  4. addi — register-এর মানে ১ যোগ
  5. sw — একই ঠিকানায় নতুন মান লেখা

একটি দুই-word-এর instruction ২০০–২০১ ঠিকানায়, address field = ৫০০, আর fetch-এর পরে PC = ২০২. Relative addressing-এ effective address কত?

  • ৫০০
  • ৭০০
  • ৭০২
  • ২০২

Memory indirect addressing-এ (EA = M[A]) instruction fetch হয়ে যাওয়ার পরে operand আনতে কতবার মেমরিতে যেতে হয়?

  • ১
  • ২
  • ০
  • ৩

RISC-V RV32I-এর load আর store instruction কোন addressing mode ব্যবহার করে?

  • শুধু base register + ১২-বিট signed offset
  • base + index register
  • memory indirect
  • post-increment

RISC-V-তে lui + addi দিয়ে 0x12345FFF মান বানাতে হবে। lui-এর ২০-বিট immediate কত হবে?

  • 0x12345
  • 0x12346
  • 0x12344
  • 0x45FFF

x86-64-এ edi = ৬ হলে lea eax, [rdi + rdi*4] চলার পরে কী ঘটে?

  • ঠিকানা ৩০ থেকে মেমরি পড়ে eax-এ রাখে
  • eax = ২৪
  • শুধু flag বদলায়, eax অপরিবর্তিত
  • eax = ৩০, কোনো মেমরি-যাত্রা নেই

Shared library-র code-এ global ডেটার জন্য PC-relative addressing কেন পছন্দ করা হয়?

  • Code আর ডেটার দূরত্ব স্থির, তাই যেকোনো ঠিকানায় load হলেও code না বদলে চলে
  • এতে মেমরি-যাত্রা direct mode-এর চেয়ে কম
  • এতে যেকোনো দূরত্বের ঠিকানায় পৌঁছানো যায়
  • এতে কখনো cache miss হয় না

শব্দকোষ

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

মনে রাখুন

  1. Addressing mode = operand খোঁজার নিয়ম; মেমরির operand-এর আসল ঠিকানা EA, operand = M[EA].
  2. মূল সূত্র: direct EA = A, indirect EA = M[A] (২ যাত্রা), register indirect EA = R, displacement EA = R + A, indexed EA = A + XR, relative EA = PC + A; immediate আর register-এ মেমরি-যাত্রা ০.
  3. পরীক্ষার ফাঁদ: relative-এ কোন PC (fetch-এর পরে ২০২ → EA ৭০২), auto-increment-এ পুরোনো R আর auto-decrement-এ নতুন R.
  4. RISC-V: load-store-এর একমাত্র mode imm12(rs1); a[i] = slli + add + lw; global = lui/auipc + lw, %hi-তে +0x800; post-increment = lw + addi.
  5. x86-64 আর ARM: [base + index*scale + disp], RIP-relative, মেমরি-না-ছোঁয়া lea; ARM-এ [r1, #4]! আর [r1], #4. PC-relative ঠিকানাই PIC, shared library আর ASLR-কে সস্তা করে।

পরের পর্বে: PC-relative ঠিকানার সবচেয়ে বড় ব্যবহার — branch আর jump. C-র if, while, for আর switch assembly-তে কীভাবে লেখা হয়, আর branch offset কীভাবে হিসাব হয়।

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

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