সরাসরি পর্বে যান
পর্ব ২০ · ৫৬-এর মধ্যে · মডিউল ৩

ডায়নামিক মেমরি — চলার সময় দরকারমতো জায়গা নেওয়া আর ফেরত দেওয়া

Dynamic Memory — The Heap, malloc, calloc, realloc & free

Taught in Bangla, this lesson covers dynamic memory in C: the program memory map, stack versus heap, malloc, calloc, realloc and free, leaks, use-after-free and double free, growable arrays, and finding bugs with AddressSanitizer and Valgrind.

এই পর্বে যা শিখবেন

  • একটি চলমান প্রোগ্রামের মেমরি কোন চার ভাগে সাজানো, আর কোন জিনিস কোথায় থাকে
  • malloc, calloc, realloc, free — কোনটি কখন, আর নিরাপদে ব্যবহারের নিয়ম
  • Memory leak, use-after-free, double free — কেন এগুলো শুধু বাগ নয়, নিরাপত্তার বড় ঝুঁকি
  • নিজে নিজে বড় হয় এমন array, আর “দ্বিগুণ করো” কৌশল কেন কাজ করে
  • AddressSanitizer, Valgrind আর “কে free করবে” নিয়ম দিয়ে AI-এর লেখা মেমরি-কোড যাচাই

১ · সমস্যা: কতজন আসবে, আগে জানা নেই

ধরুন আপনি একটি কোচিং সেন্টারের জন্য রেজাল্ট-প্রোগ্রাম লিখছেন। পর্ব ১৬-এ শিখেছেন, অনেকগুলো নম্বর রাখতে array লাগে — int marks[100];. কিন্তু প্রশ্ন হলো, ১০০ কেন? এই ব্যাচে ৩৫ জন, পরের ব্যাচে হয়তো ২৪০ জন। আর যদি প্রোগ্রামটি সারা দেশের বোর্ড পরীক্ষার ফল সামলায়, তাহলে সংখ্যাটি কয়েক লাখ।

আকার আগে থেকে ঠিক করে দিলে দুটি বিপদের একটি ঘটবেই:

📦 খুব বড় ধরলে

int marks[1000000]; লিখলেন “নিরাপদ থাকার জন্য”। ৩৫ জনের জন্যও প্রায় ৪ MB জায়গা আটকে রইল। আর এটি লোকাল ভ্যারিয়েবল হলে অনেক সিস্টেমে (যেমন Windows-এ, যেখানে stack সাধারণত ১ MB) stack-এ জায়গাই হবে না — পর্ব ১৫-এর সেই stack overflow.

অপচয়, অথবা crash

📉 খুব ছোট ধরলে

int marks[100]; আর ছাত্র এলো ১০১ জন। শেষজনের নম্বর array-র সীমার বাইরে লেখা হবে — buffer overflow, যা পাশের ডেটা নষ্ট করে।

ডেটা নষ্ট, নিরাপত্তা-ত্রুটি

বাস্তবের প্রায় সব প্রোগ্রামেই এই সমস্যা। একটি চ্যাট অ্যাপ জানে না আজ কতগুলো মেসেজ আসবে। একটি ই-কমার্স সার্ভার জানে না ঈদের আগের রাতে কতগুলো অর্ডার আসবে। একটি ছবি-এডিটর জানে না আপনি কত বড় ছবি খুলবেন। সংখ্যাটি জানা যায় প্রোগ্রাম চলার সময়, কোড লেখার সময় নয়।

মূল ধারণা

সমাধান হলো চলার সময় অপারেটিং সিস্টেমের কাছ থেকে ঠিক যতটুকু দরকার ততটুকু মেমরি চেয়ে নেওয়া, আর কাজ শেষে ফেরত দেওয়া। একেই বলে dynamic memory allocation. C-তে এই চাওয়া আর ফেরত দেওয়া — দুটোই আপনাকে নিজের হাতে করতে হয়। এটাই C-র শক্তি, আবার এটাই C-র সবচেয়ে বড় ফাঁদ।

একটি বিয়ের অনুষ্ঠানের কথা ভাবুন। অতিথি কতজন আসবেন নিশ্চিত না হলে আপনি কমিউনিটি সেন্টারে স্থায়ীভাবে ১০০০ চেয়ার কিনে রাখেন না। বরং ডেকোরেটরের কাছ থেকে দরকারমতো চেয়ার ভাড়া নেন, আর অনুষ্ঠান শেষে ফেরত দেন। Heap হলো সেই ডেকোরেটরের গুদাম। ফেরত না দিলে কী হয়? পরের অনুষ্ঠানের জন্য গুদামে আর চেয়ার থাকে না।

২ · একটি প্রোগ্রামের মেমরির মানচিত্র

Heap কোথায় থাকে বুঝতে হলে আগে পুরো ছবিটি দেখা দরকার। আপনি যখন একটি প্রোগ্রাম চালান, অপারেটিং সিস্টেম তাকে নিজস্ব একটি মেমরি-এলাকা দেয়। সেই এলাকা কয়েকটি ভাগে সাজানো — একে বলে প্রোগ্রামের memory map. প্রতিটি ভাগের কাজ আলাদা, নিয়ম আলাদা। নিচের ডায়াগ্রামে “পরে” চেপে একটি একটি করে ভাগ দেখুন:

Stack ↓উঁচু ঠিকানা · লোকাল ভ্যারিয়েবল, ফাংশনের frame · নিচের দিকে বাড়ে
· · · ফাঁকা জায়গা · · ·stack আর heap এই ফাঁকের দিকেই বাড়ে
Heap ↑malloc থেকে পাওয়া block · উপরের দিকে বাড়ে
Data + BSSglobal ও static ভ্যারিয়েবল
Text (code)নিচু ঠিকানা · আপনার প্রোগ্রামের machine code
Text segmentশুধু পড়া যায়
int main(void) { ... }      /* সব ফাংশনের machine code */
"Hello, Dhaka!"              /* string literal — প্রায়ই পাশের read-only অংশে */
Data ও BSSপ্রোগ্রামের পুরো জীবনজুড়ে টিকে থাকে
int total_students = 35;     /* মান দেওয়া global → data */
int counter;                 /* মান না দেওয়া global → bss (শুরুতে 0) */
static int calls = 0;        /* static → bss (0 দেওয়া) */
Heapআপনি চাইলে পান, আপনিই ফেরত দেন
int *marks = malloc(n * sizeof *marks);
/* marks নিজে stack-এ থাকে, কিন্তু যে block-এর ঠিকানা সে ধরে আছে, সেটি heap-এ */
free(marks);                 /* ফেরত না দিলে প্রোগ্রাম শেষ হওয়া পর্যন্ত আটকে থাকে */
Stackনিজে থেকে নেয়, নিজে থেকে ছাড়ে
void report(void) {
    int i;                   /* লোকাল → stack */
    char name[20];            /* লোকাল array → stack */
}                            /* ফাংশন শেষ → পুরো frame মুছে গেল */
মাঝের ফাঁকা জায়গাদুই দিক থেকে বাড়ে
Stack  : ফাংশনের ভেতরে ফাংশন ডাকলে  → নিচে নামে
Heap   : malloc ডাকলে                → উপরে ওঠে
Stack-এর সীমা ছোট (প্রায়ই 1-8 MB) — পেরোলে stack overflow
Heap অনেক বড় — যতক্ষণ RAM আর OS দেয়
সব একসাথেএকটি ছোট প্রোগ্রাম, চারটি ভাগ
int batch_size = 35;               /* → Data  */
int main(void) {                   /* → Text  */
    int n = batch_size;            /* → Stack */
    int *marks = malloc(n * sizeof *marks);
                                   /* marks → Stack, block → Heap */
    free(marks);
    return 0;
}
  1. Text — প্রোগ্রামের নিজের কোড। সবচেয়ে নিচে থাকে text segment, যেখানে আপনার সব ফাংশনের machine code বসানো হয়। এই অংশ সাধারণত শুধু পড়া যায় — ভুল করেও যাতে কোড বদলে না যায়। String literal (যেমন "Hello, Dhaka!") সাধারণত এর পাশেই একটি read-only অংশে থাকে।
  2. Data ও BSS — global ভ্যারিয়েবলের ঘর। যে global বা static ভ্যারিয়েবলকে শুরুতেই মান দিয়েছেন, সেগুলো data segment-এ। যেগুলোকে মান দেননি বা শূন্য দিয়েছেন, সেগুলো BSS-এ — প্রোগ্রাম শুরুর সময় পুরো অংশটি শূন্য দিয়ে ভরা হয়। দুটির আকারই compile করার সময় ঠিক হয়ে যায়, আর প্রোগ্রাম শেষ না হওয়া পর্যন্ত এরা থাকে।
  3. Heap — চাইলেই পাওয়া যায় এমন বড় গুদাম। malloc ডাকলে এখান থেকে জায়গা আসে। কত বড়, কতক্ষণ থাকবে — দুটোই আপনি ঠিক করেন। ফাংশন শেষ হলেও heap-এর block মুছে যায় না; আপনি free না ডাকা পর্যন্ত থাকে। খেয়াল করুন, এটি উপরের দিকে বাড়ে।
  4. Stack — ফাংশনের খসড়া খাতা। পর্ব ১৩-এ দেখেছেন, প্রতিটি ফাংশন ডাকলে একটি stack frame তৈরি হয়, আর তাতে থাকে লোকাল ভ্যারিয়েবল। ফাংশন শেষ হলে frame নিজে থেকেই মুছে যায় — কিছু ফেরত দিতে হয় না। এই এলাকাকে বলে stack. এটি সবচেয়ে উঁচু ঠিকানা থেকে নিচের দিকে বাড়ে।
  5. মাঝের ফাঁকা জায়গা। Stack নিচে নামে, heap উপরে ওঠে — দুজনেই মাঝের ফাঁকা জায়গার দিকে। তবে stack-এর একটি নির্দিষ্ট সীমা আছে, সাধারণত Linux-এ প্রায় 8 MB আর Windows-এ প্রায় 1 MB. সীমা পেরোলেই পর্ব ১৫-এর stack overflow. Heap-এর সীমা অনেক বড় — যতক্ষণ RAM আছে আর অপারেটিং সিস্টেম দিতে রাজি।
  6. সব একসাথে। একটি ছোট প্রোগ্রামে চারটি ভাগই ব্যবহার হচ্ছে। সবচেয়ে গুরুত্বপূর্ণ লাইনটি খেয়াল করুন: marks নামের pointer ভ্যারিয়েবলটি নিজে stack-এ থাকে, কিন্তু যে block-এর ঠিকানা সে ধরে আছে, সেটি heap-এ। main শেষ হলে marks মুছে যায় — কিন্তু heap-এর block নয়। এই পার্থক্যটি না বুঝলে পরের সব ভুলই হবে।

