Process-এর শরীর — task_struct থেকে address space পর্যন্ত
Process Anatomy
গত লেসনে দেখেছি syscall কীভাবে kernel-এ ঢোকে, কিন্তু এড়িয়ে গেছি একটা প্রশ্ন — kernel 'আপনার process' বলতে ঠিক কী বোঝে? এই লেসনে সেই প্রশ্নের উত্তর দুই দিক থেকে দেব। প্রথমে kernel-এর দৃষ্টিতে — `task_struct` নামের ~৭ KB-র একটা struct, যেখানে PID থেকে page table-এর ঠিকানা পর্যন্ত সবকিছু থাকে। তারপর process-এর নিজের দৃষ্টিতে — তার virtual address space, যেখানে code, data, heap আর stack নিজেদের জায়গা করে নেয়। শেষে দেখব process কীভাবে অবস্থা বদলায়, আর কেন কিছু অবস্থা থেকে ফেরা অসম্ভব।
আগে এটা বুঝি
আপনি ./a.out চালালেন। প্রশ্ন: এখন ঠিক কী “চলছে”?
সহজ উত্তর মনে হয় — “প্রোগ্রামটা চলছে”। কিন্তু একই প্রোগ্রাম তিনবার চালালে তিনটা আলাদা জিনিস চলে। তাদের PID আলাদা, তাদের memory আলাদা, একটা crash করলে বাকিরা অক্ষত থাকে। তাহলে “প্রোগ্রাম” শব্দটা যথেষ্ট নয়।
সঠিক শব্দ process — একটা running program-এর execution instance। Program হলো disk-এ পড়ে থাকা একটা নিষ্ক্রিয় ফাইল (ELF binary)। Process হলো সেই ফাইল থেকে তৈরি একটা জীবন্ত জিনিস — নিজস্ব memory, নিজস্ব state, নিজস্ব পরিচয়।
গত লেসনের প্রশ্নে ফিরি: getpid() ডাকলে kernel PID-টা কোথা
থেকে পড়ে? উত্তর একটামাত্র struct-এ — kernel-এর কাছে আপনার
process মানেই এই struct। আর process নিজে যেটা “নিজের memory”
বলে বিশ্বাস করে, সেটা একটা সাজানো illusion — virtual address
space।
এই লেসনে আমরা দুইটাই খুলে দেখব।
মূল ধারণা
Process কী — আর প্রোগ্রাম কী নয়
তিনটা জিনিস আলাদা রাখুন:
| কোথায় থাকে | উদাহরণ | |
|---|---|---|
| Program | Disk-এ, নিষ্ক্রিয় | /usr/bin/python3 |
| Process | Memory-তে, জীবন্ত | PID 4821, python3 চালাচ্ছে |
| Process image | একটা মুহূর্তের snapshot | core dump |
একই program একাধিকবার চালালে একাধিক process — প্রতিটার আলাদা virtual address space, আলাদা PID, একে অপরের memory পড়তে পারে না (গত দুই লেসনের isolation)।
python3 &
python3 &
ps aux | grep python3দুইটা আলাদা PID, একই binary।
Address space — process যা “নিজের memory” মনে করে
প্রতিটা process বিশ্বাস করে তার একটা flat, ব্যক্তিগত address space আছে, শূন্য থেকে শুরু। এটা একটা illusion — নিচে আসল physical memory-র সাথে virtual address-এর সম্পর্ক page table দিয়ে তৈরি (লেসন ১০-১১-এ বিস্তারিত)। আপাতত আমরা শুধু এই illusion-টার আকৃতি দেখব।
উঁচু address
┌─────────────────────────┐
│ kernel space │ ← গত লেসনে দেখেছি, user মোডে অদৃশ্য
├─────────────────────────┤ 0x00007fffffffffff
│ [stack] │ ← নিচের দিকে বাড়ে, প্রতি function call-এ
│ ↓ │
│ │
│ (ফাঁকা জায়গা, ASLR gap) │
│ │
│ ↑ │
│ mmap region │ ← shared library, mmap() করা ফাইল
├─────────────────────────┤
│ ↑ heap │ ← উপরের দিকে বাড়ে, malloc/brk
├─────────────────────────┤
│ .bss │ ← শূন্যে initialised global (ফাইলে জায়গা নেয় না)
├─────────────────────────┤
│ .data │ ← initialised global
├─────────────────────────┤
│ .rodata │ ← string literal, const
├─────────────────────────┤
│ .text │ ← machine code
└─────────────────────────┘ 0x0000000000400000 (সাধারণত)
নিচু addressLevel 3-এর ELF লেসনের সাথে সরাসরি মিল — কারণ এটা সেই একই
সংগঠন, শুধু এখন disk-এর বদলে memory-তে। .text, .rodata,
.data, .bss — ELF section গুলো loader দিয়ে যখন memory-তে
আসে, তখন সেগুলোকে বলে segment।
task_struct — kernel-এর দৃষ্টিতে একটা process
আগের প্রশ্নের উত্তর: kernel-এর কাছে একটা process মানে একটা
struct task_struct। Linux 6.x-এ এটা প্রায় ৭ কিলোবাইট।
গুরুত্বপূর্ণ কিছু field, সরলীকৃত:
struct task_struct {
pid_t pid; /* এই task-এর id */
pid_t tgid; /* thread group id — গত লেসনের ধারণা */
struct task_struct *parent; /* PPID এখান থেকে আসে */
struct list_head children;
long state; /* running/sleeping/zombie/... */
struct mm_struct *mm; /* address space — page table-এর মূল */
struct files_struct *files; /* file descriptor table */
struct signal_struct *signal; /* pending signal, handler */
struct sched_entity se; /* scheduler-এর জন্য — vruntime ইত্যাদি */
int prio;
struct cred *cred; /* uid, gid, capability */
struct fs_struct *fs; /* cwd, root */
u64 utime, stime; /* user/kernel CPU time */
struct thread_struct thread; /* সংরক্ষিত register, architecture-নির্ভর */
};এটা include/linux/sched.h-এ — আসল struct আরো অনেক বড় (২০০+
field), কিন্তু এই কয়েকটাই আমাদের প্রশ্নের উত্তর দেয়:
getpid()→current->tgidপড়ে (currentমানে বর্তমানে চলা task-এর pointer, প্রতিটা CPU-তে আলাদাভাবে রাখা)write(1, ...)→current->files-এ fd 1 খোঁজে (পরের লেসনগুলোয় বিস্তারিত)- Context switch-এ (লেসন ৭) →
threadfield-এ register সংরক্ষিত হয়
PID, PPID, PGID, session — চার স্তরের পরিচয়
Session (SID)
└── Process Group (PGID)
└── Process (PID)
└── Parent (PPID)
| কী | ব্যবহারিক প্রভাব | |
|---|---|---|
| PID | নিজের পরিচয় | kill <pid> |
| PPID | কে তৈরি করেছে | zombie reaping-এর দায়িত্ব |
| PGID | একই pipeline-এর অংশ | kill -<pgid> পুরো group-কে |
| SID | terminal-এর সাথে সম্পর্ক | logout-এ কারা বন্ধ হবে |
ls | grep foo চালালে ls আর grep আলাদা process, একই
process group। তাই Ctrl-C চাপলে terminal driver পুরো
group-কে SIGINT পাঠায় — দুটোই থামে, একসাথে।
ls | grep foo &
ps -o pid,ppid,pgid,sid,comm -jভেতরে কী ঘটছে
Process state — এবং একটা যা থেকে ফেরা যায় না
- R — Running/RunnableCPU-তে চলছে, বা চলার জন্য queue-তে
- S — Interruptible sleepকিছুর জন্য অপেক্ষা করছে, signal এলে জাগবে
- D — Uninterruptible sleepkernel operation-এর মাঝে, signal-ও উপেক্ষা করে
- T — StoppedSIGSTOP পেয়েছে, বা debugger থামিয়েছে
- Z — Zombieশেষ হয়েছে, কিন্তু exit status এখনো তোলা হয়নি
S আর D-র পার্থক্যটাই সবচেয়ে গুরুত্বপূর্ণ, আর সবচেয়ে কম বোঝা।
S state-এ একটা process ঘুমাচ্ছে কিন্তু signal দিয়ে জাগানো
যায় — Ctrl-C কাজ করবে। D state-এ process একটা kernel
operation-এর মাঝপথে — সাধারণত disk I/O — আর সেই মুহূর্তে
kernel-এর ভেতরের data structure-গুলো একটা অসম্পূর্ণ অবস্থায়
আছে। যদি এখন signal দিয়ে process থামিয়ে দেওয়া হয়, সেই
data structure অসামঞ্জস্যপূর্ণ থেকে যাবে — filesystem corruption
হতে পারে।
তাই kernel ইচ্ছাকৃতভাবে D state-এ সব signal উপেক্ষা করে,
এমনকি SIGKILL-ও। এটা bug নয় — এটা correctness-এর জন্য
প্রয়োজনীয় একটা সিদ্ধান্ত।
Zombie — মৃত কিন্তু কবর দেওয়া হয়নি
Process শেষ হলে সাথে সাথে অদৃশ্য হয় না। তার exit status কেউ পড়া পর্যন্ত সে থেকে যায় Z state-এ — একটা ছোট entry, প্রায় শুধু PID আর exit code।
pid_t pid = fork();
if (pid == 0) exit(42);
sleep(5); /* এই ৫ সেকেন্ড child একটা zombie */
wait(NULL); /* এখন সে সম্পূর্ণ চলে যায় */Parent যদি কখনো wait() না ডাকে, zombie চিরকাল থাকে —
kill -9-ও কাজ করে না, কারণ সে তো ইতিমধ্যে মৃত। এভাবে অনেক
zombie জমলে PID table ভরে যেতে পারে (গুরুতর কিন্তু বিরল সমস্যা)।
Parent নিজে মারা গেলে সন্তানরা orphan, আর তাদের PID 1
(init/systemd) reparent করে নেয় — যার একটা কাজই হলো zombie
reap করা। Level 4-এর পরের একটা লেসনে (fork-exec-wait) আমরা
এটা হাতে-কলমে দেখব।
ASLR — প্রতিবার আলাদা মানচিত্র
Address space-এর আকৃতি (উপরের figure) সবসময় একই, কিন্তু ঠিক কোথায় শুরু হয় তা প্রতিবার বদলায়:
for i in 1 2 3; do
cat /proc/self/maps | grep '\[stack\]'
doneতিনবারই আলাদা address। এটাই ASLR (Address Space Layout Randomization) — heap, stack, mmap region, এমনকি executable-এর base address (PIE হলে) প্রতিটা exec-এ randomise হয়।
কেন: exploit লিখতে হলে attacker-কে জানতে হয় কোথায় কী আছে — একটা নির্দিষ্ট function-এর address, বা stack-এর অবস্থান। ASLR সেই অনুমানকে অবিশ্বস্ত করে দেয়। এটা memory safety bug ঠেকায় না, কিন্তু সেগুলোকে কাজে লাগানো কঠিন করে।
Level 10-এ আমরা দেখব কেন ASLR একা যথেষ্ট নয় (info leak দিয়ে বাইপাস করা যায়), আর কেন সেই সাথে stack canary আর NX bit লাগে।
উদাহরণ
/proc/self/maps-কে ELF-এর সাথে মেলানো
একটা সত্যিকারের প্রোগ্রাম চালিয়ে দেখি:
cat > /tmp/probe.c <<'EOF'
#include <stdio.h>
int global_init = 42;
int global_bss;
const char *msg = "hello";
int main(void) {
int local = 1;
static int stat_var = 7;
void *heap = malloc(64);
printf("code : %p\n", (void *)main);
printf("rodata : %p\n", (void *)msg);
printf("data : %p\n", (void *)&global_init);
printf("bss : %p\n", (void *)&global_bss);
printf("heap : %p\n", heap);
printf("stack : %p\n", (void *)&local);
while(1) pause();
}
EOF
gcc -o /tmp/probe /tmp/probe.c
/tmp/probe &
sleep 1
grep -E "r-xp|rw-p|r--p" /proc/$!/maps | grep probe
kill $!Output-এর address গুলো /proc/<pid>/maps-এর নির্দিষ্ট range-এ
পড়বে — r-xp permission-এর region-এ code, r--p-এ
rodata, rw-p-এ data/bss/heap/stack।
এইভাবে যাচাই করলাম যে theory আর বাস্তবতা মেলে — যা এই
লেসনের সবচেয়ে গুরুত্বপূর্ণ শিক্ষা। .text, .data — এগুলো
বিমূর্ত ধারণা নয়, প্রতিটার একটা প্রকৃত virtual address range
আছে, আর সেটা দেখা যায়।
নিজে চালিয়ে দেখুন
/proc/self/maps পড়ে ELF section-এর সাথে মেলান
readelf -S /tmp/probe | grep -E "\.text|\.rodata|\.data|\.bss" [13] .text PROGBITS 0000000000001060 00001060
[15] .rodata PROGBITS 0000000000002000 00002000
[22] .data PROGBITS 0000000000004000 00003000
[23] .bss NOBITS 0000000000004010 00004010এই address গুলো file-এর ভেতরের offset, actual runtime address নয় (PIE binary-তে সবকিছু একটা base address যোগ করে সরে যায়)। তবু আপেক্ষিক ক্রম আর দূরত্ব ঠিক থাকে।
/tmp/probe &
PID=$!
sleep 1
cat /proc/$PID/maps | grep probe
kill $PID55a1e2400000-55a1e2401000 r--p 00000000 ... /tmp/probe
55a1e2401000-55a1e2402000 r-xp 00001000 ... /tmp/probe
55a1e2402000-55a1e2403000 r--p 00002000 ... /tmp/probe
55a1e2403000-55a1e2404000 r--p 00003000 ... /tmp/probe
55a1e2404000-55a1e2405000 rw-p 00003000 ... /tmp/probeলক্ষ্য করুন offset column (তৃতীয়টা) readelf-এর file
offset-এর সাথে মেলে। r-xp region-টা .text-এর offset
(00001000) দিয়ে শুরু হচ্ছে — এটাই প্রমাণ যে এই memory
region সত্যিই .text section।
নিজে যাচাই করুন: .rodata-র offset (00002000) কোন
r--p region-এ পড়ে? .data-র (00003000)?
Process-এর memory layout কোনো তাত্ত্বিক diagram নয় — ELF file-এর section header-এ যা লেখা, memory-তে ঠিক তাই ঘটে, শুধু address ভিন্ন।
ASLR বন্ধ করে নিশ্চিত হওয়া
for i in 1 2 3; do /tmp/probe & sleep 0.2; \
grep -m1 '\[stack\]' /proc/$!/maps; kill $! 2>/dev/null; doneতিনটা ভিন্ন address।
cat /proc/sys/kernel/randomize_va_space2 মানে সম্পূর্ণ ASLR (stack, heap, mmap, PIE base — সব
randomised)। 1 মানে আংশিক। 0 মানে বন্ধ।
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
for i in 1 2 3; do /tmp/probe & sleep 0.2; \
grep -m1 '\[stack\]' /proc/$!/maps; kill $! 2>/dev/null; done
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space # ফিরিয়ে দিনএবার তিনবারই একই address — প্রমাণ যে randomisation একটা সচেতন, বন্ধ-করা-যায় এমন feature, ভাগ্য নয়।
Address randomisation সত্যিই ঘটছে, ঐচ্ছিক নয় — আর এটা একটা kernel-স্তরের সেটিং, প্রোগ্রামের নয়।
নিজে বানান
নিজের address space-এর মানচিত্র
- বিভিন্ন ধরনের variable-এর address নিন — global, static, heap, stack
- প্রোগ্রাম চলা অবস্থাতেই /proc/self/maps পড়ুন
- প্রতিটা address কোন region-এ পড়ে তা মিলিয়ে দেখান
- একটা recursive function দিয়ে stack বৃদ্ধি পাওয়া পর্যবেক্ষণ করুন
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int global_init = 42;
int global_bss;
const char rodata_str[] = "read-only string";
typedef struct { unsigned long lo, hi; char perm[8]; char path[256]; } region_t;
static region_t regions[256];
static int nregions;
static void load_maps(void) {
FILE *f = fopen("/proc/self/maps", "r");
char line[512];
while (nregions < 256 && fgets(line, sizeof line, f)) {
region_t *r = ®ions[nregions];
r->path[0] = 0;
if (sscanf(line, "%lx-%lx %7s %*s %*s %*s %255[^\n]",
&r->lo, &r->hi, r->perm, r->path) >= 3)
nregions++;
}
fclose(f);
}
static void identify(const char *label, void *addr) {
unsigned long a = (unsigned long)addr;
for (int i = 0; i < nregions; i++)
if (a >= regions[i].lo && a < regions[i].hi) {
printf(" %-14s %014lx [%s] %s\n", label, a,
regions[i].perm,
regions[i].path[0] ? regions[i].path : "(anonymous)");
return;
}
printf(" %-14s %014lx কোনো region-এ নেই!\n", label, a);
}
static int depth_probe(int depth, unsigned long *first_frame) {
int local;
if (!*first_frame) *first_frame = (unsigned long)&local;
if (depth == 0) return (int)((*first_frame) - (unsigned long)&local);
return depth_probe(depth - 1, first_frame);
}
int main(void) {
load_maps();
void *heap = malloc(128);
static int stat_var = 7;
int local = 1;
printf("═══ address space পরিচিতি ═══\n\n");
identify("code (main)", (void *)main);
identify("rodata", (void *)rodata_str);
identify("data", &global_init);
identify("bss", &global_bss);
identify("static local", &stat_var);
identify("heap", heap);
identify("stack", &local);
unsigned long first = 0;
int consumed = depth_probe(1000, &first);
printf("\n═══ stack বৃদ্ধি ═══\n\n");
printf(" ১০০০ গভীরতার recursion-এ stack ব্যবহার: ~%d byte\n", consumed);
printf(" (প্রতি frame আনুমানিক %d byte)\n", consumed / 1000);
free(heap);
return 0;
}gcc -O0 -o addrmap addrmap.c && ./addrmap-O0 জরুরি — optimization-এ compiler local variable-কে
register-এ রাখতে পারে, stack-এ নয়, আর তখন address মেলানো
কঠিন হয়ে যায়।
নিজে বাড়ান:
mmap()দিয়ে নিজে একটা region বানান, সেটাওidentifyকরুনgetrlimit(RLIMIT_STACK, ...)দিয়ে সর্বোচ্চ stack size বের করুন, আর সেই limit ছাড়িয়ে যাওয়ার চেষ্টা করে দেখুন কী ঘটে (লেসন ১৫-এ পুরোটা)- একটা shared library-র (
libc.so) কোনো function-এর addressidentifyকরুন — সেটা.text-এর বাইরে, নিজের আলাদা mapped region-এ থাকবে PIEbinary বনাম non-PIE (gcc -no-pie) তুলনা করুন — কোনটায়.text-এর address স্থির থাকে?
বাস্তব সিস্টেমে
Process anatomy যেখানে সরাসরি কাজে লাগে
Container memory limit। docker run -m 512m আসলে cgroup-এ
(লেসন ২৯) একটা সীমা বসায় — কিন্তু সেই সীমা মাপা হয় process-এর
RSS-এর (আসলে ব্যবহৃত physical page) উপর, VSZ (virtual size)-এর
উপর নয়। একটা process 100 GB virtual address space দাবি করতে
পারে অথচ RSS মাত্র কয়েক MB — এই পার্থক্যটাই লেসন ১০-এর বিষয়।
Core dump analysis। একটা crash হওয়া process-এর core file
আক্ষরিকভাবে তার address space-এর একটা snapshot — .data,
heap, stack সব। gdb core ./program দিয়ে সেই মৃত মুহূর্তের
প্রতিটা variable দেখা যায়, কারণ layout ঠিক এই লেসনে বর্ণিত
নিয়মেই সাজানো।
ps aux-এর STAT column। R, S, D, Z, T — এই
লেসনের state machine সরাসরি সেখানে দেখা যায়। একটা database
server যদি D state-এ আটকে থাকে, সেটা প্রায় নিশ্চিতভাবে
disk I/O-তে সমস্যা — CPU-র সমস্যা নয়।
Fork bomb defence। ulimit -u (max user processes)
kernel-কে বলে একজন ব্যবহারকারী সর্বোচ্চ কতগুলো task_struct
বানাতে পারবে — প্রতিটা struct ৭ KB হলেও, লক্ষ লক্ষ zombie
বা fork bomb memory শেষ করে দিতে পারে।
Debugger breakpoint। gdb-তে break main কাজ করে কারণ
compiler symbol table-এ main-এর address জানে, আর সেই
address process-এর .text region-এই থাকে — ঠিক আজকের
experiment-এ যা দেখলাম।
Chrome-এর one-process-per-tab। প্রতিটা tab আলাদা process
কারণ তাহলে প্রতিটার আলাদা address space, আলাদা task_struct —
একটা tab crash করলে বাকিরা অক্ষত। এটা process isolation-এর
সবচেয়ে দৃশ্যমান দৈনন্দিন উদাহরণ।
যে ভুলগুলো সবাই করে
“প্রোগ্রাম আর process একই জিনিস, শব্দের পার্থক্য মাত্র।”
প্রোগ্রাম হলো disk-এ একটা নিষ্ক্রিয় ফাইল — কোনো memory নেই, কোনো PID নেই, কোনো state নেই। Process হলো সেই ফাইল থেকে তৈরি একটা জীবন্ত সত্তা।
একই প্রোগ্রাম একাধিকবার চালালে একাধিক process — প্রতিটার
নিজস্ব task_struct, নিজস্ব address space। ps aux -এ
firefox তিনবার দেখা যাওয়া মানে একটা প্রোগ্রাম, তিনটা
process।
উল্টোটাও সত্য — একটামাত্র process একাধিক প্রোগ্রামের কোড
চালাতে পারে (exec দিয়ে বদলে যায়, লেসন ৫-এ), যদিও PID
একই থেকে যায়।
এই পার্থক্যটা না বুঝলে “মেমরি leak” জাতীয় আলোচনায় বিভ্রান্তি হয় — leak হয় process-এর memory-তে, প্রোগ্রামের ফাইলে নয়।
“`kill -9` সবসময় একটা process মেরে ফেলে।”
সাধারণত হ্যাঁ, কিন্তু D state-এ থাকা process-এ SIGKILLও
উপেক্ষিত হয়।
SIGKILL (-9) সাধারণ signal-এর মতো handler দিয়ে ধরা যায়
না — এই অর্থে এটা “অপ্রতিরোধ্য”। কিন্তু kernel নিজেই এটা
uninterruptible sleep-এ পৌঁছাতে দেয় না, কারণ সেই মুহূর্তে
kernel-এর নিজের data structure একটা মধ্যবর্তী অবস্থায় আছে।
ps -eo pid,stat,comm | awk '$2 ~ /D/'যদি কোনো process এখানে দেখা যায় আর kill -9-এ সাড়া না দেয়,
সেটা bug নয় — সেটা সিস্টেম বলছে “আমি এখন একটা atomic operation
মাঝপথে, নিরাপদে থামতে পারছি না”।
সমাধান সাধারণত প্রোগ্রাম-স্তরে নয়, hardware/driver-স্তরে — disk বা network filesystem-এর সমস্যা ঠিক করা।
“Zombie process CPU বা memory খরচ করে, তাই বিপজ্জনক।”
একটা zombie প্রায় কিছুই খরচ করে না — তার code, data,
heap, stack সব মুক্ত হয়ে গেছে। যা বাকি থাকে তা শুধু
task_struct-এর একটা কঙ্কাল সংস্করণ (PID, exit status,
resource usage-এর হিসাব) — parent wait() ডাকা পর্যন্ত।
তাই একটা-দুইটা zombie সমস্যা নয়। সমস্যা হয় যখন parent
কখনো wait() ডাকে না আর হাজার হাজার child তৈরি করতে
থাকে — তখন প্রতিটা zombie সামান্য kernel memory ধরে রাখে,
আর সবচেয়ে গুরুত্বপূর্ণ, PID namespace-এ সীমিত PID সংখ্যা
ভরে যায়। একটা নতুন সংখ্যা /proc/sys/kernel/pid_max-এর বেশি
process তৈরি করা যায় না।
cat /proc/sys/kernel/pid_maxসাধারণত ৪,১৯৪,৩০৩ — বিশাল, কিন্তু একটা buggy server যদি
প্রতি সেকেন্ডে হাজারো zombie তৈরি করে আর কখনো reap না করে,
কয়েক ঘণ্টায় সেটাও ফুরাতে পারে।
“ASLR চালু থাকলে buffer overflow আর কোনো সমস্যা নয়।”
ASLR কঠিন করে, অসম্ভব করে না।
Info leak দিয়ে বাইপাস: যদি প্রোগ্রামে এমন কোনো bug থাকে যা একটা pointer মান কোনোভাবে প্রকাশ করে দেয় (একটা error message-এ, একটা log-এ), attacker সেটা থেকে base address হিসাব করে ফেলতে পারে — তারপর ASLR অকার্যকর।
Brute force: ৩২-বিট system-এ randomisation-এর জন্য মাত্র কয়েক bit থাকে, তাই বারবার চেষ্টা করে অনুমান করা সম্ভব (বিশেষত এমন সার্ভিসে যা crash-এর পর restart করে একই randomisation state ফিরিয়ে আনে না, বা যেখানে fork() ASLR পুনরায় করে না)।
64-bit-এও একটা সূক্ষ্মতা: fork()-এর পর child একই
address space layout পায় parent-এর — নতুন randomisation হয়
না। শুধু exec()-এ নতুন randomisation ঘটে।
তাই ASLR একটা স্তর, একমাত্র প্রতিরক্ষা নয়। সাথে লাগে stack canary, NX bit, আর সবচেয়ে ভালো — memory-safe ভাষা বা bounds checking। Level 10-এ পুরো ছবিটা দেখব।
বুঝেছেন কি না দেখুন
1.bss section-এর জন্য ELF ফাইলে কোনো byte সংরক্ষিত থাকে না,
শুধু একটা size। কিন্তু .data-র জন্য প্রকৃত byte থাকে। এই
পার্থক্যের ফলে কী সুবিধা হয়, আর কোন ধরনের variable কোথায়
যায় তা কীভাবে নির্ধারিত হয়?
যুক্তি
.bss section-এর জন্য ELF ফাইলে কোনো byte সংরক্ষিত থাকে না,
শুধু একটা size। কিন্তু .data-র জন্য প্রকৃত byte থাকে। এই
পার্থক্যের ফলে কী সুবিধা হয়, আর কোন ধরনের variable কোথায়
যায় তা কীভাবে নির্ধারিত হয়?সুবিধা — disk আর load time দুটোতেই।
একটা প্রোগ্রামে যদি ১০০ MB-র একটা বড় uninitialised array থাকে
(static char buffer[100*1024*1024];), সেটা .data-তে গেলে
ELF ফাইল নিজেই ১০০ MB বড় হতো, আর প্রতিবার load করার সময় সেই
১০০ MB disk থেকে পড়তে হতো।
.bss-তে গেলে ELF ফাইলে শুধু “এখানে ১০০ MB শূন্য রাখো” —
কয়েক byte-এর একটা metadata। Loader শুধু একটা zero-filled
anonymous mapping বানায় (mmap(MAP_ANONYMOUS) ঘরানার), যা
kernel demand-paging দিয়ে lazily allocate করে (লেসন ১০-১৩)।
নির্ধারণের নিয়ম — compiler/linker করে, স্বয়ংক্রিয়ভাবে:
int a; /* .bss — initial value নেই */
int b = 0; /* .bss — শূন্যেই initialised, তাই একই কথা */
int c = 5; /* .data — non-zero initial value */
const char *d = "x"; /* pointer .data-তে, string literal .rodata-তে */
static int e; /* .bss */Compiler প্রতিটা global/static variable-এর initial value
দেখে সিদ্ধান্ত নেয়। = 0 লেখা আর কিছু না লেখা — দুটোই
.bss-এ যায়, কারণ ফলাফল একই (linker/loader optimization)।
gcc -c test.c -o test.o
size test.osize কমান্ড প্রতিটা section-এর আকার আলাদা করে দেখায় —
নিজে যাচাই করুন।
ব্যবহারিক পরিণতি: এই কারণেই একটা বিশাল static array
থাকা প্রোগ্রামের binary ফাইল ছোট থাকে, কিন্তু চালানোর সময়
memory-তে বড় জায়গা নেয় — ls -l আর ps-এর RSS সংখ্যা
মিলবে না, আর এটা bug নয়।
2ps -eo pid,stat,comm চালিয়ে একটা process Ss state দেখাচ্ছে
(কেবল S নয়)। আরেকটা R+। এই বাড়তি অক্ষরগুলোর অর্থ কী?
প্রয়োগ
ps -eo pid,stat,comm চালিয়ে একটা process Ss state দেখাচ্ছে
(কেবল S নয়)। আরেকটা R+। এই বাড়তি অক্ষরগুলোর অর্থ কী?ps-এর STAT column-এ প্রথম অক্ষরটাই মূল state (R/S/D/T/Z),
তারপরের অক্ষরগুলো modifier:
| Modifier | অর্থ |
|---|---|
< | উচ্চ priority (high-priority, “nice”-এর ঋণাত্মক) |
N | নিম্ন priority |
L | কিছু page memory-তে লক করা আছে (real-time-এর জন্য) |
s | session leader — SID == PID |
l | multi-threaded |
+ | foreground process group-এ আছে (আপনার terminal-এ) |
Ss — sleeping (S) এবং session leader (s)। এটা প্রায়
নিশ্চিতভাবে একটা shell বা daemon যেটা নিজে একটা নতুন session
শুরু করেছে (setsid() ডেকে) — যেমন আপনার login shell, বা
sshd-এর একটা child।
R+ — running/runnable (R) এবং foreground group-এ (+)।
এটা এমন একটা প্রোগ্রাম যা এই মুহূর্তে আপনার terminal-এ চলছে
আর active — যেমন আপনি যদি এই মুহূর্তে ps কমান্ডটাই দেখতে
পারতেন, সেটা নিজেই R+ হতো।
যাচাই করুন:
sleep 100 &
ps -eo pid,ppid,stat,comm | grep sleepsleep 100 & ব্যাকগ্রাউন্ডে গেছে, তাই foreground নয় —
STAT-এ + থাকবে না, শুধু S।
sleep 100(এবার & ছাড়া, অন্য terminal থেকে দেখুন) — এবার S+,
কারণ এটা foreground-এ ঘুমাচ্ছে।
গুরুত্ব: + না থাকা মানে সেই process Ctrl-C দিয়ে
থামানো যাবে না (কারণ terminal driver SIGINT শুধু foreground
group-কে পাঠায়) — এটাই লেসন ২৩ ও ২৫-এর signal আলোচনায়
গুরুত্বপূর্ণ হয়ে ফিরবে।
3আপনার একটা production সার্ভিস মাঝেমধ্যে হ্যাং হয়ে যায়, আর
তখন ps-এ STAT column-এ D দেখা যায়। কীভাবে root cause
খুঁজে বের করবেন — কোন tool, কোন ক্রমে?
ডিজাইন
ps-এ STAT column-এ D দেখা যায়। কীভাবে root cause
খুঁজে বের করবেন — কোন tool, কোন ক্রমে?ধাপ ১ — নিশ্চিত করুন এটা সত্যিই D state, আর কতক্ষণ ধরে।
ps -eo pid,stat,etime,comm | grep " D"etime দেখায় process কতক্ষণ চলছে — কিন্তু D state-এ কতক্ষণ
আটকে সেটা আলাদা করে জানতে:
watch -n1 'ps -eo pid,stat,comm | grep " D"'কয়েক সেকেন্ডের বেশি স্থায়ী D state সন্দেহজনক — সাধারণ disk read কয়েক মিলিসেকেন্ডে শেষ হয়।
ধাপ ২ — কোন kernel function-এ আটকে তা দেখুন।
cat /proc/<pid>/wchan; echo
cat /proc/<pid>/stack # root লাগবে, kernel debug symbol দরকারwchan একটা function নাম দেবে — যেমন wait_on_page_bit
(disk page-এর জন্য অপেক্ষা), nfs_wait_bit_killable (NFS),
__wait_rcu_gp। নামটাই root cause-এর দিকনির্দেশ করে।
ধাপ ৩ — কী resource-এ আটকে তা দেখুন।
sudo cat /proc/<pid>/syscall # কোন syscall-এ আটকে
sudo ls -la /proc/<pid>/fd/ # কোন file/socket খোলা
iostat -x 1 # disk latency অস্বাভাবিক কি নাwchan-এ NFS-সংক্রান্ত নাম থাকলে সেই NFS server-এর অবস্থা
দেখুন। Local disk হলে iostat-এ %util আর await দেখুন —
disk নিজে ধীর বা মৃত কি না।
ধাপ ৪ — dmesg-এ hardware error খুঁজুন।
dmesg -T | grep -iE "error|timeout|reset" | tail -50Disk controller error, SCSI timeout, বা storage array-এর পাশ থেকে network সমস্যা — এগুলো প্রায়ই D state-এর প্রকৃত কারণ, প্রোগ্রামের কোড নয়।
যা করবেন না: এই অবস্থায় kill -9 বারবার চেষ্টা করা —
এটা কাজ করবে না (D state signal উপেক্ষা করে), আর সময় নষ্ট
করবে। D state-এ আটকে থাকা process-এর জন্য একমাত্র বাস্তব
সমাধান হয় underlying I/O সমস্যা সমাধান, নয়তো পুরো মেশিন
reboot।
দীর্ঘমেয়াদী প্রতিরোধ: NFS mount-এ soft,intr,timeo=
option ব্যবহার করুন (hard mount-এর বদলে) যাতে timeout-এ
অন্তত signal-এ সাড়া দেয়। Application-স্তরে I/O timeout সেট
করুন যাতে একটা ধীর disk পুরো worker pool আটকে না দেয়।
এই diagnostic flow-টাই লেসন ৩০-এ (/proc ও observability)
আমরা আরো সাধারণ toolkit হিসেবে ফিরে দেখব।
4একটা প্রোগ্রামে int arr[1000000] = {0}; (global, সব শূন্যে
initialised) লেখা আছে। এটা .data-তে যাবে না .bss-তে? আর
int arr[1000000] = {1}; (শুধু প্রথম element non-zero,
বাকিরা automatically শূন্য) কোথায় যাবে?
প্রয়োগ
int arr[1000000] = {0}; (global, সব শূন্যে
initialised) লেখা আছে। এটা .data-তে যাবে না .bss-তে? আর
int arr[1000000] = {1}; (শুধু প্রথম element non-zero,
বাকিরা automatically শূন্য) কোথায় যাবে?প্রথমটা — {0} — যাবে .bss-তে।
C-এর নিয়মে {0}-দিয়ে initialise করা array-র সব element শূন্য
হয় — আর সব-শূন্য মানে .bss-এর সংজ্ঞার সাথে হুবহু মিলে যায়।
Compiler এটা চেনে আর optimize করে .bss-এ পাঠায়, .data-তে
৪ MB (১০ লক্ষ × ৪ byte) real zero byte সংরক্ষণ করে না।
cat > t1.c <<'EOF'
int arr[1000000] = {0};
int main(void) { return arr[0]; }
EOF
gcc -c t1.c -o t1.o && size t1.o text data bss dec hex filename
... ... 4000000 ... ... t1.odata column প্রায় শূন্য, bss column ৪,০০০,০০০ — নিশ্চিত।
দ্বিতীয়টা — {1} — যাবে .data-তে, পুরোটাই।
C-তে {1} মানে প্রথম element 1, বাকি সব automatically
শূন্যে initialised (এটা C standard-এর নিয়ম — partial
initializer list-এর বাকিটা শূন্য হয়)। কিন্তু array-টা এখন
আর “সব শূন্য” নয় — একটা non-zero byte আছে।
এখানে compiler-এর দুইটা বৈধ পথ আছে:
- সরল পথ: পুরো ৪ MB
.data-তে রাখা (একটা non-zero element-এর জন্য পুরোটাই real byte সংরক্ষণ) - চতুর পথ: সবচেয়ে ছোট continuous non-zero অংশটুকু
.data-তে, বাকি.bss-তে (কিছু compiler/linker এটা করে,.dataআর.bss-এর মাঝে বিভাজন করে)
cat > t2.c <<'EOF'
int arr[1000000] = {1};
int main(void) { return arr[0]; }
EOF
gcc -c t2.c -o t2.o && size t2.oআপনার compiler কোন পথ নিয়েছে সেটা size-এর আউটপুট দেখেই
বোঝা যাবে — data column বড় হলে প্রথম পথ, ছোট হয়ে bss
বড় হলে দ্বিতীয় পথ।
সাধারণ শিক্ষা: compiler-এর decision কোনো রহস্য নয় —
size, readelf -S, আর objdump -h দিয়ে সরাসরি যাচাই
করা যায়। যখনই “compiler নিশ্চয়ই এটা করে” ভাবছেন, চালিয়ে
দেখাই সবচেয়ে নির্ভরযোগ্য উত্তর।
5Docker container-এর docker stats-এ একটা container-এর memory
usage দেখাচ্ছে “50 MiB / 512 MiB”। কিন্তু সেই container-এর
ভেতরে চলা প্রোগ্রাম যদি mmap() দিয়ে ১ GB virtual memory
reserve করে রাখে (কিন্তু touch না করে), সেই সংখ্যাটা কি বদলাবে?
যুক্তি
docker stats-এ একটা container-এর memory
usage দেখাচ্ছে “50 MiB / 512 MiB”। কিন্তু সেই container-এর
ভেতরে চলা প্রোগ্রাম যদি mmap() দিয়ে ১ GB virtual memory
reserve করে রাখে (কিন্তু touch না করে), সেই সংখ্যাটা কি বদলাবে?না, প্রায় বদলাবে না।
docker stats cgroup-এর memory accounting থেকে সংখ্যা নেয়,
আর cgroup memory controller physical page usage (RSS-এর
কাছাকাছি একটা মেট্রিক) মাপে, virtual address space নয়।
mmap() দিয়ে address space reserve করা মানে শুধু page
table-এ একটা entry আর task_struct-এর mm-এ একটা VMA
(Virtual Memory Area) যোগ হওয়া — কোনো physical page এখনো
বরাদ্দ হয়নি। এই লেসনে দেখেছি address space একটা illusion;
সেই illusion-এর একটা অংশ দাবি করা মানে বাস্তব সম্পদ খরচ হওয়া
নয়, যতক্ষণ না touch করা হয় (লেসন ১০-১৩-এ demand paging
বিস্তারিত)।
পার্থক্যটা যাচাই করা যায়:
void *p = mmap(NULL, 1UL << 30, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
/* এখানে docker stats: প্রায় অপরিবর্তিত */
memset(p, 1, 1UL << 30); /* প্রতিটা byte touch */
/* এখানে docker stats: ~1 GB বেড়ে যাবে — যদি limit-এ না ঠেকে */memset চালালে প্রতিটা page-এ page fault হবে, kernel সেগুলো
physical frame দিয়ে ভরবে (লেসন ১৩), আর তখনই RSS বাড়বে —
আর তখনই cgroup limit-এ ঠেকলে OOM killer container-টাকে
মেরে ফেলবে।
ব্যবহারিক পরিণতি: এই কারণেই “আমার প্রোগ্রাম শুধু malloc
করেছে, এখনো লেখেনি, তবু OOM হলো” — এটা স্ববিরোধী শোনায় কিন্তু
আসলে ঠিক এই আচরণেরই একটা রূপ। যদি allocator বড় একটা chunk
mmap করে আর zero-out করার জন্য touch করে (কিছু allocator
security বা predictability-র জন্য করে), তখনই সমস্যা।
Overcommit-এর সাথে সংযোগ: /proc/sys/vm/overcommit_memory
সেটিং নির্ধারণ করে kernel কি mmap()-এর সময়েই প্রত্যাখ্যান
করবে (যদি মোট virtual demand physical+swap-এর চেয়ে বেশি
হয়), নাকি allow করে দিয়ে পরে touch করার সময় OOM killer-এর
উপর ছেড়ে দেবে। এটাই লেসন ১০-এর কেন্দ্রীয় বিষয়।
Level 12-এ আমরা এই cgroup accounting বিস্তারিত দেখব, আর
docker stats-এর সংখ্যা ঠিক কোন /sys/fs/cgroup/ ফাইল
থেকে আসে তা খুঁজে বের করব।
এরপর কী
পরের লেসন — fork, exec, wait
এই লেসনে আমরা একটা process-এর ভেতরটা দেখলাম — তার struct, তার address space, তার state machine। কিন্তু একটা মৌলিক প্রশ্ন এখনো বাকি: একটা নতুন process কোথা থেকে আসে?
আপনি shell-এ ls টাইপ করলেন। কেউ একজন task_struct বানাল,
তাতে PID বসাল, তার জন্য একটা address space তৈরি করল, আর
ls-এর কোড সেখানে লোড করল। কে করল এটা, আর কীভাবে?
উত্তরটা Unix-এর সবচেয়ে অদ্ভুত আর সবচেয়ে সুন্দর design
decision-গুলোর একটা: fork() — একটা function যেটা
দুইবার return করে। একবার parent-এ, একবার child-এ। এই
একটা idea থেকে পুরো Unix process model তৈরি — shell pipeline,
& দিয়ে background job, সার্ভার-এর worker process, সবকিছু।
পরের লেসনে আমরা দেখব fork() কীভাবে এত সস্তা হতে পারে যখন
একটা পুরো address space কপি করার কথা (উত্তর: copy-on-write,
যা এখন আমরা page table নিয়ে যা শিখেছি তার সরাসরি প্রয়োগ),
exec() কীভাবে একটা process-এর “পরিচয়” বদলে দেয় তার PID
অক্ষত রেখে, আর কেন zombie আজকের লেসনে যা দেখেছি তার একটা
স্বাভাবিক পরিণতি — parent যদি কখনো wait() না ডাকে।
আরও পড়ুন
- Linux kernel source — include/linux/sched.h (struct task_struct) · প্রকৃত সংজ্ঞা — এই লেসনের প্রতিটা field এখানে যাচাইযোগ্য, যদিও kernel version-ভেদে সামান্য বদলায়
- The Linux Programming Interface, Chapter 6 — Process — Michael Kerrisk · Process-এর memory layout ও PID/PGID/session সম্পর্কের সবচেয়ে নির্ভুল বর্ণনা
- proc(5) man page · /proc/<pid>/status ও /proc/<pid>/stat-এর প্রতিটা field-এর প্রামাণ্য অর্থ