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

ARM64 (AArch64) Basics — এক syntax-এ, বিশুদ্ধ RISC-এ

ARM64 (AArch64) Basics

ARM64 (AArch64) — গত module-এর risc-vs-cisc আলোচনার একদম বিশুদ্ধ বাস্তবায়ন। একটাই syntax (কোনো AT&T/Intel ফোর্ক নেই), x0-x30/w0-w30-এর সরল রেজিস্টার-এলিয়াসিং, x30/lr link register দিয়ে return-address সামলানোর ভিন্ন কৌশল, আর load-store architecture-এর কঠোর নিয়ম -- arithmetic instruction কখনো memory ছোঁয় না। x86-64-র সাথে পাশাপাশি একই ফাংশন কম্পাইল করে এই সব পার্থক্য concrete করে দেখানো হবে।

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

  • ARM64-র একক, canonical syntax ব্যাখ্যা করতে পারবেন — কেন এখানে x86-64-র মতো কোনো ঐতিহাসিক AT&T/Intel বিভাজন নেই
  • ARM64-র register set (x0-x30/w0-w30 aliasing, sp, xzr/wzr) বর্ণনা করতে পারবেন, আর এটা x86-64-র অসমমিত al/ax/eax/rax স্কিমের চেয়ে কীভাবে সরল
  • Link register (x30/lr)-এর ভূমিকা ব্যাখ্যা করতে পারবেন — bl/ret কীভাবে return address সামলায় x86-64-র সবসময়-stack-push করা call/ret থেকে ভিন্নভাবে, আর leaf function-এর জন্য এর perf-প্রাসঙ্গিক তাৎপর্য যুক্তি দিয়ে বলতে পারবেন
  • Load-store architecture ব্যাখ্যা করতে পারবেন — কেন ARM64-এ শুধু ldr/str memory স্পর্শ করে, arithmetic instruction কখনোই memory operand নেয় না, x86-64-র সরাসরি বৈসাদৃশ্যে
  • একই সাধারণ C অপারেশন x86-64 আর ARM64-এ কম্পাইল হলে কীভাবে ভিন্ন দেখতে হয় তা concrete উদাহরণ দিয়ে দেখাতে পারবেন
  • একটা compiled ARM64 binary-র disassembly পড়ে ldr/str বনাম arithmetic instruction আলাদা করে চিনতে পারবেন

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

আগে এটা বুঝি

গত লেসনের শেষে একটা পর্যবেক্ষণ রেখে এসেছিলাম — x86-64-এ add [rbp-4], eax-এর মতো instruction সরাসরি memory-তে read-modify-write করতে পারে, একটাই instruction-এ। এই লেসনে আমরা একটা সম্পূর্ণ ভিন্ন দর্শনের ISA দেখব, যেখানে এমন instruction আদৌ অস্তিত্বহীনARM64, আনুষ্ঠানিকভাবে AArch64 (৬৪-বিট ARM architecture)।

গত module-এর risc-vs-cisc লেসনে RISC আর CISC-এর তাত্ত্বিক পার্থক্য দেখেছিলেন — uniform instruction length, load-store architecture, বড় register file, hardwired control। ARM64 এই দর্শনের একটা প্রায়-বিশুদ্ধ বাস্তবায়ন — RISC-V-র মতোই RISC-ঘরানার, কিন্তু বাস্তব দুনিয়ায় RISC-V-র চেয়েও অনেক বেশি ব্যাপকভাবে ব্যবহৃত (প্রতিটা আধুনিক smartphone, Apple-এর M-সিরিজ ল্যাপটপ/ডেস্কটপ, AWS-এর Graviton সার্ভার — এই সবকিছুর মূলে ARM64)।

আর এই লেসনে একটা স্বস্তির খবরও আছে — গত লেসনের AT&T-বনাম-Intel বিভ্রান্তি এখানে নেই। ARM64-এর একটাই canonical syntax, সবখানে, সব টুলে। কেন এই পার্থক্য? আর load-store architecture ঠিক কী শর্ত আরোপ করে, তা কী পরিমাণ instruction-সংখ্যায় খরচ হয়? এই লেসন সেই প্রশ্নগুলোর concrete, তুলনামূলক উত্তর দেবে — বারবার x86-64-র সাথে পাশাপাশি রেখে।

মূল ধারণা

কেন কোনো AT&T/Intel-সদৃশ বিভাজন নেই

x86-64-এর সেই ঐতিহাসিক syntax-ফোর্কের একটা নির্দিষ্ট উৎস ছিল — GNU-এর as Unix-এর পুরনো assembler-ঐতিহ্য (AT&T) অনুসরণ করেছিল, যখন Intel তাদের নিজস্ব official manual-এ একটা ভিন্ন, স্বাধীনভাবে-বিকশিত কনভেনশন ব্যবহার করত — দুটোই x86-এর জন্মের অনেক আগে থেকে (AT&T) বা তার নিজস্ব প্রতিষ্ঠানিক-ইতিহাস থেকে (Intel) আলাদাভাবে বিকশিত হয়ে এসেছিল, তাই যখন GNU টুলচেইন x86 সমর্থন করল, দুইটা প্রতিদ্বন্দ্বী কনভেনশন একসাথে বাস করা শুরু করল।

ARM-এর গল্প ভিন্ন। Arm Ltd (মূলত Acorn Computers, ১৯৮৫-তে প্রথম ARM ডিজাইন) শুরু থেকেই তাদের নিজস্ব official Architecture Reference Manual-এ একটা একক, সুনির্দিষ্ট assembly syntax প্রকাশ করেছে — কোনো প্রতিদ্বন্দ্বী “Unix-ঐতিহ্য” এই ISA-র জন্য আগে থেকেই বিদ্যমান ছিল না যেটা GNU টুলচেইনকে একটা ভিন্ন কনভেনশন উত্তরাধিকারসূত্রে নিতে বাধ্য করত। GNU-এর ARM assembler (as, ARM target-এ) তাই সরাসরি Arm-এর নিজস্ব official syntax গ্রহণ করল — কোনো প্রতিযোগী কনভেনশন তৈরি হওয়ার সুযোগই হয়নি।

Register set — x0-x30 / w0-w30, একটা সরল দুই-স্তরের aliasing

গত লেসনে x86-64-র sub-register স্কিম দেখেছিলেন — rax/eax/ax/al/ah, চার স্তরের ওভারল্যাপিং, কিছু register-এ upper-byte অ্যাক্সেসও আছে (ah), কিছুতে নেই (r8b)। ARM64-র স্কিম উল্লেখযোগ্যভাবে সরল — প্রতিটা ৬৪-বিট register-এর ঠিক দুইটা view, না বেশি, না কম:

৬৪-বিট নাম৩২-বিট নামসম্পর্ক
x0w0w0 হলো x0-এর নিচের ৩২ বিট
x1w1w1 হলো x1-এর নিচের ৩২ বিট
x30w30w30 হলো x30-এর নিচের ৩২ বিট (link register, নিচে দেখুন)

মোট ৩১টা general-purpose register (x0-x30), প্রতিটার একটা ৩২-বিট view (w0-w30) — কোনো ah-এর মতো “উপরের-byte-শুধু” special-case নেই, কোনো register-ভেদে ভিন্ন সুবিধা নেই। এটাই এই লেসনের অবজেক্টিভে উল্লেখিত “একটা পরিষ্কার sub-register aliasing উদাহরণ” — একটা সহজ, uniform নিয়ম, প্রতিটা register-এর জন্য সমান।

x86-64-র মতোই ৩২-বিট (w) register-এ লেখা উপরের ৩২ বিট zero-extend করে (গত লেসনের সেই zero-extension আচরণের একটা সমান্তরাল নিয়ম ARM64-তেও বিদ্যমান — w0-এ লিখলে x0-এর উপরের ৩২ বিট স্বয়ংক্রিয়ভাবে শূন্য হয়ে যায়)।

sp — আলাদা, বিশেষ-নিয়ন্ত্রিত একটা register