এই ছবিটি সরলীকৃত। আধুনিক অপারেটিং সিস্টেম নিরাপত্তার জন্য প্রতিবার প্রোগ্রাম চালানোর সময় stack আর heap-এর ঠিকানা এলোমেলো করে দেয়, তাই একই প্রোগ্রাম দুবার চালালে ঠিকানা আলাদা দেখবেন। আর এই ঠিকানাগুলো আসল RAM-এর ঠিকানাও নয় — প্রতিটি প্রোগ্রাম নিজের একটি “কাল্পনিক” মেমরি দেখে, যা অপারেটিং সিস্টেম আসল RAM-এর সাথে মিলিয়ে দেয়। এ নিয়ে বিস্তারিত Operating Systems কোর্সে।

৩ · Stack বনাম heap — কোনটি কখন

দুটোই মেমরি, দুটোই RAM-এ থাকে। তাহলে পার্থক্য কোথায়? পার্থক্য হলো কে জায়গার হিসাব রাখে আর জায়গাটি কতক্ষণ থাকে।

বিষয়StackHeap
কে নেয়, কে ছাড়েCompiler নিজে — ফাংশন ঢুকলে নেয়, বেরোলে ছাড়েআপনি — malloc দিয়ে নেন, free দিয়ে ছাড়েন
কতক্ষণ থাকেফাংশন শেষ হওয়া পর্যন্তআপনি free না করা পর্যন্ত
আকারছোট — কয়েক MBবড় — RAM যতটুকু দেয়
গতিখুব দ্রুত — শুধু একটি register সরানোতুলনামূলক ধীর — খালি জায়গা খুঁজতে হয়
আকার কখন ঠিক হয়মূলত compile করার সময়চলার সময়, যত খুশি
ভুল হলেStack overflow, ফেরত দেওয়া লোকালের ঠিকানাLeak, use-after-free, double free

Stack হলো চায়ের দোকানের ট্রে। কাপ রাখলেন, চা খেলেন, দোকানদার নিজেই সরিয়ে নিলেন — আপনাকে কিছু মনে রাখতে হলো না। কিন্তু ট্রে-তে বড়জোর কয়েকটি কাপ ধরে। Heap হলো হোটেলের রুম। রিসেপশনে চাইলে যত দিন খুশি থাকতে পারেন, বড় পরিবারও আঁটে। কিন্তু চেক-আউটের সময় চাবি ফেরত দেওয়ার দায়িত্ব আপনার। কেউ মনে করিয়ে দেবে না।

তাহলে নিয়মটি সহজ। ছোট, নির্দিষ্ট আকারের, শুধু এই ফাংশনেই লাগবে — stack. আকার চলার সময় জানা যাবে, অনেক বড়, অথবা ফাংশন শেষ হওয়ার পরেও লাগবে — heap. শেষ শর্তটি খুব গুরুত্বপূর্ণ। কোনো ফাংশন যদি একটি নতুন array বানিয়ে ফেরত দিতে চায়, লোকাল array-র ঠিকানা ফেরত দিলে চলবে না — ফাংশন শেষ হতেই সেই জায়গা মুছে যায়। তখন heap-ই একমাত্র ভরসা।

৪ · চারটি ফাংশন — malloc, calloc, realloc, free

Heap ব্যবহারের সব কাজ চারটি ফাংশনে। চারটিই আছে <stdlib.h> header-এ। এদের পেছনে কাজ করে allocator নামের একটি কোড, যে heap-এর কোন অংশ খালি আর কোনটি কার — সেই হিসাব রাখে।

malloc(bytes)

malloc নির্দিষ্ট সংখ্যক byte-এর একটি block দেয় এবং তার শুরুর ঠিকানা ফেরত দেয়। ভেতরের মান garbage value — আগে যা ছিল তাই।

int *p = malloc(n * sizeof *p);

calloc(count, size)

calloc কয়টি জিনিস আর প্রতিটির আকার আলাদা করে নেয়, আর পুরো block শূন্য দিয়ে ভরে দেয়। গুণ করতে গিয়ে overflow হলে নিরাপদে NULL দেয়।

int *p = calloc(n, sizeof *p);

realloc(ptr, bytes)

realloc আগের block বড় বা ছোট করে। পাশে জায়গা না থাকলে নতুন জায়গায় সরিয়ে পুরোনো ডেটা কপি করে, পুরোনো block ছেড়ে দেয়। ঠিকানা বদলাতে পারে!

tmp = realloc(p, new_n * sizeof *p);

free(ptr)

free block-টি heap-এ ফেরত দেয়। ঠিক সেই ঠিকানাটিই দিতে হবে যা malloc/calloc/realloc দিয়েছিল। free(NULL) নিরাপদ — কিছুই করে না।

free(p); p = NULL;

তিনটি অভ্যাস যা প্রতিবার লাগবে

১. সবসময় NULL যাচাই। মেমরি না পেলে malloc একটি NULL pointer ফেরত দেয়। যাচাই না করে সেটিতে লিখলে প্রোগ্রাম crash করে। “আমার ল্যাপটপে 16 GB RAM, ব্যর্থ হবে কেন?” — কারণ ছোট ফোন, এমবেডেড বোর্ড, বা মেমরি-সীমা দেওয়া সার্ভারেও এই কোড চলতে পারে। আর ভুল ইনপুটে n যদি বিশাল হয়, তখনো ব্যর্থ হবে।

২. sizeof *p ব্যবহার করুন, টাইপের নাম নয়। malloc(n * sizeof(int)) চলে, কিন্তু পরে কেউ p-কে double * বানালে এই লাইন বদলাতে ভুলে যাবে। malloc(n * sizeof *p) লিখলে p যে টাইপেরই হোক, আকার ঠিক থাকে। আর C-তে malloc-এর ফলাফলকে cast করার দরকার নেই (C++-এ লাগে, C-তে নয়)।

৩. গুণফলের overflow খেয়াল করুন। n * sizeof *p যদি size_t-এর সীমা ছাড়িয়ে যায়, পর্ব ০৮-এর wraparound-এর মতো ঘুরে ছোট একটি সংখ্যা হয়ে যায়। তখন malloc খুশি মনে ছোট block দেবে, আর আপনি বড় ভেবে লিখতে থাকবেন — buffer overflow. বাইরের ইনপুট থেকে আসা n-এর ক্ষেত্রে আগে সীমা যাচাই করুন, অথবা calloc(n, sizeof *p) ব্যবহার করুন — আধুনিক calloc এই গুণফল নিজেই যাচাই করে।

