Foundationপ্রথম নীতি থেকে
LEVEL 4লেসন ১৪/২৯কঠিন১ ঘণ্টা ৫ মিনিট

Stack বনাম Heap — memory-র দুই সম্পূর্ণ ভিন্ন দর্শন

Stack and Heap

গত লেসনে আমরা heap-এর ভেতরের জগৎ দেখেছি — chunk, bin, arena, brk বনাম mmap। এই লেসনে stack-কে সরাসরি পাশে রেখে তুলনা করব: কেন stack allocation প্রায় বিনামূল্যে অথচ heap-এর প্রতিটা allocation-এ বই রাখতে হয়, কীভাবে guard page একটা runaway recursion-কে একটা নিয়ন্ত্রিত crash-এ পরিণত করে, alloca/VLA কেন বিপজ্জনক, আর কীভাবে sigaltstack দিয়ে stack overflow-কেই catch করা যায়।

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

  • একটা process-এর address space-এ stack আর heap-এর অবস্থান, growth direction, আর তাদের মাঝের gap নির্ভুলভাবে আঁকতে এবং ব্যাখ্যা করতে পারবেন
  • Stack allocation কেন heap allocation-এর চেয়ে বহুগুণ দ্রুত — bookkeeping structure, free-list traversal, আর fragmentation-এর অনুপস্থিতি/উপস্থিতি দিয়ে concretely derive করতে পারবেন, শুধু মুখস্থ না করে
  • Guard page কীভাবে একটা runaway stack overflow-কে নিয়ন্ত্রিত SIGSEGV-তে পরিণত করে তা ব্যাখ্যা করতে পারবেন, আর ulimit -s/RLIMIT_STACK দিয়ে সেই সীমা কীভাবে নিয়ন্ত্রণ করা হয় তা বলতে পারবেন
  • alloca()/VLA-র নির্দিষ্ট বিপদগুলো চিহ্নিত করতে পারবেন, আর Stack Clash-এর মতো বাস্তব দুর্বলতার সাথে guard page-এর সম্পর্ক ব্যাখ্যা করতে পারবেন
  • Thread stack size কীভাবে নির্ধারিত ও override হয় তা বলতে পারবেন, আর কেন হাজার হাজার thread স্পন করা একটা virtual-address-space সমস্যা হয়ে দাঁড়াতে পারে তা হিসাব করতে পারবেন
  • sigaltstack()/SA_ONSTACK দিয়ে stack overflow signal নিজেই catch করার একটা প্রোগ্রাম লিখতে পারবেন, আর ব্যাখ্যা করতে পারবেন কেন normal signal handler এই একটা নির্দিষ্ট ক্ষেত্রে ব্যর্থ হয়

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

আগে এটা বুঝি

গত লেসনের শেষে একটা প্রতিশ্রুতি ছিল — stack আর heap-কে পাশাপাশি রেখে দেখা। এখন রাখার সময়।

একটা সংখ্যা দিয়ে শুরু করা যাক। ধরুন দুইটা প্রোগ্রাম, দুটোই ১ কোটি বার একই কাজ করছে — একটা ৬৪ byte-এর সাময়িক বাফার নিচ্ছে, তাতে কিছু লিখছে, তারপর ছেড়ে দিচ্ছে। প্রথম প্রোগ্রাম প্রতিবার malloc(64) ডেকে free() করে। দ্বিতীয় প্রোগ্রাম প্রতিবার একটা function-এর ভেতরে char buf[64]; — একটা সাধারণ local array — ঘোষণা করে, ব্যবহার করে, function থেকে বেরিয়ে যায়।

গত লেসনের LayerTrace অনুযায়ী, malloc(64)-এর সবচেয়ে ভালো ক্ষেত্র (tcache hit) নিজেই লাগে প্রায় ২০-৩০ ns — একটা linked-list pop, কোনো syscall না। সেটা ইতিমধ্যেই দ্রুত। তবু দ্বিতীয় প্রোগ্রাম এর চেয়েও দ্রুত, প্রায়ই এমনভাবে যে তুলনাটা মাপারই দরকার হয় না — কারণ buf-এর জন্য জায়গা বরাদ্দ করতে কোনো runtime instruction-ই লাগে না যেটা শুধু buf-এর জন্য নির্দিষ্ট। পুরো function-এর সব local variable একসাথে একটা মাত্র sub rsp, N দিয়ে বরাদ্দ হয়ে যায়, একবারে, function শুরুতে — আর buf যতই বড় বা ছোট হোক, সেই একটা instruction-এর খরচ একই।

এই পার্থক্যটা কৌতূহলের বিষয় না — এটা প্রায় প্রতিটা perf-sensitive সিদ্ধান্তের ভিত্তি, আর প্রায় প্রতিটা systems-ইন্টারভিউতে জিজ্ঞাসিত প্রশ্ন: “stack বনাম heap — কোনটা দ্রুত, আর কেন?” বেশিরভাগ উত্তর থামে “stack দ্রুত কারণ এটা just a pointer bump”-এ — সত্যি, কিন্তু অসম্পূর্ণ। কেন এটা “just a pointer bump” হতে পারল, অথচ heap পারল না — এই প্রশ্নের উত্তরই এই লেসনের কেন্দ্রীয় derivation।

সাথে সাথে আরেকটা প্রশ্ন — stack-এর ওই “just a pointer bump” সুবিধাটার একটা মূল্য আছে। heap প্রায় সীমাহীন (virtual address space যতটা পাওয়া যায়), কিন্তু stack-এর একটা কড়া, প্রায়ই ৮ MB-এর কাছাকাছি সীমা — আর সেই সীমা ছাড়ালে কী ঘটে, কেন সেটা নীরব memory-করাপশন না হয়ে একটা পরিষ্কার crash হয়, আর সেই crash-কেও কীভাবে ধরা যায় — সবই এই লেসনে।

মূল ধারণা

Address space-এ দুইটা প্রতিবেশী, দুইটা ভিন্ন নিয়ম

গত মডিউলের virtual-memory-intro লেসন থেকে মনে করুন — প্রতিটা process তার নিজস্ব, ব্যক্তিগত virtual address space পায়। process-anatomy লেসনে সেই space-এর একটা সম্পূর্ণ মানচিত্র দেখেছিলেন: .text, .rodata, .data, .bss, তারপর heap, তারপর mmap region, তারপর stack। এই লেসনে আমরা সেই ছবির শুধু দুইটা অঞ্চলে জুম করব — heap আর stack — আর দেখব কেন তারা বিপরীত দিকে বাড়ে

উঁচু address
┌─────────────────────────┐
│   kernel space           │
├─────────────────────────┤
│                          │  ← current stack pointer (rsp)
│   [stack]                │     এখান থেকে নিচের দিকে বাড়ে
│         ↓ বৃদ্ধির দিক     │
├─────────────────────────┤  ← guard page (একটা বা একাধিক unmapped page)
│░░░ কোনো VMA নেই ░░░░░░░░│     touch করলেই SIGSEGV
├─────────────────────────┤  ← RLIMIT_STACK-এ নির্ধারিত সর্বোচ্চ গভীরতা
│                          │
│   (ফাঁকা জায়গা — ASLR gap, │
│    mmap region-ও এখানে)  │
│                          │
├─────────────────────────┤
│         ↑ heap            │  এখান থেকে উপরের দিকে বাড়ে
│                          │  ← current brk/program break
├─────────────────────────┤
│   .bss / .data / .text   │
└─────────────────────────┘
নিচু address
Stack আর heap — বিপরীত দিক থেকে একই ফাঁকা জায়গার দিকে এগোয়। Guard page স্ট্যাকের সবচেয়ে-নিচু ব্যবহৃত পাতার ঠিক নিচে, ইচ্ছাকৃতভাবে unmapped।

দুইটা অঞ্চল একই “মাঝখানের ফাঁকা জায়গার” দিকে বাড়ছে, বিপরীত দিক থেকে — এটা ইচ্ছাকৃত ডিজাইন, একটা পুরনো, ব্যবহারিক কারণে। একটা প্রোগ্রাম compile-করার সময় জানা থাকে না ঠিক কত heap বা কত stack লাগবে — কোনটা বেশি বাড়বে সেটাও আগে বলা যায় না। দুইটাকে বিপরীত প্রান্তে বসিয়ে মাঝখানে ফাঁকা রাখলে, দুইটাই তাদের নিজের প্রয়োজনমতো বাড়তে পারে, একে অপরের ভবিষ্যত জায়গা দখল না করে। (আধুনিক Linux-এ ASLR আর একটা mmap region এই ফাঁকা জায়গার মাঝেই বসে — কিন্তু মূল নীতি অপরিবর্তিত: heap আর stack একে অপরের দিকে “ধাক্কা” দেয় না, স্বাধীনভাবে বাড়ে।)

Stack বনাম heap — এক নজরে

