Register, Program Counter, আর Flags — CPU-র নিজের 'ভেরিয়েবল'
Registers, Program Counter, and Flags
Level 2-এর register file হার্ডওয়্যারটাই এখন ISA-স্তরে ফিরে আসছে — কতগুলো general-purpose register, প্রতিটা কত চওড়া, আর দুইটা বিশেষ-উদ্দেশ্য register (PC, SP) কীভাবে fetch-decode-execute আর function call সম্ভব করে। সাথে flags register — Level 2-র subtractor/comparator লেসনের ZF/CF/OF/SF সার্কিট এবার conditional branch-এর ভিত্তি হিসেবে কাজে লাগবে, আর RISC-V-র flags-বিহীন design দেখাবে এটাও একটা choice, বাধ্যতা না।
আগে এটা বুঝি
গত দুই লেসনে বারবার একটা নাম এসেছে যেটা আমরা কখনো পুরোপুরি সংজ্ঞায়িত করিনি — register। toy ISA-র Rd, Rs, Rt, RISC-V-র x0-x31, x86-64-র rax-r15। আমরা জানি register মানে কী হার্ডওয়্যার হিসেবে — Level 2-র registers-and-register-files লেসনে নিজে বানিয়েছি: কয়েকটা flip-flop সমান্তরালে, একটা common clock শেয়ার করে, write-enable দিয়ে নিয়ন্ত্রিত। আর register file মানে একগুচ্ছ register, একটা decoder দিয়ে address করা, read/write port-সহ।
কিন্তু একটা বাস্তব ISA-তে ঠিক কতগুলো এমন register থাকে? কেন সেই নির্দিষ্ট সংখ্যা — ৮ না, ১৬ না, ৩২? আর সব register কি সমান? গত লেসনের RISC-V লুপ উদাহরণে a0, a1, a2, t0 — এই নামগুলো কি নিছক নাম, নাকি প্রতিটার আলাদা ভূমিকা আছে? আর সবচেয়ে গুরুত্বপূর্ণ প্রশ্ন — একটা CPU কীভাবে জানে পরবর্তী কোন instruction execute করতে হবে? কোনো instruction-এ কি “পরের instruction” বলে কিছু লেখা থাকে?
এই লেসনে আমরা তিনটা নির্দিষ্ট জিনিস দেখব যেগুলো প্রতিটা বাস্তব ISA-র register model-এর কেন্দ্রে থাকে — সাধারণ general-purpose register, বিশেষ-উদ্দেশ্য Program Counter (PC) যেটা “পরের instruction কোথায়” প্রশ্নের উত্তর ধরে রাখে, stack pointer (SP) যেটা function call-এর জন্য memory-র একটা নির্দিষ্ট অংশ manage করে, আর flags/status register যেটা গত মডিউলের subtractor/comparator লেসনের ZF/CF/OF/SF সার্কিট থেকে সরাসরি আসে — এবার দেখব সেই bit-গুলো ঠিক কীভাবে conditional branch-এর সিদ্ধান্ত হয়ে ওঠে। শেষে RISC-V-র একটা চমকপ্রদ সিদ্ধান্ত দেখব — এই ISA-টার কোনো flags register-ই নেই, যা এই লেসনের নিজস্ব “twist”।
মূল ধারণা
General-purpose register — সংখ্যা আর width বাস্তব ISA-তে
Level 2-এর register-file লেসনে “register file” শব্দটা এসেছিল বিমূর্তভাবে — কতগুলো register থাকবে সেটা তখন প্রশ্ন ছিল না, শুধু কীভাবে read/write port দিয়ে একটা register-ব্যাংক বানানো যায় সেটাই ছিল বিষয়। এখন সেই সংখ্যাটাই কেন্দ্রীয় প্রশ্ন:
| ISA | General-purpose register সংখ্যা | Width | নামকরণ |
|---|---|---|---|
| x86-64 | ১৬টা | ৬৪ bit | rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, r8–r15 |
| ARM64 (AArch64) | ৩১টা + xzr (সবসময়-শূন্য) | ৬৪ bit | x0–x30, স্বতন্ত্র sp |
| RISC-V (RV64) | ৩২টা, যার মধ্যে x0 হার্ডওয়্যার্ড শূন্য | ৬৪ bit | x0–x31 (বা ABI নাম zero, ra, sp, gp, tp, t0-t6, s0-s11, a0-a7) |
তিনটা সংখ্যাই ৩২-এর কাছাকাছি বা কম — এটা কাকতালীয় না। গত লেসনের bit-encoding diagram-এ দেখেছেন একটা register-field সাধারণত ৫ বিট (2^5 = 32টা encoding), কারণ এর চেয়ে বেশি register রাখলে প্রতিটা instruction-এ register-field আরও চওড়া করতে হবে, যা fixed ৩২-বিট (বা x86-64-র ক্ষেত্রে কম্প্যাক্ট byte) instruction budget-এ চাপ ফেলে। এইজন্যই ৩২ একটা “স্বাভাবিক” সংখ্যা হয়ে উঠেছে — RISC-V, MIPS, বেশিরভাগ আধুনিক RISC ISA-ই এই সংখ্যায় মিলিত হয়েছে, যদিও একে অপরের থেকে স্বাধীনভাবে ডিজাইন করা।
x86-64-এ ১৬টা কেন এত কম মনে হয়? ইতিহাসের ছাপ — মূল ৮০৮৬ (১৯৭৮)-এ মাত্র ৮টা ১৬-বিট register ছিল (ax, bx, cx, dx, si, di, bp, sp), প্রতিটার একটা নির্দিষ্ট ঐতিহাসিক “বিশেষত্ব”ও ছিল (যেমন cx loop-counter হিসেবে ব্যবহারের রেওয়াজ, ax-এ multiplication/division-এর ফলাফল স্বয়ংক্রিয়ভাবে যাওয়া)। x86-64 (২০০৩) এই ৮টাকে ৬৪-বিটে বাড়িয়ে (eax→rax) আরও ৮টা নতুন (r8-r15) যোগ করে ১৬-এ পৌঁছেছে — কিন্তু backward compatibility-র কারণে পুরোনো ৮টার ঐতিহাসিক নাম আর আংশিক-বিশেষত্ব রয়ে গেছে। এটা গত লেসনের x86-64-র CISC heritage-এর আরেকটা প্রকাশ — নতুন কিছু ডিজাইন করার বদলে পুরোনো কাঠামোর উপর স্তরে স্তরে যোগ করা হয়েছে।
Program Counter (PC) — “পরের instruction কোথায়” প্রশ্নের উত্তর
Program Counter (কিছু ISA-তে “Instruction Pointer”, x86-64-এ rip) একটা বিশেষ-উদ্দেশ্য register যেটা সবসময় পরবর্তী execute হতে যাওয়া instruction-এর memory address ধরে রাখে। এটা হার্ডওয়্যার হিসেবে ঠিক অন্য যেকোনো register-এর মতোই — কয়েকটা flip-flop, write-enable, সব Level 2-র সেই একই building block দিয়ে বানানো। পার্থক্যটা হার্ডওয়্যারে না, ব্যবহারে: PC-কে সাধারণ arithmetic instruction সরাসরি target করে না (ADD rd, rs, rt-এ PC কখনো rd হয় না), কিন্তু control-unit প্রতিটা fetch cycle-এ স্বয়ংক্রিয়ভাবে এটা পড়ে ও আপডেট করে।
সবচেয়ে সরল নিয়ম — sequential বৃদ্ধি। যদি কোনো branch/jump না ঘটে, প্রতিটা fetch-এর পর PC instruction-এর দৈর্ঘ্য পরিমাণ বাড়ে:
- Fixed-length ISA-তে (RISC-V, ARM64): প্রতিটা fetch-এ
PC ← PC + 4— নিঃশর্তভাবে, কারণ প্রতিটা instruction ঠিক ৪ byte (গত লেসনের experiment-এ ঠিক এই প্যাটার্নই দেখেছেন — ঠিকানা সবসময় ৪ করে বাড়ে)। - Variable-length ISA-তে (x86-64):
PC ← PC + (এই instruction-টার আসল byte-দৈর্ঘ্য)— যেটা instruction-ভেদে ১ থেকে ১৫ পর্যন্ত হতে পারে, তাই CPU-কে আগে decode করেই জানতে হয় ঠিক কত বাড়াতে হবে (গত লেসনের length-decode সমস্যার সরাসরি ফলাফল — PC আপডেট করতেও decode আগে লাগে)।
Branch/jump/call-এ ব্যতিক্রম। control-flow instruction (গত লেসন১-এর চার শ্রেণির তৃতীয়টা) এই স্বাভাবিক sequential বৃদ্ধি ভেঙে PC-তে একটা নতুন, ভিন্ন address বসিয়ে দেয় — condition সত্য হলে (conditional branch) বা নিঃশর্তভাবে (unconditional jump)। এটাই if/while/for-এর হার্ডওয়্যার ভিত্তি — একটা loop আসলে PC-কে বারবার একই আগের address-এ ফিরিয়ে নেওয়া।
- PC বর্তমান মান ধরে রাখেযেমন 0x1000 — পরবর্তী instruction-এর ঠিকানা
- Fetch — Mem[PC] থেকে instruction পড়া হয়instruction bit-প্যাটার্ন CPU-তে আসে, PC নিজে এখনও অপরিবর্তিত
- Decode — instruction-টা কী, তার দৈর্ঘ্য কতsequential না branch তা নির্ধারিত হয়
- সাধারণ instruction হলে: PC ← PC + lengthপরের instruction ঠিক পরের ঠিকানায়
- Taken branch/jump হলে: PC ← target addresstarget ভিন্ন কোথাও — instruction-এর ভেতরে থাকা offset বা register থেকে গণনা করা
- পরবর্তী চক্র — নতুন PC মান দিয়ে আবার fetchfetch-decode-execute cycle-এর পরবর্তী লেসনে সম্পূর্ণ বিস্তারিত
Stack pointer (SP) — call stack-এর “top” নির্দেশক
Stack pointer আরেকটা বিশেষ-উদ্দেশ্য register, যেটা memory-র একটা নির্দিষ্ট, ক্রমাগত-বদলানো অঞ্চলের ঠিকানা ধরে রাখে — call stack-এর “top” (সবচেয়ে সাম্প্রতিক যোগ করা উপাদানের ঠিকানা)। প্রতিটা function call-এ local variable, return address, saved register রাখার জন্য এই stack ব্যবহৃত হয়।
নিয়ম — বেশিরভাগ ISA-তে stack নিচের দিকে বাড়ে। PUSH করলে SP কমে (memory-র নিচের দিকে চলে যায়), POP করলে SP বাড়ে। এটা প্রথম শোনায় উল্টো মনে হতে পারে, কিন্তু ঐতিহাসিক কারণ আছে — memory-র নিচের অংশ (address ০-এর কাছাকাছি) সাধারণত program code/data-র জন্য বরাদ্দ, উপরের অংশ stack-এর জন্য, দুইটা বিপরীত দিক থেকে বাড়লে মাঝখানে যতটা সম্ভব জায়গা ফাঁকা থাকে (heap-ও প্রায়ই নিচ থেকে উপরে বাড়ে, বিপরীত দিকে, দুটো মাঝখানে “মিলিত” না হওয়া পর্যন্ত)।
Memory ঠিকানা (উচ্চ থেকে নিম্ন)
0x7fff_ffff ┌─────────────────┐ ← stack-এর শুরু (উচ্চ ঠিকানা)
│ (আগের frame) │
├─────────────────┤
│ return address │
│ saved registers │ ← function call-এ push হওয়া
│ local variables │
0x7fff_fe00 ├─────────────────┤ ← SP এখানে (current top)
│ (এখনো অব্যবহৃত) │
└─────────────────┘
↓ PUSH করলে SP আরও কমে (নিচে নামে)Flags/status register — Level 2-র circuit-এর সরাসরি ফিরে আসা
গত মডিউলের digital-logic/combinational-subtractors-comparators লেসনে চারটা ALU status flag derive করেছিলেন সরাসরি circuit থেকে — মনে করিয়ে দেওয়া যাক:
| Flag | সংক্ষেপ | কী নির্দেশ করে |
|---|---|---|
| Zero flag | ZF | ফলাফল ঠিক শূন্য কি না |
| Carry flag | CF | unsigned arithmetic-এ overflow/borrow হয়েছে কি না |
| Overflow flag | OF | signed arithmetic-এ overflow হয়েছে কি না (Cₙ₋₁ ⊕ Cₙ) |
| Negative/Sign flag | SF (x86), N (ARM) | ফলাফলের sign bit — raw ঋণাত্মক দেখাচ্ছে কি না |
সেই লেসনে এই flag-গুলো ছিল নিছক circuit output — কোনো ব্যবহারিক প্রসঙ্গ ছাড়া। এখন সেই প্রসঙ্গটা আসছে। x86-64-এ এই bit-গুলো একটা একক register-এ (RFLAGS) জমা থাকে, প্রতিটা ADD/SUB/CMP-এর পর হালনাগাদ হয়, আর conditional jump instruction (JE, JL, JG, …) ঠিক এই bit-গুলো পড়ে সিদ্ধান্ত নেয় branch নেবে কি না।
সবচেয়ে সরাসরি callback — সেই লেসনের signed comparison নিয়মটা মনে করুন: A \< B (signed) ⟺ SF ⊕ OF = 1। এটা কোনো তাত্ত্বিক সূত্র ছিল না — এটাই আক্ষরিক অর্থে x86-64-র JL (jump if less, signed) instruction-এর hardware semantics। যখন compiler if (a \< b) কম্পাইল করে x86-64-এ, সে প্রথমে CMP a, b emit করে (যেটা ভেতরে subtractor circuit চালিয়ে ZF/CF/SF/OF সেট করে, ফলাফলটা ফেলে দেয়), তারপর JL emit করে — যেটা hardware-এ ঠিক SF ⊕ OF চেক করে। আপনি যে circuit derive করেছিলেন, সেটাই আজ প্রতিটা if statement-এর নিচে চলছে।
ভেতরে কী ঘটছে
PC কীভাবে datapath-এর সাথে জোড়া লাগে
গত মডিউলের datapath-and-control-unit লেসনে datapath মানে ছিল register file + ALU + memory + MUX, একটা bus system দিয়ে জোড়া। PC ঠিক এই একই datapath-এর একটা অতিরিক্ত register — কিন্তু তার input একটু বিশেষ। বেশিরভাগ ISA-তে PC-র পরবর্তী মান একটা ছোট MUX দিয়ে বাছাই হয় তিনটা সম্ভাব্য উৎস থেকে:
┌─────────────────────┐
PC+4 ──▶│ │
│ MUX (control unit │──▶ PC (পরবর্তী মান)
target ▶│ সিদ্ধান্ত নেয়) │
│ │
রেজিস্টার│ │
থেকে ▶│ │
(ret) └─────────────────────┘
কোনটা বাছাই হবে তা নির্ভর করে decode-এর ফলাফলের উপর:
- সাধারণ instruction → PC+4 (বা PC + variable length, x86-64-এ)
- taken conditional branch → target (instruction-এর ভেতরের offset থেকে গণনা করা)
- unconditional jump/call → target
- return (RET) → register/stack থেকে পড়া address (ফাংশন যেখান থেকে ডাকা হয়েছিল)এই সিদ্ধান্তটা (কোন উৎস বাছাই হবে) নেয় control unit, ঠিক flags পড়ে (conditional branch-এর জন্য) আর opcode পড়ে (কোন ধরনের instruction তা জানতে) — datapath-and-control-unit লেসনের সেই একই FSM-এর কাজ, শুধু এখন একটা নতুন input (flags) আর একটা নতুন output (PC MUX select) যোগ হয়েছে।
কেন RISC-V-এ কোনো flags register নেই — একটা সচেতন design সিদ্ধান্ত
এতক্ষণ x86-64/ARM64-র flags model বর্ণনা করার পর একটা গুরুত্বপূর্ণ ব্যতিক্রম বলা দরকার: RISC-V-এ কোনো condition-code/flags register-ই নেই। এটা কোনো অসম্পূর্ণতা না — সচেতন ডিজাইন সিদ্ধান্ত, আর এর পেছনের যুক্তিটা RISC দর্শনের সাথেই সামঞ্জস্যপূর্ণ।
RISC-V-এ conditional branch instruction (BEQ, BNE, BLT, BGE, BLTU, BGEU) সরাসরি দুইটা register তুলনা করে branch নেয়, কোনো আলাদা compare-instruction বা flags-write লাগে না:
x86-64 (দুই instruction, flags-এর মাধ্যমে) RISC-V (এক instruction, সরাসরি)
cmp rax, rbx blt a0, a1, target
jl target ; SF⊕OF পড়ে সিদ্ধান্ত ; a0 \< a1 হলে সরাসরি জাম্পকেন এই সিদ্ধান্ত? flags register একটা লুকানো, implicit dependency তৈরি করে — একটা CMP instruction আর তার পরের JL instruction-এর মধ্যে একটা অদৃশ্য সংযোগ থাকে (flags register-এর মাধ্যমে) যা instruction-এর সাধারণ operand list-এ দেখা যায় না। Out-of-order execution engine-এর জন্য (পরবর্তী মডিউল লেসনে বিস্তারিত) এই লুকানো dependency ট্র্যাক করা, আর flags register-কে register renaming-এর আওতায় আনা, বাড়তি জটিলতা যোগ করে — flags register নিজেই একটা “hazard”-এর উৎস হতে পারে যদি দুইটা পরপর instruction একই flags লিখতে চায় কিন্তু ভিন্ন ক্রমে execute হয়। RISC-V এই পুরো সমস্যাশ্রেণিটাই এড়িয়ে গেছে flags বাদ দিয়ে — compare-and-branch একটা instruction, তার সব dependency স্পষ্টভাবে ঘোষিত operand-এর মধ্যে, কোনো লুকানো state নেই। এটা ঠিক আগের লেসনের কেন্দ্রীয় থিমেরই আরেকটা উদাহরণ — RISC hardware-এর জটিলতা (এখানে: hazard tracking) কমাতে ISA-স্তরে একটু ভিন্ন approach বেছে নেয়।
ARM64 অবশ্য flags (NZCV — Negative, Zero, Carry, oVerflow) রেখে দিয়েছে, কিন্তু একটা compromise করেছে — সাধারণ arithmetic instruction (ADD, SUB) ডিফল্টে flags বদলায় না; শুধু বিশেষ “S” suffix-সহ ভ্যারিয়েন্ট (ADDS, SUBS) flags সেট করে। এর মানে compiler প্রয়োজন না হলে flags-write এড়িয়ে যেতে পারে, যা flags-নির্ভর hazard কিছুটা কমায় — RISC-V-র “flags নাই” আর x86-64-র “সবসময় flags লেখা”-র মাঝামাঝি একটা অবস্থান।
উদাহরণ
একটা max ফাংশন — তিনটা ISA-তে register/PC/flags ব্যবহারের পার্থক্য দেখা
// max.c
int max(int a, int b) {
if (a > b) return a;
return b;
}x86-64 (-O2, System V ABI-তে a আসে edi-তে, b আসে esi-তে):
mov eax, edi ; eax = a (return value register-এ প্রস্তুত রাখা)
cmp edi, esi ; flags আপডেট: a - b
cmovle eax, esi ; SF≠OF অথবা ZF হলে (a <= b), eax = b — flags পড়ে সিদ্ধান্ত
ret ; PC ← [rsp] (return address stack থেকে), rsp বাড়েRISC-V (ABI নাম-সহ, a আসে a0, b আসে a1):
bge a1, a0, .use_b # a1 >= a0 হলে সরাসরি jump — flags লাগেনি, দুই register সরাসরি তুলনা
mv a0, a0 # a ইতিমধ্যে a0-এ, রিটার্ন-প্রস্তুত
ret # ra register-এ থাকা return address-এ jump — কোনো stack access লাগেনি!
.use_b:
mv a0, a1
retARM64 (ABI নাম-সহ, a আসে w0, b আসে w1):
cmp w0, w1 ; NZCV flags আপডেট
csel w0, w0, w1, gt ; greater হলে w0, নাহলে w1 — conditional select, flags পড়ে
ret ; x30 (link register)-এ থাকা return address-এ jumpতিনটাতেই তিনটা গুরুত্বপূর্ণ পার্থক্য স্পষ্ট: (১) x86-64/ARM64 flags ব্যবহার করে conditional সিদ্ধান্ত নেয় (cmovle/csel), RISC-V সরাসরি register তুলনা করে branch নেয় (bge) — আগের সেকশনের flags-বনাম-flags-বিহীন পার্থক্যের বাস্তব প্রতিফলন। (২) x86-64-র ret stack থেকে return address পড়ে ([rsp]), কিন্তু RISC-V/ARM64-র ret সরাসরি একটা register (ra/x30) থেকে পড়ে — কোনো memory access ছাড়াই, যদি ফাংশনটা “leaf function” হয় (কোনো আরেকটা ফাংশন কল করে না)। (৩) এই leaf-function optimization RISC ISA-গুলোতে বিশেষভাবে সস্তা কারণ তাদের একটা dedicated link register আছে (RISC-V-র ra, ARM64-র x30) — x86-64-র কোনো dedicated link register নেই, CALL instruction স্বয়ংক্রিয়ভাবে return address stack-এ push করে, RET স্বয়ংক্রিয়ভাবে pop করে, তাই memory access সবসময় লাগে।
Calling convention — hardware register-এর উপর একটা software চুক্তি
উপরের উদাহরণে register-গুলোর নির্দিষ্ট ভূমিকা লক্ষ্য করেছেন — a/b নির্দিষ্ট register-এই আসছে (edi/esi, a0/a1, w0/w1), return value নির্দিষ্ট register-এই যাচ্ছে (eax, a0, w0)। এটা hardware-এর নিয়ম না — এটা একটা software convention, যাকে বলে calling convention বা ABI (Application Binary Interface)-এর অংশ। ISA শুধু বলে দেয় ১৬টা/৩২টা general-purpose register আছে; কোনটা argument-এর জন্য, কোনটা return value-র জন্য — সেটা কোনো hardware circuit নির্ধারণ করে না, বরং compiler-লেখক আর OS-লেখকদের মধ্যে একটা সম্মত চুক্তি, যাতে ভিন্ন কম্পাইলারে কম্পাইল করা কোড একে অপরকে সঠিকভাবে call করতে পারে।
এই convention-এর একটা কেন্দ্রীয় ধারণা — কোন register কে বাঁচাবে:
| Caller-saved (volatile) | Callee-saved (non-volatile) | |
|---|---|---|
| মানে | Function call-এর পর এই register-এর মান বদলে যেতে পারে ধরে নিতে হবে | Called function-এর দায়িত্ব এই register-এর মান অক্ষুণ্ণ রাখা |
| কে বাঁচায় | Caller (যে call করছে), যদি call-এর পরও দরকার হয় সে নিজে আগে থেকে বাঁচিয়ে রাখে | Callee (যে called হচ্ছে), সে যদি ব্যবহার করে তবে ফেরার আগে পুরোনো মান ফিরিয়ে দেয় |
| x86-64 উদাহরণ | rax, rcx, rdx, rsi, rdi, r8–r11 | rbx, rbp, r12–r15 |
| RISC-V উদাহরণ | t0–t6 (temporary) | s0–s11 (saved) |
| ARM64 উদাহরণ | x0–x18 | x19–x28 |
এই বিভাজনটা কেন দরকার? একটা ফাংশন যদি কোনো মান একটা register-এ রাখে, তারপর আরেকটা ফাংশন call করে — সেই মানটা কি call-এর পরও নিরাপদ থাকবে? উত্তরটা নির্ভর করে register-টা caller-saved না callee-saved তার উপর। এই পুরো ব্যবস্থাটাই একটা সামাজিক চুক্তি, hardware কোনোভাবেই এটা যাচাই করে না — যদি একটা compiler bug থাকে যা এই convention ভাঙে, hardware কোনো error দেবে না, শুধু ভুল ফলাফল বা crash হবে। পুরো convention আর ABI-র বিস্তারিত নিয়ম (কোন argument কোথায়, red zone, stack alignment) assembly মডিউলে বিস্তারিত আসবে — এখানে শুধু ধারণাটা স্থাপন করা হলো: hardware register-এর সংখ্যা আর width নির্দিষ্ট করে, তাদের “ভূমিকা” নির্দিষ্ট করে সফটওয়্যার-কনভেনশন।
নিজে চালিয়ে দেখুন
GDB দিয়ে live PC, SP, আর flags পর্যবেক্ষণ করুন
একটা ছোট প্রোগ্রাম কম্পাইল করে debugger-এর ভেতর step-by-step চালিয়ে register-গুলো live দেখা যাক:
// max.c
int max(int a, int b) {
if (a > b) return a;
return b;
}
int main() {
int r = max(3, 7);
return r;
}gcc -O0 -g max.c -o max
gdb ./maxGDB-এর ভেতরে:
(gdb) break max
(gdb) run
(gdb) disassemble
(gdb) stepi
(gdb) info registers rip eflags
(gdb) stepi
(gdb) info registers rip eflagsযা লক্ষ্য করবেন:
১. প্রতিটা stepi-র পর rip (x86-64-র PC-র নাম) বদলায় — disassemble আউটপুটের সাথে মিলিয়ে দেখুন ঠিক ততটাই বেড়েছে যতটা সেই instruction-এর byte-length ছিল।
২. cmp instruction-এর পর eflags-এ ZF/SF/OF-এর মতো flag বদলে যাবে (GDB eflags আউটপুটে [ ZF SF ... ] আকারে symbolic নাম দেখায়)। কিন্তু mov বা push-এর মতো instruction-এর পর eflags অপরিবর্তিত থাকবে — শুধু arithmetic/compare instruction-ই flags লেখে।
৩. info registers rsp দিয়ে stack pointer দেখুন — call max execute হওয়ার ঠিক আগে-পরে rsp-র মান তুলনা করুন, দেখবেন ঠিক ৮ কমেছে (৬৪-বিট return address push হওয়ার ফলে)।
নিজে যাচাই করুন: যেই instruction-এ jle/jg-এর মতো একটা conditional jump আছে, তার ঠিক আগে eflags নোট করুন, তারপর stepi করে দেখুন PC কোথায় গেল — নিজের হাতে ব্যাখ্যা করুন কেন সেই দিকেই গেল (এই লেসনের SF⊕OF/ZF নিয়মের সাথে মিলিয়ে)।
PC প্রতিটা instruction-এর পর precisely ততটাই বাড়ে যতটা সেই instruction-এর byte-length, branch নিলে অসংলগ্নভাবে লাফায়; flags register শুধুমাত্র arithmetic/compare instruction-এর পরই বদলায়, বাকি সময় অপরিবর্তিত থাকে — উভয়ই এই লেসনের তাত্ত্বিক দাবির সরাসরি, hands-on প্রমাণ।
নিজে বানান
Toy ISA-তে PC, flags register, আর একটা conditional branch যোগ করুন
- গত লেসনের toy ISA স্পেক (ADD, SUB, LOAD, STORE) থেকে শুরু করুন, আর প্রথম লেসনের Q4-এর BEQ প্রস্তাবটা আবার বের করুন
- একটা register model section লিখুন যেখানে PC স্পষ্টভাবে একটা আলাদা, বিশেষ-উদ্দেশ্য register হিসেবে ঘোষিত (কত bit চওড়া — মেমরির আকারের সমান হতে হবে)
- একটা ২-বিট flags register ডিজাইন করুন — শুধু ZF আর একটা negative flag (SF) রাখুন, সরলতার জন্য
- ADD আর SUB-এর semantics আপডেট করুন যাতে তারা ZF আর SF আপডেট করে (ঠিক Level 2-এর subtractor/comparator লেসনের ZF সংজ্ঞা ব্যবহার করে — ফলাফল শূন্য হলে ZF=1)
- BEQ instruction-এর সম্পূর্ণ semantics লিখুন — এবার দুইভাবে: (ক) x86-64 স্টাইলে flags পড়ে, (খ) RISC-V স্টাইলে সরাসরি দুইটা register তুলনা করে — দুটোর মধ্যে কোনটা আপনার toy ISA-র জন্য বেশি যুক্তিসঙ্গত মনে হয় আর কেন তা এক অনুচ্ছেদে লিখুন
RISC-V-র creator-রা flags বাদ দেওয়ার সিদ্ধান্তটা নিয়েছিলেন hardware simplicity-র যুক্তিতে (hood সেকশনে দেখেছেন) — কিন্তু আপনার toy ISA-তে out-of-order execution নেই (single-cycle datapath), তাই সেই নির্দিষ্ট যুক্তিটা এখানে প্রযোজ্য নাও হতে পারে। এই ব্যায়ামের আসল লক্ষ্য: একটা design decision-এর পেছনের কারণটা বুঝে, সেই কারণ আপনার নিজের প্রসঙ্গে প্রযোজ্য কি না যাচাই করা — এটাই গত লেসনের শেষ প্রশ্নের (design question) ঠিক একই দক্ষতা, এবার flags-এর প্রসঙ্গে প্রয়োগ করা।
একটা সাহায্যকারী কাঠামো (flags-ভিত্তিক পথের জন্য):
── BEQ (flags-ভিত্তিক সংস্করণ) ─────────────────────
Encoding : opcode = ?? (নতুন opcode বরাদ্দ করুন)
Syntax : BEQ offset (তুলনা আগেই SUB দিয়ে হয়ে গেছে ধরে নিয়ে)
Semantics: if (ZF == 1) PC ← PC + offset
else PC ← PC + (instruction দৈর্ঘ্য)
Flags : কোনটাই বদলায় না (শুধু পড়ে)
Notes : ব্যবহারের আগে অবশ্যই একটা SUB Rd, Rs, Rt চালিয়ে ZF সেট করতে হবে —
এই নির্ভরতাটাই hood সেকশনে আলোচিত "লুকানো dependency" সমস্যানিজে বাড়ান: আপনার BEQ-এর দুইটা সংস্করণ (flags-ভিত্তিক আর register-তুলনা-ভিত্তিক) লিখে ফেলার পর, একটা ছোট প্রোগ্রাম (৩-৪টা instruction, একটা loop) দুইভাবেই লিখুন — কোন সংস্করণে মোট instruction সংখ্যা কম লাগল? এটাই ঠিক গত লেসনের RISC-V bnez বনাম x86-64 cmp+jl-এর তুলনার ক্ষুদ্র সংস্করণ, এবার নিজের হাতে করা।
বাস্তব সিস্টেমে
Register, PC, আর flags যেখানে বাস্তব সিদ্ধান্ত নির্ধারণ করে
ARM64-র link register (x30) — ফাংশন-কল-ভারী কোডে বাস্তব পারফরম্যান্স সুবিধা। যেহেতু return address সরাসরি একটা register-এ থাকে (stack-এ push করার প্রয়োজন ছাড়াই), leaf function (যেগুলো আর কিছু call করে না — বাস্তব কোডে বিস্ময়করভাবে সাধারণ) সম্পূর্ণ stack access ছাড়াই চলতে পারে। x86-64-এ প্রতিটা CALL/RET-এই stack access বাধ্যতামূলক। এই পার্থক্যটা ARM-এর power-efficiency গল্পের একটা ছোট কিন্তু বাস্তব অংশ।
Buffer overflow আর stack pointer — নিরাপত্তার একটা ক্লাসিক দুর্বলতা। যেহেতু SP আর return address একই stack-এ পাশাপাশি থাকে (বিশেষত x86-64-এ, যেখানে return address সরাসরি stack-এ), একটা প্রোগ্রামে buffer-এর সীমা ছাড়িয়ে লেখা (buffer overflow bug) return address-কেই ওভাররাইট করে দিতে পারে — আক্রমণকারী PC-কে যেকোনো ঠিকানায় “হাইজ্যাক” করতে পারে। এই একই দুর্বলতা security মডিউলে বিস্তারিত আসবে, কিন্তু ভিত্তিটা এখানেই — SP আর PC-র মধ্যেকার memory-ভিত্তিক সম্পর্কই এই আক্রমণ সম্ভব করে তোলে।
Register windows — SPARC-র একটা পরিত্যক্ত পরীক্ষা। SPARC (Sun Microsystems-এর RISC ISA) একসময় “register window” নামে একটা কৌশল ব্যবহার করত — প্রতিটা function call নতুন একগুচ্ছ register “দেখত” (একটা larger physical register file-এর ঘূর্ণায়মান windowed অংশ), যাতে function call-এ register save/restore-এর দরকার কমে। প্রযুক্তিগতভাবে চতুর হলেও, বেশি hardware জটিলতা আর সীমিত সুবিধার কারণে আধুনিক ISA-গুলো (ARM64, RISC-V) এই কৌশল বাদ দিয়ে সরল calling convention-ই বেছে নিয়েছে — আরেকটা উদাহরণ যেখানে RISC দর্শনের ভেতরেও “সরলতা” এর পক্ষে সিদ্ধান্ত জিতেছে।
x86-64-র “red zone” — ABI-স্তরের একটা optimization। System V AMD64 ABI-তে leaf function-গুলো stack pointer-এর নিচে ১২৮ byte পর্যন্ত ব্যবহার করতে পারে SUB rsp-জাতীয় কোনো instruction ছাড়াই (এই অঞ্চলটাকে “red zone” বলা হয়) — এটা কোনো hardware feature না, বরং একটা compiler-convention যা কিছু ছোট leaf function-এ stack-adjustment instruction বাদ দিতে দেয়, performance সুবিধার জন্য।
Debugger-এ register inspection — প্রতিদিনের বাস্তব ব্যবহার। এই লেসনের experiment-এই যেমন দেখলেন, GDB-র info registers কমান্ড ঠিক PC, SP, flags, আর general-purpose register-গুলো live দেখায় — এটাই মূলত প্রতিটা low-level debugging সেশনের ভিত্তি (crash dump বিশ্লেষণ, segfault-এর কারণ খোঁজা, malware reverse-engineering)।
RISC-V-র zero register (x0) — hardware-এনফোর্সড একটা কৌশলী optimization। RISC-V-তে x0 সবসময় ০ পড়ায়, তাতে যাই লেখা হোক না কেন — এটা কোনো নিয়ম না মেনে চলা convention, hardware সরাসরি এটা এনফোর্স করে (write সবসময় silently discard হয়)। এর সুবিধা — অনেক common operation x0 ব্যবহার করে বিনামূল্যে পাওয়া যায়, যেমন mv rd, rs আসলে add rd, rs, x0 (কোনো আলাদা “move” instruction লাগেনি, opcode space বাঁচল — RISC দর্শনের “কম instruction, বেশি composition” নীতির একটা সুন্দর উদাহরণ)।
যে ভুলগুলো সবাই করে
“সব ISA-তেই একটা flags/condition-code register থাকতেই হয় — এটা CPU ডিজাইনের একটা অপরিহার্য অংশ।”
না — RISC-V-ই এই লেসনের সবচেয়ে স্পষ্ট প্রতি-উদাহরণ। RISC-V-এ কোনো flags/condition-code register নেই; conditional branch instruction (BEQ, BLT, …) সরাসরি দুইটা register তুলনা করে, কোনো implicit state ছাড়াই। hood সেকশনে দেখেছেন এটা একটা সচেতন সিদ্ধান্ত — out-of-order execution-এ hazard tracking সরল রাখার জন্য।
ARM64 আর x86-64 flags রেখেছে ঐতিহাসিক ধারাবাহিকতা আর কিছু encoding-সুবিধার কারণে (একটা compare করে একাধিক পরবর্তী branch-এ সেই ফলাফল পুনর্ব্যবহার করা যায়, প্রতিবার নতুন compare ছাড়াই)। দুইটাই বৈধ design, কোনোটাই “বাধ্যতামূলক” না।
“Caller-saved আর callee-saved register-এর নিয়ম hardware নিজেই এনফোর্স করে — ভুল করলে CPU error দেবে।”
সম্পূর্ণ ভুল, আর এটা একটা গুরুত্বপূর্ণ সংশোধন। CPU hardware জানেই না কোন register caller-saved আর কোনটা callee-saved — এই বিভাজন সম্পূর্ণভাবে একটা software convention (ABI-র অংশ), compiler-লেখক আর OS-লেখকদের মধ্যে সম্মত একটা চুক্তি। ISA স্পেক শুধু register-গুলো বলে দেয় (কতগুলো, কত bit); কোনটা কীসের জন্য — সেই সিদ্ধান্ত ISA-র বাইরের, একটা পৃথক ABI ডকুমেন্টের বিষয়।
যদি একটা compiler bug এই convention ভাঙে (যেমন একটা callee-saved register না বাঁচিয়েই ব্যবহার করে ফেলে), hardware কোনো error দেবে না — প্রোগ্রামটা চলতেই থাকবে, কিন্তু ভুল ফলাফল দেবে বা অপ্রত্যাশিতভাবে crash করবে, প্রায়ই debug করা কঠিন এমন এক জায়গায় যেটা আসল bug-এর উৎস থেকে অনেক দূরে।
“PC হলো hardware-এ একটা সম্পূর্ণ ভিন্ন ধরনের বিশেষ circuit, general-purpose register থেকে গঠনগতভাবে আলাদা।”
হার্ডওয়্যার-গঠনের দিক থেকে না — PC আর একটা সাধারণ register একই building block দিয়ে তৈরি (Level 2-এর flip-flop + write-enable কাঠামো)। পার্থক্যটা গঠনে না, সংযোগে আর ব্যবহারে: PC control unit দিয়ে স্বয়ংক্রিয়ভাবে প্রতিটা fetch-এ আপডেট হয় (একটা বিশেষ MUX-এর মাধ্যমে, hood সেকশনে দেখেছেন), সাধারণ arithmetic instruction-এর destination হিসেবে ব্যবহৃত হয় না, আর সাধারণত register file-এর “৩২টা register”-এর একটা হিসেবে গোনা হয় না (RISC-V-তে PC স্পষ্টভাবে x0-x31-এর বাইরের একটা আলাদা register — অথচ x86-64-এ rip অনেকটা একইরকম আলাদা আচরণ করে যদিও নামকরণ ভিন্ন)।
সংক্ষেপে: hardware-এর “মূল উপাদান” (flip-flop, MUX) সব জায়গায় একই — Level 2-র শিক্ষা এখানেও প্রযোজ্য — পার্থক্যটা কীভাবে সেগুলো জোড়া লাগানো হয়েছে তাতে, ISA-স্তরের নামকরণ/ভূমিকায়।
“Stack pointer একটা hardware-এনফোর্সড সীমানা — stack overflow হলে CPU নিজে থেকেই সেটা আটকায়।”
বেশিরভাগ ক্ষেত্রে ভুল। SP নিজে শুধু একটা ঠিকানা ধরে রাখা register — এটা কোনো বিল্ট-ইন “সীমা” জানে না। যদি একটা প্রোগ্রাম stack-এ এত বেশি push করে যে SP তার বরাদ্দ করা memory অঞ্চলের বাইরে চলে যায় (stack overflow), সেটা সরাসরি hardware আটকায় না — সাধারণত OS একটা guard page (একটা বিশেষভাবে চিহ্নিত, “access করলেই fault” মেমরি পাতা) বসিয়ে দেয় stack-এর ঠিক শেষে, আর SP সেই page-এ পৌঁছালে একটা page-fault exception ঘটে, যেটা OS ধরে প্রোগ্রামটা terminate করে (সাধারণত “segmentation fault” নামে পরিচিত)। এই সুরক্ষাটা hardware-এর একটা নির্দিষ্ট feature (memory protection, পরবর্তী মডিউলে বিস্তারিত) ব্যবহার করে বাস্তবায়িত, কিন্তু SP register নিজে সম্পূর্ণ passive — এটা শুধু একটা ঠিকানা, কোনো বুদ্ধিমত্তা নেই তার ভেতরে।
বুঝেছেন কি না দেখুন
1x86-64, ARM64, আর RISC-V-এ general-purpose register সংখ্যা কত কত, আর কেন এই সংখ্যাগুলো মোটামুটি ৩২-এর কাছাকাছি বা কম — এনকোডিং-এর সাথে সম্পর্ক দিয়ে ব্যাখ্যা করুন।
স্মরণ
সংখ্যা: x86-64-এ ১৬টা (৬৪ bit), ARM64-এ ৩১টা + xzr (৬৪ bit), RISC-V-এ ৩২টা (x0-x31, ৬৪ bit RV64-এ, x0 হার্ডওয়্যার্ড শূন্য)।
কারণ: instruction encoding-এ প্রতিটা register-reference একটা fixed-width bit-field দিয়ে প্রকাশ করতে হয়। ৫ বিট দিয়ে ঠিক 2^5 = 32টা register এনকোড করা যায়, কোনো bit নষ্ট না করে। এর বেশি register রাখতে চাইলে field আরও চওড়া করতে হবে, যা fixed ৩২-বিট (RISC-V/ARM64) instruction budget-এ চাপ ফেলবে (একটা তিন-operand instruction-এ তিনটা register field লাগে — 3 × 5 = 15 বিট, বাকি ১৭ বিট opcode আর অন্য তথ্যের জন্য)। x86-64-র ১৬টা কম ঐতিহাসিক কারণে (মূল ৮০৮৬-র ৮টা রেজিস্টার backward-compatible ভাবে দ্বিগুণ করা হয়েছে), encoding-budget-এর সরাসরি ফলাফল না।
2একটা fixed-length RISC ISA-তে (RISC-V/ARM64) PC কীভাবে আপডেট হয় সাধারণ (non-branch) instruction-এর পর, আর x86-64-এ কীভাবে হয় — পার্থক্যটা ব্যাখ্যা করুন, আর এই পার্থক্যটা কেন গত লেসনের length-decode আলোচনার সাথে সরাসরি সম্পর্কিত তা যুক্তি দিন।
যুক্তি
RISC-V/ARM64: PC ← PC + 4, সবসময় নিঃশর্তভাবে (branch না হলে) — কারণ প্রতিটা instruction ঠিক ৪ byte, তাই পরের instruction ঠিক কোথায় তা decode ছাড়াই জানা যায়।
x86-64: PC ← PC + (এই instruction-এর প্রকৃত byte-length) — যেটা instruction-ভেদে ১ থেকে ১৫ পর্যন্ত হতে পারে। এই দৈর্ঘ্যটা জানতে CPU-কে অন্তত instruction-এর prefix/opcode/ModRM/SIB অংশ decode করতেই হয়।
সম্পর্ক: এটা ঠিক গত লেসনের length-decode সমস্যার আরেকটা প্রকাশ। fixed-length ISA-তে “পরের instruction কোথায়” প্রশ্নের উত্তর decode-independent (একটা constant যোগ করলেই চলে); variable-length ISA-তে এই উত্তরটাই decode-এর একটা ফলাফল — PC আপডেট করার আগেও length-decode ধাপ সম্পূর্ণ হতে হয়। এটাই দেখায় কেন instruction-length সিদ্ধান্তটা শুধু decode-কেই না, এমনকি সবচেয়ে মৌলিক “পরের instruction কোথায়” প্রশ্নকেও প্রভাবিত করে।
3digital-logic/combinational-subtractors-comparators লেসনে derive করা নিয়ম A \< B (signed) ⟺ SF ⊕ OF = 1 — এই লেসনে বলা হলো এটাই x86-64-র JL instruction-এর আসল hardware semantics। যদি কোনো কারণে OF flag CPU-তে ভুলভাবে সবসময় ০ আটকে থাকত (একটা hypothetical hardware bug), JL instruction কোন নির্দিষ্ট ইনপুটে ভুল সিদ্ধান্ত নিত?
প্রয়োগ
A \< B (signed) ⟺ SF ⊕ OF = 1 — এই লেসনে বলা হলো এটাই x86-64-র JL instruction-এর আসল hardware semantics। যদি কোনো কারণে OF flag CPU-তে ভুলভাবে সবসময় ০ আটকে থাকত (একটা hypothetical hardware bug), JL instruction কোন নির্দিষ্ট ইনপুটে ভুল সিদ্ধান্ত নিত?এই প্রশ্নের উত্তর সরাসরি সেই আগের লেসনের বিশ্লেষণ থেকে আসে। নিয়মটা ছিল: যখন প্রকৃত signed overflow ঘটেনি (OF=0), raw SF নিজেই নির্ভরযোগ্য sign নির্দেশক, তাই SF ⊕ 0 = SF সঠিক থাকে — এই ক্ষেত্রে OF স্থায়ী ০ ধরে রাখলেও কোনো সমস্যা হতো না।
কিন্তু যখন প্রকৃত signed overflow ঘটে (সত্যিকারের OF=1 হওয়া উচিত, যেমন দুইটা বড় positive সংখ্যা যোগ করলে যদি ফলাফল signed range ছাড়িয়ে যায় এবং sign bit ভুলভাবে negative দেখায়), তখন সঠিক নিয়ম হলো SF ⊕ OF (যা SF-কে invert করে সঠিক উত্তর দেয়)। যদি OF জোর করে ০ আটকে থাকে, hardware ভুলভাবে গণনা করবে SF ⊕ 0 = SF — অর্থাৎ raw, ভ্রান্ত sign bit-টাই সরাসরি ব্যবহার হবে, উল্টো উত্তর দেবে।
নির্দিষ্ট উদাহরণ (৪-বিট, আগের লেসনের ধাঁচে): A = 7 (0111), B = -8 (1000)-এর মতো একটা তুলনায় যেখানে প্রকৃত OF=1 হওয়া উচিত (কারণ A - B = 15, যা 4-বিট signed range-এর বাইরে) — সেই ক্ষেত্রে ভুল OF=0 ধরে নিলে JL ভুল দিকে branch নেবে, প্রোগ্রামের logic ভেঙে যাবে ঠিক সেই সীমানা-case-গুলোতে যেখানে overflow প্রকৃতপক্ষে ঘটে — সাধারণ, ছোট সংখ্যার তুলনায় বাগটা কখনো ধরা পড়বে না, শুধু বড়/সীমানা-সংলগ্ন মানে প্রকাশ পাবে।
4RISC-V flags register বাদ দিয়েছে hazard-tracking সরল রাখার যুক্তিতে। কিন্তু ARM64 flags রেখেছে, শুধু ডিফল্টে বন্ধ রেখেছে (S-suffix ছাড়া arithmetic instruction flags বদলায় না)। এই দুইটা design-এর মধ্যে trade-off-টা কী?
যুক্তি
RISC-V-র সুবিধা: সম্পূর্ণ কোনো implicit/hidden state নেই — প্রতিটা instruction-এর সব dependency তার explicit operand-এই দৃশ্যমান। Hazard-detection hardware (particularly out-of-order execution-এ, পরবর্তী মডিউলে বিস্তারিত) কোনো flags register নিয়ে register-renaming বা extra tracking করতে হয় না — যুক্তি সবচেয়ে সরল।
RISC-V-র খরচ: compare-and-branch একটা instruction-এ মিশে থাকায়, একটা compare-এর ফলাফল যদি একাধিক পরের branch-এ পুনর্ব্যবহার করতে চান (যেমন একটা switch-এর মতো গঠনে), প্রতিবার নতুন compare instruction লাগে — flags-ভিত্তিক ISA-তে একবার compare করে ফলাফল একাধিকবার পড়া যায়।
ARM64-র compromise-এর সুবিধা: compiler নিজে বেছে নিতে পারে কখন flags দরকার (S-suffix ব্যবহার করে) আর কখন না (সাধারণ instruction ব্যবহার করে) — যখন flags দরকার নেই, hazard-tracking overhead-ও নেই (কারণ hardware জানে সেই নির্দিষ্ট instruction flags লেখেনি)। এটা RISC-V-র সম্পূর্ণ-অনুপস্থিতি আর x86-64-র সম্পূর্ণ-সবসময়-উপস্থিতির মাঝামাঝি একটা নমনীয় অবস্থান — compiler-কে সিদ্ধান্তের ক্ষমতা দেয়, কিন্তু encoding-এ একটা বাড়তি বিট (S-suffix flag) খরচ করে।
সংক্ষেপে: এটা আরেকটা “জটিলতা কোথায় রাখব” প্রশ্ন — RISC-V hardware simplicity সর্বোচ্চ করেছে (কিছু ক্ষেত্রে বেশি instruction-এর বিনিময়ে), ARM64 একটা মধ্যপন্থা নিয়েছে (সামান্য hardware জটিলতার বিনিময়ে flexibility)।
5একজন সহপাঠী দাবি করছেন: “caller-saved/callee-saved নিয়ম আসলে অপ্রয়োজনীয় — compiler প্রতিটা function call-এর আগে-পরে সব register save/restore করে দিলেই তো হয়, তাহলে এই জটিল দুই-ভাগ ব্যবস্থার দরকার কী?” এই প্রস্তাবটা কতটা যুক্তিসঙ্গত? কোন trade-off সে উপেক্ষা করছে?
ডিজাইন
প্রস্তাবটা কাজ করবে — কোনো correctness সমস্যা হবে না যদি সবসময় সব register save/restore করা হয়। কিন্তু সে একটা গুরুত্বপূর্ণ trade-off উপেক্ষা করছে: প্রতিটা save/restore একটা memory access (stack-এ push/pop), যেটা সময় নেয়।
যদি প্রতিটা function call সব ১৬-৩২টা register save করত (ব্যবহার হোক বা না হোক), এটা বিশাল অপচয় হতো — বেশিরভাগ ছোট ফাংশন মাত্র কয়েকটা register ব্যবহার করে। Caller-saved/callee-saved বিভাজনের আসল সুবিধা হলো এটা শুধু প্রয়োজনীয় register-ই save করতে বাধ্য করে:
- যদি caller জানে একটা মান তার caller-saved register-এ আছে আর call-এর পরও দরকার, শুধু সেই একটা register-ই সে বাঁচায় — বাকিগুলো (যেগুলো caller ব্যবহারই করেনি, বা যাদের মান আর দরকার নেই) বাঁচানোর দরকার নেই।
- যদি callee শুধু কয়েকটা callee-saved register ব্যবহার করে, শুধু সেই কয়েকটাই সে বাঁচায় — বাকি callee-saved register (যেগুলো সে ব্যবহারই করেনি) বাঁচানোর দরকার নেই।
মূল অন্তর্দৃষ্টি: এই দুই-ভাগ ব্যবস্থা আসলে একটা optimization — compiler প্রতিটা register-এর জন্য স্থানীয়ভাবে সিদ্ধান্ত নিতে পারে “এটা বাঁচানো দরকার কি না”, “সব বাঁচাও” নীতির চেয়ে অনেক কম memory-traffic লাগে। এটা এই লেসনের earlier থিমেরই একটা উদাহরণ — একটা আপাত-জটিল নিয়ম (দুই ভাগে register ভাগ করা) আসলে একটা performance trade-off-এর যুক্তিসঙ্গত সমাধান, নিছক ঐতিহাসিক জটিলতা না।
এরপর কী
পরের প্রশ্ন — এই সব bit আসলে কীভাবে একসাথে packed হয়
এই তিন লেসন মিলিয়ে আমরা এখন জানি ISA কী (একটা চুক্তি), কেন CISC আর RISC ভিন্ন পথ নিয়েছে (instruction-এর জটিলতা কোথায় রাখা হবে), আর register/PC/SP/flags কী ভূমিকা পালন করে। কিন্তু একটা প্রশ্ন এখনও অস্পষ্ট রয়ে গেছে যেটা প্রায় প্রতিটা লেসনেই উঁকি দিয়েছে — গত লেসনের bit-diagram-এ opcode, register field, offset — এই সবকিছু ঠিক কীভাবে একটা instruction-এর ৩২ বিট বা ভ্যারিয়েবল বাইটগুলোতে প্যাক করা হয়, আর CPU কীভাবে সেই bit-প্যাটার্ন থেকে ফিরে “এটা কোন instruction, কোন register, কোন immediate” বের করে আনে?
পরের লেসনে আমরা instruction encoding আর decoding-এর বিস্তারিত দেখব — RISC-V-র R-type/I-type/S-type/B-type ফরম্যাট, x86-64-র prefix/opcode/ModRM/SIB কাঠামো, আর কেন ভিন্ন instruction ভিন্ন “format” ব্যবহার করে (একটা branch-এর offset field-এর প্রয়োজন একটা arithmetic instruction-এর তৃতীয় register field-এর প্রয়োজন থেকে আলাদা)। এটাই এই মডিউলের শেষ ভিত্তি-লেসন — এরপর থেকে আমরা fetch-decode-execute cycle-এর পূর্ণ বিস্তারিত, তারপর memory hierarchy আর pipelining-এর জগতে প্রবেশ করব।
আরও পড়ুন
- The RISC-V Instruction Set Manual, Volume I: Unprivileged ISA · RISC-V-র register model (x0-x31), আর কেন এতে কোনো flags/condition-code register নেই তার আনুষ্ঠানিক ব্যাখ্যা
- Intel 64 and IA-32 Architectures Software Developer's Manual · x86-64-র RFLAGS register-এর সম্পূর্ণ বিট-বিবরণ, আর System V AMD64 ABI-র calling convention
- Computer Organization and Design: RISC-V Edition — David A. Patterson, John L. Hennessy · Register file, PC, আর calling convention-কে hardware/software সেতু হিসেবে দেখা এই লেসনের কেন্দ্রীয় ফ্রেম, বইটা থেকেই ধার করা
- Compiler Explorer (godbolt.org) · এই লেসনের experiment-এ register allocation ও flags-ব্যবহার পর্যবেক্ষণের জন্য ব্যবহৃত