এবার একটি আসল উদাহরণ। প্রোগ্রাম প্রথমে জানবে কতজন ছাত্র, তারপর ঠিক ততগুলো ঘরের জায়গা নেবে, নম্বর পড়বে, গড় বের করবে, আর গড়ের চেয়ে বেশি পাওয়াদের নম্বর দেখাবে। ইনপুট আগে থেকেই দেওয়া আছে — ৫ জন ছাত্র:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n;
    if (scanf("%d", &n) != 1 || n <= 0) return 1;
    int *marks = malloc((size_t)n * sizeof *marks);
    if (marks == NULL) { printf("Out of memory\n"); return 1; }
    double sum = 0;
    for (int i = 0; i < n; i++) {
        if (scanf("%d", &marks[i]) != 1) marks[i] = 0;
        sum += marks[i];
    }
    double avg = sum / n;
    printf("Average: %.2f\nAbove average:", avg);
    for (int i = 0; i < n; i++)
        if (marks[i] > avg) printf(" %d", marks[i]);
    printf("\n");
    free(marks);
    marks = NULL;
    return 0;
}

প্রথম লাইনের ইনপুট 5 পড়ার পরেই প্রোগ্রাম জানল কতগুলো ঘর লাগবে, আর malloc দিয়ে ঠিক ৫টি int-এর জায়গা নিল। (size_t)n লেখা হয়েছে যাতে গুণ int-এ না হয়ে বড় size_t-এ হয়। এরপর marks[i] — ঠিক সাধারণ array-র মতোই ব্যবহার, কারণ পর্ব ১৮-এ দেখেছেন pointer আর array-র ঠিকানা-হিসাব একই। গড় ৭৪, তাই ছাপা হয় 85 91. শেষে free(marks) দিয়ে জায়গা ফেরত, আর marks = NULL দিয়ে পুরোনো ঠিকানাটিও মুছে ফেলা — যাতে ভুল করে আবার ব্যবহার না হয়। ইনপুটে ৫-এর বদলে ৫০০০ দিলেও একই কোড চলবে; এটাই dynamic memory-র মূল সুবিধা।

৫ · একটি block-এর জীবনচক্র — জন্ম থেকে “মৃত্যুর পরে”

এবার heap-এর ভেতরে চোখ রাখি। নিচের কোডে একটি block নেওয়া হচ্ছে, ব্যবহার হচ্ছে, ফেরত দেওয়া হচ্ছে — তারপর একটি সাধারণ ভুল। ডান দিকে heap-এর একটি block-এর অবস্থা খেয়াল করুন। শেষের দিকের ধাপগুলোতে লেখা আছে “যা ঘটতে পারে” — কারণ সেখানে C-র নিয়ম আর কোনো নিশ্চয়তা দেয় না।

1int *p = malloc(3 * sizeof *p);2if (p == NULL) return 1;3p[0] = 50; p[1] = 70; p[2] = 90;4free(p);5char *msg = malloc(12);6strcpy(msg, "OTP 482913");7p[0] = 99;   /* use-after-free! */8printf("msg = %s\n", msg);
Stack ও heap-এর অবস্থা
p
block 0x5000-এ
block-এর মালিক
msg
  1. শুরু। Heap-এ ঠিকানা 0x5000-এ ১২ byte-এর একটি খালি জায়গা আছে। এখনো কেউ নেয়নি। p নামের pointer-এ আপাতত কোনো অর্থপূর্ণ মান নেই।
  2. malloc — চাবি হাতে পেলেন। ৩টি int মানে ১২ byte চাওয়া হলো। Allocator খালি জায়গাটি খুঁজে দিল, আর p-তে এলো তার ঠিকানা 0x5000. ভেতরের মান এখনো আবর্জনা (? ? ?) — malloc শূন্য করে দেয় না।
  3. NULL যাচাই। মেমরি পাওয়া গেছে কি না দেখা হলো। এই লাইনটি ছোট, কিন্তু পেশাদার কোডে এটি কখনো বাদ যায় না।
  4. ব্যবহার। তিনটি নম্বর লেখা হলো। এই মুহূর্তে block-টির বৈধ মালিক p, আর সব ঠিকঠাক।
  5. free — চাবি ফেরত। Block heap-এ ফেরত গেল। Allocator এখন এটিকে “খালি” তালিকায় রাখল, এবং প্রায়ই block-এর প্রথম কয়েকটি byte-এ নিজের হিসাব লিখে রাখে। কিন্তু খেয়াল করুন — p-এর ভেতরে এখনো 0x5000 লেখা! free আপনার pointer বদলায় না। এখন p একটি dangling pointer — পর্ব ১৮-এ যার কথা বলেছিলাম।
  6. যা ঘটতে পারে — একই block অন্য কারও হাতে। প্রোগ্রামের অন্য অংশ ১২ byte চাইল। Allocator সদ্য খালি হওয়া ঠিক সেই block-টিই দিল — এটি খুবই সাধারণ, কারণ সাম্প্রতিক খালি জায়গা আবার দেওয়া দ্রুত। এখন p আর msg একই জায়গা দেখাচ্ছে, অথচ বৈধ মালিক শুধু msg.
  7. নতুন মালিকের গোপন তথ্য। msg-এ একটি OTP রাখা হলো। এই block-এ এখন সংবেদনশীল তথ্য।
  8. যা ঘটতে পারে — use-after-free. পুরোনো p দিয়ে লেখা হলো p[0] = 99. এটিই use-after-free. প্রোগ্রাম crash করল না, কোনো error দিল না — চুপচাপ msg-এর প্রথম ৪টি byte নষ্ট হয়ে গেল — 99 মানে ASCII-তে 'c', তারপর তিনটি শূন্য।
  9. ফল — অন্য কারও ডেটা বদলে গেছে। msg ছাপালে আসে শুধু c. বাগ যে লাইনে (লাইন ৭), লক্ষণ দেখা দিল অন্য লাইনে, অন্য ভ্যারিয়েবলে। বড় প্রোগ্রামে এই দুটি জায়গা হাজার লাইন দূরে হতে পারে। আর হ্যাকার যদি p দিয়ে কী লেখা হবে তা নিয়ন্ত্রণ করতে পারে, তাহলে সে অন্যের ডেটা — এমনকি প্রোগ্রাম কোন কোড চালাবে তা-ও — বদলে দিতে পারে।

উপরের ধাপ ৬–৯ কোনো নিশ্চিত ঘটনা নয়। Use-after-free হলো undefined behavior — একই কোড একবার “ঠিকঠাক” চলবে, আরেকবার crash করবে, আরেকবার অন্যের ডেটা নষ্ট করবে। ঠিক এই অনিশ্চয়তার কারণেই এমন বাগ মাসের পর মাস লুকিয়ে থাকে। সমাধান সহজ অভ্যাস: free(p); লেখার ঠিক পরের লাইনেই p = NULL;. তখন ভুল করে ব্যবহার করলে সাধারণ কম্পিউটারে প্রায় নিশ্চিতভাবে crash হবে — নীরবে ডেটা নষ্ট হবে না।

৬ · পাঁচটি বিপজ্জনক ভুল — এবং কেন এগুলো বাস্তবে এত ক্ষতি করে

Heap-এর প্রায় সব বাগ এই পাঁচ ধরনের কোনো একটি। নামগুলো মনে রাখুন — কারণ sanitizer আর Valgrind ঠিক এই নামেই ভুল জানায়।

ভুলকোডে কেমন দেখায়ফল
Memory leakmalloc করেছেন, free করেননি; অথবা free-র আগেই pointer-এ অন্য ঠিকানা বসিয়ে দিয়েছেনমেমরি ধীরে ধীরে ফুরায়; দীর্ঘ সময় চলা প্রোগ্রাম শেষে থেমে যায়
Use-after-freefree(p); তারপর p[0] = 1;অন্যের ডেটা নষ্ট, crash, নিরাপত্তা-ত্রুটি
Double freeএকই pointer দুবার freeAllocator-এর হিসাব নষ্ট; প্রায়ই সঙ্গে সঙ্গে abort, কখনো হামলার সুযোগ
Dangling pointerfree-র পরে বা ফাংশন শেষ হওয়ার পরে পুরোনো ঠিকানা রেখে দেওয়াপরে use-after-free বা double free-র জন্ম দেয়
Heap-এর নয় এমন জিনিস freeint a[10]; free(a); বা free(p + 1);Undefined behavior; সাধারণত free(): invalid pointer দিয়ে abort

Memory leak — হোটেলের চাবি যে কখনো ফেরত আসে না

একটি হোটেলে ১০০টি রুম। প্রতিদিন কয়েকজন অতিথি চেক-আউটের সময় চাবি ফেরত না দিয়ে চলে যান। রিসেপশনের খাতায় রুমগুলো তখনো “ভাড়া দেওয়া” লেখা থাকে, অথচ ভেতরে কেউ নেই। প্রথম সপ্তাহে কেউ টের পায় না। কয়েক মাস পরে দেখা যায় হোটেল প্রায় খালি, কিন্তু নতুন অতিথিকে দেওয়ার মতো একটি রুমও নেই।