StackHeap
Allocation policyকড়া LIFO — সবচেয়ে সাম্প্রতিক allocation-টাই সবার আগে free হয়যেকোনো ক্রম — allocate/free-এর ক্রমের কোনো বাধ্যবাধকতা নেই
কে বরাদ্দ করেCompiler-generated code (prologue-এর sub rsp, N)Library function (malloc/free, ptmalloc2-র bin/arena যন্ত্রপাতি)
Runtime bookkeepingকোনোটাই না — একটা register (rsp) নিজেই সব তথ্য বহন করেchunk header, bin, free-list, coalescing — গত লেসনের পুরো যন্ত্রপাতি
আকার কখন ঠিক হয়সাধারণত compile time-এ (VLA/alloca ব্যতিক্রম, নিচে আলোচিত)runtime-এ, প্রতিটা কলে ভিন্ন হতে পারে
আকারের সীমাছোট, নির্দিষ্ট (সাধারণত ~৮ MB প্রধান thread-এ, ulimit -s-এ নিয়ন্ত্রিত)বড়, virtual address space/physical memory দ্বারা সীমিত
Free করাস্বয়ংক্রিয় — function return করলেই, প্রোগ্রামারের কোনো কাজ নেইম্যানুয়াল (free()) — বা managed ভাষায় garbage collector-এর কাজ
LifetimeEnclosing scope-এর সাথে বাঁধা — function ফেরত এলে সব local variable অবৈধপ্রোগ্রামার যতক্ষণ চান, function boundary-নির্বিশেষে
Growth direction (x86-64 Linux)নিচের দিকে (উঁচু থেকে নিচু address-এ)উপরের দিকে (নিচু থেকে উঁচু address-এ)
Allocation costকার্যত O(1)O(1), একটা register arithmetic — পুরো frame-এর জন্য একবারেBin lookup ব্যর্থ হলে সম্ভাব্য split/coalesce/lock — গড়ে দ্রুত, worst-case ভারী
Thread-safetyস্বয়ংক্রিয় — প্রতিটা thread-এর নিজস্ব stack, শেয়ার হয় নাShared — arena/lock দরকার (গত লেসনের বিষয়)

এই টেবিলের প্রতিটা সারিই আসলে একই একটা মূল পার্থক্য থেকে বেরিয়ে আসছে: stack-এর allocation pattern compile-time-এই সম্পূর্ণ পূর্বনির্ধারিত, heap-এর না। পরের অংশে এই একটা বাক্যকেই derive করে দেখাব কেন এটাই stack-কে “বিনামূল্যে” বানায়।

কেন stack allocation প্রায় বিনামূল্যে — একটা derivation, assertion না

গত লেসনে malloc(64) কল করলে কী ঘটে তার একটা সম্পূর্ণ LayerTrace দেখেছিলেন — tcache চেক, না পেলে fastbin, না পেলে smallbin/unsorted bin, arena lock নেওয়ার সম্ভাবনা, না পেলে wilderness chunk split, একদম শেষে sbrk()। প্রতিটা ধাপ দরকার কারণ malloc জানে না পরের request-টা কত বড় হবে, কখন আসবে, বা কোন ক্রমে free হবে। এই অনিশ্চয়তাই বাধ্য করে একটা general-purpose বই-রক্ষণ ব্যবস্থা (bin, free-list) রাখতে — যেকোনো আকারের, যেকোনো ক্রমের request পরিবেশন করার জন্য প্রস্তুত থাকতে হয়।

Stack-এ এই অনিশ্চয়তা নেই — দুইটা কারণে:

১. আকার compile-time-এ জানা থাকে। একটা function-এর ভেতরের সব local variable — int a, char buf[64], struct Foo f — এদের প্রত্যেকের আকার compiler আগে থেকেই জানে (VLA বাদে, যা নিচে আলাদাভাবে আলোচিত)। Compiler এই সবগুলোর মোট আকার একবারে যোগ করে, alignment-এর জন্য সামান্য round up করে, আর assembly/stack-frames-and-function-calls লেসনে দেখা সেই একটা sub rsp, N instruction-এ বসিয়ে দেয়। buf-এর জন্য আলাদা কোনো “allocate” call নেই — এটা ইতিমধ্যেই ওই N-এর ভেতরে হিসাব করা আছে।

২. Lifetime ক্রম compile-time-এ জানা থাকে, আর তা সবসময় LIFO। একটা local variable-এর জীবনকাল ঠিক তার enclosing scope-এর জীবনকালের সমান — আর function call-এর নিয়মই হলো, সবচেয়ে ভেতরের call সবার আগে ফেরত আসে (একটা f() যদি g()-কে ডাকে, g() অবশ্যই f()-এর আগে শেষ হবে)। এই কড়া nesting-ই stack-কে “stack” নাম দিয়েছে — আর এর মানে free করার জন্য কোনো “কোন chunk free হচ্ছে, তার প্রতিবেশী কে” জাতীয় প্রশ্নের উত্তর খোঁজার দরকার নেই। পুরো frame-টাই একবারে অবৈধ হয়ে যায়, rsp/rbp ফিরিয়ে দেওয়ার মধ্য দিয়ে (leave; ret — সেই একই লেসনের epilogue)।

এই দুইটা মিলিয়ে ফলাফল — allocate = একটা compile-time-নির্ধারিত ধ্রুবক দিয়ে rsp কমানো, free = সেই ধ্রুবক দিয়ে rsp বাড়ানো (বা rbp-তে ফিরিয়ে দেওয়া)। কোনো chunk header লেখা লাগে না (আকার তো instruction-এই encode করা আছে, memory-তে না)। কোনো free-list খোঁজা লাগে না (frame-এর ভেতরে জায়গা বাছাইয়ের কোনো প্রশ্নই নেই — পুরো N byte-ই এই frame-এর, প্রথম থেকে শেষ পর্যন্ত)। কোনো coalescing লাগে না (fragmentation বলে কিছু নেই যখন allocation-deallocation ক্রম সবসময় নিজে থেকেই সুশৃঙ্খল)।

Guard page — overflow-কে নীরব করাপশন থেকে পরিষ্কার crash-এ বদলানো

Heap প্রায় যতটুকু virtual address space পাওয়া যায় ততটুকু বাড়তে পারে (sbrk()/mmap() উভয়ই bounded শুধু address-space ফুরিয়ে গেলে)। Stack-এর সীমা অনেক কড়া, আর ইচ্ছাকৃতভাবে

একটা process শুরু হওয়ার সময় kernel তার প্রধান thread-এর জন্য একটা stack VMA তৈরি করে, VM_GROWSDOWN flag দিয়ে চিহ্নিত — এই flag-টাই page-faults-and-demand-paging লেসনে দেখা page fault handler-কে বলে দেয়, এই VMA-র নিচের দিকে touch করা একটা বৈধ, প্রত্যাশিত ঘটনা, error না। প্রতিটা নতুন recursive call যখন stack-এর আরেকটু নিচে touch করে, আর সেই page এখনো mapped না, page fault handler সেটা চিনে VMA-টা এক page নিচে বাড়িয়ে দেয়, একটা নতুন physical frame lazily বরাদ্দ করে — ঠিক সেই একই lazy allocation যা গত দুই লেসনে mmap()/heap-এর ক্ষেত্রে দেখেছেন।

কিন্তু এই বৃদ্ধি অসীম না। ঠিক তার নিচেই ইচ্ছাকৃতভাবে রাখা আছে একটা guard page (বা একাধিক page-এর একটা “gap”) — কোনো VMA-ই এই অঞ্চলটা দাবি করে না। যখন stack pointer এই অঞ্চলে পড়ে (হয় কারণ RLIMIT_STACK-এ নির্ধারিত সর্বোচ্চ গভীরতা পার হয়ে গেছে, নয়তো কারণ ঠিক তার নিচে অন্য একটা VMA — heap, mmap region, অন্য thread-এর stack — বসে আছে আর মাঝের gap পার হয়ে গেছে), page fault handler এবার VMA বাড়াতে পারে না — কোনো বৈধ সম্প্রসারণ সম্ভব না, তাই এটা একটা সত্যিকারের error হিসেবে চিহ্নিত হয়, আর process SIGSEGV পায়।

ulimit -s / RLIMIT_STACK — সীমাটা কে ঠিক করে, আর কখন

Stack-এর সর্বোচ্চ আকার নির্ধারিত হয় process/thread শুরু হওয়ার সময়, আর সাধারণত সেশন জুড়ে অপরিবর্তিত থাকে। শেলে ulimit -s চালালে বর্তমান soft RLIMIT_STACK মান দেখা যায় — বেশিরভাগ Linux distro-তে ডিফল্ট ৮ MB (8192 KB)। এই সীমা তিনভাবে বদলানো যায়:

  • শেলে ulimit -s \<KB\> — এই শেল থেকে চালু হওয়া প্রতিটা প্রোগ্রামের জন্য (child process inherit করে)
  • প্রোগ্রামের ভেতর থেকে setrlimit(RLIMIT_STACK, ...) — কিন্তু শুধু নতুন থ্রেড/exec-এ কার্যকর, চলমান প্রধান thread-এর ইতিমধ্যে-বসানো stack VMA-র আকার পেছনে গিয়ে বদলানো যায় না
  • pthread_attr_setstacksize() — প্রতিটা নতুন pthread-এর জন্য আলাদাভাবে, RLIMIT_STACK-এর থেকে সম্পূর্ণ স্বাধীন একটা মান

alloca() আর VLA — stack-এর ভেতরেই dynamic allocation, malloc না

alloca(size) — POSIX-এ প্রমিত না, কিন্তু প্রায় সব platform-এ পাওয়া যায় — malloc-এর মতো দেখতে, কিন্তু সম্পূর্ণ ভিন্ন প্রক্রিয়া। এটা heap থেকে কিছুই নেয় না — এটা সরাসরি বর্তমান stack frame-কে বড় করে, rsp-কে runtime-এ গণনা করা size দিয়ে কমিয়ে। ঠিক assembly/stack-frames-and-function-calls লেসনে দেখা VLA-র prologue-এর মতোই (সেখানে দেখেছিলেন sub একটা compile-time constant-এর বদলে একটা runtime-এ গণনা করা মান নেয়) — আসলে alloca আর C99 VLA (int buf[n];, যেখানে n একটা runtime variable) প্রায় একই যন্ত্রপাতি ব্যবহার করে।

