Foundationপ্রথম নীতি থেকে
LEVEL 3লেসন ৩/১৭মাঝারি৫০ মিনিট

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, বাধ্যতা না।

এই লেসন শেষে আপনি পারবেন

  • x86-64, ARM64, আর RISC-V-র general-purpose register-এর সংখ্যা ও width বলতে পারবেন, আর ব্যাখ্যা করতে পারবেন কেন এই সংখ্যাগুলো এমন — encoding budget-এর সাথে সম্পর্ক ধরে
  • Program Counter (PC) ঠিক কী ধরে রাখে, প্রতিটা fetch-এ কীভাবে বদলায়, আর branch/call instruction কীভাবে সেই স্বাভাবিক বৃদ্ধি ভেঙে দেয় তা ব্যাখ্যা করতে পারবেন
  • Stack pointer (SP) কীভাবে call stack-এর 'top' নির্দেশ করে, push/pop-এ কীভাবে বদলায়, আর stack কেন নিচের দিকে বাড়ে তা ব্যাখ্যা করতে পারবেন
  • Zero/carry/overflow/negative flag ঠিক কোন ALU circuit থেকে আসে (Level 2-র সরাসরি callback) আর conditional branch instruction কীভাবে সেই flag পড়ে সিদ্ধান্ত নেয় তা ব্যাখ্যা করতে পারবেন
  • RISC-V কেন flags register পুরোপুরি বাদ দিয়েছে, x86-64/ARM64 কেন রেখেছে — এই design পার্থক্যের কারণ যুক্তি দিয়ে ব্যাখ্যা করতে পারবেন
  • caller-saved বনাম callee-saved register-এর মূল ধারণা বর্ণনা করতে পারবেন — এটা যে একটা software convention, hardware-এনফোর্সড নিয়ম না, তা স্পষ্ট করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

গত দুই লেসনে বারবার একটা নাম এসেছে যেটা আমরা কখনো পুরোপুরি সংজ্ঞায়িত করিনি — 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-ব্যাংক বানানো যায় সেটাই ছিল বিষয়। এখন সেই সংখ্যাটাই কেন্দ্রীয় প্রশ্ন:

ISAGeneral-purpose register সংখ্যাWidthনামকরণ
x86-64১৬টা৬৪ bitrax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, r8r15
ARM64 (AArch64)৩১টা + xzr (সবসময়-শূন্য)৬৪ bitx0x30, স্বতন্ত্র sp
RISC-V (RV64)৩২টা, যার মধ্যে x0 হার্ডওয়্যার্ড শূন্য৬৪ bitx0x31 (বা 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 (২০০৩) এই ৮টাকে ৬৪-বিটে বাড়িয়ে (eaxrax) আরও ৮টা নতুন (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-র জীবনচক্র — একটা সাধারণ fetch বনাম একটা branch
  1. PC বর্তমান মান ধরে রাখেযেমন 0x1000 — পরবর্তী instruction-এর ঠিকানা
  2. Fetch — Mem[PC] থেকে instruction পড়া হয়instruction bit-প্যাটার্ন CPU-তে আসে, PC নিজে এখনও অপরিবর্তিত
  3. Decode — instruction-টা কী, তার দৈর্ঘ্য কতsequential না branch তা নির্ধারিত হয়
  4. সাধারণ instruction হলে: PC ← PC + lengthপরের instruction ঠিক পরের ঠিকানায়
  5. Taken branch/jump হলে: PC ← target addresstarget ভিন্ন কোথাও — instruction-এর ভেতরে থাকা offset বা register থেকে গণনা করা
  6. পরবর্তী চক্র — নতুন 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 flagZFফলাফল ঠিক শূন্য কি না
Carry flagCFunsigned arithmetic-এ overflow/borrow হয়েছে কি না
Overflow flagOFsigned arithmetic-এ overflow হয়েছে কি না (Cₙ₋₁ ⊕ Cₙ)
Negative/Sign flagSF (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
    ret

ARM64 (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, r8r11rbx, rbp, r12r15
RISC-V উদাহরণt0t6 (temporary)s0s11 (saved)
ARM64 উদাহরণx0x18x19x28

এই বিভাজনটা কেন দরকার? একটা ফাংশন যদি কোনো মান একটা 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 নির্দিষ্ট করে, তাদের “ভূমিকা” নির্দিষ্ট করে সফটওয়্যার-কনভেনশন।

নিজে চালিয়ে দেখুন

EXPERIMENT

GDB দিয়ে live PC, SP, আর flags পর্যবেক্ষণ করুন

Linux/WSL (GDB)· ২৫ মিনিট

একটা ছোট প্রোগ্রাম কম্পাইল করে 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 ./max

GDB-এর ভেতরে:

(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 প্রমাণ।

নিজে বানান

BUILD IT

Toy ISA-তে PC, flags register, আর একটা conditional branch যোগ করুন

Markdown / pseudocode · ●●●○○
  1. গত লেসনের toy ISA স্পেক (ADD, SUB, LOAD, STORE) থেকে শুরু করুন, আর প্রথম লেসনের Q4-এর BEQ প্রস্তাবটা আবার বের করুন
  2. একটা register model section লিখুন যেখানে PC স্পষ্টভাবে একটা আলাদা, বিশেষ-উদ্দেশ্য register হিসেবে ঘোষিত (কত bit চওড়া — মেমরির আকারের সমান হতে হবে)
  3. একটা ২-বিট flags register ডিজাইন করুন — শুধু ZF আর একটা negative flag (SF) রাখুন, সরলতার জন্য
  4. ADD আর SUB-এর semantics আপডেট করুন যাতে তারা ZF আর SF আপডেট করে (ঠিক Level 2-এর subtractor/comparator লেসনের ZF সংজ্ঞা ব্যবহার করে — ফলাফল শূন্য হলে ZF=1)
  5. 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 — এটা শুধু একটা ঠিকানা, কোনো বুদ্ধিমত্তা নেই তার ভেতরে।

বুঝেছেন কি না দেখুন

1

x86-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 কোথায়” প্রশ্নকেও প্রভাবিত করে।

3

digital-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 কোন নির্দিষ্ট ইনপুটে ভুল সিদ্ধান্ত নিত?

প্রয়োগ

এই প্রশ্নের উত্তর সরাসরি সেই আগের লেসনের বিশ্লেষণ থেকে আসে। নিয়মটা ছিল: যখন প্রকৃত 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 প্রকৃতপক্ষে ঘটে — সাধারণ, ছোট সংখ্যার তুলনায় বাগটা কখনো ধরা পড়বে না, শুধু বড়/সীমানা-সংলগ্ন মানে প্রকাশ পাবে।

4

RISC-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-ব্যবহার পর্যবেক্ষণের জন্য ব্যবহৃত