Memory leak ঠিক এমন। একটি ছোট প্রোগ্রাম যা কয়েক সেকেন্ড চলে বন্ধ হয়, তার leak প্রায় চোখেই পড়ে না — প্রোগ্রাম শেষ হলে অপারেটিং সিস্টেম পুরো মেমরি ফেরত নিয়ে নেয়। আসল বিপদ দীর্ঘ সময় চলা প্রোগ্রামে: ওয়েব সার্ভার, ডেটাবেস, মোবাইল অ্যাপ, রাউটারের firmware — যেগুলো দিনের পর দিন বন্ধ হয় না। নিচের সার্ভারটি প্রতিটি request-এ মাত্র 1 KB হারায়:

1while (server_is_running) {2    char *buf = malloc(1024);3    read_request(buf);4    send_reply(buf);56}
সার্ভারের অবস্থা
সামলানো request
হারানো মেমরি
প্রোগ্রামের মোট RAM
অবস্থা
  1. সার্ভার চালু হলো। শুরুতে সে প্রায় 20 MB মেমরি ব্যবহার করছে। প্রতিটি request এলে লুপ একবার ঘোরে — একটি 1 KB buffer নেয়, request পড়ে, উত্তর পাঠায়।
  2. প্রথম request. লুপ শেষ হতেই buf নামের লোকাল pointer মুছে গেল (সে stack-এ ছিল)। কিন্তু heap-এর 1 KB block মুছল না — আর তার ঠিকানা এখন কারও কাছে নেই। এই block আর কখনো free করা সম্ভব নয়। এটাই leak.
  3. এক হাজার request. প্রায় 1 MB হারিয়েছে। পরীক্ষার সময় ডেভেলপার কয়েকশো request পাঠিয়ে দেখেছিলেন — সব ঠিক। এই কারণেই leak পরীক্ষায় ধরা পড়ে না।
  4. দশ লাখ request. একটি ব্যস্ত সার্ভারে এটি কয়েক ঘণ্টার ব্যাপার। হারানো মেমরি এখন প্রায় 1 GB. সার্ভার ধীর হচ্ছে, কারণ অপারেটিং সিস্টেম RAM বাঁচাতে ডিস্কে ডেটা সরাচ্ছে।
  5. Out of memory. একসময় RAM ফুরিয়ে যায়। Linux তখন সবচেয়ে বেশি মেমরি খাওয়া প্রোগ্রামকে জোর করে বন্ধ করে দেয় — টার্মিনালে শুধু Killed. সার্ভার বন্ধ, সব গ্রাহক error দেখছেন। রাতে নিয়ম করে restart দেওয়া এর সমাধান নয় — সেটি রোগ না সারিয়ে শুধু ব্যথানাশক খাওয়া।
  6. সমাধান — একটি লাইন। লুপের শেষে free(buf); যোগ হলো। এখন প্রতিটি request একই 1 KB নেয় আর ফেরত দেয়; allocator প্রায়ই একই block বারবার দেয়। দশ লাখ request-এর পরেও মেমরি 20 MB-তে স্থির।

Use-after-free কেন হ্যাকারদের প্রিয়

Leak প্রোগ্রামকে ধীরে ধীরে মারে, কিন্তু গোপন তথ্য ফাঁস করে না। Use-after-free আরও বিপজ্জনক, কারণ free হওয়া block প্রায়ই অন্য কারও কাছে যায়। আক্রমণকারী সেই নতুন ডেটা নিজে বেছে দিতে পারলে পুরোনো pointer দিয়ে চলা কোড তারই সাজানো ডেটা পড়বে। ব্রাউজারে এটি সবচেয়ে সহজ, কারণ যেকোনো ওয়েবসাইটের JavaScript ব্রাউজারকে দিয়ে মেমরি নেওয়া-ছাড়া করাতে পারে। এজন্যই বড় ব্রাউজারগুলোর গুরুতর নিরাপত্তা-ত্রুটির তালিকায় use-after-free বছরের পর বছর উপরের দিকে থাকে (বিস্তারিত ভাবনার প্রশ্ন ৩-এ)।

Double free আর ভুল জিনিস free

Allocator প্রতিটি block-এর আশেপাশে নিজের হিসাব রাখে — block কত বড়, পরের খালি block কোনটি। একই block দুবার free করলে খালি-তালিকায় একই জায়গা দুবার ঢোকে। ফলে পরে দুটি আলাদা malloc একই জায়গা পেতে পারে — যা আবার use-after-free-র দরজা খোলে। Stack-এর array বা block-এর মাঝখানের ঠিকানা free করলে allocator এমন জায়গায় নিজের হিসাব খোঁজে যেখানে তা নেই। আধুনিক glibc অনেক ক্ষেত্রেই এটি ধরে ফেলে প্রোগ্রাম থামিয়ে দেয়:

free(): double free detected in tcache 2
Aborted (core dumped)

ভুলটি অন্তত সশব্দে ধরা পড়ল — কিন্তু সব ভুল এভাবে ধরা পড়ে না।

৭ · বাড়তে পারে এমন array — capacity দ্বিগুণ করার কৌশল

অনেক সময় শুরুতে সংখ্যাটিও জানা থাকে না। যেমন একটি ফাইল থেকে লাইন পড়ছেন, কতগুলো লাইন জানেন না। Python-এ আপনি list.append() লেখেন, C++-এ vector.push_back(). এই সুবিধাজনক জিনিসগুলোর ভেতরে আসলে কী আছে? একটি heap array, আর realloc. একে বলে dynamic array.

কৌশলটি হলো দুটি সংখ্যা আলাদা রাখা: size — কতগুলো ঘর এখন ভরা, আর capacity — মোট কতগুলো ঘরের জায়গা নেওয়া আছে। যতক্ষণ size < capacity, নতুন জিনিস সরাসরি পরের ঘরে বসে। ঘর ফুরালে capacity দ্বিগুণ করা হয়। নিচে দেখুন ১০, ২০, ৩০ … ৮০ যোগ করলে কী ঘটে:

1if (size == cap) {2    size_t newcap = cap ? cap * 2 : 1;3    int *tmp = realloc(data, newcap * sizeof *tmp);4    if (tmp == NULL) { /* data এখনো অক্ষত */ }5    data = tmp;  cap = newcap;6}7data[size++] = x;
Array-র অবস্থা
নতুন x
size
capacity
data
block
মোট কপি
  1. খালি array. শুরুতে কোনো জায়গাই নেওয়া হয়নি: data = NULL, size ও capacity দুটোই 0. এখনো heap-এর কোনো খরচ নেই।
  2. ১০ যোগ — প্রথম জায়গা। size == cap (0 == 0), তাই বাড়াতে হবে। নতুন capacity 1. realloc(NULL, …) ঠিক malloc-এর মতো কাজ করে — নতুন block পাওয়া গেল 0x100-এ। কপি করার মতো কিছু ছিল না।
  3. ২০ যোগ — জায়গা ফুরিয়েছে। capacity 1 থেকে 2. ধরুন 0x100-এর ঠিক পাশে খালি জায়গা নেই, তাই realloc নতুন block নিল 0x200-এ, পুরোনো ১টি মান কপি করল, আর পুরোনো block নিজেই free করে দিল। ঠিকানা বদলে গেছে — পুরোনো ঠিকানা কেউ রেখে দিলে সেটি এখন dangling.
  4. ৩০ যোগ — আবার দ্বিগুণ। capacity 2 থেকে 4, আরেকবার সরানো (0x300), ২টি মান কপি। মোট কপি এখন ৩. শেষ ঘরটি (_) খালি — ভবিষ্যতের জন্য রাখা।
  5. ৪০ যোগ — কোনো খরচ নেই। জায়গা আছে, সরাসরি পরের ঘরে বসল। realloc ডাকাই হলো না। দ্বিগুণ করার সুফল এখানেই।
  6. ৫০ যোগ — 4 থেকে 8. ৪টি মান কপি, মোট কপি ৭. এবার একসাথে ৩টি খালি ঘর পাওয়া গেল। প্রতিবার দ্বিগুণ হওয়ায় পরের বড় খরচ আসতে দেরি হয় দ্বিগুণ সময়।
  7. ৬০, ৭০, ৮০ — বিনা খরচে। তিনটিই খালি ঘরে বসল। মোট ৮টি জিনিস যোগ করতে কপি লাগল মাত্র ৭টি — অর্থাৎ গড়ে একটিরও কম। প্রতিবার মাত্র ১ ঘর বাড়ালে লাগত 0+1+2+…+7 = ২৮টি কপি। দশ লাখ জিনিসে এই পার্থক্য হয় প্রায় দশ লাখ বনাম প্রায় পঞ্চাশ হাজার কোটি কপি।