এই কারণেই alloca-র ফেরত দেওয়া pointer কখনো free() করতে হয় না — যে function-টা alloca ডেকেছে, সেটা ফেরত এলেই ওই memory স্বয়ংক্রিয়ভাবে অবৈধ (frame pop হয়ে গেছে)। কিন্তু ঠিক এই একই বৈশিষ্ট্যই তিনটা নির্দিষ্ট বিপদ তৈরি করে:

  1. কোনো failure-signal নেই। malloc ব্যর্থ হলে NULL ফেরত দেয়, যা চেক করা যায়। alloca-র কোনো ব্যর্থতা-সংকেত নেই — যদি size এত বড় হয় যে stack overflow ঘটে যায়, আচরণ undefined (POSIX-বহির্ভূত হওয়ায় সংজ্ঞায়িতও না) — সাধারণত সরাসরি SIGSEGV, কোনো সতর্কবার্তা ছাড়াই।
  2. ফলাফল frame-এর বাইরে বাঁচে না। alloca-র ফেরত দেওয়া pointer কল করা function থেকে return করা, বা একটা longer-lived structure-এ রেখে দেওয়া — দুইটাই সম্পূর্ণ ভুল, কারণ function ফেরত এলেই সেই memory অন্য কোনো পরবর্তী call-এর frame-এর অংশ হয়ে যায়।
  3. আকার attacker-নিয়ন্ত্রিত হলে বিপজ্জনক। যদি size কোনো untrusted input (যেমন একটা network packet-এর length field) থেকে আসে, একটা একক বড় alloca call rsp-কে সরাসরি guard page-এর উপর দিয়ে লাফিয়ে তার পরের mapping-এ ফেলে দিতে পারে, guard page-কে কখনো touch না করেই — এই লেসনের misconception সেকশনে এটাই Stack Clash নামের একটা বাস্তব, নামকরণ করা দুর্বলতা হিসেবে আলোচিত হবে।

Thread stack — প্রতিটা thread-এর নিজস্ব, স্বাধীন stack

threads লেসনে দেখেছেন প্রতিটা thread তার নিজস্ব execution context বহন করে — আর stack তারই একটা অংশ। যখন pthread_create() একটা নতুন thread বানায়, kernel/glibc তার জন্য একটা সম্পূর্ণ নতুন, স্বাধীন stack region (mmap()-এর মাধ্যমে, guard page-সহ) বরাদ্দ করে — এটা প্রধান thread-এর stack-এর সম্প্রসারণ না, একটা সম্পূর্ণ পৃথক VMA, address space-এর অন্য কোথাও (সাধারণত mmap region-এর কাছাকাছি)।

pthread_create(3) man page অনুযায়ী, নতুন thread-এর ডিফল্ট stack size প্রধান thread-এর RLIMIT_STACK soft limit-এর সমান — অর্থাৎ যদি সেটা ৮ MB হয়, প্রতিটা নতুন pthread-ও ডিফল্টভাবে ৮ MB reserve করে, যদি না pthread_attr_setstacksize() দিয়ে স্পষ্টভাবে ছোট করা হয়। এই একটা তথ্যের হিসাবযোগ্য ফলাফল আছে, যা এই লেসনের check সেকশনে সরাসরি প্রশ্ন হয়ে আসবে।

একই ধারণা, ভিন্ন সংখ্যা — Linux, macOS, Windows পাশাপাশি

Guard page আর fixed-size stack কোনো Linux-নির্দিষ্ট কৌশল না — প্রতিটা mainstream OS একই সমস্যার সমাধান করে, শুধু ডিফল্ট সংখ্যা আর প্রয়োগের বিস্তারিত ভিন্ন:

Systemপ্রধান thread-এর ডিফল্ট stackনতুন thread-এর ডিফল্টকীভাবে বদলানো যায়
Linux (glibc)~৮ MB (RLIMIT_STACK)প্রধান thread-এর সমানulimit -s, pthread_attr_setstacksize()
macOS~৮ MB (main thread)মাত্র ~৫১২ KB (pthread ডিফল্ট, অনেক ছোট)pthread_attr_setstacksize()
Windowsসাধারণত ১ MB, কিন্তু PE header-এর SizeOfStackReserve field-এ link time-এই এমবেড করাCreateThread()-এর dwStackSize argument-এ প্রতিটা thread আলাদাভাবেLinker flag, বা CreateThread argument

macOS-এর pthread ডিফল্ট (~৫১২ KB) প্রধান thread-এর (~৮ MB) চেয়ে ১৬ গুণ ছোট — এই লেসনের check সেকশনের প্রথম প্রশ্নের ঠিক সেই ধরনের একটা বাস্তব উদাহরণ, শুধু ভিন্ন OS-এ। Windows-এর পদ্ধতি সবচেয়ে ভিন্ন — stack size কোনো runtime resource limit না, বরং executable-এর build-time metadata-তে (PE header) সরাসরি এমবেড করা একটা সংখ্যা, যা loader প্রতিটা নতুন process/thread তৈরির সময় পড়ে।

ভেতরে কী ঘটছে

প্রথমে স্বাভাবিক কেসটা — একটা function call-এর সম্পূর্ণ register-level জীবনচক্র

Runaway recursion দেখার আগে, একটা সাধারণ, সফল function call-এ ঠিক কী ঘটে তা assembly/stack-frames-and-function-calls লেসনের ভাষায় একবার সংক্ষেপে দেখে নেওয়া যাক — কারণ overflow আসলে এই একই প্রক্রিয়ার একটা নিরীহ পুনরাবৃত্তি, যা শুধু থামতে ব্যর্থ হয়েছে।

int compute(int x) { int a, b; ...; return a + b; } — একটা একক, স্বাভাবিক call
  1. caller: call compute — return address push হলোrsp ৮ byte কমল; এটাই একমাত্র 'allocation' যা compiler-generated না, বরং call instruction নিজেই করে
  2. prologue: push rbp; mov rsp, rbpcaller-র frame pointer বাঁচানো হলো, নতুন frame-এর ভিত্তি স্থাপিত — কোনো bin/free-list লুকআপ নেই
  3. prologue: sub rsp, N — পুরো frame একবারে বরাদ্দa, b, আর alignment padding মিলিয়ে গণনা করা N — একটা মাত্র instruction, a আর b-র জন্য আলাদা কোনো allocate call নেই
  4. function body চলে — a, b সরাসরি rbp-সাপেক্ষ offset-এ ব্যবহৃতকোনো pointer dereference, কোনো header লুকআপ — শুধু rbp-8, rbp-16 জাতীয় সরাসরি ঠিকানা
  5. epilogue: leave (= mov rbp,rsp; pop rbp)পুরো frame একবারে অবৈধ — a আর b আলাদা করে 'free' হয়নি, পুরো N byte-ই একসাথে rsp-র হিসাবে ফিরে গেছে
  6. ret — return address pop, caller-এ ফেরতrsp আবার ঠিক সেখানে যেখানে call-এর ঠিক আগে ছিল — কোনো fragmentation, কোনো leak-এর প্রশ্নই ওঠে না

ছয়টা ধাপের মধ্যে ঠিক দুইটা আসলে “allocation/deallocation” (ধাপ ৩ আর ৫) — আর দুইটাই একটা মাত্র instruction, frame-এর ভেতরের variable সংখ্যা যাই হোক না কেন। গত লেসনের malloc(64) LayerTrace-এর সাত ধাপের সাথে এটা মিলিয়ে দেখুন — সেখানে tcache/fastbin/smallbin/wilderness-এর মধ্য দিয়ে যেতে হয়েছিল ঠিক এই কারণেই যে heap-এর প্রতিটা request runtime-এ, স্বাধীনভাবে, অননুমেয় ক্রমে আসে। এখানে পুরো frame-এর “অনুরোধ” compiler নিজেই আগে থেকে জানত, তাই কোনো runtime সিদ্ধান্তের দরকারই পড়েনি।

এবার bug-যুক্ত কেসটা — একটা runaway recursion, প্রথম call থেকে SIGSEGV পর্যন্ত

উপরের প্রতিটা call ঠিক একই prologue/epilogue চালায় — পার্থক্য শুধু এটা থামে না

একটা bug-যুক্ত recursive function — যেখানে base case কখনো মেলে না
  1. প্রথম কয়েক হাজার call — স্বাভাবিকপ্রতিটা call-এ prologue একটা sub rsp, N চালায়; rsp নিচের দিকে নামতে থাকে, কিন্তু এখনো ইতিমধ্যে-mapped পাতার ভেতরেই
  2. rsp একটা এখনো-mapped-না পাতায় নামলএকটা page fault — কিন্তু এই VMA-টা VM_GROWSDOWN হিসেবে চিহ্নিত, তাই handler এটাকে error ধরে না
  3. handler VMA-টা এক page নিচে বাড়ায়, নতুন frame lazily মাপগত দুই লেসনের demand paging-এর সেই একই প্রক্রিয়া — এখানে প্রয়োগ হলো stack-এ
  4. recursion চলতেই থাকে — কয়েক হাজার আরও callপ্রতিটা নতুন গভীরতায় এই একই lazy-extension প্যাটার্ন পুনরাবৃত্তি হয়, কোনো error ছাড়াই
  5. rsp guard page-এ (বা RLIMIT_STACK সীমায়) পৌঁছালএবার handler VMA বাড়াতে পারে না — নিচে হয় কোনো mapping নেই এমন একটা reserved gap, নয়তো সর্বোচ্চ অনুমোদিত গভীরতা শেষ
  6. page fault handler এবার সত্যিকারের error রিপোর্ট করেকোনো বৈধ সম্প্রসারণ সম্ভব না — এটা আর 'lazy allocation' না, একটা প্রকৃত অবৈধ access
  7. Kernel process-কে SIGSEGV পাঠায়ডিফল্ট disposition — process টার্মিনেট, core dump (থাকলে)। এই লেসনের build সেকশনে দেখব কীভাবে এই সিগন্যালই catch করা যায়

