Addressing Modes — অপারেন্ড আসলে কোথায় থাকে
Addressing Modes
একটা instruction-এর operand field-এ যা লেখা থাকে তা প্রায়ই মান নিজেই না — বরং মান কোথায় খুঁজতে হবে তার একটা রেসিপি। Immediate সরাসরি মান দেয়, register direct একটা রেজিস্টারের মান দেয়, বাকি সব mode আসলে একটা effective address গণনা করে — register indirect, base+displacement, PC-relative, indexed — প্রতিটাই এক-একটা ভিন্ন গণনা। RISC ইচ্ছাকৃতভাবে এই তালিকা ছোট রাখে যাতে সেই গণনা সবসময় একটাই সরল ADD হয়, hardware-এ একটাই পথ লাগে।
আগে এটা বুঝি
গত লেসনে আমরা lw t1, -8(s0) decode করেছি — bit-field দিয়ে বের করেছি
এটা একটা I-type LOAD, rd=t1, rs1=s0, imm=-8। কিন্তু একটা প্রশ্ন
ইচ্ছাকৃতভাবে অমীমাংসিত রাখা হয়েছিল: এই -8(s0) অংশটা মানে কী?
s0-এর মান থেকে 8 বিয়োগ করে যা পাওয়া যায়, সেটাই কি চূড়ান্ত ফলাফল?
নাকি সেটা একটা মেমোরি ঠিকানা, যেখানে গিয়ে আরেকটা মান পড়তে হবে?
উত্তর, এই instruction-এর ক্ষেত্রে: দ্বিতীয়টা। s0-8 একটা ঠিকানা,
আর lw সেই ঠিকানায় গিয়ে একটা word পড়ে আনে। কিন্তু এটা প্রতিটা
instruction-এ সত্যি না — addi t0, zero, 5-এ 5 সরাসরি একটা মান,
কোনো ঠিকানা না, কোনো মেমোরি অ্যাক্সেস নেই।
এই পার্থক্যটাই — একটা operand field সরাসরি মান দেয়, নাকি মান কোথায় খুঁজতে হবে তার একটা রেসিপি দেয় — addressing mode-এর বিষয়। গত লেসন শিখিয়েছে operand field-এর বিট কোথায়। আজকের লেসন শেখাবে সেই বিট থেকে প্রকৃত মান পর্যন্ত পথ কী।
মূল ধারণা
Addressing mode মানে কী
একটা addressing mode হলো নিয়ম — instruction-এর operand field-এর বিট থেকে অপারেন্ডের প্রকৃত মান (বা তার ঠিকানা) কীভাবে বের করতে হবে। প্রতিটা mode-এর একটা কেন্দ্রীয় প্রশ্নের উত্তর দিতে হয়:
মান কি সরাসরি এখানে আছে, নাকি এই বিটগুলো দিয়ে একটা ঠিকানা গণনা করে সেই ঠিকানায় গিয়ে মান আনতে হবে?
যখন ঠিকানা গণনা করতে হয়, সেই গণনার ফলাফলকে বলে effective address (EA) — শব্দটা মনে রাখুন, পুরো লেসন জুড়ে ফিরে আসবে।
ছয়টা mode, একে একে
১. Immediate addressing — মান সরাসরি instruction-এ
addi t0, zero, 5 # t0 ← 0 + 55 কোনো register নম্বর বা ঠিকানা না — এটা নিজেই মান, সরাসরি
instruction-এর বিটে বসানো (গত লেসনের I-type imm[11:0] ফিল্ড)।
কোনো effective address গণনা নেই, কোনো memory access নেই — মান
ইতিমধ্যেই হাতে আছে, শুধু sign-extend করে ব্যবহার করা হয়।
| Effective address গণনা? | না |
| Memory access? | না |
| গতি | সবচেয়ে দ্রুত — কোনো বাড়তি ধাপ নেই |
২. Register direct addressing — মান একটা register-এ
add t3, t1, t2 # t3 ← t1 + t2Operand field register file-এর একটা ঠিকানা (register নম্বর) ধরে আছে — সেই register-এর content-ই মান। এখানেও কোনো memory access নেই, কিন্তু একটা বাড়তি ধাপ আছে immediate-এর তুলনায়: register file থেকে read করতে হবে (Level ২-র register file লেসন — এক clock cycle-এর মধ্যেই সম্ভব, কারণ register file নিজেই দ্রুততম storage)।
| Effective address গণনা? | না (register নম্বরই সরাসরি “ঠিকানা”, কিন্তু register file-এর, memory-র না) |
| Memory access? | না — register file access, ভিন্ন storage |
৩. Register indirect addressing — register একটা মেমোরি ঠিকানা ধরে আছে
lw t1, 0(s0) # t1 ← Memory[s0] (s0 নিজেই একটা ঠিকানা ধরে আছে)এখানে s0-এর মান সরাসরি effective address — কোনো offset যোগ
হচ্ছে না (offset = 0)। এটা technically RV32I-তে base+displacement-এরই
একটা বিশেষ কেস (offset=0), কিন্তু ধারণাগতভাবে এটাই “খাঁটি” register
indirect — একটা register শুধুই একটা ঠিকানা বহন করছে, তার নিজের মান
কোনো কাজে লাগছে না, শুধু কোথায় যেতে হবে তা বলছে।
x86-এর ধ্রুপদী উদাহরণ:
mov eax, [ebx] ; eax ← Memory[ebx]
| Effective address গণনা? | হ্যাঁ, কিন্তু তুচ্ছ — EA = Reg |
| Memory access? | হ্যাঁ — EA থেকে read (বা write) |
৪. Base + displacement addressing — সবচেয়ে ব্যবহৃত mode
lw t1, -8(s0) # t1 ← Memory[s0 - 8]EA = base_register + constant_offset। এই লেসনের সবচেয়ে গুরুত্বপূর্ণ
mode — কেন, নিচে বিস্তারিত অংশে।
| Effective address গণনা? | হ্যাঁ — s0 + (-8), একটা ADD |
| Memory access? | হ্যাঁ — EA থেকে read/write |
৫. PC-relative addressing — ঠিকানা program counter-এর সাপেক্ষে
beq t0, t1, offset # যদি t0 == t1, PC ← PC + offsetEA = PC + offset, যেখানে offset instruction-এ এনকোড করা (গত
লেসনের B-type/J-type ফরম্যাট)। PC নিজেই একটা register — তাই এটা
ধারণাগতভাবে base+displacement-এরই একটা বিশেষ রূপ, শুধু base register
সবসময় PC (কোনো rs1 field দিয়ে বেছে নেওয়ার দরকার নেই)।
| Effective address গণনা? | হ্যাঁ — PC + offset |
| Memory access? | branch/jump-এ data memory access নেই — EA-টাই পরের instruction fetch-এর ঠিকানা হয়ে যায় |
৬. Indexed addressing — base + index register (+ scale)
mov eax, [ebx + ecx*4] ; eax ← Memory[ebx + ecx*4] — array[i], i in ecx, element size 4EA = base_register + (index_register × scale) + optional_displacement।
এখানে দুইটা register যোগ হচ্ছে effective address গণনায় — একটা
base (array-এর শুরু), একটা index (কোন element)। scale (১, ২, ৪, বা
৮) element-এর byte size, তাই index register-কে বাইট-অফসেটে রূপান্তর
করতে আলাদা কোনো shift instruction লাগে না — এটা এক instruction-এই
বসানো।
RV32I-তে এই mode নেই — array indexing করতে হলে explicit ভাবে কয়েকটা instruction লাগে (নিচে “example” অংশে)।
| Effective address গণনা? | হ্যাঁ — একটা multiply (বা shift) + দুইটা add |
| Memory access? | হ্যাঁ |
সারসংক্ষেপ টেবিল
| Mode | উদাহরণ | Effective address | Memory access |
|---|---|---|---|
| Immediate | addi t0, zero, 5 | — (মান সরাসরি) | না |
| Register direct | add t3, t1, t2 | — (register file access) | না (memory-তে না) |
| Register indirect | lw t1, 0(s0) | s0 | হ্যাঁ |
| Base + displacement | lw t1, -8(s0) | s0 - 8 | হ্যাঁ |
| PC-relative | beq t0, t1, off | PC + off | না (instruction fetch-এর ঠিকানা) |
| Indexed | [ebx + ecx*4] | ebx + ecx*4 | হ্যাঁ |
ভেতরে কী ঘটছে
Base + displacement কেন সবচেয়ে গুরুত্বপূর্ণ — স্থানীয় ভেরিয়েবল ও struct field
স্থানীয় ভেরিয়েবল। একটা ফাংশনের local variable stack-এ থাকে,
frame pointer (s0, বা কখনো সরাসরি sp)-এর সাপেক্ষে একটা fixed
offset-এ। Compiler প্রতিটা local variable-কে compile-time-এই একটা
offset বরাদ্দ করে — b হয়তো s0-8, c হয়তো s0-12, a হয়তো
s0-4। এই লেসনের driving instruction-গুলো (lw t1, -8(s0)) precisely
এভাবেই কাজ করে — offset compiler ঠিক করে দেয়, base register রানটাইমে
frame-এর প্রকৃত ঠিকানা ধরে রাখে।
Struct field access। Level ১-এর
serialization-binary-formats লেসনে আমরা offsetof(struct A, b)
গণনা করেছিলাম — কম্পাইলার প্রতিটা field-কে struct-এর শুরু থেকে একটা
নির্দিষ্ট byte offset-এ বসায় (padding-সহ)। এখন সেই offset-টাই সরাসরি
base+displacement addressing-এর displacement হয়ে যায়:
struct Point { int x; int y; }; // offsetof(x)=0, offsetof(y)=4
int get_y(struct Point *p) { return p->y; }lw a1, 4(a0) # a0 = p (struct-এর ঠিকানা), offset 4 = y-এর অবস্থানএই একটা instruction-ই offsetof-এর concrete, hardware-level
বাস্তবায়ন — যে সংখ্যাটা offsetof(struct Point, y) compile-time-এ
হিসেব করে দিয়েছিল, সেটাই এখানে 4(a0)-এর 4 হয়ে বসেছে। Struct
layout একটা বিমূর্ত ধারণা মনে হতে পারত, কিন্তু এখানে দেখা যাচ্ছে এটা
সরাসরি একটা instruction-এর bit-encoded constant।
PC-relative addressing-এর দুই ব্যবহার
ব্যবহার ১ — branch/jump। beq t0, t1, offset — শর্ত সত্য হলে
PC ← PC + offset। Offset সবসময় relative, absolute ঠিকানা না —
কারণ কম্পাইলার/লিংকার প্রায়ই জানে না প্রোগ্রামটা memory-তে কোথায়
লোড হবে (নিচে দেখুন), কিন্তু জানে “if-branch থেকে else-branch কত
byte দূরে” — সেই তথ্যটাই যথেষ্ট।
ব্যবহার ২ — position-independent code (PIC), global data access। এটাই কম আলোচিত কিন্তু সমান গুরুত্বপূর্ণ ব্যবহার। ধরুন একটা global variable-এর ঠিকানা লাগবে, কিন্তু প্রোগ্রামটা memory-তে ঠিক কোথায় লোড হবে তা রান-টাইম পর্যন্ত অজানা (shared library, ASLR — address space layout randomization, security-র জন্য প্রতিবার এলোমেলো ঠিকানায় লোড করা)। যদি instruction-এ absolute ঠিকানা hardcode করা থাকত, প্রতিবার লোডের সময় সেটা পাল্টাতে হতো (relocation) — ধীর, আর shared library-তে একাধিক প্রসেসের মধ্যে code page শেয়ার করা কঠিন হয়ে যেত।
RV32I-এর সমাধান — auipc (add upper immediate to PC):
auipc t0, %pcrel_hi(symbol) # t0 ← PC + upper 20 bits of offset
lw t1, %pcrel_lo(symbol)(t0) # t1 ← Memory[t0 + lower 12 bits]symbol-এর ঠিকানা কখনোই absolute হিসেবে instruction-এ বসে না —
শুধু “current PC থেকে কত দূরে” এনকোড করা থাকে। প্রোগ্রামটা যেখানেই
লোড হোক না কেন (PC-র মান যাই হোক), PC + offset সবসময় সঠিক ঠিকানায়
পৌঁছাবে — কোনো relocation লাগে না। এটাই PIC-এর মূলনীতি, আর আধুনিক
প্রায় সব shared library/executable এভাবেই কম্পাইল হয়।
একটা সংখ্যা দিয়ে PC-relative-এর effective address গণনা
গত লেসনে (instruction-encoding) আমরা beq t1, t2, 8 হাতে এনকোড
করেছিলাম — 0x00730463, যার B-type immediate পুনর্গঠন করলে পাওয়া
যায় offset = 8। এই লেসনের addressing-mode ভাষায় সেই একই instruction-এর
effective address গণনা এখন সম্পূর্ণ করা যাক।
ধরুন এই beq instruction নিজে PC = 0x2000-এ বসে আছে (এটাই তার
নিজের ঠিকানা, যা আগের FETCH-এ ইতিমধ্যে instruction memory-তে পাঠানো
হয়েছিল)।
যদি t1 == t2 (branch নেওয়া হয়), পরের FETCH-এ PC-র মান হবে
0x2008, স্বাভাবিক PC+4 = 0x2004-এর বদলে — অর্থাৎ পরের একটা
instruction (0x2004-এ যা-ই থাকুক) সম্পূর্ণ স্কিপ হয়ে যাবে।
যদি t1 != t2 (branch নেওয়া না হয়), স্বাভাবিক PC ← PC + 4-ই
বহাল থাকে — effective address গণনা হয়েছে ঠিকই (ALU-তে, প্রতিটা
beq-এই, শর্ত যাই হোক না কেন), কিন্তু ব্যবহৃত হয়নি।
কেন RISC ইচ্ছাকৃতভাবে কম addressing mode রাখে
লেসন ২-এর RISC/CISC আলোচনা মনে করুন — RISC দর্শনের একটা মূল স্তম্ভ ছিল “simple, uniform instruction, সহজে pipeline করা যায়।” Addressing mode ঠিক সেই দর্শনের একটা কেন্দ্রীয় প্রয়োগক্ষেত্র।
RV32I-তে memory instruction-এর addressing mode ঠিক একটা —
base+displacement (offset(register))। আর কিছু না — কোনো indexed
mode, কোনো autoincrement/autodecrement, কোনো double-indirection নেই।
জটিল ঠিকানা দরকার হলে (array indexing, struct-এর-ভেতরে-array,
pointer-এর-pointer) কম্পাইলার সেটা কয়েকটা সরল instruction (shift,
add) দিয়ে synthesize করে — hardware-এ নতুন addressing mode
যোগ না করে।
কেন এটা pipelining সহজ করে — তিনটা concrete কারণ:
১. একটাই effective-address-computation পথ। Datapath-and-control-unit
লেসনে (Level ২) আমরা দেখেছি ALU বারবার re-used হয় — PC+4 গণনায়,
memory address গণনায়, branch address গণনায়, R-type operation-এ।
যদি একাধিক addressing mode সমর্থন করতে হতো (কিছু mode-এ shift+add,
কিছুতে শুধু add, কিছুতে double indirection), প্রতিটা mode-এর জন্য
আলাদা datapath পথ বা বাড়তি cycle লাগত — single, uniform ADD পথের
বদলে।
২. প্রতিটা memory instruction-এর latency predictable। একটা
lw সবসময় ঠিক এক ADD (address গণনা) + এক memory access নেয় — কখনোই
বেশি না। যদি কোনো mode-এ double indirection (ঠিকানা থেকে পড়া মানও
আবার একটা ঠিকানা, আবার পড়তে হয়) থাকত, সেই instruction-এর latency
mode ভেদে বদলাতো — pipeline-এর প্রতিটা stage-এ “কত সময় লাগবে” নিশ্চিতভাবে
জানা না গেলে fixed-length pipeline ডিজাইন করা কঠিন হয়ে যায় (Level
৩-এর পরের pipelining topics-এ এই সমস্যা বিস্তারিত)।
৩. Decoder সরলতা — গত লেসনের সরাসরি সম্প্রসারণ। কম addressing mode মানে immediate ফিল্ড, register ফিল্ড সবসময় একই রকম আচরণ করে — কোনো mode-নির্দিষ্ট “এই ক্ষেত্রে ভিন্নভাবে পড়ো” জটিলতা যোগ হয় না decode logic-এ।
x86-এর indexed addressing হাতে decode করা — ModRM+SIB
গত লেসনে x86-64-এর ModRM byte নাম-মাত্র উল্লেখ হয়েছিল। এখানে সেটা
সম্পূর্ণ করা যাক — mov eax, [ebx + ecx*4] (এই লেসনের indexed
addressing উদাহরণ) ঠিক কীভাবে বিটে এনকোড হয়:
opcode ModRM SIB
8B 04 8BModRM byte (0x04 = 00 000 100): mod=00 (কোনো displacement
নেই, আর rm=100 মানে “সরাসরি register না, একটা SIB byte পরেই
আসছে”), reg=000 (destination eax), rm=100 (SIB byte আসছে এই
সংকেত)।
SIB byte (0x8B = 10 001 011): scale=10 (বাইনারি 10 =
4×), index=001 (ecx), base=011 (ebx)।
উদাহরণ
সম্পূর্ণ উদাহরণ — array indexing, RISC-V-তে “হাতে বানানো”
int arr[10];
int get(int i) { return arr[i]; }arr একটা global array (ধরি তার ঠিকানা compile-time-এ প্রায় জানা,
auipc-ভিত্তিক PC-relative গণনায় পাওয়া যায়), i প্যারামিটার হিসেবে
a0-তে আসে। int = ৪ byte, তাই element-এর byte offset = i × 4।
auipc t0, %pcrel_hi(arr) # t0 ← arr-এর ঠিকানার উপরের অংশ (PC-relative)
addi t0, t0, %pcrel_lo(arr) # t0 ← arr-এর সম্পূর্ণ base ঠিকানা
slli t1, a0, 2 # t1 ← i × 4 (2-বিট left shift = ×4)
add t2, t0, t1 # t2 ← base + (i×4) — effective address, register indirect-এর জন্য প্রস্তুত
lw a0, 0(t2) # a0 ← Memory[t2] — চূড়ান্ত read, register indirect modeপাঁচটা instruction, চারটা আলাদা addressing mode একসাথে কাজ করছে:
auipc-তে PC-relative, addi-তে immediate, slli-তে immediate
(shift amount), শেষ lw-তে register indirect (offset =0, কারণ
সব গণনা আগেই t2-তে হয়ে গেছে)।
তুলনায়, x86-64-এর একটাই instruction দিয়ে এই কাজ (base already loaded, index in ecx):
mov eax, [ebx + ecx*4] ; একটাই instruction, একটাই indexed addressing mode- arr[i] — সোর্স কোডে একটা array accessএকটা মাত্র expression
- base address গণনা (auipc + addi)PC-relative addressing — arr কোথায় লোড হবে জানা না থাকলেও কাজ করে
- index-কে byte offset-এ রূপান্তর (slli)i × 4 — sizeof(int)-এর সাথে scale করা, immediate addressing (shift amount)
- effective address গণনা (add)base + byte_offset — নিজে ALU-তে একটা সাধারণ ADD
- memory access (lw)register indirect — গণনা করা ঠিকানা থেকে সরাসরি পড়া
এই ট্রেসটাই RISC দর্শনের সবচেয়ে concrete প্রমাণ — x86-এর “এক instruction, এক addressing mode” যা করে, RV32I সেটা “পাঁচটা সরল instruction, প্রতিটা একটাই uniform addressing mode” দিয়ে করে। কোনো ক্ষমতা হারানো হয়নি — শুধু জটিলতা hardware থেকে সরে compiler-এর কাজে চলে গেছে (compiler একবার optimize করে, hardware প্রতি cycle-এ চলে — আগের লেসনের সেই একই “একবারের কাজ কঠিন, বারবারের কাজ সহজ” নীতি)।
দ্বিতীয় উদাহরণ — 2D array, দুইটা addressing ধাপ একসাথে
int grid[10][20];
int cell(int i, int j) { return grid[i][j]; }grid[i][j]-এর effective address গণনা row-major layout ধরে (C-এর
ডিফল্ট): base + (i × 20 + j) × 4 (প্রতি row-তে ২০টা int, প্রতিটা
৪ byte)। RISC-V-তে (base ইতিমধ্যে t0-তে ধরে নিয়ে, PC-relative
গণনার পর):
li t1, 20 # t1 = row width (immediate addressing)
mul t2, a0, t1 # t2 = i × 20 (a0 = i)
add t2, t2, a1 # t2 = i×20 + j (a1 = j)
slli t2, t2, 2 # t2 = (i×20+j) × 4 (byte offset)
add t3, t0, t2 # t3 = base + byte_offset — effective address
lw a0, 0(t3) # register indirect — চূড়ান্ত readছয়টা instruction, একটামাত্র addressing mode (base+displacement,
offset=0-এ) শেষে ব্যবহৃত হচ্ছে — বাকি সব কাজ effective address
তৈরিতে। তুলনায় x86-64-এ এই একই access প্রায়ই একটা মাত্র
mov eax, [rdi + rax*4]-জাতীয় instruction-এ ধরে (যদি row-width
২, ৪, বা ৮-এর গুণিতক হয়, scale ফিল্ডে সরাসরি বসে যায়; না হলে
multiply আলাদা করে করতে হয়, x86-ও তখন RISC-V-র মতোই কয়েক instruction-এ
ভাগ হয়ে যায় — indexed addressing-এর সীমা শুধু scale 1,2,4,8-এই,
কোনো arbitrary multiplier সরাসরি সমর্থিত না)।
নিজে চালিয়ে দেখুন
Compiler Explorer-এ struct field access আর array indexing পাশাপাশি
১. godbolt.org-এ RISC-V target (riscv32-unknown-elf-gcc, -O1)
বেছে নিন।
2. এই কোডটা কম্পাইল করুন:
struct Point { int x; int y; int z; };
int get_z(struct Point *p) { return p->z; }- Emitted assembly-তে
lwinstruction-এর offset লক্ষ্য করুন — এটা8হওয়া উচিত (xoffset0,yoffset4,zoffset8— প্রতিটাint৪ byte)। Level ১-এরoffsetofexperiment-এর সাথে তুলনা করুন — একই সংখ্যা। - এবার একই ফাইলে array indexing যোগ করুন:
int arr[20];
int get(int i) { return arr[i]; }- Emitted assembly-তে
slli/sll(shift,×4করতে) আর একটাaddinstruction খুঁজুন — এটাই এই লেসনের “example” অংশের হাতে-করা ট্রেসের compiler-জেনারেটেড সংস্করণ। Offset compile-time ধ্রুবক না হওয়ায় (কারণiএকটা ভেরিয়েবল), কম্পাইলার কেন runtime-এ shift+add করতে বাধ্য হলো তা লক্ষ্য করুন। - (ঐচ্ছিক) একই দুইটা ফাংশন x86-64 target-এ কম্পাইল করে দেখুন —
array indexing-এ একটা মাত্র
mov eax, [rdi + rsi*4]-জাতীয় instruction দেখা উচিত।
Base+displacement addressing-এর offset compiler-এর গণনা করা struct layout-এর সাথে হুবহু মেলে, আর array indexing সত্যিই কয়েকটা instruction-এ ভেঙে যায় RISC-V-তে।
gdb দিয়ে স্ট্যাক ফ্রেমে স্থানীয় ভেরিয়েবলের প্রকৃত ঠিকানা দেখুন
// test.c
int main() {
int b = 7, c = 35;
int a = b + c;
return a;
}gcc -g -O0 test.c -o test
gdb ./test
(gdb) break main
(gdb) run
(gdb) next
(gdb) next
(gdb) print &b
(gdb) print &c
(gdb) print $sp
(gdb) print (void*)&b - (void*)$sp # b-এর offset $sp থেকে&b আর &c-এর ঠিকানা $sp/frame pointer থেকে একটা fixed distance-এ
থাকবে — ঠিক এই লেসনের base+displacement মডেল অনুযায়ী। disassemble main চালিয়ে সেই একই offset কোনো lw/mov [rbp-N]-জাতীয় instruction-এ
এনকোড হয়ে আছে কি না মিলিয়ে দেখুন।
base+displacement addressing শুধু তত্ত্ব না — একটা চলমান প্রোগ্রামে সত্যিই স্ট্যাক ফ্রেমের ভেতর নির্দিষ্ট offset-এ ভেরিয়েবল বসে থাকে, যা $fp/$sp দিয়ে হিসাব করে দেখা যায়।
নিজে বানান
একটা effective-address calculator — প্রতিটা mode-এর জন্য
- একটা ছোট register file (dict, register নাম → মান) আর একটা PC ভেরিয়েবল বানান
- immediate(value) ফাংশন লিখুন — শুধু মানটাই রিটার্ন করে, কোনো memory access নেই
- register_direct(regfile, reg) ফাংশন লিখুন — regfile[reg] রিটার্ন করে
- register_indirect(regfile, mem, reg) ফাংশন লিখুন — mem[regfile[reg]] রিটার্ন করে
- base_displacement(regfile, mem, base_reg, offset) ফাংশন লিখুন — mem[regfile[base_reg] + offset] রিটার্ন করে, negative offset সঠিকভাবে সামলে
- pc_relative(mem, pc, offset) ফাংশন লিখুন — effective address = pc + offset রিটার্ন করে (memory access তোলা থাকুক caller-এর জন্য)
- indexed(regfile, mem, base_reg, index_reg, scale) ফাংশন লিখুন — mem[regfile[base_reg] + regfile[index_reg]*scale] রিটার্ন করে
- এই লেসনের প্রতিটা উদাহরণ (lw t1,-8(s0) ইত্যাদি) দিয়ে প্রতিটা ফাংশন টেস্ট করুন
এই ছোট library-টা পরের লেসনের (fetch-decode-execute) EXECUTE ও MEMORY স্টেজের একটা প্রাথমিক সংস্করণ — সেখানে এই একই ফাংশনগুলো একটা সম্পূর্ণ fetch-decode-execute loop-এর ভেতরে বসানো হবে। এখানে আলাদা করে প্র্যাকটিস করাটাই সেই বড় ধাপের প্রস্তুতি।
একটা সূক্ষ্ম বিষয় — negative offset। base_displacement-এ
offset প্রায়ই negative হবে (s0-এর নিচে, স্ট্যাক frame-এ স্থানীয়
ভেরিয়েবল)। Python-এর dict/list index negative হ্যান্ডল করে ভিন্নভাবে
(শেষ থেকে গোনে) — নিশ্চিত করুন আপনার mem মডেল (হয়তো একটা বড় dict,
ঠিকানা → মান, list না) negative ঠিকানা মানে সত্যিই “কম ঠিকানা,”
Python-এর “শেষ থেকে” আচরণ না।
বাস্তব সিস্টেমে
Addressing mode বাস্তব সিস্টেমে
IBM System/360 (১৯৬৪) — base+displacement addressing-এর জন্ম।
এই লেসনের সবচেয়ে ব্যবহৃত mode কোনো RISC-আন্দোলনের আবিষ্কার না —
System/360-এই এর মূল কারণ ছিল সম্পূর্ণ ভিন্ন: absolute ঠিকানা
instruction-এ সরাসরি বসালে প্রোগ্রামকে memory-র একটা নির্দিষ্ট
জায়গায় লোড করতেই হতো (কোনো relocation ছাড়া)। IBM-এর ইঞ্জিনিয়াররা
তাই প্রতিটা মেমোরি-রেফারেন্সকে একটা “base register + displacement”
জোড়ায় ভাঙলেন — প্রোগ্রাম শুরুতে যেকোনো base register-এ তার আসল
লোড-ঠিকানা বসিয়ে দিলেই, বাকি সব ঠিকানা স্বয়ংক্রিয়ভাবে সঠিক হয়ে
যেত। ৬০ বছর পর, RISC-V-এর lw t1, -8(s0)-এও ঠিক একই মূলনীতি কাজ
করছে — শুধু কারণটা এখন আংশিকভাবে ভিন্ন (relocation-এর বদলে
স্ট্যাক-ফ্রেম/struct layout), কিন্তু গাণিতিক কাঠামো অভিন্ন।
প্রতিটা function prologue/epilogue। যেকোনো compiled প্রোগ্রামের
প্রতিটা ফাংশনের শুরুতে stack frame সেটআপ হয়, আর সেই frame-এর প্রতিটা
local variable base+displacement দিয়ে access হয় — এই লেসনের -8(s0)
প্যাটার্নটা literally পৃথিবীর প্রতিটা compiled C/C++/Rust/Go প্রোগ্রামে
কোটি কোটি বার ঘটে।
ARM-এর pre/post-increment addressing — memcpy-জাতীয় loop-এ। ARM
(RISC-V-এর চেয়ে বেশি addressing mode সমর্থন করে) একটা বিশেষ mode
রাখে যেখানে load/store-এর সাথেই base register auto-increment/decrement
হয় (ldr r0, [r1], #4 — পড়ার পরে r1 += 4)। এটা array/buffer-এর
মধ্য দিয়ে loop করার সময় একটা আলাদা add instruction বাঁচায় — একটা
মাঝামাঝি সিদ্ধান্ত, pure RISC-V-এর চেয়ে একটু বেশি addressing জটিলতা,
কিন্তু VAX-এর ধারে-কাছেও না।
Position-independent code ও ASLR। আধুনিক Linux/Windows প্রতিটা
প্রোগ্রাম রান করার সময় randomize করে দেয় কোথায় কোড/ডেটা লোড হবে
(security — buffer-overflow আক্রমণে ঠিকানা অনুমান করা কঠিন করতে)।
এটা সম্ভবই হতো না যদি কম্পাইলার absolute ঠিকানা hardcode করত —
PC-relative addressing (এই লেসনের auipc উদাহরণ) এটাকে সম্ভব করে।
Shared library (.so/.dll)। একই shared library-র code page
একাধিক প্রসেসের মধ্যে physical memory-তে শেয়ার হয় (efficiency)।
এটা কাজ করে কারণ কোড নিজে কোনো absolute ঠিকানার উপর নির্ভর করে না —
সব PC-relative। ভিন্ন প্রসেসে library ভিন্ন virtual address-এ ম্যাপ
হলেও, একই code bytes সঠিকভাবে কাজ করে।
Compiler optimization — addressing mode selection। GCC/LLVM backend-এর একটা কাজ হলো, target ISA যেসব addressing mode সমর্থন করে তার মধ্যে সবচেয়ে সস্তাটা বাছা — x86-64-এ যদি একটা array access একটা instruction-এ (indexed mode) ফিট করে, তাই ব্যবহার করবে; RISC-V-এ বাধ্য হয়ে shift+add এ ভেঙে দেবে। এই লেসনের উদাহরণগুলো ঠিক এই decision-এরই ফলাফল।
Database/interpreter-এর bytecode-এও একই ধারণা। অনেক bytecode VM (Python-এর CPython, JVM)-এর নিজস্ব “addressing mode”-সদৃশ ধারণা আছে — local variable access (register direct-সদৃশ, একটা index দিয়ে), attribute access (base+displacement-সদৃশ, object header + offset)। এই লেসনের ধারণাগুলো শুধু hardware ISA-তে সীমাবদ্ধ না — যেকোনো instruction-চালিত সিস্টেমে একই প্রশ্ন ফিরে আসে: operand-এর মান কোথায়, আর কীভাবে খুঁজে বের করব।
যে ভুলগুলো সবাই করে
“Register indirect আর base+displacement আসলে একই জিনিস, আলাদা করে শেখার দরকার নেই।”
কাছাকাছি কিন্তু অভিন্ন না। Register indirect-এ (offset=0 বিশেষ কেস
ধরলেও) ধারণাগতভাবে কোনো compile-time constant offset নেই — শুধু
register-এর মানই ঠিকানা। Base+displacement-এ offset একটা compile-time
জানা ধ্রুবক, instruction-এ এনকোড করা, যেটা compiler নির্ধারণ করে
(struct field-এর অবস্থান, স্ট্যাক frame-এর অবস্থান)। ব্যবহারিক
পার্থক্য বড়— register indirect দিয়ে runtime-এ যেকোনো গণনা-করা
ঠিকানায় যাওয়া যায়, base+displacement শুধু একটা fixed, compile-time-নির্ধারিত
দূরত্বে।
“PC-relative addressing শুধু branch/jump-এর জন্য, ডেটা access-এ এর কোনো ভূমিকা নেই।”
এই লেসনের auipc উদাহরণ ঠিক এর বিপরীত দেখায় — PC-relative addressing
global variable/function-এর ঠিকানা বের করতেও ব্যবহৃত হয়, বিশেষত
position-independent code-এ। বাস্তবে আধুনিক কম্পাইল-করা প্রোগ্রামে
এই ব্যবহারটা branch-এর চেয়ে কম দৃশ্যমান না — প্রতিটা global variable
access-ই সম্ভবত একটা auipc-জাতীয় instruction দিয়ে শুরু হয়।
“RISC-V-তে কম addressing mode মানে RISC-V array/struct-এর মতো জটিল ডেটা access করতে পারে না।”
ক্ষমতার দিক থেকে RV32I ঠিক ততটাই সক্ষম — এই লেসনের “example” অংশের
arr[i] উদাহরণ প্রমাণ করে সেটা। পার্থক্য শুধু কোথায় জটিলতা থাকে
— x86-এ hardware-এর একটা addressing-mode circuit-এ (SIB byte decode),
RISC-V-এ কম্পাইলারের কোড-জেনারেশন সিদ্ধান্তে (কয়েকটা instruction
জোড়া লাগানো)। ফলাফল একই ঠিকানায় পৌঁছায়, শুধু instruction সংখ্যা
আলাদা।
“যেহেতু effective address গণনা 'শুধু একটা যোগ', এটা তুচ্ছ, hardware-এ আলাদা মনোযোগ দরকার নেই।”
প্রতিটা load/store instruction-এ এই “শুধু একটা যোগ” আসলে ALU-র একটা
পূর্ণ ব্যবহার — Level ২-র datapath-and-control-unit লেসনের multi-cycle
ALU reuse টেবিল মনে করুন, যেখানে memory-reference address গণনা ALU-র
একটা প্রধান ব্যবহার হিসেবে তালিকাভুক্ত ছিল। পরের লেসনে দেখা যাবে এই
“তুচ্ছ” যোগটাই প্রতিটা lw/sw-এর EXECUTE স্টেজের পুরো কাজ — hardware
বাজেটে এটা একেবারেই তুচ্ছ না, বরং একটা পুরো pipeline stage-এর সমান
গুরুত্বপূর্ণ।
বুঝেছেন কি না দেখুন
1lui t0, 0x10 — এই instruction-এ কোন addressing mode ব্যবহৃত হচ্ছে, আর কোনো memory access ঘটে কি?স্মরণ
Immediate addressing। 0x10 মানটা সরাসরি instruction-এর
imm[31:12] ফিল্ডে বসানো (গত লেসনের U-type ফরম্যাট), কোনো register
বা ঠিকানার মধ্য দিয়ে যাওয়ার দরকার নেই। কোনো memory access ঘটে না
— এটা শুধু t0 ← 0x10 × 4096 গণনা, সম্পূর্ণভাবে instruction-এর নিজের
বিট থেকে।
2একটা struct-এ struct Node { int val; struct Node *next; }; (৬৪-বিট সিস্টেমে, pointer ৮ byte, int ৪ byte + ৪ byte padding ধরুন)। node->next access করতে কোন addressing mode, কত offset লাগবে?প্রয়োগ
Base+displacement addressing। val offset 0-এ (৪ byte), তারপর
alignment-এর জন্য ৪ byte padding (কারণ pointer-কে ৮-byte boundary-তে
বসতে হবে), তাই next শুরু হয় offset 8-এ।
ld t0, 8(a0) # (RV64-তে ld — 64-bit load; a0 = node pointer)8 সংখ্যাটা ঠিক Level ১-এর offsetof(struct Node, next) যা রিটার্ন
করত — এই লেসনের কেন্দ্রীয় দাবির আরেকটা concrete উদাহরণ: struct
layout সরাসরি addressing mode-এর displacement constant হয়ে ওঠে।
3ধরুন কেউ RV32I-তে “double register indirect” (Memory[Memory[reg]] — একটা ঠিকানা থেকে পড়া মান নিজেও আরেকটা ঠিকানা, সেখান থেকে আবার পড়া) addressing mode যোগ করার প্রস্তাব দিলো। এটা pipelining-এর দৃষ্টিকোণ থেকে কেন সমস্যাজনক হবে?যুক্তি
কারণ এই mode-এ একটা instruction-এর মধ্যেই দুইটা sequential memory access লাগবে — প্রথম access-এর ফলাফল ছাড়া দ্বিতীয় access শুরুই করা যাবে না (data dependency নিজের ভেতরে)। এর মানে এই একটা instruction-এর latency অন্য সব memory instruction-এর দ্বিগুণ (দুইটা memory access, সিরিয়ালি) — একটা fixed-length pipeline-এ (যেখানে প্রতিটা instruction নির্দিষ্ট সংখ্যক stage/cycle-এ MEM স্টেজ পার হওয়ার কথা) এই instruction-টা হয় pipeline-কে স্টল করবে, নয় সম্পূর্ণ আলাদা, দীর্ঘ একটা পথ দাবি করবে। এটাই VAX-এর “displacement deferred” mode-এর সমস্যা ছিল — Patterson-Ditzel-এর ১৯৮০-এর সমালোচনার একটা মূল উদাহরণ।
4নিচের প্রতিটা instruction-এর addressing mode চিহ্নিত করুন: (ক) andi t0, t1, 0xFF (খ) jal ra, offset (গ) sw t2, 12(sp)প্রয়োগ
(ক) Immediate — 0xFF সরাসরি instruction-এ, কোনো memory access
নেই, register direct-ও আছে (t1 পড়া হচ্ছে) কিন্তু constant অংশটা
immediate।
(খ) PC-relative — offset, PC-এর সাপেক্ষে; effective address
= PC + offset, নতুন instruction fetch-এর ঠিকানা হয়ে যায়। কোনো
data-memory access নেই।
(গ) Base + displacement — sp + 12 effective address, t2-এর
মান সেখানে লেখা হয় (store, তাই memory write)।
5ARM-এর pre/post-increment addressing (ldr r0, [r1], #4 — load করার সাথে সাথে r1 += 4) RV32I-তে নেই। এই একই effect RV32I-তে কয়টা instruction দিয়ে, কীভাবে পাবেন?ডিজাইন
দুইটা আলাদা instruction — একটা base+displacement load, একটা immediate-add:
lw t0, 0(t1) # t0 ← Memory[t1] (ARM-এর মূল load)
addi t1, t1, 4 # t1 ← t1 + 4 (auto-increment, আলাদা করে)এটাই এই লেসনের কেন্দ্রীয় থিমের আরেকটা উদাহরণ — RISC-V একটা compound addressing mode-কে দুইটা সরল, independent instruction-এ ভেঙে দেয়। বিনিময়ে code এক instruction বেশি লম্বা, কিন্তু hardware-এ কোনো বিশেষ “একই instruction-এ দুইটা register বদলানো” mechanism লাগে না — প্রতিটা instruction ঠিক একটা register-ই লেখে, uniform থাকে।
6গত লেসনে হাতে-এনকোড করা beq t1, t2, 8 instruction ঠিকানা 0x2000-এ বসে আছে। যদি t1 == t2 হয়, effective address কত, আর সেই EA দিয়ে কি কোনো data memory access ঘটবে?প্রয়োগ
EA = PC + offset = 0x2000 + 8 = 0x2008। এটা PC-relative addressing
— কোনো data memory access ঘটবে না, কারণ এই EA নিজেই পরের
instruction fetch-এর ঠিকানা হয়ে যাবে (PC ← 0x2008)। এটা register
indirect বা base+displacement-এর মতো “EA-তে গিয়ে একটা মান পড়ো”
না — এখানে EA নিজেই গন্তব্য, আরেকটা fetch শুরু করার জন্য।
এরপর কী
এতক্ষণে আমাদের হাতে দুইটা টুকরো আছে — instruction-এর বিট কোথায় কী মানে (গত লেসন), আর সেই বিট থেকে অপারেন্ডের প্রকৃত মান কীভাবে বের হয় (এই লেসন)। এখন এই দুইটা টুকরো, আর Level ২-এর সম্পূর্ণ datapath+control unit, একসাথে জোড়া লাগানোর সময় — এই মডিউলের driving question-এর চূড়ান্ত উত্তর।
পরের লেসনে আমরা a = b + c সম্পূর্ণভাবে ট্রেস করব — C সোর্স থেকে
assembly, assembly থেকে machine code (এই দুই লেসনে শেখা নিয়মেই),
আর তারপর সেই bits কীভাবে FETCH → DECODE → EXECUTE → MEMORY →
WRITEBACK — এই পাঁচটা ধাপ পার হয়ে সিলিকনে বাস্তবায়িত হয়, cycle
বাই cycle। Level ২-র শেষ লেসনের toy CPU-টাই সেখানে ফিরে আসবে —
এবার একটা toy ৩-instruction ISA না, একটা প্রকৃত RV32I প্রোগ্রাম
চালিয়ে।
আরও পড়ুন
- The RISC-V Instruction Set Manual, Volume I — Chapter 2.6, Load and Store Instructions — RISC-V International · RV32I-এর একমাত্র সমর্থিত addressing mode — base+displacement — এর প্রামাণ্য বিবরণ
- The Case for the Reduced Instruction Set Computer — David A. Patterson, David R. Ditzel · ১৯৮০-এর মূল RISC ম্যানিফেস্টো — VAX-এর জটিল addressing mode-কে সরাসরি সমালোচনা করে RISC-এর যুক্তি প্রতিষ্ঠা করে
- Computer Organization and Design (RISC-V Edition), Chapter 2 — Addressing for 32-bit Immediates — David A. Patterson, John L. Hennessy · base+displacement addressing কীভাবে array/struct access-এ ব্যবহৃত হয় তার টেক্সটবুক আলোচনা
- Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 1 — Section 3.7, Addressing Modes — Intel Corporation · x86-এর SIB byte-ভিত্তিক base+index*scale+displacement এনকোডিং-এর প্রামাণ্য উৎস