কেন দ্বিগুণ কাজ করে

দ্বিগুণ করলে কপির সংখ্যা হয় 1 + 2 + 4 + … — আর এই যোগফল সবসময় শেষ সংখ্যার দ্বিগুণের চেয়ে কম। ফলে n টি জিনিস যোগ করতে মোট কপি 2n-এর কম। মাঝে মাঝে একটি যোগ খুব ব্যয়বহুল হয়, কিন্তু মোট খরচ সবার মধ্যে ভাগ করলে প্রতিটির গড় খরচ স্থির থাকে। একে বলে amortized O(1). এটা অনেকটা মাসিক বাসভাড়ার মতো — মাসের শুরুতে একবারে বড় টাকা দেন, কিন্তু প্রতিদিনের হিসাবে খরচ সামান্য।

এবার পুরো কৌশলটি চালিয়ে দেখুন। প্রোগ্রামটি ১ থেকে ১০ পর্যন্ত সংখ্যার বর্গ রাখছে, আর প্রতিবার জায়গা বাড়ানোর সময় একটি লাইন ছাপছে:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *data = NULL;
    size_t size = 0, cap = 0;
    for (int x = 1; x <= 10; x++) {
        if (size == cap) {
            size_t newcap = cap ? cap * 2 : 1;
            int *tmp = realloc(data, newcap * sizeof *tmp);
            if (tmp == NULL) { free(data); return 1; }
            printf("full at size %zu: capacity %zu -> %zu\n", size, cap, newcap);
            data = tmp;
            cap = newcap;
        }
        data[size++] = x * x;
    }
    printf("size=%zu capacity=%zu last=%d\n", size, cap, data[size - 1]);
    free(data);
    return 0;
}

আউটপুটে capacity বাড়বে 0 → 1 → 2 → 4 → 8 → 16 — মোট ১০টি জিনিসের জন্য মাত্র ৫ বার realloc. সবচেয়ে গুরুত্বপূর্ণ লাইন হলো int *tmp = realloc(data, …). সরাসরি data = realloc(data, …) লিখলে, realloc ব্যর্থ হয়ে NULL দিলে data-তে NULL বসে যেত — আর পুরোনো block-এর ঠিকানা চিরতরে হারিয়ে যেত (leak), যদিও realloc সেটি ছেড়ে দেয়নি। তাই আগে tmp-এ নিন, সফল হলে তবেই data = tmp. শেষে একটিমাত্র free(data) — কারণ realloc পুরোনো block-গুলো নিজেই ছেড়ে দিয়েছে। শেষ লাইনে capacity 16 কিন্তু size 10 — ৬টি ঘর খালি পড়ে আছে। এটাই দ্বিগুণ কৌশলের দাম: গতির বিনিময়ে কিছু মেমরি বাড়তি।

৮ · ভুল ধরার যন্ত্র — AddressSanitizer, Valgrind আর মালিকানার নিয়ম

আগেই দেখেছেন, heap-এর বাগ প্রায়ই নীরব থাকে। চোখে দেখে খোঁজা প্রায় অসম্ভব। তাই পেশাদাররা টুল ব্যবহার করেন। পর্ব ০৩-এ sanitizer-এর কথা বলেছিলাম — মেট্রো স্টেশনের সিকিউরিটি গেটের মতো। এবার তার আসল কাজ দেখার সময়।

AddressSanitizer (ASan)

Compile করার সময় একটি flag দিলেই compiler প্রতিটি মেমরি-ব্যবহারের পাশে পাহারা বসায়। Free হওয়া block কিছুক্ষণ “কোয়ারেন্টাইনে” রাখে, যাতে use-after-free হাতেনাতে ধরা পড়ে। প্রোগ্রাম প্রায় দুই গুণ ধীর হয় — পরীক্ষার জন্য যথেষ্ট দ্রুত।

GCC, Clang (Linux, Mac); MSVC-তেও আছে

Valgrind

Valgrind নতুন করে compile করতে হয় না — তৈরি প্রোগ্রামটিই নিজের নিয়ন্ত্রণে চালিয়ে প্রতিটি মেমরি-ব্যবহার পরীক্ষা করে, আর শেষে leak-এর পূর্ণ হিসাব দেয়। তবে প্রোগ্রাম অনেক গুণ ধীর চলে।

মূলত Linux
gcc -g -fsanitize=address grow.c -o grow && ./grow
valgrind --leak-check=full ./grow

ধরুন উপরের অংশ ৫-এর মতো use-after-free আছে। ASan সঙ্গে সঙ্গে প্রোগ্রাম থামিয়ে এমন রিপোর্ট দেবে (সংক্ষেপে):

ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
WRITE of size 4 at 0x602000000010 thread T0
    #0 in main uaf.c:7
freed by thread T0 here:
    #1 in main uaf.c:4
previously allocated by thread T0 here:
    #1 in main uaf.c:1

রিপোর্টটি পড়তে শিখুন — এতে তিনটি তথ্য আছে: কোথায় ভুল ব্যবহার হলো (লাইন ৭), কোথায় free হয়েছিল (লাইন ৪), আর কোথায় নেওয়া হয়েছিল (লাইন ১)। পুরো জীবনচক্র এক নজরে! Linux-এ ASan প্রোগ্রাম শেষে leak-ও জানায় — LeakSanitizer: detected memory leaks. আর Valgrind leak পেলে বলে definitely lost: 1024 bytes in 1 blocks; কোনো leak না থাকলে বলে All heap blocks were freed -- no leaks are possible. শেষ লাইনটিই আপনার লক্ষ্য।

Windows-এর MSYS2 GCC-তে ASan ঠিকমতো কাজ নাও করতে পারে। সহজ পথ হলো WSL (Windows-এর ভেতরে Linux) ব্যবহার করা, অথবা Visual Studio-র MSVC compiler-এ /fsanitize=address. Mac-এ Clang-এর ASan চলে, তবে leak খোঁজার অংশটি (LeakSanitizer) সেখানে প্রায়ই কাজ করে না বা আলাদা করে চালু করতে হয়।

মালিকানার নিয়ম — কে free করবে?

টুল ভুল ধরে, কিন্তু ভুল না করার উপায় হলো নকশা। বড় প্রোগ্রামে একটি block এক ফাংশনে তৈরি হয়, আরেক ফাংশনে ব্যবহার হয়, তৃতীয় কোনো ফাংশনে হয়তো রাখা হয়। তখন প্রশ্ন ওঠে — free কে করবে? দুজনেই করলে double free, কেউ না করলে leak. সমাধান হলো ownership বা মালিকানার স্পষ্ট নিয়ম:

  • যে ফাংশন malloc করে ফেরত দেয়, সে লিখে জানাবে: “ফেরত পাওয়া block-টি ডাকা ফাংশনকে free করতে হবে।” যেমন standard library-র strdup বা getline-এর নথিতে ঠিক এমন কথা লেখা থাকে।
  • যে নেয়, সে ছাড়ে — একই স্তরে। যতটা সম্ভব একই ফাংশনে বা জোড়া ফাংশনে (list_create / list_destroy) নেওয়া আর ছাড়া রাখুন।
  • ধার দেওয়া মানে মালিকানা দেওয়া নয়। কোনো ফাংশন শুধু পড়ার জন্য pointer পেলে সে কখনো free করবে না।
  • সব বের হওয়ার পথে free. ফাংশনের মাঝখানে error-এ return করলেও আগে নেওয়া block ছাড়তে হবে — leak-এর সবচেয়ে সাধারণ উৎস এটাই।

৯ · AI-এর যুগে ডায়নামিক মেমরি

AI-কে “একটি dynamic array লিখে দাও” বললে কয়েক সেকেন্ডে কোড পাবেন, এবং বেশিরভাগ সময় তা চলবেও। সমস্যা হলো, মেমরির বাগ “চলে” — আপনার ছোট পরীক্ষায় কোনো লক্ষণ দেখায় না। AI নিজে কোডটি দীর্ঘ সময় চালায় না, sanitizer দিয়েও না। তাই এই দায়িত্ব আপনার।

AI সাধারণত যেসব মেমরি-ভুল করে

AI-এর ভুলকোডে কেমন দেখায়কীভাবে ধরবেন
free ভুলে যাওয়া, বিশেষ করে error-এর পথেমাঝখানে return -1;, তার আগে free নেইপ্রতিটি return থেকে উল্টো দিকে পড়ুন; Valgrind/ASan
NULL যাচাই বাদp = malloc(…); p[0] = …প্রতিটি malloc/calloc/realloc-এর পরের লাইন দেখুন
realloc সরাসরি একই pointer-এp = realloc(p, n);ব্যর্থ হলে পুরোনো block হারায় — tmp ব্যবহার করতে বলুন
pointer আর element-এর আকার গুলিয়ে ফেলাmalloc(n * sizeof(p)) বা malloc(sizeof(s))sizeof *p হওয়ার কথা; pointer-এর আকার প্রায়ই 8 byte, জিনিসের আকার নয়
string-এর শেষ '\0'-এর জায়গা না রাখাmalloc(strlen(s))হওয়ার কথা strlen(s) + 1 (পর্ব ১৯)
লোকাল array-র ঠিকানা ফেরত দেওয়াint a[10]; … return a;-Wall warning দেয়; heap থেকে নিতে হবে