লক্ষ করুন — উপরের ট্রেসের প্রথম পাঁচটা ধাপ সম্পূর্ণ স্বাভাবিক, error না। Stack বৃদ্ধি নিজেই একটা lazy, demand-paged প্রক্রিয়া — ঠিক heap-এর মতোই। পার্থক্যটা শুধু কোথায় থেমে যেতে বাধ্য করা হয়েছে। Heap-এর জন্য সেই সীমা মূলত virtual-address-space/physical-memory — কার্যত অনেক দূরের। Stack-এর জন্য সেই সীমা ইচ্ছাকৃতভাবে কাছে টেনে আনা — একটা bug-যুক্ত recursion-কে যত দ্রুত সম্ভব ধরার জন্য।

     rsp (এখানে) ──────►  ┌──────────────┐  উঁচু address
                           │  mapped page  │  ← সর্বশেষ সফল recursive call
                           ├──────────────┤
                           │  mapped page  │
                           ├──────────────┤
                           │  mapped page  │  ← stack-এর বর্তমান সবচেয়ে-নিচু সীমা
                           ├──────────────┤
     পরের touch এখানে ────►│░░ guard gap ░░│  ← কোনো VMA নেই — touch করলেই SIGSEGV
                           ├──────────────┤
                           │  heap বা mmap  │  ← অন্য কোনো VMA-র শুরু
                           │  region-এর     │
                           │  ভিন্ন mapping  │
                           └──────────────┘  নিচু address
guard page-এর কাছে zoom — শেষ কয়েকটা mapped page, তারপর gap, তারপর অন্য কোনো VMA।

উদাহরণ

একটা সংখ্যাভিত্তিক তুলনা — frame আকার থেকে overflow-এর গভীরতা

ধরুন একটা recursive function-এর প্রতিটা call-এ stack frame ব্যবহার করে ২০০ byte (কয়েকটা local variable আর saved registers মিলিয়ে, বাস্তবসম্মত একটা আকার)। ডিফল্ট ৮ MB stack ধরে নিলে:

সর্বোচ্চ গভীরতা8×1024×1024byte200byte=8,388,60820041,943বার\text{সর্বোচ্চ গভীরতা} \approx \frac{8 \times 1024 \times 1024\,\text{byte}}{200\,\text{byte}} = \frac{8{,}388{,}608}{200} \approx 41{,}943\,\text{বার}

অর্থাৎ প্রায় ৪২,০০০ বার recursive call করার পরেই SIGSEGV — যা প্রথম শোনায় অনেক মনে হতে পারে, কিন্তু বাস্তবে অনেক common algorithm (একটা balanced-না-হওয়া tree-তে naive recursive traversal, deeply-nested JSON parse, একটা দীর্ঘ linked list-এর উপর naive recursive length গণনা) সহজেই এই সংখ্যা পার করে ফেলতে পারে, বিশেষত যদি প্রতিটা frame-এ কয়েকটা বড় local array থাকে (frame আকার কয়েক KB হলে সর্বোচ্চ গভীরতা নেমে আসে কয়েক হাজারে, এমনকি কয়েকশোতেও)।

তুলনা — একই কাজ heap-এ করলে overhead কত? ধরুন ওই একই ৪২,০০০ “allocation” যদি প্রতিটাই একটা আলাদা malloc(200)/free(200) কল হতো (যা সাধারণত কেউ করে না, কিন্তু তুলনার জন্য):

Stack (৪২,০০০ recursive call)Heap (৪২,০০০টা পৃথক malloc/free)
Allocation instruction সংখ্যা১টা (sub rsp, N, প্রতিটা function-এই একবার লেখা, কিন্তু বারবার চলা)৪২,০০০টা পৃথক malloc কল
প্রতি-allocation bookkeepingকোনোটাই নাchunk header (~১৬ byte), সম্ভাব্য bin traversal
মোট overhead (tcache hit ধরে)কার্যত শূন্য৪২,০০০ × ২৫ ns ≈ ১.০৫ ms শুধু allocate-এ, আরও তত সমান free-এ
Memory overheadশূন্য (chunk header নেই)৪২,০০০ × ১৬ byte ≈ ৬৭২,০০০ byte ≈ ৬৫৬ KB শুধু header-এই

এই তুলনাটা কৃত্রিম (কেউ recursion-কে malloc দিয়ে বাস্তবায়ন করে না), কিন্তু সংখ্যাটা concrete — একই কাজের জন্য heap ব্যবহার করলে মিলিসেকেন্ড-স্কেল সময় আর কয়েকশো KB বাড়তি memory যোগ হতো, যেখানে stack-ভিত্তিক recursion-এ এই খরচ কার্যত শূন্য (function-call overhead-এর মধ্যেই মিশে থাকে, যা এমনিতেই দিতে হতো)।

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

EXPERIMENT

ulimit -s বদলে guard page-এর সীমা সরাসরি অনুভব করুন

Linux· ১৫ মিনিট
/* deep_recurse.c — প্রতিটা call-এ একটা মোটামুটি নির্দিষ্ট আকারের
 * local array নিয়ে recursive ভাবে আরও গভীরে যায়, আর কত গভীরতায়
 * পৌঁছাল তা গুনে রাখে (একটা static counter দিয়ে, যাতে optimizer
 * পুরো call-টাই মুছে না দেয়)।
 *
 * বানান: gcc -O0 -o deep_recurse deep_recurse.c
 */
#include \<stdio.h>

static volatile long depth = 0;

void recurse(void) {
    char pad[512];               /* প্রতি frame-এ ~৫১২ byte চাপ */
    pad[0] = (char)depth;        /* optimizer যেন array না ফেলে দেয় */
    depth++;
    recurse();
}

int main(void) {
    recurse();
    printf("এখানে পৌঁছানো উচিত না: depth = %ld\n", depth);
    return 0;
}
gcc -O0 -o deep_recurse deep_recurse.c

# ডিফল্ট stack size-এ চালান
ulimit -s
./deep_recurse; echo "exit code: $?"     # সাধারণত 139 = 128 + SIGSEGV(11)

# এবার stack size উল্লেখযোগ্যভাবে ছোট করে চালান
( ulimit -s 256; ./deep_recurse; echo "exit code: $?" )

dmesg | tail-এ (root/sudo লাগতে পারে) দেখবেন প্রতিটা crash-এ একটা entry, যেমন:

deep_recurse[12345]: segfault at 7ffd12340000 ip 0000... sp 00007ffd12340ff8 error 6 in deep_recurse[...]

error 6 মানে (kernel-এর error code এনকোডিং-এ) — write access, আর page present ছিল না, কোনো valid mapping পাওয়া যায়নি। sp (stack pointer)-এর ঠিকানা লক্ষ করুন — এটাই সেই ঠিকানা যেখানে guard page-এ প্রথম touch ঘটেছিল।

ulimit -s 256 (মাত্র ২৫৬ KB) দিয়ে চালানো সংস্করণ প্রায় ৩২ গুণ (৮১৯২ ÷ ২৫৬) দ্রুত crash করবে, কারণ guard page-এর নিচের সীমা এখন অনেক কাছে। এটাই সরাসরি প্রমাণ করে — overflow “কতদূরে” ঘটবে তা কোনো hard-coded ভাষা-নিয়ম না, বরং একটা runtime-এ পড়া যাওয়া, বদলানো যাওয়া সংখ্যা।

এটা কী প্রমাণ করে

Stack overflow-এর 'কতদূরে ঘটবে' সম্পূর্ণ নির্ধারিত হয় RLIMIT_STACK দিয়ে — একটা তাত্ত্বিক সংখ্যা না, সরাসরি পরিমাপযোগ্য আর নিয়ন্ত্রণযোগ্য।

EXPERIMENT

Stack বনাম heap allocation — সময় সরাসরি মেপে দেখুন

Linux / macOS· ১৫ মিনিট
/* speed_compare.c — একই কাজ (একটা ৬৪-byte বাফার নেওয়া, একটা byte
 * লেখা, ছেড়ে দেওয়া) দুইভাবে ১ কোটি বার — stack-ভিত্তিক আর
 * heap-ভিত্তিক — করে সময় তুলনা করে।
 *
 * বানান: gcc -O0 -o speed_compare speed_compare.c
 * (-O0 জরুরি — নাহলে optimizer পুরো লুপ মুছে দিতে পারে)
 */
#include \<stdio.h>
#include \<stdlib.h>
#include \<time.h>

#define N 10000000

__attribute__((noinline))
void stack_version(int i) {
    char buf[64];
    buf[0] = (char)i;            /* touch করে, optimize-away ঠেকাতে */
}

__attribute__((noinline))
void heap_version(int i) {
    char *buf = malloc(64);
    buf[0] = (char)i;
    free(buf);
}

double elapsed_ms(struct timespec a, struct timespec b) {
    return (b.tv_sec - a.tv_sec) * 1000.0 + (b.tv_nsec - a.tv_nsec) / 1e6;
}

int main(void) {
    struct timespec t0, t1, t2;

    clock_gettime(CLOCK_MONOTONIC, &t0);
    for (int i = 0; i \< N; i++) stack_version(i);
    clock_gettime(CLOCK_MONOTONIC, &t1);
    for (int i = 0; i \< N; i++) heap_version(i);
    clock_gettime(CLOCK_MONOTONIC, &t2);

    printf("Stack: %.2f ms মোট (%.2f ns/call)\n",
           elapsed_ms(t0, t1), elapsed_ms(t0, t1) * 1e6 / N);
    printf("Heap:  %.2f ms মোট (%.2f ns/call)\n",
           elapsed_ms(t1, t2), elapsed_ms(t1, t2) * 1e6 / N);
    printf("অনুপাত: heap প্রায় %.1fx ধীর\n",
           elapsed_ms(t1, t2) / elapsed_ms(t0, t1));
    return 0;
}