Stack pointer (sp) x0-x30-এর অংশ না — এটা একটা সম্পূর্ণ আলাদা, ৩২তম physical register, বিশেষ বিধিনিষেধসহ। বেশিরভাগ সাধারণ arithmetic instruction সরাসরি sp-কে operand হিসেবে নিতে পারে না — sp-এর মান বদলাতে হলে প্রথমে একটা সাধারণ register-এ কপি করতে হয় (mov x0, sp), সেখানে গণনা করে, তারপর ফেরত sp-তে বসাতে হয় (কিছু নির্দিষ্ট instruction, যেমন add sp, sp, #16, সরাসরি sp নিয়ে কাজ করার অনুমতি পায়, কিন্তু সাধারণ arithmetic instruction-এর মতো পূর্ণ স্বাধীনতা নেই)।

xzr/wzr — একটা hardwired-শূন্য register

ARM64-তে x31-এর জায়গায় (encoding-এ যেখানে x31 বসত) প্রসঙ্গ-নির্ভরভাবে হয় sp নয়তো xzr (zero register, ৩২-বিট view wzr) — একটা register যেটা সবসময় শূন্য পড়ে, আর তাতে লেখা মানে সেই লেখা নীরবে বাতিল হয়ে যায়। এটা RISC-V-এর x0 (গত module-এর registers-pc-flags লেসনে উল্লেখিত hardwired-zero) থেকে সরাসরি ধার করা একটা নকশা-প্যাটার্ন — উভয় ISA-ই বুঝেছে “শূন্য” এত ঘন ঘন দরকার হয় (তুলনা, initialization, dummy-write) যে এটাকে একটা বিনামূল্যের, hardware-supported ধ্রুবক বানিয়ে দেওয়া উপযোগী।

এবার এই লেসনের সবচেয়ে গুরুত্বপূর্ণ ডিজাইন-পার্থক্যে আসি। গত লেসনে দেখেছিলেন x86-64-এ call/ret সবসময় stack-এ push/pop করে return address সামলায় — কোনো ব্যতিক্রম নেই, এমনকি সবচেয়ে ছোট leaf function-এও।

ARM64 এই একই সমস্যার একটা সম্পূর্ণ ভিন্ন সমাধান বেছে নিয়েছে — x30 (alias lr, link register) নামের একটা নির্দিষ্ট, dedicated register। ফাংশন-কল instruction bl (Branch with Link) দুইটা কাজ করে: (১) পরের instruction-এর ঠিকানা সরাসরি x30/lr-এ বসায় (কোনো memory access নেই, শুধু একটা register-write), (২) target-এ jump করে। ফেরার instruction ret সরাসরি x30/lr-এর মান পড়ে PC-তে jump করে — আবারও, কোনো memory access নেই।

bl     square      ; x30 ← পরের instruction-এর ঠিকানা (register-write, memory না), তারপর jump square-এ
...                ; ফেরত আসবে এখানে
x86-64 call/ret বনাম ARM64 bl/ret -- return address কোথায় থাকে
  1. x86-64: call targetreturn address stack-এ push (memory write, rsp কমে)
  2. x86-64: ... target-এর কোড চলে ...return address memory-তে বসে আছে
  3. x86-64: retstack থেকে pop (memory read, rsp বাড়ে), PC-তে বসে
  4. ARM64: bl targetreturn address সরাসরি x30/lr register-এ (memory access নেই)
  5. ARM64: ... target-এর কোড চলে ...return address শুধু x30 register-এ, memory-তে না (যদি target আরেকটা bl না করে)
  6. ARM64: retসরাসরি x30-এর মান PC-তে (আবারও, memory access নেই)

Leaf function-এর জন্য বিশাল তাৎপর্য। একটা leaf function — এমন একটা function যেটা নিজে অন্য কোনো function call করে না — ARM64-তে সম্পূর্ণ zero memory access নিয়ে সম্পন্ন হতে পারে তার নিজের entry/exit-এ: bl করলে x30-এ ঠিকানা বসে, function তার কাজ করে (শুধু register-এ, যদি memory-access দরকার না হয়), ret সরাসরি x30 থেকে ফেরত যায় — stack-এ কোনো push/pop লাগেই না। x86-64-তে একই leaf function হলেও call বাধ্যতামূলকভাবে stack-এ push করবে, ret pop করবে — এই memory traffic ISA-স্তরে বেক-ইন করা, এড়ানোর উপায় নেই।

Non-leaf function-এ কী হয়? যদি একটা function নিজে আরেকটা function call করে (bl আবার চালায়), তাহলে সমস্যা — দ্বিতীয় bl x30-কে ওভাররাইট করে দেবে, প্রথম function-এর নিজের return address হারিয়ে যাবে। তাই non-leaf function-কে নিজে থেকেই সচেতনভাবে x30-কে stack-এ সংরক্ষণ (save) করতে হয়, প্রথম নেস্টেড bl-এর আগে — সাধারণত stp (store pair) instruction দিয়ে, যা এক instruction-এ দুইটা register একসাথে push করে:

stp    x29, x30, [sp, #-16]!   ; frame pointer আর link register একসাথে সংরক্ষণ
bl     helper_function           ; এখন নিরাপদে nested call -- x30 বদলাবে, কিন্তু আমাদের কপি stack-এ নিরাপদ
...
ldp    x29, x30, [sp], #16       ; ফেরত আনা
ret                                ; এখন x30-এ আমাদের নিজের return address, সঠিকভাবে ফেরত যাবে

এটা পরের লেসনগুলোর (stack frame, prologue/epilogue) একটা পূর্বরূপ — সেখানে এই প্যাটার্ন বিস্তারিতভাবে আসবে।

ভেতরে কী ঘটছে

Load-store architecture — কঠোর নিয়ম, কংক্রিট উদাহরণ

গত module-এর risc-vs-cisc লেসনে load-store architecture-এর সংজ্ঞা এসেছিল তাত্ত্বিকভাবে — “শুধু LOAD/STORE memory স্পর্শ করে, arithmetic শুধু register-এ।” ARM64 এই নিয়ম কঠোরভাবে মেনে চলে, কোনো ব্যতিক্রম নেই — এবার একটা সম্পূর্ণ concrete উদাহরণ দিয়ে দেখি ঠিক কী পার্থক্য তৈরি করে।

একটা সহজ C statement — একটা pointer-এর মাধ্যমে একটা int বাড়ানো:

void increment(int *p) {
    *p += 1;
}

x86-64 (একটাই instruction, memory-তে সরাসরি read-modify-write):

add    dword ptr [rdi], 1      ; একটা instruction: memory থেকে পড়ো, ১ যোগ করো, memory-তে ফেরত লেখো
ret

ARM64 (তিনটা instruction, প্রতিটা ধাপ আলাদা, স্পষ্ট):

ldr    w8, [x0]        ; ধাপ ১ -- memory থেকে register-এ পড়ো (LOAD)
add    w8, w8, #1      ; ধাপ ২ -- register-এই arithmetic (memory স্পর্শ নেই)
str    w8, [x0]        ; ধাপ ৩ -- register থেকে memory-তে লেখো (STORE)
ret
x86-64 (register-memory, CISC heritage):
  memory ──read+modify+write──▶ memory     (১টা instruction, hardware ভেতরে জটিল)

ARM64 (load-store, RISC principle):
  memory ──read──▶ register ──arithmetic──▶ register ──write──▶ memory
  (ldr)                        (add)                    (str)
  ৩টা instruction, প্রতিটা hardware সরল, uniform
একই কাজ -- x86-64-র এক-instruction register-memory approach বনাম ARM64-র তিন-instruction load-store approach।

এটাই এই module আর গত module জুড়ে বারবার ফিরে আসা থিমের একটা নিখুঁত, হাতে-ধরা উদাহরণ — জটিলতা কোথাও না কোথাও থাকতেই হয়, প্রশ্ন শুধু কোথায়। x86-64-এ hardware-এর ভেতরে (একটা add [mem], reg-কে internally মাইক্রো-অপারেশনে ভাঙতে হয় — memory-read + add + memory-write, নাম যাই দেওয়া হোক, hardware ভেতরে ঠিক ARM64-র মতোই তিনটা ধাপ চালায়, কিন্তু প্রোগ্রামারের/compiler-এর কাছে একটাই instruction হিসেবে দেখায়)। ARM64-এ এই তিনটা ধাপ explicitly, compiler/assembler-স্তরে দৃশ্যমান — hardware সরল থাকে (প্রতিটা instruction একটাই সরল কাজ করে), কিন্তু compiler-কে বেশি instruction generate করতে হয়।

Condition flags — NZCV, x86-64-র সাথে সমান্তরাল

গত লেসনে x86-64-র flags register (ZF/CF/OF/SF) দেখেছিলেন। ARM64-র সমতুল্য — NZCV (Negative, Zero, Carry, oVerflow) — একটা PSTATE register-এর অংশ, একই ধারণা, ভিন্ন নাম আর সামান্য ভিন্ন bit-ক্রম:

ARM64 flagসমতুল্য x86-64 flagঅর্থ
NSFফলাফল ঋণাত্মক (sign bit)
ZZFফলাফল শূন্য
CCFUnsigned overflow/borrow
VOFSigned overflow

গুরুত্বপূর্ণ পার্থক্য — ARM64-তে সাধারণ arithmetic instruction ডিফল্টে flags বদলায় না। গত module-এর registers-pc-flags লেসনের সেই ARM64-বিশেষ নিয়ম মনে করুন — add w0, w1, w2 flags স্পর্শ করে না; শুধু S-suffix ভ্যারিয়েন্ট (adds, subs) flags সেট করে। তুলনা করার জন্য নির্দিষ্ট cmp instruction আছে (ভেতরে আসলে subs with result discarded, ঠিক x86-64-র cmp-এর মতোই ধারণাগতভাবে):

cmp    w0, w1           ; w0 - w1 গণনা করে, শুধু NZCV আপডেট করে, ফলাফল ফেলে দেয়
b.lt   .target           ; N ≠ V হলে jump (signed "কম", ঠিক x86-64-র SF≠OF-এর সমান্তরাল)
ARM64 conditional branchশর্তসমতুল্য x86-64
b.eqZ=1je
b.neZ=0jne
b.ltN≠Vjl
b.geN=Vjge
b.gtZ=0 এবং N=Vjg
b.loC=0 (unsigned কম)jb
b.hiC=1 এবং Z=0 (unsigned বেশি)ja

এই টেবিল গত লেসনের x86-64 flag-টেবিলের প্রায় হুবহু একটা “অনুবাদ” — signed condition (N≠V) আর x86-64-র (SF≠OF) গাণিতিকভাবে ঠিক একই যুক্তি প্রকাশ করে, শুধু flag-এর নাম ভিন্ন।

Addressing mode — load-store-এর ভেতরেই সমৃদ্ধি

Load-store architecture মানে এই না যে ARM64-র addressing সীমিত — বরং সব addressing-জটিলতা ldr/str-এর ভেতরেই কেন্দ্রীভূত। গত লেসনের sum_array উদাহরণে দেখা ldr w4, [x0, x3, lsl #2] একটা register-offset-with-shift addressing mode-এর উদাহরণ — x0 (base) + x3 (index) shifted left by ২ (lsl #2, অর্থাৎ ×৪, ঠিক x86-64-র scale-৪-এর সমতুল্য, int-এর আকারের জন্য)।

Addressing modeসিনট্যাক্সব্যবহার
Base register[x0]সরাসরি ঠিকানা
Base + immediate offset[x0, #16]struct field access
Base + register (scaled)[x0, x1, lsl #2]array indexing
Pre-indexed[x0, #16]!ঠিকানা প্রথমে বাড়িয়ে তারপর access, base আপডেট হয়ে যায়
Post-indexed[x0], #16আগে access, তারপর base বাড়ে — loop-এ pointer-walk-এর জন্য উপযোগী

Pre/post-indexed addressing একটা বাড়তি সুবিধা — একটা instruction-ই memory access আর pointer-update দুটোই করে ফেলে (ldr w1, [x0], #4 — পড়ো, তারপর x0 স্বয়ংক্রিয়ভাবে ৪ বাড়াও, পরের array element-এর জন্য প্রস্তুত) — একটা loop-এ আলাদা add x0, x0, #4 লেখার প্রয়োজনই থাকে না। এটা “load-store মানেই কম সুবিধাজনক” ধারণার একটা প্রতিষেধক — RISC hardware সরল রেখেও ergonomic addressing সম্ভব।

উদাহরণ

add ফাংশন — দুই ISA-তে, পাশাপাশি

সবচেয়ে ছোট সম্ভব উদাহরণ দিয়ে শুরু — দুইটা int যোগ করা:

int add(int a, int b) {
    return a + b;
}

x86-64 (-O2, System V ABI-তে a আসে edi, b আসে esi):

add:
    lea    eax, [rdi+rsi]
    ret

কম্পাইলার এখানে গত লেসনের lea-ট্রিক ব্যবহার করেছে — সরাসরি একটা add-এর বদলে lea, কারণ lea flags স্পর্শ করে না (একটা ছোট optimization, পরের instruction-এ flags-এর দরকার না থাকলে) আর একই cycle-এ addition করে ফেলতে পারে ঠিকানা-গণনা-hardware ব্যবহার করে।

ARM64 (-O2, AAPCS64-তে a আসে w0, b আসে w1, ফলাফল w0-এ):

add:
    add    w0, w0, w1
    ret

লক্ষ করুন — ARM64-র সংস্করণ এখানে এক লাইনেই সোজাসুজি, x86-64-র lea-ট্রিকের মতো কোনো পরোক্ষ কৌশল লাগেনি (কারণ ARM64-এ সাধারণ add ডিফল্টে flags-ও বদলায় না, তাই কোনো optimization-প্রয়োজন ছিল না একটা সরাসরি add-এর বদলে অন্য কিছু বেছে নেওয়ার)। আর ret-এ কোনো stack access নেই — add একটা leaf function (নিজে কাউকে call করে না), তাই x30/lr-এই return address আছে, bl-এর সময় থেকেই, কোনো push/pop লাগেনি।

sum_array — গত লেসনের ঠিক সেই ফাংশন, এবার ARM64-তে

গত লেসনে এই একই ফাংশন x86-64-এ AT&T আর Intel দুই syntax-এই দেখেছিলাম। এখন ARM64-তে — তিনটা ISA/syntax-এর পূর্ণ তুলনার সুযোগ:

int sum_array(int *arr, int n) {
    int total = 0;
    for (int i = 0; i \< n; i++) {
        total += arr[i];
    }
    return total;
}

AAPCS64-তে arr আসে x0-এ (pointer, তাই ৬৪-বিট), n আসে w1-এ। -O2-ঘরানার output:

sum_array:
    cmp    w1, #0
    ble    .L_end
    mov    w2, #0          ; total = 0
    mov    x3, #0          ; i = 0 (৬৪-বিটে, ঠিকানা-গণনার জন্য)
.L_loop:
    ldr    w4, [x0, x3, lsl #2]   ; w4 = arr[i]  -- base+index shifted-left-2 addressing
    add    w2, w2, w4              ; total += arr[i]  -- সম্পূর্ণ register-এ
    add    x3, x3, #1              ; i++
    cmp    w3, w1
    blt    .L_loop
    mov    w0, w2
    ret
.L_end:
    mov    w0, #0
    ret
x86-64 (AT&T, গত লেসন):
    movl    (%rdx,%rax,4), %eax    ; একটা instruction: memory থেকে পড়া
    addl    %eax, -4(%rbp)         ; একটা instruction: সরাসরি memory-তে যোগ (register-memory!)

ARM64 (এই লেসন):
    ldr     w4, [x0, x3, lsl #2]   ; আলাদা LOAD
    add     w2, w2, w4              ; আলাদা arithmetic, সম্পূর্ণ register-এ
তিনটা সংস্করণ, একই ফাংশন -- একটা মূল পার্থক্য চিহ্নিত করার জন্য।

এই একটা তুলনাই এই module-এর তিনটা লেসনের মূল থিম একসাথে দেখায় — একই উচ্চ-স্তরের কোড, তিনটা ভিন্ন পথে machine code হয়ে ওঠে, প্রতিটা তার নিজস্ব ISA-দর্শনের সাথে সামঞ্জস্যপূর্ণ।

-O0 বনাম -O2 — এমনকি leaf function-ও frame পায়, optimization ছাড়া

উপরের add আর sum_array উদাহরণ দুটোই -O2 (optimized) output ছিল। কৌতূহলবশত -O0 (কোনো optimization ছাড়া, ডিবাগিং-বান্ধব) দিয়ে কম্পাইল করলে add-এর মতো একটা ছোট্ট leaf function-ও একটা সম্পূর্ণ frame-setup পায়:

add:                                    ; -O0, optimization ছাড়া
    sub    sp, sp, #16          ; local variable-এর জন্য stack জায়গা বরাদ্দ
    str    w0, [sp, #12]        ; a-কে stack-এ সংরক্ষণ (unused optimization skip না করে)
    str    w1, [sp, #8]         ; b-কে stack-এ সংরক্ষণ
    ldr    w0, [sp, #12]        ; a আবার পড়া
    ldr    w1, [sp, #8]         ; b আবার পড়া
    add    w0, w0, w1           ; আসল কাজ -- বাকি সবই "নিরাপদ কিন্তু অপ্রয়োজনীয়"
    add    sp, sp, #16          ; stack জায়গা ফেরত
    ret

এই -O0 সংস্করণে add (নিজে leaf function হওয়া সত্ত্বেও) x30-কেই স্পর্শ করেনি (কারণ কোনো nested call নেই) — কিন্তু তবুও a, b-কে অপ্রয়োজনীয়ভাবে stack-এ store করে আবার load করছে, যেটা -O2-এ সম্পূর্ণ বাদ পড়ে গিয়েছিল (গত লেসনের example section-এ দেখা এক-লাইনের add w0, w0, w1 সংস্করণ)।

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

EXPERIMENT

Compiler Explorer দিয়ে x86-64 বনাম ARM64 পাশাপাশি দেখুন

যেকোনো ব্রাউজার (godbolt.org), বা Linux-এ cross-compiler থাকলে টার্মিনালেও· ১৫ মিনিট

পদ্ধতি ১ — Compiler Explorer (সবচেয়ে সহজ, কোনো ইনস্টল লাগে না):

১. godbolt.org খুলুন
২. এই লেসনের increment() ফাংশন পেস্ট করুন:
       void increment(int *p) { *p += 1; }
৩. বাম প্যানেলে compiler x86-64 gcc (trunk) নির্বাচন করুন, -O2 flag দিন
৪. একটা দ্বিতীয় compiler panel যোগ করুন ("+ Add compiler"), সেখানে
   ARM64 gcc (trunk) নির্বাচন করুন, একই -O2 flag
৫. দুই panel পাশাপাশি তুলনা করুন -- x86-64-এ একটা add [mem], imm,
   ARM64-এ ldr/add/str তিনটা আলাদা লাইন দেখবেন

পদ্ধতি ২ — নিজের মেশিনে cross-compiler (যদি ইনস্টল থাকে, Linux-এ):

# ARM64 cross-compiler ইনস্টল থাকলে (Debian/Ubuntu-তে):
# sudo apt install gcc-aarch64-linux-gnu

aarch64-linux-gnu-gcc -S -O2 increment.c -o increment_arm64.s
cat increment_arm64.s

gcc -S -O2 increment.c -o increment_x86.s
cat increment_x86.s

# তুলনা করুন -- ARM64-তে অন্তত দুইটা instruction (ldr+add+str-এর কোনো
# সমন্বয়) দেখবেন, x86-64-তে একটাই add

Apple Silicon Mac ব্যবহারকারীদের জন্য (নিজেই native ARM64):

gcc -S -O2 increment.c -o increment.s    # সরাসরি ARM64 output, cross-compiler লাগবে না
cat increment.s
এটা কী প্রমাণ করে

load-store architecture-এর দাবি সত্যি -- একই C কোডের ARM64 output-এ arithmetic instruction কখনো memory operand ব্যবহার করে না, শুধু ldr/str করে -- এটা একটা তাত্ত্বিক নিয়ম না, প্রতিটা কম্পাইল করা ফাংশনে পর্যবেক্ষণযোগ্য প্যাটার্ন।

নিজে বানান

BUILD IT

Load-store নিয়ম নিজে ভাঙার চেষ্টা করুন -- আর ব্যর্থ হওয়াটাই শিক্ষা

ARM64 assembly (as, aarch64-linux-gnu-as বা Compiler Explorer) · ●●○○○
  1. একটা ছোট ARM64 assembly ফাইল লিখুন যেখানে ইচ্ছাকৃতভাবে একটা arithmetic instruction-এ সরাসরি memory operand দেওয়ার চেষ্টা করুন, যেমন add w0, [x1], w2 -- এটা বৈধ ARM64 syntax না
  2. as দিয়ে (aarch64 target) অ্যাসেম্বল করার চেষ্টা করুন, assembler error বার্তা পড়ুন
  3. এবার সঠিকভাবে তিন-ধাপে লিখুন -- ldr দিয়ে memory থেকে পড়া, add দিয়ে register arithmetic, str দিয়ে ফেরত লেখা
  4. এই সংস্করণ সফলভাবে অ্যাসেম্বল করে objdump -d দিয়ে machine code দেখুন
  5. গণনা করুন -- এই তিন-instruction সংস্করণ কতগুলো memory access করে (২টা -- একটা ldr, একটা str), x86-64-র এক-instruction সংস্করণ কতগুলো করত (also ২টা, ভেতরে -- কিন্তু programmer/compiler-এর কাছে একটাই instruction হিসেবে দেখা যায়)

মূল লক্ষ্য — শুধু পড়ে “load-store মানে memory-arithmetic নিষিদ্ধ” মেনে না নিয়ে, নিজে ভাঙার চেষ্টা করে, assembler-এর প্রত্যাখ্যান নিজের চোখে দেখা।

// ভুল -- ARM64-এ এমন কোনো syntax নেই (arithmetic instruction memory operand নেয় না)
add    w0, [x1], w2

// সঠিক -- তিন ধাপ
ldr    w3, [x1]
add    w0, w3, w2
str    w0, [x1]
aarch64-linux-gnu-as broken.s -o broken.o
# error: expected register, got memory reference -- বা অনুরূপ সিনট্যাক্স-এরর

aarch64-linux-gnu-as fixed.s -o fixed.o    # সফল
objdump -d fixed.o

একটা আকর্ষণীয় পর্যবেক্ষণ: যদিও x86-64-র add [rdi], eax “একটা instruction” দেখতে, ভেতরে CPU-কে এখনও একটা memory-read আর একটা memory-write করতেই হয় (ARM64-র মতোই মোট memory-access সংখ্যা একই) — পার্থক্যটা programmer-এর/compiler-এর কাছে দৃশ্যমান জটিলতার পরিমাণ, hardware-এর প্রকৃত কাজের পরিমাণ না। এটাই RISC/CISC-এর “জটিলতা কোথায় রাখা হবে” থিমের একটা নিখুঁত, ছোট প্রমাণ।

বাস্তব সিস্টেমে

যেখানে ARM64 প্রতিদিন চলছে

Apple Silicon (M1/M2/M3/M4)। ২০২০ সাল থেকে প্রতিটা নতুন Mac ARM64-চালিত — Apple-এর নিজস্ব ডিজাইন করা core, একই ARM64 ISA যা এই লেসনে শেখানো হয়েছে, যদিও microarchitecture সম্পূর্ণ Apple-এর নিজস্ব।

প্রায় প্রতিটা Android ফোন। Android ecosystem-এর প্রায় সম্পূর্ণটাই ARM64-চালিত (কিছু পুরনো ডিভাইস ৩২-বিট ARM/AArch32 এখনো ব্যবহার করে, কিন্তু আধুনিক সব ফ্ল্যাগশিপ AArch64)।

AWS Graviton — ক্লাউড সার্ভারে ARM64। Amazon-এর নিজস্ব ডিজাইন করা Graviton প্রসেসর সিরিজ ARM64-ভিত্তিক, বিশেষভাবে ভালো performance-per-watt-এর জন্য পরিচিত — অনেক production workload এখন x86-64 থেকে ARM64-তে স্থানান্তরিত হচ্ছে ঠিক এই কারণে, cloud খরচ কমানোর একটা বাস্তব প্রকৌশল-সিদ্ধান্ত।

Raspberry Pi 4/5 (64-bit mode)। শিক্ষামূলক এবং hobbyist কমিউনিটিতে সবচেয়ে জনপ্রিয় single-board computer, এখন ডিফল্টে ৬৪-বিট ARM64 OS চালায় — এই লেসনের সব উদাহরণ সরাসরি একটা Raspberry Pi-তে নিজে চালিয়ে দেখা সম্ভব।

iPhone-এর A-সিরিজ চিপ। Apple-এর মোবাইল প্রসেসর সিরিজ, ARM64-ভিত্তিক — একটা iPhone-এর প্রতিটা অ্যাপ শেষমেশ এই লেসনের ldr/str/add instruction-এই কম্পাইল হয়ে চলছে।

Nintendo Switch। এর Tegra প্রসেসর (Cortex-A57 cores) ARMv8-A architecture-এর, অর্থাৎ AArch64 সমর্থন করে — একটা গেম কনসোল থেকে শুরু করে সবচেয়ে শক্তিশালী cloud সার্ভার পর্যন্ত, একই মৌলিক ISA পরিবারের বিস্তৃতি দেখায়।

Rosetta 2 — গত লেসনের forward-reference-এর পূর্ণাঙ্গ প্রেক্ষাপট। Apple-এর Rosetta 2 x86-64 binary-কে (Intel Mac-এর জন্য কম্পাইল করা পুরনো software) চলার সময় ARM64 machine code-এ অনুবাদ করে (dynamic binary translation) — এই একই লেসনের সব পার্থক্য (register-memory বনাম load-store, syntax differences না হলেও ISA-স্তরের semantic গ্যাপ) তাকে সামলাতে হয় প্রতিটা translated instruction-এ, বিশেষভাবে জটিল x86-64-র সেই memory-directly-touching instruction-গুলোর জন্য, যেগুলোকে ARM64-এর একাধিক ldr/arithmetic/str-এ ভাঙতে হয় — এই লেসনের increment() উদাহরণের ঠিক এই রূপান্তরটাই Rosetta 2-কে প্রতিনিয়ত করতে হয়, রানটাইমে।

big.LITTLE / heterogeneous computing। আধুনিক ARM64 SoC (System-on-Chip)-এ প্রায়ই দুই ধরনের core থাকে — “big” (দ্রুত, বেশি power-খরচ) আর “LITTLE” (ধীর, কম power) — দুটোই একই ARM64 ISA চালায়, তাই একই compiled binary যেকোনো core-এ চলতে পারে, OS scheduler কাজের প্রকৃতি অনুযায়ী বেছে নেয় কোন core-এ পাঠাবে — একটা ISA-স্তরের uniformity যা এই ধরনের heterogeneous hardware ডিজাইন সম্ভব করে।

যে ভুলগুলো সবাই করে

“ARM64-তেও নিশ্চয়ই AT&T আর Intel-এর মতো কোনো syntax বিভাজন আছে -- যেকোনো CPU architecture-এই এমন হওয়া স্বাভাবিক।”

এই লেসনের শুরুতেই যা প্রতিষ্ঠিত হয়েছিল — এই বিভাজন x86-64-র একটা নির্দিষ্ট ঐতিহাসিক দুর্ঘটনা (Unix AT&T-ঐতিহ্য বনাম Intel-এর independently-বিকশিত official manual), সব ISA-র সহজাত বৈশিষ্ট্য না।

ARM64-এর একটাই canonical syntax — Arm Ltd-র নিজস্ব official Architecture Reference Manual-এ প্রকাশিত, আর GNU টুলচেইনসহ সব প্রধান assembler/disassembler এটাই অনুসরণ করে, কোনো প্রতিদ্বন্দ্বী কনভেনশন ছাড়াই। RISC-V-রও (গত module-এ পরিচিত) একটাই canonical syntax — এটাই বরং বেশিরভাগ আধুনিক ISA-র স্বাভাবিক অবস্থা; x86-64-র দ্বৈততাই ব্যতিক্রম, নিয়ম না।

“Load-store architecture মানে ARM64 arithmetic-এ 'দুর্বল' -- x86-64 এক instruction-এ যা করে, ARM64-তে তিনটা লাগে, তাই তিনগুণ ধীর।”

এই লেসনের hood সেকশনের warn Callout-এই এই ভুল ধারণার প্রতিষেধক দেওয়া হয়েছিল — instruction সংখ্যা সরাসরি speed-এর মাপকাঠি না (গত module-এর risc-vs-cisc লেসনেও এই একই ভুল ধারণা চিহ্নিত হয়েছিল)।

x86-64-র “এক instruction” add [mem], reg-ও ভেতরে হার্ডওয়্যার-স্তরে memory-read + arithmetic + memory-write আলাদাভাবেই করে — শুধু programmer/compiler-এর কাছে একটা instruction হিসেবে “প্যাকেজড” দেখায়। ARM64-র তিন instruction-এর প্রতিটাই সরল, uniform, দ্রুত-decode — যেখানে x86-64-র জটিল instruction-এর decode/execution hardware নিজেই বাড়তি জটিল (length-decode সমস্যা, গত module-এর risc-vs-cisc লেসনের বিষয়)। প্রকৃত পারফরম্যান্স নির্ভর করে বাস্তব pipeline depth, cache behavior, আর নির্দিষ্ট microarchitecture-এর উপর — কোনো সরল instruction-গণনা-ভিত্তিক সিদ্ধান্তে পৌঁছানো ভুল।

“Link register (x30/lr) মানে ARM64-তে function call-এ কখনো stack লাগে না -- পুরো stack ধারণাটাই এখানে অপ্রয়োজনীয়।”

এই লেসনের concept সেকশনেই স্পষ্ট করা হয়েছিল — এই সুবিধা শুধু leaf function-এর জন্য প্রযোজ্য।

যেকোনো non-leaf function (যেটা নিজে অন্তত একটা function call করে) অবশ্যই তার নিজের x30 stack-এ সংরক্ষণ করতে হবে, প্রথম nested bl-এর আগে — নাহলে সেই nested call নিজের return address x30-এ লিখে আগেরটা মুছে ফেলবে। বাস্তব প্রোগ্রামে বেশিরভাগ function-ই non-leaf (একে অপরকে call করে), তাই ARM64-তে stack ব্যবহার এখনো সর্বব্যাপী — শুধু leaf function-এর একটা নির্দিষ্ট, সাধারণ কিন্তু সার্বজনীন-না উপশ্রেণির জন্য এই “stack-মুক্ত call” সুবিধাটা প্রযোজ্য।

“w0-w30 আর x0-x30 আসলে ভিন্ন, স্বাধীন রেজিস্টার -- ঠিক যেমন x86-64-এ eax আর rax কিছুটা ভিন্ন সত্তা মনে হতে পারে।”

এটা একটা ভুল সমান্তরাল টানা — ARM64-তে w0 আর x0 একই একটা physical register-এর দুইটা ভিন্ন-প্রস্থ view, x86-64-র মতোই আসলে (eax/raxও একই physical register-এর view), কিন্তু ARM64-র নিয়মটা অনেক বেশি uniform আর predictable — প্রতিটা xN-এর ঠিক একটা wN counterpart (নিচের ৩২ বিট), কোনো exception নেই, কোনো ah-এর মতো বিশেষ upper-byte-only view নেই, কোনো r8-r15-এর মতো “নতুন register, পুরনো ঐতিহাসিক নাম নেই” অসামঞ্জস্যও নেই।

এই সরলতাই এই লেসনের objective-এ উল্লেখিত মূল শিক্ষণীয় পয়েন্ট — sub-register aliasing ধারণাগতভাবে দুই ISA-তেই এক (একই physical storage-এর ভিন্ন-প্রস্থ access), কিন্তু ARM64-র বাস্তবায়ন x86-64-র চেয়ে অনেক বেশি clean, ঐতিহাসিক জঞ্জাল-মুক্ত।

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

1

ARM64-তে w5-এ 0xFFFFFFFF লেখা হলো। এর আগে x5 ছিল 0x123456789ABCDEF0। এখন x5-এর মান কত?

স্মরণ

x5 = 0x00000000FFFFFFFF

গত লেসনের x86-64-র zero-extension নিয়মের ঠিক সমান্তরাল একটা নিয়ম ARM64-তেও প্রযোজ্য — একটা ৩২-বিট sub-register (w5)-এ লেখা স্বয়ংক্রিয়ভাবে ৬৪-বিট register (x5)-এর উপরের ৩২ বিট শূন্য করে দেয়। তাই আগের 0x12345678 (উপরের অর্ধেক) হারিয়ে যায়, নতুন মান হয় 0x00000000FFFFFFFF — শুধু নিচের ৩২ বিট (0xFFFFFFFF) থেকে যায়, উপরের বিট শূন্যে রিসেট হয়।

2

নিচের ARM64 assembly-তে একটা সিনট্যাক্স ভুল আছে — এটা assemble করার চেষ্টা করলে error দেবে। কোন লাইনে সমস্যা, আর কেন?

myFunc:
    ldr    w0, [x1]
    add    w0, [x1], w2
    str    w0, [x1]
    ret
যুক্তি

দ্বিতীয় লাইন (add w0, [x1], w2) ভুল — এটা load-store architecture-এর একটা মৌলিক নিয়ম ভাঙছে।

ARM64-এ add একটা arithmetic instruction — এই ধরনের instruction কখনোই সরাসরি memory operand ([x1]-এর মতো bracket-syntax) নিতে পারে না। শুধু ldr/str (আর তাদের কিছু নির্দিষ্ট আত্মীয়, যেমন ldp/stp) memory স্পর্শ করার অনুমতি পায়। এটা এই লেসনের হুড সেকশনের কেন্দ্রীয় নিয়মের সরাসরি লঙ্ঘন — x86-64-এ add [mem], reg-এর মতো একটা instruction সম্পূর্ণ বৈধ, কিন্তু ARM64-এ এমন কিছুর অস্তিত্বই নেই।

সঠিক সংস্করণ (মনে রাখুন — মূল উদ্দেশ্যটা সম্ভবত *x1 += w2 করা ছিল):

myFunc:
    ldr    w0, [x1]        ; load
    add    w0, w0, w2      ; register-only arithmetic
    str    w0, [x1]        ; store
    ret

(লক্ষ করুন প্রথম আর তৃতীয় লাইন আগেই সঠিক ছিল — শুধু মাঝের arithmetic লাইনটাই ভুলভাবে একটা memory operand অন্তর্ভুক্ত করার চেষ্টা করেছিল।)

3

একটা ARM64 function f() নিজে কোনো অন্য function call করে না (leaf function)। এটা কল হওয়ার সময় (bl f) আর ফেরত আসার সময় (ret) মোট কতটা memory access হয় — শুধু return-address সামলানোর জন্য (function-এর নিজস্ব কাজের memory access বাদ দিয়ে)? x86-64-তে একই leaf function-এর জন্য এই সংখ্যাটা কত হতো?

প্রয়োগ

ARM64: শূন্য (0) memory access।

bl f return address সরাসরি x30/lr register-এ বসায় — এটা একটা register-write, memory-access না। ret সরাসরি x30-এর মান পড়ে PC-তে বসায় — এটাও একটা register-read, memory-access না। যেহেতু f() leaf function (নিজে bl চালায় না), তার x30 কখনো ওভাররাইট হওয়ার ঝুঁকি নেই, তাই তাকে stack-এ সংরক্ষণ করারও দরকার নেই — সম্পূর্ণ call/return cycle-এ return-address-সংক্রান্ত কোনো memory access ঘটে না

x86-64: দুইটা (2) memory access।

call f return address অবশ্যই stack-এ push করে — একটা memory write (rsp কমিয়ে, সেই ঠিকানায় লেখা)। ret সেই ঠিকানা stack থেকে pop করে — একটা memory read। x86-64-র ISA-design-এ এই push/pop বাধ্যতামূলক, leaf function হোক বা না হোক — কোনো ব্যতিক্রম নেই।

এই পার্থক্যটাই (২ বনাম ০) এই লেসনের link register আলোচনার কেন্দ্রীয় পরিমাণগত পয়েন্ট — বাস্তব প্রোগ্রামে অসংখ্য ছোট leaf function (getter, helper, recursive base case) থাকে, আর প্রতিটাতে ARM64 এই memory-access সম্পূর্ণ এড়িয়ে যায়, যেখানে x86-64-কে (আধুনিক Return-Stack-Buffer prediction থাকা সত্ত্বেও) প্রতিটাতেই আসল memory traffic বহন করতে হয়।

4

আপনি একটা recursive function ডিজাইন করছেন যা নিজেকেই বারবার call করে (যেমন factorial বা Fibonacci)। ARM64-তে, এই function-এর প্রতিটা recursive call-এ কি x30 stack-এ সংরক্ষণ করা লাগবে? যুক্তি দিন।

ডিজাইন

হ্যাঁ, প্রতিটা recursive call-এই x30 stack-এ সংরক্ষণ করা লাগবে — কারণ একটা recursive function সংজ্ঞা অনুযায়ীই non-leaf: এটা নিজেকে (বা অন্তত একটা function-কে, এক্ষেত্রে নিজেকেই) call করে, তাই এটা leaf function-এর সেই বিশেষ “stack-মুক্ত” সুবিধা পায় না।

কেন প্রয়োজন, ধাপে ধাপে:

factorial(n) যখন নিজেকে factorial(n-1) হিসেবে call করে (একটা bl factorial instruction দিয়ে), সেই bl-এর কারণে x30-এ নতুন একটা return address বসে যাবে (এই recursive call-এর জন্য) — কিন্তু factorial(n)-এর নিজের return address (যেখানে তাকে ফিরে যেতে হবে, তার caller-এর কাছে) তখন সেই একই x30-এই ছিল, ওভাররাইট হয়ে যাবে যদি আগে সংরক্ষণ না করা হয়।

তাই factorial-এর prologue-এ (function শুরুতে) x30-কে stack-এ push করতে হবে (সাধারণত stp x29, x30, [sp, #-16]! দিয়ে, frame pointer-এর সাথে একসাথে), recursive bl factorial call করার আগেই। প্রতিটা recursion-স্তর তার নিজস্ব stack frame-এ নিজস্ব x30-এর কপি রাখে — n স্তরের recursion মানে nটা আলাদা সংরক্ষিত return address, স্বাভাবিকভাবেই stack-এর মতোই (LIFO) সংগঠিত।

সংক্ষেপে: link register-এর “memory-মুক্ত” সুবিধা শুধুই leaf function-এ প্রযোজ্য একটা optimization — recursive function-এর মতো inherently non-leaf ক্ষেত্রে ARM64-ও x86-64-র মতোই stack-নির্ভর, কোনো “জাদু” এড়ানোর উপায় নেই।

5

নিচের ARM64 assembly-তে b.lt-এর ঠিক আগে কোন instruction চলেছিল বলে ধরে নেওয়া যায়, আর N/V flag-এর কোন combination-এ jump নেওয়া হবে?

    cmp    w0, w1
    b.lt   .target
যুক্তি

cmp w0, w1 অবশ্যই b.lt-এর ঠিক আগে চলেছিল — কারণ b.lt NZCV flags পড়ে সিদ্ধান্ত নেয়, আর flags শুধু cmp (বা S-suffix arithmetic, যেমন adds, subs)-এর মতো instruction দিয়েই আপডেট হয় (এই লেসনের হুড সেকশনে দেখানো হয়েছিল — ARM64-এ সাধারণ add/sub ডিফল্টে flags বদলায় না)।

cmp w0, w1 ভেতরে w0 - w1 গণনা করে (ফলাফল ফেলে দেয়, শুধু flags বসায়)। b.lt জাম্প নেবে যখন N ≠ V (এই লেসনের condition-টেবিল অনুযায়ী, signed “কম” নির্ধারণের নিয়ম) — অর্থাৎ w0 \< w1 (signed তুলনায়) হলে।

এই নিয়মটা গত লেসনের x86-64-র jl (SF ≠ OF হলে jump) নিয়মের গাণিতিকভাবে হুবহু সমতুল্য — শুধু flag-নাম ভিন্ন (N/V বনাম SF/OF), অন্তর্নিহিত hardware-যুক্তি অভিন্ন: signed subtraction-এর sign bit যদি “প্রত্যাশিত” overflow bit-এর সাথে না মেলে, প্রকৃত গাণিতিক ফলাফল ঋণাত্মক।

এরপর কী

পরের বিষয় — Register ব্যবহার একটা কনভেনশনে পরিণত হওয়া

এই তিনটা লেসন জুড়ে আমরা দেখেছি pipeline (assembler-linker-loader), তারপর দুইটা প্রতিদ্বন্দ্বী কিন্তু সমমূল্যের syntax (AT&T/Intel), আর এখন একটা সম্পূর্ণ ভিন্ন ISA-দর্শন (ARM64-র load-store, single-syntax approach) — তিনটাই এই module-এর driving question-এর একেকটা টুকরো উত্তর দিয়েছে: আমার C function compile হয়ে ঠিক কোন instruction-গুলো হলো, আর কেন?

কিন্তু একটা প্রশ্ন এখনো অনুত্তরিত রয়ে গেছে, যা এই লেসনের link register আলোচনায় বারবার ছুঁয়ে গেছে — যখন একটা function call হয় (call/bl), argument (যেমন a, b) ঠিক কোন register-এ বসে? কে ঠিক করে a edi/w0-এ যাবে, b esi/w1-এ? কোন register caller-কে (কল-কারীকে) সংরক্ষণ করতে হয়, কোনটা callee-কে (কল-হওয়া ফাংশনকে)? এই প্রশ্নগুলোর উত্তর কোনো hardware-নিয়ম না — এটা একটা সফটওয়্যার কনভেনশন, যাকে বলে calling convention (x86-64-এ System V ABI, ARM64-এ AAPCS64) — module-এর পরবর্তী বিষয়গুলোর একটা, যেখানে এই লেসনগুলোতে যা “ধরে নেওয়া হয়েছিল” (যেমন a edi-তে আসে) তার প্রকৃত নিয়ম-বই উন্মোচিত হবে, সাথে stack frame, prologue/epilogue-এর সম্পূর্ণ প্রক্রিয়া — এই লেসনের stp x29, x30-এর মতো প্যাটার্নগুলো তখন তাদের পূর্ণাঙ্গ প্রেক্ষাপট পাবে।

আরও পড়ুন

  • Arm Architecture Reference Manual for A-profile Architecture — Arm Limited · AArch64 instruction set-এর প্রামাণ্য, সম্পূর্ণ বর্ণনা — এই লেসনের সব instruction encoding এখান থেকে যাচাইযোগ্য
  • Procedure Call Standard for the Arm 64-bit Architecture (AAPCS64) — Arm Limited · Register ব্যবহারের কনভেনশন (x0-x7 argument, x30 link register, callee-saved সেট) — পরের লেসনগুলোর ভিত্তি
  • Computer Organization and Design (RISC-V Edition) — RISC ISA design principle-এর সাধারণ আলোচনা — David A. Patterson, John L. Hennessy · Load-store architecture-এর সাধারণ RISC-দর্শন, যা ARM64-তেও প্রযোজ্য
  • Compiler Explorer (godbolt.org) · এই লেসনের experiment-এ ARM64 target সিলেক্ট করে x86-64-র সাথে পাশাপাশি তুলনার জন্য ব্যবহৃত