মেমরি-সংক্রান্ত কোড চাইলে শুরুতেই নিয়মগুলো বলে দিন। শেষ অংশটি খেয়াল করুন — AI-কে দিয়েই মালিকানার হিসাব লিখিয়ে নিচ্ছেন, যাতে আপনি মিলিয়ে দেখতে পারেন।

C-তে একটি dynamic array লিখে দাও যেখানে int যোগ করা যায় (push) এবং শেষে সব মেমরি ছেড়ে দেওয়া যায়। নিয়মগুলো মানবে: ১. প্রতিটি malloc/calloc/realloc-এর পরে NULL যাচাই করবে। ২. realloc-এর ফল আগে একটি আলাদা tmp pointer-এ নেবে। ৩. আকারের জন্য sizeof *p ব্যবহার করবে, টাইপের নাম নয়। ৪. capacity দ্বিগুণ করবে, আর n * size গুণের overflow যাচাই করবে। ৫. কোড শেষে একটি ছোট তালিকা দাও: কোন ফাংশন মেমরি নেয়, কোনটি ছাড়ে, আর কে কোনটির মালিক। আমি কোডটি gcc -Wall -Wextra -g -fsanitize=address দিয়ে চালিয়ে পরীক্ষা করব।

AI-এর মেমরি-কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

malloc করলে free, free করলে NULL.
বিশ্বাস নয় — sanitizer দিয়ে প্রমাণ।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Java, Python, Go নিজেই মেমরি ফেরত নেয় — তাহলে C কেন এই কঠিন কাজটি প্রোগ্রামারের ঘাড়ে চাপায়?

Java, Python বা Go-তে একটি garbage collector (GC) থাকে। সে মাঝে মাঝে খুঁজে দেখে কোন বস্তু আর কেউ ব্যবহার করছে না, তারপর নিজেই ফেরত নেয়। প্রোগ্রামারের জীবন অনেক সহজ — leak কম, use-after-free প্রায় অসম্ভব। তাহলে C কেন এটা দেয় না? কারণ প্রতিটি সুবিধার একটি দাম আছে।

  • নিয়ন্ত্রণ: C তৈরি হয়েছিল ১৯৭০-এর দশকে, Unix লেখার জন্য — যখন কম্পিউটারে মেমরি ছিল মাত্র কয়েক ডজন কিলোবাইট। অপারেটিং সিস্টেম নিজেই তো মেমরির ব্যবস্থাপক; তার নিচে আরেকজন ব্যবস্থাপক বসানো যায় না। আজও kernel, driver, firmware-এর ক্ষেত্রে একই কথা।
  • অপেক্ষার সময় (latency): GC কখন চলবে, কতক্ষণ চলবে — তা সবসময় আগে থেকে বলা যায় না। একটি গেমে ফ্রেম আটকে যাওয়া, শেয়ারবাজারের ট্রেডিং সার্ভারে কয়েক মিলিসেকেন্ড দেরি, বা গাড়ির ব্রেক-কন্ট্রোলারে হঠাৎ থেমে যাওয়া — এসব জায়গায় অনিশ্চিত বিরতি গ্রহণযোগ্য নয়। C-তে free কখন হবে, আপনি নিজে জানেন।
  • মেমরির খরচ: GC ভালোভাবে চলতে সাধারণত দরকারের চেয়ে বেশি মেমরি হাতে রাখে। ছোট মাইক্রোকন্ট্রোলারে তা সম্ভব নয়।

কিন্তু দামও বিশাল। Microsoft ও Google-এর মতো কোম্পানি প্রকাশ্যে জানিয়েছে, তাদের পণ্যের গুরুতর নিরাপত্তা-ত্রুটির বড় অংশই মেমরি-ব্যবস্থাপনার ভুল থেকে আসে — ঠিক এই পর্বের leak, use-after-free, buffer overflow. অর্থাৎ নিয়ন্ত্রণের স্বাধীনতার বিনিময়ে নিরাপত্তার দায়িত্বও পুরোপুরি মানুষের কাঁধে।

মাঝামাঝি পথও আছে। C++ ব্যবহার করে RAII — বস্তু scope থেকে বেরোলেই নিজে থেকে ছাড়া হয়। Python আসলে বেশিরভাগ বস্তু reference counting দিয়ে সঙ্গে সঙ্গে ছাড়ে, আর GC শুধু চক্রাকার সম্পর্ক সামলায়। Rust compile করার সময়ই নিয়ম যাচাই করে (প্রশ্ন ৪)।

মূল উপলব্ধি: মেমরি ব্যবস্থাপনা কোনো “ঠিক” বা “ভুল” পদ্ধতি নয় — এটি নিয়ন্ত্রণ, গতি, অনুমানযোগ্যতা আর নিরাপত্তার মধ্যে একটি দর-কষাকষি। C সবচেয়ে বেশি নিয়ন্ত্রণ দেয়, তাই সবচেয়ে বেশি দায়িত্বও দেয়।

প্র ০২malloc আসলে মেমরি পায় কোথা থেকে? free করলে কি মেমরি সঙ্গে সঙ্গে অপারেটিং সিস্টেমে ফেরত যায়?

malloc কোনো system call নয় — এটি C library-র একটি সাধারণ ফাংশন। প্রতিবার malloc ডাকলে যদি অপারেটিং সিস্টেমের কাছে যেতে হতো, তা হতো খুব ধীর, কারণ OS-এর কাছে যাওয়া (system call) তুলনামূলক ব্যয়বহুল। তাই কাজটি দুই স্তরে হয়।

  • পাইকারি কেনা: Allocator অপারেটিং সিস্টেমের কাছ থেকে একবারে বড় বড় টুকরো নেয়। Linux-এ এর দুটি পথ — brk/sbrk (heap-এর শেষ সীমা উপরে ঠেলে দেওয়া) আর mmap (মেমরির যেকোনো জায়গায় নতুন পৃষ্ঠা চাওয়া)। glibc-এর allocator খুব বড় অনুরোধ (ডিফল্টে প্রায় ১২৮ KB-র বেশি) সরাসরি mmap দিয়ে সামলায়। Windows-এ একই কাজ করে VirtualAlloc ও Heap API.
  • খুচরা বিক্রি: সেই বড় টুকরো থেকে allocator ছোট ছোট block কেটে আপনাকে দেয়। প্রতিটি block-এর ঠিক আগে সে কিছু হিসাব (যেমন block-এর আকার) লিখে রাখে — এজন্যই free(p) শুধু ঠিকানা পেয়েই জানে কত বড় block ছাড়তে হবে। আর এজন্যই ভুল ঠিকানা free করলে বা double free করলে এই হিসাব নষ্ট হয়।
  • ফেরত দেওয়া: free সাধারণত block-টি allocator-এর “খালি” তালিকায় রাখে, OS-কে ফেরত দেয় না — কারণ খুব সম্ভব আপনি শিগগিরই আবার চাইবেন। শুধু বড় mmap-এর block বা heap-এর একেবারে শেষের বড় খালি অংশ OS-এ ফেরত যায়। এজন্য free করার পরেও Task Manager-এ প্রোগ্রামের মেমরি কমতে না-ও দেখতে পারেন — সেটি leak নয়।

আরও একটি মজার তথ্য: Linux প্রায়ই malloc-এর অনুরোধে “হ্যাঁ” বলে দেয়, কিন্তু আসল RAM দেয় তখনই, যখন আপনি সেই জায়গায় প্রথমবার লেখেন। একে বলে overcommit. ফলে malloc সফল হয়েও পরে মেমরি ফুরিয়ে প্রোগ্রাম বন্ধ হতে পারে।

মূল উপলব্ধি: malloc আর OS-এর মাঝে একজন “পাইকার” আছে — allocator. পাইকারি কিনে খুচরা বেচার এই নকশা গতি আনে, কিন্তু allocator-এর নিজের হিসাবও তখন রক্ষা করার মতো একটি গুরুত্বপূর্ণ জিনিস হয়ে দাঁড়ায়।

প্র ০৩Use-after-free এত সহজে হামলার অস্ত্র হয়ে ওঠে কেন? সাধারণ একটি বাগ কীভাবে পুরো কম্পিউটারের নিয়ন্ত্রণ দিয়ে দিতে পারে?