Typical আউটপুট (মেশিন/glibc সংস্করণভেদে সংখ্যা বদলাবে, কিন্তু অনুপাতের মাত্রা একই থাকে):

Stack: 8.13 ms মোট (0.81 ns/call)
Heap:  187.42 ms মোট (18.74 ns/call)
অনুপাত: heap প্রায় 23.1x ধীর

এই অনুপাত (প্রায় ২০-৩০x, tcache hit-এর ক্ষেত্রেও) প্রায় সবসময় দেখা যায় — এটাই এই লেসনের concept সেকশনের derivation-এর হাতে-কলমে প্রমাণ। লক্ষ করুন heap সংস্করণ এখানেও সবচেয়ে ভালো ক্ষেত্র (একই আকারের বারবার allocation, tcache প্রতিবার hit) — একটা মিশ্র-আকারের, fragmentation-প্রবণ workload-এ এই ব্যবধান আরও বড় হতো।

এটা কী প্রমাণ করে

এই লেসনের কেন্দ্রীয় দাবি — stack allocation heap allocation-এর চেয়ে বহুগুণ দ্রুত — একটা অনুমান না, একটা মিনিটে নিজে চালিয়ে যাচাইযোগ্য সংখ্যা।

নিজে বানান

BUILD IT

sigaltstack + SA_ONSTACK — নিজেই stack overflow ধরুন

C · ●●●○○
  1. একটা alternate signal stack-এর জন্য static memory (global array বা malloc — অবশ্যই বর্তমান stack-এর বাইরে) বরাদ্দ করুন
  2. sigaltstack() দিয়ে সেই memory-কে kernel-এর কাছে alternate stack হিসেবে রেজিস্টার করুন
  3. sigaction() দিয়ে SIGSEGV-র জন্য একটা handler বসান, SA_ONSTACK | SA_SIGINFO flag সহ
  4. Handler-এর ভেতরে শুধু async-signal-safe function (write(), _exit()) ব্যবহার করুন — printf() না
  5. একটা bug-যুক্ত recursive function দিয়ে overflow ঘটিয়ে handler-টা কাজ করছে কিনা যাচাই করুন

sigaltstack ছাড়া কেন এটা কাজ করে না, প্রথমে এক লাইনে — normal signal delivery kernel-কে বর্তমান (blown) stack-এই একটা “signal frame” (saved registers, siginfo_t, ফেরার ঠিকানা) push করতে হয়, handler-কে ডাকার আগে। যে stack ইতিমধ্যে guard page-এ ধাক্কা খেয়েছে, তাতে আর এক byte জায়গাও নেই — সেই push নিজেই আবার fault করে, আর kernel এই recursive-fault পরিস্থিতি ধরে ফেলে সরাসরি process-কে মেরে ফেলে, handler কখনো চলার সুযোগই পায় না। sigaltstack এই সমস্যাটা এড়ায় শুধু signal frame-টাকে একটা সম্পূর্ণ ভিন্ন, স্বাস্থ্যবান memory region-এ push করতে বলে দিয়ে।

/* catch_overflow.c — SA_ONSTACK দিয়ে stack overflow-কে নিজে ধরা।
 *
 * বানান: gcc -O0 -o catch_overflow catch_overflow.c
 */
#define _GNU_SOURCE
#include \<signal.h>
#include \<stdio.h>
#include \<stdlib.h>
#include \<unistd.h>
#include \<string.h>

#define ALT_STACK_SIZE (64 * 1024)   /* ৬৪ KB — handler-এর নিজস্ব কাজের জন্য যথেষ্ট */

static char alt_stack[ALT_STACK_SIZE];  /* .bss-এ — normal stack-এর সম্পূর্ণ বাইরে */

static void on_segv(int sig, siginfo_t *info, void *ucontext) {
    (void)sig; (void)ucontext;
    const char msg[] = "\n[ধরা পড়েছে] SIGSEGV — সম্ভবত stack overflow, ঠিকানা: ";
    write(STDERR_FILENO, msg, sizeof(msg) - 1);

    /* ঠিকানাটা hex-এ ম্যানুয়ালি লিখি — printf async-signal-safe না */
    char hexbuf[20];
    unsigned long addr = (unsigned long)info->si_addr;
    int pos = 18;
    hexbuf[19] = '\n';
    for (int i = 0; i \< 16; i++) {
        int nibble = addr & 0xF;
        hexbuf[pos--] = nibble \< 10 ? '0' + nibble : 'a' + nibble - 10;
        addr >>= 4;
    }
    write(STDERR_FILENO, hexbuf, 20);

    _exit(1);   /* return করা নিরাপদ না — মূল stack-এর অবস্থা অনির্দিষ্ট */
}

static volatile long depth = 0;

void recurse(void) {
    char pad[1024];
    pad[0] = (char)depth;
    depth++;
    recurse();
}

int main(void) {
    stack_t ss;
    ss.ss_sp = alt_stack;
    ss.ss_size = ALT_STACK_SIZE;
    ss.ss_flags = 0;
    if (sigaltstack(&ss, NULL) == -1) { perror("sigaltstack"); return 1; }

    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_sigaction = on_segv;
    sa.sa_flags = SA_ONSTACK | SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    if (sigaction(SIGSEGV, &sa, NULL) == -1) { perror("sigaction"); return 1; }

    printf("recursion শুরু হচ্ছে — overflow আসছে...\n");
    fflush(stdout);
    recurse();

    printf("এখানে পৌঁছানো উচিত না\n");
    return 0;
}

Typical আউটপুট:

recursion শুরু হচ্ছে — overflow আসছে...

[ধরা পড়েছে] SIGSEGV — সম্ভবত stack overflow, ঠিকানা: 00007ffd12340000

SA_ONSTACK ছাড়া (শুধু sa.sa_flags = SA_SIGINFO; করে চালিয়ে দেখুন) একই প্রোগ্রাম শুধু নীরবে SIGSEGV-তে মারা যাবে — কোনো message ছাড়াই, কারণ handler push-ই করা যাচ্ছে না।

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

Stack Clash (CVE-2017-1000364 এবং সম্পর্কিত, Qualys ২০১৭) — একটা বাস্তব, নামকরণ করা দুর্বলতা যা সরাসরি এই লেসনের guard page ধারণার উপর নির্ভরশীল। ২০১৭-র আগে ডিফল্ট guard gap ছিল মাত্র একটা page (৪ KB)। একটা একক বড় alloca()/VLA call (বিশেষত attacker-নিয়ন্ত্রিত আকারে) rsp-কে এক লাফে সেই ৪ KB-র পুরোটা পার হয়ে, নিচের mapping-এ সরাসরি ফেলে দিতে পারত — guard page-কে কখনো touch (এবং তাই কখনো fault) না করেই, কারণ কেউ ওই মাঝের page-টা আসলে access করছিল না, শুধু rsp তার উপর দিয়ে লাফিয়ে গিয়েছিল। ফলাফল ছিল stack আর heap-এর মধ্যে নিয়ন্ত্রণহীন সংঘর্ষ, privilege escalation পর্যন্ত সম্ভব। প্রতিকার দুই স্তরে এসেছিল — kernel guard gap বাড়িয়ে ১ MB করা হলো (commit “mm: enlarge stack guard gap”, kernel ৪.১১/৪.১২), আর compiler-এ যোগ হলো -fstack-clash-protection flag, যা বড় stack allocation-কে ছোট ছোট “probe”-এ ভেঙে প্রতিটা page ক্রমান্বয়ে touch করতে বাধ্য করে — লাফ দেওয়ার সুযোগ না রেখে।

Go goroutine — একটা সম্পূর্ণ ভিন্ন দর্শনের stack। OS thread-এর stack এই লেসনে দেখা মতোই — fixed-size, kernel-বরাদ্দ, guard page-সহ। Go-র নিজস্ব runtime এই মডেল সম্পূর্ণ বর্জন করেছে — প্রতিটা goroutine একটা ছোট (প্রায় ২ KB) heap-বরাদ্দ “stack” দিয়ে শুরু হয়, আর প্রয়োজনে runtime নিজেই সেটাকে বড় একটা নতুন heap region-এ কপি করে বাড়ায় (segmented বা copying stack)। এই কারণেই একটা প্রোগ্রাম লক্ষ লক্ষ goroutine চালাতে পারে যেখানে লক্ষ লক্ষ OS thread (প্রতিটা ৮ MB reserve করে) practically অসম্ভব — নিচের thread stack size পয়েন্টের সাথে সরাসরি সম্পর্কিত একটা বাস্তব ট্রেড-অফ সিদ্ধান্ত।

High-concurrency server-এ thread stack size ম্যানুয়ালি ছোট করা। এই লেসনের concept সেকশনে দেখা তথ্য অনুযায়ী, প্রতিটা pthread ডিফল্টভাবে RLIMIT_STACK-এর সমান (প্রায়ই ৮ MB) reserve করে। হাজার হাজার connection-per-thread মডেলে চলা সার্ভার (পুরনো Apache prefork-এর মতো আর্কিটেকচার, বা JVM-ভিত্তিক thread-pool সার্ভিস) প্রায়ই pthread_attr_setstacksize()/-Xss দিয়ে এই মান নামিয়ে আনে (JVM-এ ডিফল্ট প্রায়ই ৫১২ KB-১ MB) — নাহলে শুধু stack reservation-এই virtual address space শেষ হয়ে যেতে পারে, এমনকি physical memory-তে চাপ না পড়লেও (কারণ পুরোটাই demand-paged, বেশিরভাগ কখনো touch হয় না — তবু reserve করা থাকে)।

Windows-এর _chkstk — Stack Clash-এর সমস্যাটা আগেই সমাধান করা। এই লেসনের misconception-সংলগ্ন Stack Clash আলোচনায় দেখা “বড় একটা লাফে guard page পার হওয়া” সমস্যাটা Windows কখনোই পুরোপুরি ভোগেনি — কারণ Windows-এর ABI বহু আগে থেকেই একটা ভিন্ন কৌশল বাধ্যতামূলক করে রেখেছিল। যখনই কোনো function-এর stack frame এক page (৪ KB)-এর চেয়ে বড় হয়, compiler স্বয়ংক্রিয়ভাবে prologue-এর শুরুতে একটা _chkstk (বা __chkstk_ms) কল ঢুকিয়ে দেয় — যেটা rsp কমানোর আগেই প্রতিটা page ক্রমান্বয়ে “probe” করে (একটা byte পড়ে/লিখে) নিশ্চিত করে guard page-এ পৌঁছালে ঠিক তখনই fault হয়, কখনো তার ওপর দিয়ে লাফ দিয়ে না। Linux-এর -fstack-clash-protection (২০১৭-পরবর্তী, ঐচ্ছিক) কার্যত এই একই কৌশলের একটা later retrofit — Windows-এ এটা দশকের পর দশক ধরে ডিফল্ট আচরণ।

JVM-এর StackOverflowError — raw SIGSEGV-কে একটা catchable exception-এ রূপান্তর। Java-তে গভীর recursion একটা raw crash দেয় না — বরং একটা ধরা-যায়-এমন StackOverflowError object throw করে, যা catch ব্লকে ধরে সুন্দরভাবে সামলানো যায়। HotSpot JVM ভেতরে ভেতরে ঠিক এই লেসনের build সেকশনের কৌশলেরই একটা production-grade সংস্করণ ব্যবহার করে — Linux-এ sigaltstack/SA_ONSTACK দিয়ে একটা alternate signal stack-এ SIGSEGV ধরে, faulting address guard-page অঞ্চলের ভেতরে কিনা যাচাই করে, আর হ্যাঁ হলে raw সিগন্যালটাকে একটা normal Java exception object-এ রূপান্তর করে ছুঁড়ে দেয় — যেন এটা কখনো একটা OS-level fault ছিলই না। এটা প্রমাণ করে এই লেসনের build সেকশনের কৌশল কোনো নিছক শিক্ষামূলক ব্যায়াম না — এটাই একটা বহু-কোটি-ডিভাইসে চলা production runtime-এর প্রকৃত প্রয়োগ।

Kernel-এর নিজস্ব stack — আরও ছোট, আরও কড়া। Linux kernel-এর প্রতিটা thread-এর নিজস্ব kernel-mode stack আছে, x86-64-এ সাধারণত মাত্র ১৬ KB — userspace-এর ৮ MB-এর তুলনায় প্রায় ৫০০ গুণ ছোট। এই কারণেই kernel কোড-এ বড় local array বা গভীর recursion কড়াভাবে নিষিদ্ধ (linter/static analysis দিয়ে যাচাই করা হয়) — এই লেসনের “frame আকার × গভীরতা” হিসাবটাই kernel developer-দের প্রতিদিনের বাস্তবতা, শুধু সীমাটা অনেক কাছে।

Coroutine/green thread — heap-কে stack হিসেবে ব্যবহার করা। Python-এর greenlet, Lua-র coroutine, পুরনো ucontext-ভিত্তিক fiber লাইব্রেরি — এরা প্রতিটা lightweight “thread of control”-এর জন্য একটা malloc-করা heap region বরাদ্দ করে, তারপর সেই region-এর ভেতরেই rsp/rbp-সদৃশ pointer manage করে, ঠিক একটা OS thread-এর stack-এর মতোই আচরণ করিয়ে। এটা প্রমাণ করে stack আসলে hardware-এ কোনো বিশেষ ট্যাগ-করা memory না — শুধু একটা register (rsp) যা একটা নির্দিষ্ট region-কে “stack হিসেবে গণ্য করার” প্রতিশ্রুতি দিয়ে রাখে। Heap memory-ও এই ভূমিকা পালন করতে পারে, যদি software নিজে সেই শৃঙ্খলা বজায় রাখে।

Deeply-nested input দিয়ে stack-overflow DoS। Recursive-descent parser (JSON, XML, নিজস্ব DSL) যদি nesting depth-এর উপর কোনো সীমা না রাখে, একটা ইচ্ছাকৃতভাবে গভীরভাবে nested input ([[[[[[...]]]]]]) একটা মূলত নিরীহ সার্ভিসকে সরাসরি crash করাতে পারে — একটা বাস্তব, বহুবার-রিপোর্ট-হওয়া denial-of-service প্যাটার্ন। প্রতিকার সাধারণত হয় একটা explicit depth counter (parse করার সময় গণনা রেখে একটা সীমায় ব্যর্থ হওয়া) অথবা recursion-কে সম্পূর্ণ একটা heap-ভিত্তিক explicit stack (একটা std::vector/array কে worklist হিসেবে ব্যবহার) দিয়ে প্রতিস্থাপন করা — যা এই লেসনের misconception সেকশনের একটা প্রশ্নের সরাসরি উত্তর।

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

“Heap allocation ধীর কারণ এটা kernel/syscall-এর সাথে জড়িত।”

ভুল, আর গত লেসনের একটা misconception-এরই প্রতিধ্বনি। malloc-এর বেশিরভাগ কল (tcache/fastbin hit) কোনো syscall-ই করে না, সম্পূর্ণ userspace-এ থাকে। এই লেসনের experiment-এ দেখা ~২০x ধীরগতি syscall overhead থেকে আসেনি — এসেছে bookkeeping-এর খরচ থেকে (chunk header লেখা/পড়া, size-class বাছাই, সম্ভাব্য bin traversal) যা stack-এর একেবারেই লাগে না, কারণ concept সেকশনে দেখা derivation অনুযায়ী stack-এর কোনো bookkeeping-ই দরকার নেই। “ধীরতা” আর “kernel-এর সাথে জড়িত হওয়া” দুইটা আলাদা জিনিস — এখানে প্রথমটা সত্যি, দ্বিতীয়টা না।

“Stack overflow মানেই প্রোগ্রামে একটা bug (যেমন base case ছাড়া অসীম recursion)।”

প্রায়ই সত্যি, কিন্তু সবসময় না। একটা সম্পূর্ণ সঠিক, সসীম recursive function-ও overflow করতে পারে যদি input যথেষ্ট গভীর হয় — এই লেসনের realworld সেকশনের deeply-nested-JSON উদাহরণটাই তার প্রমাণ। এখানে “bug” আসলে input-এর সাথে সম্পর্কিত না, বরং ডিজাইনের সীমাবদ্ধতা — একটা O(depth)O(\text{depth}) stack ব্যবহার করা algorithm একটা fixed, ছোট resource-এর (৮ MB stack) উপর নির্ভরশীল, যেখানে heap-ভিত্তিক বিকল্প (recursion-কে explicit worklist/stack data structure দিয়ে প্রতিস্থাপন, যা heap-এ বরাদ্দ, তাই অনেক বড় হতে পারে) একই কাজ করতে পারে অনেক বেশি গভীরতায়। সমাধান তাই সবসময় “recursion-এর bug ঠিক করা” না — কখনো কখনো “recursion-এর বদলে heap-ভিত্তিক iteration ব্যবহার করা”।

“alloca() শুধু একটা দ্রুততর malloc — যেখানেই malloc(n) লেখা যেত, সেখানে alloca(n) লিখলেই নিরাপদে দ্রুত হয়ে যাবে।”

বিপজ্জনক ভুল ধারণা। Concept সেকশনে দেখা তিনটা পার্থক্যের প্রতিটাই এই দাবিকে খণ্ডন করে — alloca-র কোনো failure-signal নেই (malloc ব্যর্থ হলে NULL, alloca ব্যর্থ হলে undefined behavior), ফলাফল কল করা function-এর বাইরে বাঁচে না (যেখানে malloc-এর ফলাফল যেকোনো সময় পর্যন্ত বাঁচতে পারে, free() না করা পর্যন্ত), আর আকার যদি untrusted input থেকে আসে তাহলে realworld সেকশনের Stack Clash-এর মতো একটা প্রকৃত নিরাপত্তা-ঝুঁকি তৈরি হয়। alloca শুধু তখনই যুক্তিসঙ্গত যখন আকার ছোট, compile-time-এ যাচাইযোগ্যভাবে bounded, আর ফলাফল কখনো frame-এর বাইরে যাবে না তা নিশ্চিত — অন্য সব ক্ষেত্রে malloc/free নিরাপদ পছন্দ, alloca না।

“ulimit -s দিয়ে stack size বাড়িয়ে (বা unlimited করে) দিলে stack-overflow crash-এর সমস্যা নিরাপদে সমাধান হয়ে যায়।”

আংশিক সত্যি, কিন্তু বিপজ্জনকভাবে অসম্পূর্ণ। প্রথমত, এটা একটা প্রকৃত infinite-recursion bug ঠিক করে না — শুধু crash-টা পরে ঘটায় (এখন হয়তো ৪২,০০০-এর বদলে ৪২ কোটি call পরে), সমস্যাটা লুকিয়ে রাখে। দ্বিতীয়ত, ulimit -s শুধু প্রধান thread-কে প্রভাবিত করে যেভাবে shell-এর মাধ্যমে চালানো হয় — thread stack size পয়েন্টে দেখা তথ্য অনুযায়ী, pthread_create() দিয়ে বানানো একটা worker thread-এর জন্য যদি কোথাও স্পষ্টভাবে pthread_attr_setstacksize() দিয়ে একটা ছোট মান বসানো থাকে (যেমন high-concurrency server-এ সাধারণ প্রাক্টিস, realworld সেকশনে দেখা), সেই thread ulimit -s-এর নতুন বড় মান পাবেই না — সেই নির্দিষ্ট thread-এ আগের মতোই দ্রুত overflow ঘটবে। “Stack size বাড়িয়ে দাও” একটা সাময়িক workaround হতে পারে, কিন্তু কখনোই মূল কারণের replacement না।

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

1

একটা recursive function-এর প্রতিটা call ৪০০ byte stack frame ব্যবহার করে। প্রধান thread-এর RLIMIT_STACK soft limit ৮ MB। প্রায় কত গভীরতায় SIGSEGV আসবে? আর যদি এই একই function একটা pthread_attr_setstacksize() দিয়ে ৫১২ KB stack-এ বসানো একটা worker thread-এ চলে, সংখ্যাটা কীভাবে বদলাবে?

প্রয়োগ

প্রধান thread-এ:

8×1024×1024400=8,388,60840020,971বার\frac{8 \times 1024 \times 1024}{400} = \frac{8{,}388{,}608}{400} \approx 20{,}971\,\text{বার}

৫১২ KB worker thread-এ:

512×1024400=524,2884001,310বার\frac{512 \times 1024}{400} = \frac{524{,}288}{400} \approx 1{,}310\,\text{বার}

প্রায় ১৬ গুণ কম গভীরতায় (৫১২ KB / ৮ MB = ১/১৬ অনুপাত সরাসরি প্রতিফলিত) SIGSEGV আসবে ছোট stack-এর thread-এ। এটাই এই লেসনের শেষ misconception-এর ঠিক প্রয়োগ — ulimit -s প্রধান thread-এর জন্য যাই বসানো থাকুক, এই worker thread তার নিজস্ব, স্বতন্ত্র, স্পষ্টভাবে ছোট stack size ব্যবহার করবে।

2

Compiler কীভাবে জানে ঠিক কত byte sub rsp, N-এ বসাতে হবে? আর এই তথ্য compile-time-এ জানা থাকাটাই কীভাবে stack allocation-কে heap allocation থেকে fundamentally সহজ বানায়, শুধু “দ্রুত” না বলে ঠিক কোন runtime কাজ এড়ানো যাচ্ছে তা চিহ্নিত করে ব্যাখ্যা করুন।

যুক্তি

Compiler একটা function-এর body পার্স করার সময় প্রতিটা local variable-এর ঘোষিত টাইপ দেখে তার আকার নির্ভুলভাবে জানে (int = ৪ byte, char[64] = ৬৪ byte, একটা struct-এর আকার তার member-দের যোগফল প্লাস alignment padding)। সবগুলো local variable-এর আকার যোগ করে, ৮/১৬-byte alignment-এ round up করে, compiler একটা একক ধ্রুবক N গণনা করে — যা function-এর machine code-এই সরাসরি লেখা থাকে (sub rsp, 48 জাতীয়), কোনো runtime গণনা ছাড়াই।

এই তথ্য আগে থেকে জানা থাকার ফলে যে runtime কাজগুলো সম্পূর্ণ এড়ানো যায়:

  • কোনো “কতটুকু জায়গা লাগবে” সিদ্ধান্ত runtime-এ নেওয়ার দরকার নেই — এটা একটা compile-time constant, প্রতিটা call-এ একই।
  • কোনো “কোথায় জায়গা আছে” খোঁজার দরকার নেই — heap-এর bin/free-list ঠিক এই প্রশ্নের উত্তর খোঁজে (“এই আকারের একটা free chunk কোথায় আছে?”), stack-এ এই প্রশ্নটাই অস্তিত্বহীন, কারণ পরবর্তী N byte সবসময় rsp-এর ঠিক নিচেই, নিঃশর্তভাবে সংরক্ষিত।
  • কোনো “এই allocation-টা কতদিন বাঁচবে” ট্র্যাক করার দরকার নেই — lifetime স্বয়ংক্রিয়ভাবে function-এর scope-এর সমান, compiler এটা জানে, তাই free-ও compile-time-এ নির্ধারিত একটা instruction (add rsp, N বা leave)।

Heap-এ এই তিনটা প্রশ্নের কোনোটারই উত্তর compile-time-এ জানা যায় না (আকার runtime argument-নির্ভর, request-এর ক্রম অননুমেয়, lifetime প্রোগ্রামারের সিদ্ধান্তের উপর) — তাই প্রতিটাকেই runtime-এ, প্রতিটা কলে সমাধান করতে হয়, আর সেই সমাধানের জন্যই bin/free-list/header-এর পুরো যন্ত্রপাতি দরকার হয়।

3

একটা সার্ভার naive thread-per-connection মডেলে চলে, প্রতিটা connection-এর জন্য একটা নতুন pthread স্পন করে, ডিফল্ট stack size ব্যবহার করে (ধরুন RLIMIT_STACK = ৮ MB)। একসাথে ১০,০০০ connection থাকলে শুধু stack reservation-এই মোট কত virtual address space লাগবে? এটা কি সত্যিই একটা সমস্যা — physical memory-র উপর কী প্রভাব পড়বে, আর pthread_attr_setstacksize() দিয়ে এটা ছোট করলে ঠিক কী trade-off আসে?

ডিজাইন

মোট reservation:

10,000×8MB=80,000MB=78.125GB10{,}000 \times 8\,\text{MB} = 80{,}000\,\text{MB} = 78.125\,\text{GB}

Physical memory-র উপর প্রভাব — সরাসরি না, কিন্তু পরোক্ষভাবে গুরুত্বপূর্ণ। গত কয়েক লেসনের demand paging অনুযায়ী, প্রতিটা thread-এর ৮ MB শুধু virtual address space reserve করে — physical frame বরাদ্দ হয় শুধু যতটুকু আসলে touch হয়। বেশিরভাগ connection handler সম্ভবত কয়েক KB-র বেশি stack ব্যবহার করে না, তাই প্রকৃত RSS impact ৭৮ GB-র তুলনায় অনেক ছোট।

তাহলে সমস্যাটা কোথায়? কয়েক জায়গায়:

  • ৩২-বিট সিস্টেমে (যেখানে ব্যবহারযোগ্য address space মাত্র ~৩ GB), এই ৭৮ GB দাবিই address space-কে সম্পূর্ণ শেষ করে দিত — নতুন thread তৈরিই ব্যর্থ হতো, physical memory থাকা সত্ত্বেও।
  • ৬৪-বিট সিস্টেমেও, RLIMIT_AS (মোট virtual address space-এর একটা resource limit, অনেক container/সিস্টেমে সেট করা থাকে) এই বিশাল reservation-এর কারণে সহজেই অতিক্রান্ত হয়ে যেতে পারে — নতুন thread তৈরি ব্যর্থ হয়, যদিও প্রকৃত ব্যবহৃত memory সামান্য।
  • guard gap-সহ প্রতিটা stack একটা আলাদা VMA — ১০,০০০টা VMA kernel-এর VMA-ট্র্যাকিং গঠন (red-black tree) আর /proc/[pid]/maps পার্স করার মতো tooling-কে ধীর করে দিতে পারে।

pthread_attr_setstacksize() দিয়ে ছোট করার trade-off:

৮ MB ডিফল্ট৫১২ KB (উদাহরণ)
মোট reservation (১০,০০০ thread)৭৮.১২৫ GB৫ GB
Address-space চাপবেশি — সহজেই সীমায় পৌঁছাতে পারেকম
Overflow ঝুঁকিকম — প্রায় সব বাস্তবিক ব্যবহারের জন্য যথেষ্টবেশি — একটা গভীর call chain (deeply-nested logging, বড় local buffer) সহজে ছাড়িয়ে যেতে পারে

বাস্তবসম্মত সিদ্ধান্ত — প্রোফাইল করে প্রকৃত সর্বোচ্চ stack ব্যবহার মাপা (যেমন pthread_attr_getstacksize দিয়ে না, বরং guard-page-এর কাছাকাছি একটা “canary” pattern লিখে সর্বোচ্চ touch হওয়া গভীরতা মাপা), তারপর তার উপর একটা নিরাপদ margin (২-৪x) রেখে একটা মাঝারি মান বসানো — সম্পূর্ণ ডিফল্ট বা অতিরিক্ত আক্রমণাত্মকভাবে ছোট, কোনোটাই সেরা না। এটা ঠিক গত লেসনের arena-count সিদ্ধান্তের মতোই একটা profile-first, guess-না-করার সিদ্ধান্ত।

4

একটা recursive function বাগ-যুক্ত — base case কখনো মেলে না। বর্ণনা করুন, ধাপে ধাপে, ঠিক কী ঘটে প্রথম call থেকে process টার্মিনেট হওয়া পর্যন্ত — VMA, page fault, আর guard page-এর ভাষা ব্যবহার করে।

প্রয়োগ

এই লেসনের hood সেকশনের LayerTrace-এর সংক্ষিপ্ত পুনর্গঠন:

১. প্রতিটা recursive call একটা sub rsp, N চালায় (concept সেকশনের derivation অনুযায়ী, compile-time-নির্ধারিত)। প্রথম কয়েক হাজার call-এ rsp এখনো ইতিমধ্যে-mapped পাতার ভেতরেই থাকে — কিছুই বিশেষ ঘটে না।

২. এক পর্যায়ে rsp একটা এখনো-mapped-না পাতায় নামে। একটা page fault হয় — কিন্তু stack VMA VM_GROWSDOWN হিসেবে চিহ্নিত, তাই page fault handler (গত লেসনগুলোর demand-paging যন্ত্রপাতি) এটাকে error না ধরে VMA-টা এক page নিচে বাড়িয়ে দেয়, নতুন physical frame lazily বরাদ্দ করে।

৩. এই lazy-extension প্যাটার্ন recursion চলতে থাকা অবস্থায় বারবার পুনরাবৃত্তি হয় — প্রতিটা নতুন গভীরতায় একই প্রক্রিয়া, কোনো error ছাড়াই, যতক্ষণ না RLIMIT_STACK-এ নির্ধারিত সর্বোচ্চ সীমায় পৌঁছায়।

৪. সীমার ঠিক পরেই বসে থাকা guard page-এ rsp যখন পৌঁছায়, page fault handler আবার ডাকা হয় — কিন্তু এবার VMA বাড়ানোর কোনো উপায় নেই (হয় সর্বোচ্চ গভীরতা শেষ, নয়তো ঠিক নিচে অন্য একটা VMA বসে আছে)। এবার এটা একটা প্রকৃত, অবৈধ access হিসেবে চিহ্নিত হয়, lazy allocation না।

৫. Kernel process-কে SIGSEGV পাঠায়। কোনো custom handler (এই লেসনের build সেকশনের sigaltstack-ভিত্তিক প্যাটার্ন) না থাকলে, ডিফল্ট disposition process-কে সাথে সাথে টার্মিনেট করে, একটা core dump তৈরি করে (যদি ulimit -c অনুমতি দেয়)।

5

একটা function-এ একটা সাময়িক বাফার দরকার। তিনটা আলাদা পরিস্থিতিতে — (ক) আকার সবসময় একটা compile-time constant (যেমন ১২৮ byte), (খ) আকার একটা runtime variable কিন্তু যাচাই করে ছোট রাখা (যেমন ≤ ৪ KB, একটা assert দিয়ে নিশ্চিত), (গ) আকার সরাসরি একটা network packet-এর একটা length field থেকে আসে, কোনো upper bound ছাড়াই — প্রতিটার জন্য stack (plain array/VLA/alloca) নাকি heap (malloc) ব্যবহার করবেন, আর কেন?

ডিজাইন
পরিস্থিতিসুপারিশকারণ
(ক) Compile-time constantসাধারণ local array (char buf[128];)সবচেয়ে সস্তা, সবচেয়ে নিরাপদ — compiler পুরো আকার যাচাই করে, কোনো runtime সিদ্ধান্তই নেই, কোনো alloca/VLA-র বাড়তি ঝুঁকিও নেই
(খ) ছোট, যাচাই-করা runtime বাউন্ডসতর্কতার সাথে VLA/alloca, শুধু যদি upper bound (৪ KB) নিশ্চিতভাবে enforced থাকে assert/if-check দিয়ে, আর function ইতিমধ্যে গভীর recursion-এর অংশ না হয়এই আকার (৪ KB) একটা সাধারণ ৮ MB stack-এর তুলনায় নগণ্য, তাই ঝুঁকি কম — কিন্তু এই ব্যবহার সবসময় একটা explicit, code-reviewed bound-এর সাথে থাকা উচিত, “assume করা” bound না
(গ) Unbounded, attacker-নিয়ন্ত্রিতঅবশ্যই heap (malloc), কখনো alloca/VLA নাMisconception সেকশনের Stack Clash আলোচনা অনুযায়ী, একটা untrusted, unbounded আকারের stack allocation সরাসরি guard page-কে “লাফ দিয়ে” পার হয়ে একটা প্রকৃত নিরাপত্তা-দুর্বলতা তৈরি করতে পারে। malloc এই একই আকারে ব্যর্থ হলে NULL ফেরত দেয় — একটা চেকযোগ্য, নিরাপদ ব্যর্থতা, guard-page-জাম্পের নীরব করাপশনের বিপরীত

সাধারণ নীতি — stack allocation-এর “প্রায় বিনামূল্যে” সুবিধা শুধু তখনই পাওয়া উচিত যখন আকার compile-time-এ জানা বা অন্তত tightly bounded; আকার যত বেশি “বাইরের জগতের” নিয়ন্ত্রণে (network input, ফাইলের content, user input), heap তত বেশি সঠিক পছন্দ — ধীর হলেও, কারণ heap ব্যর্থতাকে চেক করা যায়, guard-page-জাম্পের মতো নীরবে করাপ্ট হয় না।

6

Python-এর মতো একটা garbage-collected ভাষায় heap object কখনো explicit free() পায় না — GC নিজে বের করে কোনটা আর reachable না। এই লেসনে দেখা stack-heap তুলনার কোন সারিগুলো তবুও অপরিবর্তিত থাকে, আর কোনটা GC-র উপস্থিতিতে বদলে যায়?

যুক্তি

অপরিবর্তিত থাকে — stack-এর দিকটা সম্পূর্ণ, heap-এর প্রায় সবটা:

  • Stack এখনো compile-time-known আকার, LIFO, guard page-বাঁধা, স্বয়ংক্রিয়ভাবে free — GC-র সাথে stack-এর কোনো সম্পর্কই নেই, কারণ local variable-এর scope-based lifetime এখনো compiler নিজেই নিশ্চিত করে (একটা GC থাকা ভাষাতেও local int/reference variable-গুলো তাদের নিজস্ব stack slot-এই থাকে)।
  • Heap এখনো arbitrary-order, arbitrary-size allocation পরিবেশন করে — GC থাকুক বা না থাকুক, allocator-কে (CPython-এর pymalloc, JVM-এর TLAB-ভিত্তিক allocator) তখনও bookkeeping করতেই হয়, কারণ request-এর আকার/ক্রম এখনো runtime-নির্ধারিত, compile-time-known না। GC allocation-কে “বিনামূল্যে” বানায় না — এই লেসনের derivation অনুযায়ী সেটা কাঠামোগতভাবেই অসম্ভব।

বদলে যায় — শুধু “কে, কখন free করে” প্রশ্নের উত্তর:

  • C/C++-এ: প্রোগ্রামার ম্যানুয়ালি প্রতিটা malloc-এর সাথে একটা free মেলায়, ভুল করলে leak বা double-free।
  • GC-যুক্ত ভাষায়: object-টা কখন ঠিক মুক্ত হবে তা প্রোগ্রামারের নিয়ন্ত্রণে না — GC নিজস্ব সময়সূচিতে (reference count শূন্যে নামলেই, বা periodic mark-sweep cycle-এ) কাজটা করে। এর ফলে leak-এর ধরন বদলে যায় (raw dangling pointer আর সম্ভব না, কিন্তু একটা ভুলবশত ধরে-রাখা reference — যেমন একটা global cache-এ কখনো না-মোছা entry — এখনো “logical leak” তৈরি করতে পারে) আর deallocation-এর সময়ে একটা নতুন খরচ যোগ হয় (GC pause/cycle), যা malloc/free-এ ছিল না।

সংক্ষেপে: GC heap-এর কাঠামোকে (bookkeeping-নির্ভরতা, arbitrary order/size) বদলায় না — শুধু deallocation-এর responsibility প্রোগ্রামার থেকে runtime-এ সরিয়ে দেয়। Stack সম্পূর্ণ অস্পৃশ্য থাকে, কারণ তার গতি ও শৃঙ্খলা প্রথম থেকেই compiler-নিশ্চিত, কোনো runtime সিদ্ধান্ত-গ্রহণকারীর (GC-সহ) প্রয়োজনই নেই। Level ৫-এর programming-languages module-এ এই “responsibility সরে যাওয়া” প্রক্রিয়ার প্রকৃত বাস্তবায়ন (mark-sweep, generational, reference-counting) বিস্তারিতভাবে দেখবেন।

এরপর কী

পরের লেসন — process-এর ভেতর থেকে বাইরের দিকে

Memory-allocation আর এই লেসন মিলিয়ে একটা process-এর নিজস্ব memory-র মানচিত্র সম্পূর্ণ হলো — text/data/bss (Level ৩-এর ELF থেকে), heap (chunk, bin, arena), আর এখন stack (LIFO, guard page, alloca-র ঝুঁকি)। এই পুরো মডিউলের প্রথম অর্ধেক জুড়ে আমরা দেখেছি একটা process কীভাবে তার নিজের ভেতরে সংগঠিত।

পরের লেসন থেকে আমরা বাইরের দিকে তাকাব — একটা process কীভাবে তার বাইরের জগতের সাথে (ফাইল, ডিভাইস, অন্য process) কথা বলে। সেই যাত্রার প্রথম ধাপ একটা প্রায়-তুচ্ছ দেখতে integer দিয়ে শুরু হয় — open()-এর ফেরত দেওয়া সংখ্যাটা, যার পেছনে আসলে কার্নেলে তিন স্তরের একটা টেবিল লুকানো।

আরও পড়ুন

  • getrlimit(2), setrlimit(2) — Linux manual page — Michael Kerrisk, man-pages project · RLIMIT_STACK-সহ সব resource limit-এর প্রামাণ্য সংজ্ঞা
  • alloca(3) — Linux manual page · alloca-র নিজস্ব man page-এই একটা পুরো বিভাগ শুধু কেন এটা এড়িয়ে চলা উচিত তা নিয়ে
  • sigaltstack(2) — Linux manual page · Alternate signal stack স্থাপন আর SA_ONSTACK flag-এর প্রামাণ্য বিবরণ
  • pthread_create(3) — Linux manual page · নতুন thread-এর ডিফল্ট stack size RLIMIT_STACK থেকে কীভাবে নির্ধারিত হয়, তার বিবরণ
  • The Stack Clash — Qualys Security Advisory, ২০১৭ · guard page-এর আকার একটা তাত্ত্বিক বিস্তারিত না, বাস্তব দুর্বলতা — এই লেসনের misconception সেকশনে আলোচিত