অংশ ৫-এর ডায়াগ্রামটি মনে করুন। free হওয়া block প্রায়ই পরের একই আকারের অনুরোধে আবার দেওয়া হয়। আক্রমণকারী এই অভ্যাসটিই কাজে লাগায়। পুরো কৌশলটি তিন ধাপে:

  • ১. ফাঁদ তৈরি: আক্রমণকারী এমন ইনপুট দেয়, যাতে প্রোগ্রাম একটি বস্তু free করে কিন্তু তার pointer কোথাও রেখে দেয়। ব্রাউজারে এটি হতে পারে একটি ওয়েবপেজের বিশেষভাবে সাজানো JavaScript.
  • ২. জায়গা দখল: সঙ্গে সঙ্গে সে একই আকারের অনেকগুলো নিজের ডেটা তৈরি করায় — যাতে free হওয়া জায়গাটি তার ডেটাতেই ভরে যায়। ব্রাউজারে JavaScript দিয়ে এটি খুব সহজ।
  • ৩. পুরোনো pointer-এর ব্যবহার: এবার প্রোগ্রাম পুরোনো pointer দিয়ে বস্তুটি ব্যবহার করে — ভাবে এটি সেই আগের বস্তু। কিন্তু ভেতরে আক্রমণকারীর ডেটা। বস্তুর ভেতরে যদি একটি ফাংশনের ঠিকানা রাখা থাকত, প্রোগ্রাম এখন আক্রমণকারীর বেছে দেওয়া ঠিকানায় লাফ দেবে।

এটি buffer overflow-এর চেয়েও সূক্ষ্ম, কারণ কোনো সীমা পেরোনো হচ্ছে না — প্রতিটি পড়া-লেখা বৈধ জায়গাতেই, শুধু ভুল সময়ে। Compiler-এর সাধারণ যাচাই তাই এটি দেখে না।

প্রতিরক্ষা: ব্রাউজার কোম্পানিগুলো একাধিক স্তরে কাজ করছে। বিশেষ allocator, যা free হওয়া জায়গা সঙ্গে সঙ্গে আবার দেয় না বা আলাদা ধরনের বস্তুর জন্য আলাদা এলাকা রাখে। “স্মার্ট” pointer, যা জানে block এখনো বেঁচে আছে কি না। প্রতিটি ওয়েবসাইট আলাদা sandbox প্রক্রিয়ায় চালানো, যাতে একটি ত্রুটি পুরো কম্পিউটারে না ছড়ায়। আর নতুন অংশ Rust-এর মতো memory-safe ভাষায় লেখা। তবু প্রতি বছর নতুন use-after-free ধরা পড়ে, কারণ কোটি কোটি লাইনের পুরোনো C/C++ কোড রাতারাতি বদলানো যায় না।

মূল উপলব্ধি: নিরাপত্তায় “ছোট বাগ” বলে কিছু নেই। যে বাগ শুধু “কখনো কখনো ভুল মান দেখায়”, সঠিক হাতে সেটিই পুরো সিস্টেমের চাবি হয়ে যেতে পারে।

প্র ০৪Rust দাবি করে GC ছাড়াই use-after-free অসম্ভব — compile করার সময়ই এটা কীভাবে সম্ভব?

Rust এই পর্বের “মালিকানার নিয়ম”-কেই ভাষার আইন বানিয়ে ফেলেছে। C-তে ownership শুধু একটি ভালো অভ্যাস, যা মন্তব্যে লেখা থাকে। Rust-এ compiler নিজেই প্রতিটি নিয়ম যাচাই করে, আর ভাঙলে কোড compile-ই হয় না।

  • একজনই মালিক: প্রতিটি মানের ঠিক একজন মালিক থাকে। মালিক scope থেকে বেরোলেই compiler নিজে free-র কোড বসিয়ে দেয়। ফলে free ভুলে যাওয়া (leak) প্রায় হয় না, আর দুবার free (double free) সম্ভবই নয় — কারণ মালিক একজন।
  • মালিকানা হস্তান্তর: একটি মান অন্য ভ্যারিয়েবলে দিলে পুরোনোটি আর ব্যবহার করা যায় না। C-তে q = p; free(p); করে q ব্যবহার করা যায়; Rust-এ এটি compile error.
  • ধার দেওয়ার নিয়ম (borrow checker): অন্য কেউ শুধু ধার নিতে পারে, আর compiler নিশ্চিত করে যে ধার নেওয়া reference কখনো মালিকের চেয়ে বেশি দিন বাঁচবে না। এটিই dangling pointer আর use-after-free বন্ধ করে।

এর দাম কী? শেখা কঠিন — নতুনরা প্রথম দিকে compiler-এর সাথে “যুদ্ধ” করেন। কিছু স্বাভাবিক নকশা (যেমন দুই দিকে যুক্ত লিস্ট) লিখতে বাড়তি কৌশল লাগে। আর একেবারে নিচের স্তরের কাজে unsafe অংশ লিখতে হয়, যেখানে আবার C-র মতোই দায়িত্ব আপনার।

তবু শিল্পজগৎ এদিকে ঝুঁকছে। ২০২২ সালে Linux kernel প্রথমবার Rust-এ লেখা কোড গ্রহণ করে। যুক্তরাষ্ট্রের NSA-সহ কয়েকটি সরকারি সংস্থা নতুন সফটওয়্যারে memory-safe ভাষা ব্যবহারের পরামর্শ দিয়েছে। Android, Windows আর বিভিন্ন ব্রাউজারের নতুন অংশেও Rust ব্যবহার বাড়ছে।

মূল উপলব্ধি: যে নিয়ম মানুষ মনে রাখতে ভুলে যায়, সেটি যন্ত্রকে দিয়ে যাচাই করানোই প্রকৌশলের পরিণত পথ। C-তে নিয়মটি আপনার মাথায়; Rust-এ compiler-এর হাতে। দুটো বুঝলেই বুঝবেন আসলে কী রক্ষা করা হচ্ছে।

প্র ০৫Dynamic array জায়গা ফুরালে প্রতিবার মাত্র একটি ঘর বাড়ায় না কেন? দ্বিগুণই বা কেন — দেড় গুণ নয় কেন?

প্রতিবার realloc-এ সবচেয়ে খারাপ ক্ষেত্রে পুরোনো সব মান নতুন জায়গায় কপি হয়। তাই প্রশ্নটি আসলে — n টি জিনিস যোগ করতে মোট কতগুলো কপি?

  • প্রতিবার +১: কপি হয় 1 + 2 + 3 + … + (n−1), যা প্রায় n²/2. দশ লাখ জিনিসে প্রায় পঞ্চাশ হাজার কোটি কপি — time complexity-র ভাষায় O(n²). প্রতিবার +১০০ করলেও শুধু ১০০ দিয়ে ভাগ হয়, ধরনটি একই থাকে।
  • প্রতিবার দ্বিগুণ: কপি হয় 1 + 2 + 4 + … + n/2, যা n-এর চেয়েও কম। মোট খরচ O(n), অর্থাৎ প্রতি জিনিসে গড়ে স্থির খরচ — amortized O(1). মূল কারণ হলো, বৃদ্ধি অনুপাতে (গুণ করে) হলে প্রতিটি বড় কপির পরে ঠিক ততগুলো বিনা খরচের যোগ পাওয়া যায়।

তাহলে দেড় গুণ কেন? যেকোনো স্থির গুণক (১.৫, ২, ৩) একই O(1) গড় দেয়; পার্থক্য হলো খরচ বনাম অপচয়। দ্বিগুণে realloc কম হয়, কিন্তু সবচেয়ে খারাপ ক্ষেত্রে প্রায় অর্ধেক জায়গা খালি পড়ে থাকে। দেড় গুণে একটু বেশি কপি, কিন্তু অপচয় কম। আরও একটি সূক্ষ্ম যুক্তি আছে: দ্বিগুণ করলে প্রতিটি নতুন block আগের সব ছাড়া block-এর যোগফলের চেয়ে বড় হয়, তাই পুরোনো ছেড়ে দেওয়া জায়গা জোড়া দিয়ে কখনো নতুন block বানানো যায় না; দেড় গুণে কিছু ধাপ পরে তা সম্ভব হয়। এজন্য কিছু C++ library দেড় গুণ ব্যবহার করে, কিছু দ্বিগুণ। Python-এর list দ্বিগুণের চেয়ে অনেক কম অনুপাতে বাড়ে — তবু অনুপাতেই বাড়ে, তাই তার গড় খরচও স্থির থাকে।

আর আগে থেকেই আকার আন্দাজ করা গেলে (যেমন ফাইলের আকার দেখে), শুরুতেই একবারে সেই capacity নিয়ে নেওয়াই সবচেয়ে ভালো — একবারও realloc লাগে না। C++-এর reserve ঠিক এই কাজটিই করে।

মূল উপলব্ধি: যোগ করে বাড়ালে খরচ জমতে থাকে, গুণ করে বাড়ালে খরচ ভাগ হয়ে যায়। এই একটি চিন্তা — জ্যামিতিক বৃদ্ধি — hash table, network-এর retry, এমনকি ক্যাশের আকার ঠিক করাতেও বারবার ফিরে আসে।

অনুশীলন

এগুলোর বেশিরভাগে কোড লিখতে হবে না — মেমরির ছবি মাথায় আঁকা আর ভাবাই আসল কাজ।

  1. কে কোথায় থাকে: নিচের প্রতিটি জিনিস মেমরির কোন ভাগে থাকে — text, data, bss, heap না stack? int year = 2025; (global) · static int hits; · main-এর ভেতরে double price; · char *s = malloc(50); — এখানে s নিজে আর তার block আলাদা করে বলুন।

    year — মান দেওয়া global, তাই data. hits — মান না দেওয়া static, তাই bss, শুরুতে মান শূন্য। price — লোকাল, তাই stack. s নিজে একটি লোকাল pointer, তাই stack-এ; কিন্তু malloc-এর ৫০ byte-এর block heap-এ। আর main-এর কোড নিজে থাকে text-এ। শেষ জোড়াটিই সবচেয়ে গুরুত্বপূর্ণ: ফাংশন শেষ হলে s মুছে যাবে, block থেকে যাবে — free না করলে leak.

  2. বাগ শিকারি: নিচের ফাংশনে কয়টি মেমরি-বাগ আছে, আর কোথায়? int *load(int n) { int *a = malloc(n * sizeof(a)); if (n > 1000) return NULL; for (int i = 0; i < n; i++) a[i] = i; return a; }

    তিনটি। (১) sizeof(a) হলো pointer-এর আকার (সাধারণত 8), int-এর নয় — হওয়ার কথা sizeof *a. এখানে বাড়তি জায়গা নষ্ট হচ্ছে; অন্য টাইপে উল্টোটাও হতে পারত। (২) malloc-এর পরে NULL যাচাই নেই। (৩) n > 1000 হলে return NULL — কিন্তু তার আগেই নেওয়া block ছাড়া হয়নি, এটি leak. ঠিক করার উপায়: সীমা যাচাই malloc-এর আগে সরিয়ে নিন, NULL যাচাই যোগ করুন, আর নথিতে লিখুন “ফেরত পাওয়া array caller free করবে।”

  3. কপি গুনুন: একটি dynamic array-তে ১০২৪টি জিনিস যোগ করবেন, শুরু capacity 1 থেকে। (ক) প্রতিবার দ্বিগুণ করলে মোট কয়টি কপি? (খ) প্রতিবার ১ ঘর বাড়ালে কয়টি? কাগজে হিসাব করুন।

    (ক) capacity বাড়ে 1→2→4→…→1024. কপি হয় 1 + 2 + 4 + … + 512 = ১০২৩টি — জিনিসের সংখ্যার চেয়েও কম। (খ) কপি হয় 1 + 2 + … + 1023 = 1023 × 1024 / 2 = ৫,২৩,৭৭৬টি. প্রায় ৫০০ গুণ বেশি! মাত্র ১০২৪টি জিনিসেই এই পার্থক্য; দশ লাখে তা হয়ে যায় প্রায় পাঁচ লাখ গুণ।

  4. Sanitizer-এর হাতে ধরা পড়ুন: Linux, WSL বা Mac-এ উপরের grow.c নিন। (ক) শেষের free(data); মুছে gcc -g -fsanitize=address দিয়ে চালান। (খ) এবার free ফিরিয়ে এনে তার পরের লাইনে printf("%d\n", data[0]); যোগ করে চালান। দুইবার কী রিপোর্ট পেলেন?

    (ক) প্রোগ্রাম স্বাভাবিকভাবে শেষ হবে, তারপর LeakSanitizer জানাবে detected memory leaks — সাথে কত byte, আর কোন লাইনের realloc থেকে নেওয়া — শেষ block-এর capacity 16, তাই মোট 64 byte হারিয়েছে। (খ) printf-এর লাইনেই প্রোগ্রাম থেমে যাবে: heap-use-after-free, READ of size 4 — সাথে কোথায় free হয়েছিল আর কোথায় নেওয়া হয়েছিল। Sanitizer ছাড়া চালালে (খ) হয়তো দিব্যি একটি সংখ্যা ছাপত — এটাই use-after-free-র নীরব বিপদ।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমনিরাপদ heap ব্যবহারের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📏 কতগুলো জিনিস লাগবে জানা ও সীমা যাচাই
  2. 📦 malloc(n * sizeof *p) দিয়ে জায়গা নেওয়া
  3. 🛑 ফলাফল NULL কি না যাচাই
  4. ✍️ Block ব্যবহার করা
  5. 🔑 free(p) দিয়ে ফেরত দেওয়া
  6. 🧹 p = NULL দিয়ে পুরোনো ঠিকানা মুছে ফেলা

একটি ফাংশনের ভেতরে int *p = malloc(40); লেখা হলো। ফাংশন শেষ হলে কী থাকে?

  • p আর block দুটোই নিজে থেকে মুছে যায়
  • p মুছে যায়, কিন্তু heap-এর ৪০ byte-এর block থেকে যায়
  • p থেকে যায়, block মুছে যায়
  • প্রোগ্রাম সঙ্গে সঙ্গে crash করে

malloc আর calloc-এর মূল পার্থক্য কোনটি?

  • malloc heap থেকে নেয়, calloc stack থেকে
  • calloc-এর block free করতে হয় না
  • calloc পুরো block শূন্য করে দেয় (আর গুণফলের overflow যাচাই করে); malloc করে না
  • শুধু malloc ব্যর্থ হলে NULL দেয়

data = realloc(data, newsize); — এই লাইনে বিপদ কোথায়?

  • realloc সবসময় পুরোনো ডেটা মুছে ফেলে
  • realloc ঠিকানা বদলাতে পারে, তাই data ভুল জায়গা দেখাবে
  • realloc দিয়ে block ছোট করা যায় না
  • realloc ব্যর্থ হলে data-তে NULL বসে, পুরোনো block-এর ঠিকানা হারিয়ে leak হয়

free(p); এর পরে p-এর ভেতরে কী থাকে?

  • আগের ঠিকানাটিই — free pointer-এর মান বদলায় না
  • স্বয়ংক্রিয়ভাবে NULL
  • শূন্য (0)
  • p ভ্যারিয়েবলটিই মুছে যায়

একটি dynamic array জায়গা ফুরালে capacity দ্বিগুণ করে। n টি জিনিস যোগ করতে মোট কপির সংখ্যা মোটামুটি কেমন?

  • প্রায় n² / 2
  • 2n-এর কম — অর্থাৎ প্রতি যোগে গড়ে স্থির খরচ
  • শূন্য, কারণ realloc কখনো কপি করে না
  • প্রায় n log n

একটি সার্ভার কয়েক দিন পরপর ধীর হয়ে বন্ধ হয়ে যায়, restart করলে আবার ঠিক। কোন ভুল সবচেয়ে বেশি সন্দেহজনক, আর কীভাবে খুঁজবেন?

  • Stack overflow — recursion কমাতে হবে
  • Syntax error — compiler দিয়ে খুঁজতে হবে
  • Memory leak — Valgrind বা AddressSanitizer দিয়ে খুঁজতে হবে
  • Double free — কিছু free মুছে দিতে হবে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. প্রোগ্রামের মেমরি চার ভাগে: text, data/bss, heap, stack. Stack নিজে থেকে চলে; heap আপনার হাতে।
  2. আকার চলার সময় জানা গেলে, বড় হলে, বা ফাংশনের পরেও লাগলে — heap.
  3. malloc → NULL যাচাই → ব্যবহার → free → p = NULL. আকারে সবসময় sizeof *p.
  4. realloc ঠিকানা বদলাতে পারে — ফল আগে tmp-এ নিন। Dynamic array-তে capacity দ্বিগুণ করলে গড় খরচ স্থির।
  5. Leak, use-after-free, double free নীরব থাকে — AddressSanitizer / Valgrind দিয়ে প্রমাণ করুন, আর মালিকানা লিখে রাখুন।

পরের পর্বে: এক সারির অ্যারে নিজের মাপে বানাতে শিখলেন। কিন্তু রেজাল্ট শিট, সিট-ম্যাপ বা ছবির মতো টেবিল-আকারের ডেটা? পরের পর্বে সারি আর কলামের অ্যারে — মেমরিতে সেটি আসলে কীভাবে বসে, আর দরকারমতো মাপের টেবিল কীভাবে বানাতে হয়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান