Kernel space বনাম user space — দেয়ালটা কে বানায়
Kernel Space and User Space
গত লেসনে বারবার বলেছি isolation 'hardware দিয়ে বলপূর্বক প্রয়োগ করা হয়' — এই লেসনে সেই বলপ্রয়োগের যন্ত্রটা খুলে দেখব। CPU-র ভেতরে দুই bit-এর একটা privilege level, যা প্রতিটা instruction আর প্রতিটা memory access-এ যাচাই হয়। আমরা দেখব একটা privileged instruction চালানোর চেষ্টা করলে ঠিক কী ঘটে, kernel কেন প্রতিটা process-এর address space-এ থাকে, আর ২০১৮ সালের Meltdown কীভাবে সেই ৫০ বছরের পুরনো নকশাটাকে রাতারাতি অনিরাপদ বানিয়ে দিয়েছিল।
আগে এটা বুঝি
গত লেসনে একটা দাবি বারবার করেছি, কিন্তু প্রমাণ করিনি:
OS আপনার প্রোগ্রামকে অন্য প্রোগ্রামের memory পড়তে দেয় না।
প্রশ্নটা করুন: দেয় না মানে কী? OS তো নিজেও একটা প্রোগ্রাম। একটা প্রোগ্রাম আরেকটা প্রোগ্রামকে কীভাবে “না” বলে?
ভাবুন আপনি একটা OS লিখছেন এবং শুধু software দিয়ে এটা করতে চান। আপনি প্রতিটা user program-এর প্রতিটা memory access-এর আগে একটা check বসাবেন। কিন্তু সেই check-টা কে চালাবে? User program নিজেই? তাহলে সে সেটা বাদ দিয়ে দিতে পারে।
এখানেই একটা মৌলিক সত্য: software নিজেকে নিজে রক্ষা করতে পারে না। যে কোড CPU-তে চলছে, সে CPU-র উপর সমান অধিকার রাখে — যদি না CPU নিজে পার্থক্য করে।
তাই সমাধানটা hardware-এ। CPU-র ভেতরে একটা ছোট register আছে যা বলে দেয় “এই মুহূর্তে যে কোড চলছে সে কতটা বিশ্বাসযোগ্য”। সেই মানটার উপর ভিত্তি করে CPU প্রতিটা instruction আর প্রতিটা memory access যাচাই করে।
দুই bit-এর একটা সংখ্যা। এটুকুই পুরো আধুনিক computing-এর নিরাপত্তার ভিত্তি।
মূল ধারণা
Privilege ring — x86-এর মডেল
x86 architecture চারটা privilege level সংজ্ঞায়িত করে, যাদের বলা হয় ring:
┌───────────────────────────────┐
│ Ring 3 — user application │ ← আপনার প্রোগ্রাম
│ ┌─────────────────────────┐ │
│ │ Ring 2 — (অব্যবহৃত) │ │
│ │ ┌───────────────────┐ │ │
│ │ │ Ring 1 — (অব্যবহৃত)│ │ │
│ │ │ ┌─────────────┐ │ │ │
│ │ │ │ Ring 0 │ │ │ │ ← kernel
│ │ │ │ kernel │ │ │ │
│ │ │ └─────────────┘ │ │ │
│ │ └───────────────────┘ │ │
│ └─────────────────────────┘ │
└───────────────────────────────┘
ভেতরে = বেশি ক্ষমতাবর্তমান level-টা রাখা থাকে CPL (Current Privilege Level) নামে,
যা cs segment register-এর নিচের দুই bit।
কেন মাত্র দুটো ব্যবহৃত হয়: ring 1 আর 2 তৈরি হয়েছিল device driver-কে kernel থেকে আলাদা রাখার জন্য। কিন্তু:
- পুরনো ARM, MIPS, RISC-V — কারো চারটা level নেই। Portable OS লিখতে হলে সর্বনিম্ন সাধারণ হর ব্যবহার করতে হয়
- Paging-এর protection bit-এ মাত্র একটা bit আছে (user/supervisor) — অর্থাৎ page-স্তরে ring 1 আর 2 কে ring 0 থেকে আলাদা করাই যায় না
তাই Linux, Windows, macOS — সবাই শুধু ring 0 (kernel) আর ring 3 (user) ব্যবহার করে।
ARM64-এর মডেল — exception level
ARM একই ধারণা আরো পরিষ্কারভাবে সাজিয়েছে:
| Level | কে চলে | উদাহরণ |
|---|---|---|
| EL0 | Application | আপনার প্রোগ্রাম |
| EL1 | OS kernel | Linux, XNU |
| EL2 | Hypervisor | KVM, Hyper-V |
| EL3 | Secure monitor | ARM TrustZone, firmware |
লক্ষ্য করুন ARM-এ hypervisor-এর জন্য একটা আলাদা level আছে শুরু থেকেই — x86-কে সেটা পরে VT-x দিয়ে যোগ করতে হয়েছে।
EL3 সবচেয়ে আকর্ষণীয়: এটা secure world চালায়, যেখানে আপনার ফোনের fingerprint data আর DRM key থাকে। আপনার OS (EL1) সেই memory ছুঁতেও পারে না।
User mode ঠিক কী পারে না
তিন শ্রেণির নিষেধাজ্ঞা:
১. Privileged instruction
কিছু instruction শুধু ring 0-তে চলে:
| Instruction | কী করে | কেন নিষিদ্ধ |
|---|---|---|
cli / sti | interrupt বন্ধ/চালু | interrupt বন্ধ করে দিলে scheduler আর কখনো চলবে না — একটা প্রোগ্রাম পুরো মেশিন দখল করে নেবে |
hlt | CPU থামানো | একই কারণ |
mov cr3, rax | page table বদলানো | অন্য process-এর memory ম্যাপ করে ফেলা যেত |
lgdt / lidt | GDT/IDT বসানো | নিজের interrupt handler বসিয়ে ring 0-তে ঢোকা যেত |
wrmsr | model-specific register লেখা | CPU-র মৌলিক আচরণ বদলানো |
in / out | I/O port access | সরাসরি disk controller-এ কমান্ড পাঠানো যেত |
চেষ্টা করলে CPU একটা #GP (General Protection) fault তোলে,
যা kernel ধরে আর process-কে SIGSEGV বা SIGILL পাঠায়।
২. Kernel memory access
প্রতিটা page table entry-তে একটা U/S bit আছে (bit 2)।
0 মানে supervisor-only।
CPL 3-এ থেকে supervisor page ছোঁয়ার চেষ্টা করলে page fault। এটা instruction-স্তরের check নয় — এটা প্রতিটা memory access-এ MMU করে, hardware-এ, শূন্য অতিরিক্ত খরচে।
৩. সরাসরি hardware access
Device register গুলো হয় I/O port-এ (in/out — privileged),
নয় memory-mapped I/O-তে (supervisor page)। দুইভাবেই বন্ধ।
Address space — kernel কোথায় থাকে
এখানে একটা বিস্ময় আছে যা প্রথমে অযৌক্তিক মনে হয়:
Kernel-এর কোড আর ডেটা প্রতিটা process-এর virtual address space-এ ম্যাপ করা থাকে।
x86-64-এ 48-bit canonical address space দুই ভাগে বিভক্ত:
0xFFFF_FFFF_FFFF_FFFF ┌────────────────────────┐
│ kernel space │
│ (সব process-এ একই) │ 128 TB
0xFFFF_8000_0000_0000 ├────────────────────────┤
│ │
│ non-canonical hole │ ব্যবহার করলে #GP
│ │
0x0000_7FFF_FFFF_FFFF ├────────────────────────┤
│ user space │
│ (প্রতি process আলাদা) │ 128 TB
0x0000_0000_0000_0000 └────────────────────────┘কেন এই নকশা? কারণ প্রতিটা syscall আর প্রতিটা interrupt-এ
kernel-এর কোড চালাতে হয়। যদি kernel আলাদা address space-এ
থাকত, তাহলে প্রতিবার ঢোকা-বেরোনোর সময় page table বদলাতে হতো —
অর্থাৎ cr3 লিখতে হতো, যা পুরো TLB flush করে দেয়।
Kernel একই address space-এ থাকলে syscall শুধু একটা mode switch, কোনো address space switch নয়। TLB অক্ষত থাকে।
নিরাপত্তা আসে page table-এর U/S bit থেকে — kernel page গুলো supervisor-only, তাই ম্যাপ করা থাকলেও user mode ছুঁতে পারে না।
৫০ বছর ধরে এই নকশা নিরাপদ ছিল। ২০১৮-তে সেটা ভেঙে গেল।
ভেতরে কী ঘটছে
Mode switch — দেয়াল পার হওয়ার মুহূর্ত
CPL 3 থেকে CPL 0-তে যাওয়ার মাত্র তিনটা বৈধ পথ আছে:
- System callস্বেচ্ছায় — syscall / svc instruction
- Interruptবাইরে থেকে — timer, keyboard, network card
- Exceptionঅনিচ্ছাকৃত — page fault, divide by zero, #GP
- → CPU privilege level 0-তে সেট করেhardware, atomically
- → kernel stack-এ switch করেTSS.RSP0 থেকে — user stack বিশ্বাসযোগ্য নয়
- → IDT/MSR থেকে handler address লোড করেuser নির্ধারণ করতে পারে না
- → kernel কোড চলেএখন ring 0
তিনটা পথেই তিনটা জিনিস অপরিবর্তনীয়:
১. কোথায় যাবে সেটা kernel আগেই ঠিক করে রেখেছে। Interrupt-এর
জন্য IDT (Interrupt Descriptor Table), syscall-এর জন্য
IA32_LSTAR MSR। User program শুধু “যাও” বলতে পারে, “কোথায়
যাও” বলতে পারে না।
২. Stack বদলে যায়। Kernel user-এর stack pointer বিশ্বাস
করতে পারে না — সেটা invalid, unmapped, বা ইচ্ছাকৃতভাবে
kernel memory-র দিকে তাক করা হতে পারে। তাই hardware
TSS (Task State Segment)-এর RSP0 থেকে একটা নির্ভরযোগ্য
kernel stack লোড করে।
৩. পুরো জিনিসটা atomic। এমন কোনো মধ্যবর্তী অবস্থা নেই যেখানে CPL 0 কিন্তু stack এখনো user-এর।
Meltdown — যখন দেয়ালটা ফাঁকা হয়ে গেল
২০১৮ সালের জানুয়ারিতে প্রকাশিত Meltdown দেখাল যে “kernel page supervisor-only, তাই নিরাপদ” — এই যুক্তিটা speculative execution-এর কারণে ভেঙে পড়ে।
আক্রমণটা তিন ধাপে:
/* ১. একটা kernel address পড়ুন — এটা ব্যর্থ হবেই */
char value = *(char *)kernel_address;
/* ২. কিন্তু ব্যর্থ হওয়ার আগে CPU speculatively এটা চালিয়ে ফেলে */
char dummy = probe_array[value * 4096];
/* ৩. exception হলো, value বাতিল হলো — কিন্তু cache-এ চিহ্ন রয়ে গেল */মূল অন্তর্দৃষ্টি: CPU out-of-order execution-এ ধাপ ১-এর permission check শেষ হওয়ার আগেই ধাপ ২ চালিয়ে ফেলে। Exception পরে raise হয় আর architectural state (register) পুনরুদ্ধার হয় — কিন্তু cache-এর অবস্থা পুনরুদ্ধার হয় না।
তারপর attacker probe_array-এর ২৫৬টা cache line-এর প্রতিটার
access time মেপে দেখে কোনটা দ্রুত — সেটাই value।
এটা একটা side channel: গোপন তথ্য architectural পথে বের হয়নি, বেরিয়েছে timing-এর মাধ্যমে।
KPTI — প্রতিকার আর তার দাম
সমাধান: Kernel Page Table Isolation। যদি kernel page গুলো user address space-এ ম্যাপই না থাকে, তাহলে speculative execution-ও তাদের ছুঁতে পারবে না।
তাই এখন প্রতিটা process-এর দুইটা page table:
KPTI-র আগে KPTI-র পরে
একটা page table: user mode-এ: kernel mode-এ:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ kernel (S) │ │ (খালি) │ │ kernel (S) │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ trampoline │ │ trampoline │ │ trampoline │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ user (U) │ │ user (U) │ │ user (U) │
└──────────────┘ └──────────────┘ └──────────────┘
syscall = mode switch syscall = mode switch + CR3 লেখা + TLB প্রভাবএকটা ক্ষুদ্র trampoline অংশ দুই table-এই ম্যাপ থাকতে হয় — নাহলে page table বদলানোর কোডটাই unmap হয়ে যেত।
খরচ: প্রতিটা syscall-এ এখন cr3 লিখতে হয়। PCID (Process
Context ID) থাকলে সম্পূর্ণ TLB flush এড়ানো যায়, কিন্তু তবু
overhead থেকে যায়।
| Workload | KPTI overhead |
|---|---|
| CPU-bound (syscall কম) | ~০% |
| সাধারণ application | ২–৫% |
| Syscall-ভারী (database, I/O) | ৫–৩০% |
PostgreSQL আর Redis-এর মতো syscall-ভারী system সবচেয়ে বেশি ক্ষতিগ্রস্ত হয়েছিল।
উদাহরণ
/proc/self/maps — বিভাজনটা নিজের চোখে
একটা সাধারণ প্রোগ্রামের address space:
cat /proc/self/maps5586c4a00000-5586c4a02000 r--p 00000000 08:02 1835017 /usr/bin/cat
5586c4a02000-5586c4a07000 r-xp 00002000 08:02 1835017 /usr/bin/cat
5586c4a07000-5586c4a0a000 r--p 00007000 08:02 1835017 /usr/bin/cat
5586c4a0a000-5586c4a0b000 r--p 00009000 08:02 1835017 /usr/bin/cat
5586c4a0b000-5586c4a0c000 rw-p 0000a000 08:02 1835017 /usr/bin/cat
5586c5f3f000-5586c5f60000 rw-p 00000000 00:00 0 [heap]
7f2e8c000000-7f2e8c028000 r--p 00000000 08:02 1841234 /usr/lib/libc.so.6
...
7ffd4b7e2000-7ffd4b803000 rw-p 00000000 00:00 0 [stack]
7ffd4b8f1000-7ffd4b8f5000 r--p 00000000 00:00 0 [vvar]
7ffd4b8f5000-7ffd4b8f7000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]তিনটা জিনিস লক্ষ্য করুন:
১. সব address 0x7fff...-এর নিচে — user space-এর সীমা।
Kernel-এর কোনো entry নেই, যদিও kernel ম্যাপ করা আছে। Kernel
ইচ্ছাকৃতভাবে সেগুলো লুকায় (তথ্য ফাঁস রোধ করতে)।
২. ELF segment গুলো আলাদা permission-এ — r--p (rodata),
r-xp (code), rw-p (data)। Level 3-এর ELF লেসনের section
গুলো এখানে runtime segment হয়ে এসেছে। কোনো segment একসাথে
w আর x নয় — এটাই W^X নীতি, code injection ঠেকায়।
৩. [vdso] — একটা ছোট রহস্য। এটা kernel-এর দেওয়া একটা
shared library যা প্রতিটা process-এ ম্যাপ করা। এতে কিছু
function-এর user-mode বাস্তবায়ন আছে (gettimeofday,
clock_gettime) যাতে সেগুলোর জন্য syscall করতেই না হয়।
[vsyscall]-টা vDSO-র পুরনো, স্থির-ঠিকানার পূর্বসূরি — নিরাপত্তার
কারণে (ROP gadget হিসেবে ব্যবহারযোগ্য ছিল) এখন প্রায় অক্ষম।
Kernel address কেন দেখা যায় না
sudo cat /proc/kallsyms | grep " T sys_open"Root ছাড়া চালালে সব address 0000000000000000 দেখাবে।
cat /proc/sys/kernel/kptr_restrictএটা 1 বা 2 হলে kernel pointer লুকানো হয়। কারণ KASLR
(Kernel Address Space Layout Randomization) কাজ করে শুধু যদি
attacker kernel-এর প্রকৃত address না জানে। একটা ফাঁস হওয়া
pointer পুরো randomization বাতিল করে দেয়।
নিজে চালিয়ে দেখুন
দেয়ালটা সত্যিই আছে — একটা privileged instruction চালানোর চেষ্টা
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include <string.h>
static sigjmp_buf jump_buffer;
static volatile const char *current_test;
static void handler(int sig, siginfo_t *info, void *ctx) {
(void)ctx;
printf(" ✗ %-24s → %s (si_code=%d)\n",
current_test, strsignal(sig), info->si_code);
siglongjmp(jump_buffer, 1);
}
#define TRY(name, code) \
do { \
current_test = name; \
if (sigsetjmp(jump_buffer, 1) == 0) { \
code; \
printf(" ✓ %-24s → সফল!\n", name); \
} \
} while (0)
int main(void) {
struct sigaction sa = {0};
sa.sa_sigaction = handler;
sa.sa_flags = SA_SIGINFO;
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGILL, &sa, NULL);
sigaction(SIGBUS, &sa, NULL);
printf("CPL 3 (user mode) থেকে চেষ্টা করছি:\n\n");
TRY("সাধারণ যোগ", { volatile int x = 1 + 1; (void)x; });
TRY("cli (interrupt off)", { __asm__ volatile("cli"); });
TRY("hlt (CPU থামানো)", { __asm__ volatile("hlt"); });
TRY("cr3 পড়া", { unsigned long v;
__asm__ volatile("mov %%cr3, %0" : "=r"(v)); });
TRY("kernel memory পড়া", { volatile char c =
*(char *)0xffffffff81000000UL; (void)c; });
TRY("NULL pointer", { volatile char c = *(char *)0; (void)c; });
TRY("I/O port পড়া", { unsigned char v;
__asm__ volatile("inb $0x60, %0" : "=a"(v)); });
printf("\nএকটাও privileged operation সফল হয়নি — দেয়ালটা কাজ করছে।\n");
return 0;
}gcc -O0 -o wall wall.c && ./wallপ্রত্যাশিত output:
CPL 3 (user mode) থেকে চেষ্টা করছি:
✓ সাধারণ যোগ → সফল!
✗ cli (interrupt off) → Segmentation fault (si_code=128)
✗ hlt (CPU থামানো) → Segmentation fault (si_code=128)
✗ cr3 পড়া → Segmentation fault (si_code=128)
✗ kernel memory পড়া → Segmentation fault (si_code=1)
✗ NULL pointer → Segmentation fault (si_code=1)
✗ I/O port পড়া → Segmentation fault (si_code=128)
একটাও privileged operation সফল হয়নি — দেয়ালটা কাজ করছে।si_code লক্ষ্য করুন — এটা দুই ধরনের ব্যর্থতা আলাদা করে:
| si_code | মানে | কোনগুলো |
|---|---|---|
128 (SI_KERNEL) | #GP fault — privileged instruction | cli, hlt, cr3, inb |
1 (SEGV_MAPERR) | page fault — অবৈধ address | NULL, kernel memory |
দুইটা সম্পূর্ণ ভিন্ন hardware প্রক্রিয়া, একই signal-এ এসে মিলেছে। প্রথমটা instruction decoder ধরেছে, দ্বিতীয়টা MMU।
User mode-এর সীমাবদ্ধতা একটা নীতি নয়, একটা hardware সত্য। একই instruction kernel-এ চলে, user-এ SIGSEGV দেয় — আর সেই পার্থক্যটা CPL-এর দুই bit।
vDSO — যে syscall আসলে syscall নয়
#define _GNU_SOURCE
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <sys/syscall.h>
#define N 2000000
static double bench(const char *name, void (*fn)(void)) {
struct timespec a, b;
clock_gettime(CLOCK_MONOTONIC, &a);
for (int i = 0; i < N; i++) fn();
clock_gettime(CLOCK_MONOTONIC, &b);
double ns = ((b.tv_sec - a.tv_sec) * 1e9 + (b.tv_nsec - a.tv_nsec)) / N;
printf(" %-34s %7.1f ns/call\n", name, ns);
return ns;
}
static void via_vdso(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); }
static void via_syscall(void){ struct timespec t; syscall(SYS_clock_gettime, CLOCK_MONOTONIC, &t); }
static void real_syscall(void){ syscall(SYS_getpid); }
static void no_syscall(void) { __asm__ volatile("" ::: "memory"); }
int main(void) {
printf("প্রতিটা %d বার:\n\n", N);
double none = bench("কিছু না (baseline)", no_syscall);
double vdso = bench("clock_gettime (vDSO)", via_vdso);
double sysc = bench("clock_gettime (জোর করে syscall)", via_syscall);
double gpid = bench("getpid (আসল syscall)", real_syscall);
printf("\n mode switch-এর আনুমানিক খরচ: %.0f ns\n", gpid - none);
printf(" vDSO যা বাঁচায়: %.1f×\n", sysc / vdso);
return 0;
}gcc -O2 -o vdso vdso.c && ./vdsoসাধারণ ফলাফল (KPTI চালু একটা মেশিনে):
প্রতিটা 2000000 বার:
কিছু না (baseline) 0.3 ns/call
clock_gettime (vDSO) 22.7 ns/call
clock_gettime (জোর করে syscall) 412.5 ns/call
getpid (আসল syscall) 398.1 ns/call
mode switch-এর আনুমানিক খরচ: 398 ns
vDSO যা বাঁচায়: 18.2×এখন প্রমাণ করুন যে vDSO সত্যিই syscall করছে না:
strace -c -e trace=clock_gettime ./vdso২০ লক্ষ কলের মধ্যে clock_gettime শূন্য বার দেখাবে —
কারণ সেগুলো kernel-এ কখনো পৌঁছায়নি।
কীভাবে সম্ভব: kernel একটা shared page-এ ([vvar]) বর্তমান
সময় লিখে রাখে আর প্রতি tick-এ আপডেট করে। vDSO-র কোড সেই
page থেকে পড়ে — একটা সাধারণ memory read, কোনো mode switch নেই।
# vDSO-র কোড dump করে দেখুন
cp /proc/self/maps /tmp/m
grep vdso /tmp/mMode switch-এর খরচ বাস্তব ও পরিমাপযোগ্য। vDSO সেই খরচ এড়াতে kernel-এর কোড user space-এ ম্যাপ করে — আর পার্থক্যটা ১০ গুণেরও বেশি।
নিজে বানান
Address space explorer — নিজের প্রোগ্রামের মানচিত্র
- বিভিন্ন ধরনের object-এর address ছাপুন — global, static, heap, stack, code, libc
- /proc/self/maps পড়ে প্রতিটা address কোন region-এ পড়ে খুঁজে বের করুন
- প্রতিটা region-এর permission ছাপুন আর W^X নীতি যাচাই করুন
- ASLR-এর প্রভাব দেখতে প্রোগ্রামটা কয়েকবার চালান
- user/kernel সীমানা কোথায় তা হিসাব করে দেখান
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int global_initialised = 42;
static int global_bss;
static const char rodata_string[] = "read-only";
typedef struct {
unsigned long start, end;
char perms[5];
char path[256];
} region_t;
static region_t regions[512];
static int nregions;
static void load_maps(void) {
FILE *f = fopen("/proc/self/maps", "r");
if (!f) { perror("maps"); exit(1); }
char line[512];
while (nregions < 512 && fgets(line, sizeof line, f)) {
region_t *r = ®ions[nregions];
r->path[0] = '\0';
if (sscanf(line, "%lx-%lx %4s %*s %*s %*s %255[^\n]",
&r->start, &r->end, r->perms, r->path) >= 3)
nregions++;
}
fclose(f);
}
static const region_t *find(unsigned long addr) {
for (int i = 0; i < nregions; i++)
if (addr >= regions[i].start && addr < regions[i].end)
return ®ions[i];
return NULL;
}
static void show(const char *label, const void *p) {
unsigned long a = (unsigned long)p;
const region_t *r = find(a);
printf(" %-18s %016lx %-5s %s\n", label, a,
r ? r->perms : "?????",
r ? (r->path[0] ? r->path : "[anonymous]") : "(ম্যাপ করা নেই)");
}
int main(void) {
load_maps();
void *heap = malloc(1024);
int local = 7;
printf("\n═══ এই প্রোগ্রামের address space ═══\n\n");
show("code (main)", (void *)main);
show("rodata", rodata_string);
show("data (init)", &global_initialised);
show("bss (uninit)", &global_bss);
show("heap (malloc)", heap);
show("stack (local)", &local);
show("libc (printf)", (void *)printf);
show("vdso", (void *)load_maps); /* কাছাকাছি দেখতে */
printf("\n═══ সীমানার হিসাব ═══\n\n");
unsigned long user_max = 0x00007FFFFFFFFFFFUL;
printf(" user space সর্বোচ্চ : %016lx (%.0f TB)\n",
user_max, (double)user_max / (1UL << 40));
printf(" kernel space শুরু : %016lx\n", 0xFFFF800000000000UL);
printf(" stack এই সীমার নিচে? : %s\n",
((unsigned long)&local < user_max) ? "হ্যাঁ" : "না");
printf("\n═══ W^X যাচাই ═══\n\n");
int violations = 0;
for (int i = 0; i < nregions; i++)
if (strchr(regions[i].perms, 'w') && strchr(regions[i].perms, 'x')) {
printf(" ⚠ w+x দুটোই: %016lx-%016lx %s %s\n",
regions[i].start, regions[i].end,
regions[i].perms, regions[i].path);
violations++;
}
if (!violations)
printf(" কোনো region একসাথে writable আর executable নয় — W^X মানা হচ্ছে\n");
printf("\n মোট region: %d\n\n", nregions);
free(heap);
return 0;
}gcc -O0 -o explore explore.c && ./exploreASLR দেখতে দুইবার চালান:
./explore | grep stack
./explore | grep stackAddress প্রতিবার আলাদা হবে। এবার ASLR বন্ধ করে দেখুন:
setarch $(uname -m) -R ./explore | grep stack
setarch $(uname -m) -R ./explore | grep stackএখন একই। -R মানে “randomization বন্ধ” — debugging-এ কাজে লাগে,
কিন্তু production-এ কখনো নয়।
নিজে বাড়ান:
/proc/self/smapsপড়ে প্রতিটা region-এর প্রকৃত RSS ছাপুন, আর দেখুন VSZ আর RSS-এর ফারাক কত (lazy allocation — লেসন ১০-এ ফিরবে)mmapদিয়ে একটা নতুন region বানান বিভিন্ন permission-এ, আর maps-এ সেটা খুঁজে বের করুন- একটা
PROT_READ|PROT_WRITEpage বানিয়ে তাতে machine code লিখুন, তারপরmprotectদিয়েPROT_EXECকরে চালান — এটাই JIT compiler যা করে (Level 5-এ ফিরবে) PROT_WRITE|PROT_EXECএকসাথে চাওয়ার চেষ্টা করুন — কিছু hardened kernel-এ (আর OpenBSD-তে সবসময়) এটা ব্যর্থ হবেgetauxval(AT_SYSINFO_EHDR)দিয়ে vDSO-র base address বের করুন আর তার ELF header parse করে exported symbol গুলো ছাপুন
বাস্তব সিস্টেমে
এই দেয়ালটা যেখানে যেখানে কাজ করছে
প্রতিটা browser tab। Chrome-এর site isolation প্রতিটা origin-কে আলাদা process-এ রাখে, ঠিক এই hardware boundary-র উপর ভরসা করে। একটা ক্ষতিকর সাইট যদি renderer process দখলও করে ফেলে, সে অন্য tab-এর memory পড়তে পারে না। Spectre-এর পর এটা বাধ্যতামূলক হয়েছে।
Cloud multi-tenancy। AWS-এ আপনার VM আর অন্য কারো VM একই physical CPU-তে চলতে পারে। পুরো ব্যবসায়িক মডেলটা এই isolation-এর উপর দাঁড়ানো — আর সেজন্যই Meltdown/Spectre cloud industry-তে আতঙ্ক তৈরি করেছিল। AWS ২০১৮-তে জরুরি ভিত্তিতে পুরো fleet reboot করেছিল।
Container-এর দুর্বলতা। Container একই kernel ভাগ করে (Level 12-এ বিস্তারিত)। তাই container escape মানে শুধু একটা kernel bug — VM escape-এর চেয়ে অনেক সহজ। এই কারণেই gVisor (userspace kernel) আর Firecracker (micro-VM) তৈরি হয়েছে: আরেকটা hardware boundary যোগ করতে।
WebAssembly-র ভিন্ন পথ। WASM hardware ring ব্যবহার করে না — এটা একটা software sandbox যা bytecode verify করে আর memory access bounds-check করে। ধীর, কিন্তু হাজারো sandbox এক process-এ চালানো যায়। Cloudflare Workers এই মডেলে চলে — এই ওয়েবসাইটটা যেখানে host করা।
eBPF — নিয়ন্ত্রিত ব্যতিক্রম। eBPF user-এর লেখা কোড kernel-এ চালায়। কীভাবে নিরাপদ? একটা verifier প্রতিটা program load করার আগে প্রমাণ করে যে এটা থামবে (কোনো unbounded loop নেই) আর শুধু অনুমোদিত memory ছোঁবে। অর্থাৎ hardware boundary-র বদলে formal verification — Level 13-এর ধারণা, production-এ প্রয়োগ। লেসন ৩০-এ ফিরব।
Microkernel-এর দাবি। seL4-এ driver ring 3-এ চলে, তাই একটা buggy driver পুরো system নামাতে পারে না। দাম: প্রতিটা driver call একটা IPC, তাই বেশি context switch। QNX (গাড়িতে) আর seL4 (সামরিক ড্রোনে) সেই দাম দিতে রাজি, কারণ সেখানে crash-এর খরচ অনেক বেশি।
যে ভুলগুলো সবাই করে
“Root বা administrator মানে ring 0-তে চলা।”
সম্পূর্ণ আলাদা দুইটা ধারণা, আর গুলিয়ে ফেলাটা খুব সাধারণ।
Ring 0/3 একটা hardware বৈশিষ্ট্য — CPU-র ভেতরে দুই bit। root/uid 0 একটা software ধারণা — kernel-এর নিজের রাখা একটা সংখ্যা, যা দিয়ে সে permission check করে।
sudo চালানো প্রোগ্রামও ring 3-এ চলে। তার uid 0, তাই
kernel তাকে বেশি জিনিস করতে দেয় — কিন্তু সে এখনো cli
চালাতে পারে না, এখনো kernel memory সরাসরি পড়তে পারে না।
sudo ./wall # আগের experiment-টা — একই ভাবে সব ব্যর্থ হবেRoot থেকে ring 0-তে যাওয়ার পথ আছে (kernel module load করা,
/dev/mem লেখা) — কিন্তু সেগুলো আলাদা, স্পষ্ট পদক্ষেপ।
আর এই কারণেই আধুনিক Linux-এ root-কেও ভাগ করা হয়েছে
capability-তে (CAP_SYS_ADMIN, CAP_NET_RAW…) — যাতে
একটা প্রোগ্রামকে “সব ক্ষমতা” না দিয়ে শুধু যেটুকু দরকার
সেটুকু দেওয়া যায়।
“Kernel অন্য address space-এ থাকে, তাই user program সেটা দেখতেই পায় না।”
KPTI-র আগে kernel প্রতিটা process-এর page table-এ ম্যাপ করা থাকত। “দেখতে পায় না” এসেছিল page table-এর U/S bit থেকে, আলাদা address space থেকে নয়।
আর সেই পার্থক্যটাই Meltdown-কে সম্ভব করেছিল — speculative execution U/S bit-এর check শেষ হওয়ার আগেই ডেটা cache-এ টেনে আনত।
KPTI-র পরে দাবিটা আংশিকভাবে সত্য হয়েছে: user mode-এ থাকাকালীন kernel সত্যিই ম্যাপ করা থাকে না (একটা ছোট trampoline ছাড়া)।
কিন্তু নতুন CPU-তে hardware fix থাকায় KPTI বন্ধ থাকে, আর তখন আবার পুরনো নকশাই চলে। তাই উত্তরটা “নির্ভর করে” — আর নিজের মেশিনে যাচাই করা যায়:
cat /sys/devices/system/cpu/vulnerabilities/meltdown
dmesg | grep -i "page table isolation"“Mode switch আর context switch একই জিনিস।”
দুইটা সম্পূর্ণ ভিন্ন খরচের operation, আর গুলিয়ে ফেললে performance নিয়ে ভুল যুক্তি হয়।
| Mode switch | Context switch | |
|---|---|---|
| কী বদলায় | শুধু privilege level | পুরো process |
| Page table | অপরিবর্তিত (KPTI ছাড়া) | বদলায় (cr3) |
| TLB | অক্ষত | flush বা PCID |
| Cache | অক্ষত | সাধারণত ঠান্ডা হয়ে যায় |
| খরচ | ~১০০–৫০০ ns | ~১–১০ μs |
একটা syscall মানে দুইটা mode switch (ঢোকা + বেরোনো), কিন্তু কোনো context switch নয় — একই process ফিরে আসে।
Context switch হয় যখন scheduler অন্য process বেছে নেয় — যা syscall-এর সময় ঘটতে পারে (যদি syscall block করে), কিন্তু ঘটতেই হবে এমন নয়।
getpid() একটা syscall কিন্তু কোনো context switch নয়।
read() একটা ঠান্ডা ফাইলে দুটোই ঘটাবে।
লেসন ৭-এ আমরা context switch-এর প্রকৃত খরচ মাপব।
“Meltdown আর Spectre একই দুর্বলতা।”
সম্পর্কিত, কিন্তু ভিন্ন — আর প্রতিকারও ভিন্ন।
Meltdown privilege check bypass করে: user code সরাসরি kernel memory পড়ার চেষ্টা করে, আর speculation-এর কারণে সফল হয়। এটা মূলত Intel-এর একটা বাস্তবায়ন ত্রুটি, আর KPTI দিয়ে সম্পূর্ণ প্রতিকারযোগ্য। নতুন CPU-তে hardware-এই ঠিক করা হয়েছে।
Spectre branch predictor-কে বিভ্রান্ত করে একই privilege level-এর ভেতরে। ভুক্তভোগী প্রোগ্রামকে দিয়ে তার নিজের ডেটা ফাঁস করানো হয়। এটা speculative execution-এর মৌলিক ধারণার পরিণতি, তাই প্রায় সব out-of-order CPU আক্রান্ত — Intel, AMD, ARM।
আর Spectre-এর কোনো সম্পূর্ণ প্রতিকার নেই। যা আছে তা
আংশিক: retpoline, lfence barrier, IBRS/IBPB, আর compiler-এর
সতর্কতা। প্রতিটার performance খরচ আছে।
grep . /sys/devices/system/cpu/vulnerabilities/*এই একটা কমান্ড আপনার CPU-র সব পরিচিত speculative দুর্বলতা আর তাদের বর্তমান প্রশমন দেখাবে — সাধারণত ১০-১৫টা এন্ট্রি।
Level 11-এ speculative execution, আর Level 10-এ side channel বিস্তারিত দেখব।
বুঝেছেন কি না দেখুন
1কেন একটা OS শুধু software দিয়ে isolation প্রয়োগ করতে পারে না?
যুক্তিটা ধাপে ধাপে গঠন করুন।
যুক্তি
যুক্তিটা একটা regress-এ গিয়ে ভেঙে পড়ে।
ধরুন OS প্রতিটা memory access-এর আগে একটা check বসাতে চায়:
if (address_belongs_to_me(addr)) *addr = value;সমস্যা ১ — কে check চালাবে? যদি user program নিজের কোডে check রাখে, সে সেটা সরিয়ে দিতে পারে। Compiler বসালেও — attacker নিজের assembler ব্যবহার করবে।
সমস্যা ২ — OS যদি প্রতিটা instruction যাচাই করে? তাহলে প্রতিটা instruction interpret করতে হবে, অর্থাৎ প্রোগ্রাম ১০০ গুণ ধীর হবে। আর সেই interpreter-এর কোড নিজেই কে রক্ষা করবে?
সমস্যা ৩ — OS যদি কোড আগে scan করে? এটাই static verification, আর সাধারণ ক্ষেত্রে এটা অসিদ্ধান্তযোগ্য — “এই প্রোগ্রাম কি কখনো এই address ছোঁবে?” প্রশ্নটা halting problem-এ কমিয়ে আনা যায় (Level 13)।
সমাধান তাই বাইরে থেকে আসতে হবে। এমন একটা সত্তা যা কোড-এর অংশ নয়, আর যাকে কোড প্রভাবিত করতে পারে না — hardware।
CPU প্রতিটা access-এ CPL আর U/S bit মেলায়। এটা কোনো instruction নয় যা বাদ দেওয়া যায়; এটা silicon-এর একটা comparator যা সবসময় চলছে, শূন্য অতিরিক্ত সময়ে।
তিনটা ব্যতিক্রম যা নিয়মটা স্পষ্ট করে:
| পদ্ধতি | কীভাবে software-এ isolation পায় | দাম |
|---|---|---|
| Interpreter (JVM bytecode verifier) | প্রতিটা instruction যাচাই | গতি |
| WASM | সীমিত ISA + bounds check | সীমিত ক্ষমতা |
| eBPF | verifier প্রমাণ করে program থামবে | সীমিত ভাষা (loop প্রায় নিষিদ্ধ) |
তিনটাই কাজ করে কারণ তারা ভাষাটাকে সীমিত করেছে যাতে verification সিদ্ধান্তযোগ্য হয়। অর্থাৎ software isolation সম্ভব — কিন্তু শুধু যদি আপনি সাধারণ-উদ্দেশ্য কোড চালানো ছেড়ে দেন।
2আপনার প্রোগ্রাম SIGSEGV পেল। si_code দেখে আপনি কীভাবে বলবেন
এটা একটা privileged instruction ছিল নাকি একটা অবৈধ memory access?
প্রয়োগ
SIGSEGV পেল। si_code দেখে আপনি কীভাবে বলবেন
এটা একটা privileged instruction ছিল নাকি একটা অবৈধ memory access?siginfo_t-এর si_code আর si_addr দুটো ভিন্ন গল্প বলে।
| si_code | নাম | কারণ | si_addr |
|---|---|---|---|
1 | SEGV_MAPERR | address ম্যাপ করা নেই | সেই address |
2 | SEGV_ACCERR | ম্যাপ আছে কিন্তু permission নেই | সেই address |
128 | SI_KERNEL | #GP fault — privileged instruction | সাধারণত 0 |
static void handler(int sig, siginfo_t *info, void *ctx) {
switch (info->si_code) {
case SEGV_MAPERR:
printf("অবৈধ address: %p (ম্যাপ করা নেই)\n", info->si_addr);
break;
case SEGV_ACCERR:
printf("permission নেই: %p (হয়তো W^X বা read-only)\n", info->si_addr);
break;
case SI_KERNEL:
printf("privileged instruction — #GP fault\n");
break;
}
}তিনটা কারণ আলাদা করার ব্যবহারিক মূল্য:
SEGV_MAPERR+si_addr == 0→ NULL pointer dereference, সবচেয়ে সাধারণ bugSEGV_ACCERRএকটাr--pregion-এ → string literal-এ লেখার চেষ্টা (char *s = "abc"; s[0] = 'x';)SEGV_ACCERRexecutable region-এ লেখার চেষ্টা → JIT-এmprotectভুলে যাওয়াSI_KERNEL→ হয় inline assembly-তে ভুল, নয় corrupted function pointer এমন জায়গায় লাফিয়েছে যেখানে random byte privileged instruction হিসেবে decode হয়েছে
আরো তথ্য পেতে তৃতীয় argument ucontext_t কাজে লাগান —
সেখানে crash-এর মুহূর্তের সব register আছে:
ucontext_t *uc = ctx;
printf("RIP = %llx\n", (unsigned long long)uc->uc_mcontext.gregs[REG_RIP]);তারপর addr2line বা objdump দিয়ে সেই RIP কোন লাইন তা
বের করা যায় — এটাই crash reporter (Sentry, Crashpad) যা করে।
Level 10-এ আমরা দেখব attacker কীভাবে ইচ্ছাকৃতভাবে এই fault গুলো ঘটিয়ে memory layout সম্পর্কে তথ্য বের করে।
3আপনি একটা plugin system ডিজাইন করছেন যেখানে তৃতীয় পক্ষের কোড
আপনার server-এ চলবে। Isolation-এর কোন কৌশল বেছে নেবেন?
ডিজাইন
প্রশ্নটা আসলে: কতটা isolation বনাম কতটা খরচ, আর attacker কতটা সক্ষম ধরে নিচ্ছি।
| কৌশল | Boundary | Startup | Overhead | Escape-এর জন্য যা লাগে |
|---|---|---|---|---|
| একই process, ভাষার sandbox | কিছুই না | ~0 | ~0 | একটা memory bug |
| WASM | software verifier | ~1 ms | 10–50% | verifier bug |
| আলাদা process | hardware ring | ~1 ms | IPC খরচ | kernel bug |
| + seccomp + namespace | + syscall filter | ~2 ms | সামান্য | kernel bug (কম surface) |
| gVisor | userspace kernel | ~100 ms | 20–50% | দুইটা bug |
| microVM (Firecracker) | hardware virt | ~125 ms | 5–15% | hypervisor bug |
| আলাদা মেশিন | ভৌত | — | নেটওয়ার্ক | কিছুই না |
সিদ্ধান্তের প্রশ্নগুলো:
১. কোড কি বিশ্বাসযোগ্য? আপনার নিজের team-এর plugin আর ইন্টারনেট থেকে আসা কোড — সম্পূর্ণ ভিন্ন হুমকি।
২. Plugin কতক্ষণ চলে? ১০ ms-এর একটা transform-এর জন্য ১২৫ ms VM boot অযৌক্তিক। ঘণ্টাব্যাপী job-এর জন্য নগণ্য।
৩. কতগুলো একসাথে? হাজারো concurrent tenant হলে VM-এর memory overhead অসম্ভব হয়ে যায় — তখন WASM-ই একমাত্র বিকল্প।
৪. Plugin-এর কী দরকার? শুধু গণনা, নাকি নেটওয়ার্ক আর filesystem?
আমার সুপারিশ, ক্ষেত্রভেদে:
অবিশ্বস্ত, স্বল্পস্থায়ী, উচ্চ-ঘনত্ব (edge function, user script) → WASM। Cloudflare Workers ঠিক এই কারণে V8 isolate + WASM বেছে নিয়েছে — হাজারো tenant এক process-এ, মাইক্রোসেকেন্ডে startup।
অবিশ্বস্ত, দীর্ঘস্থায়ী, সাধারণ কোড (CI runner, user-submitted container) → microVM। AWS Lambda আর Fargate Firecracker ব্যবহার করে ঠিক এই কারণে।
মাঝারি বিশ্বাস, native গতি দরকার → আলাদা process +
seccomp + namespace, আর সাথে RLIMIT দিয়ে resource cap।
যা করবেন না: অবিশ্বস্ত native কোড নিজের process-এ
dlopen করা। কোনো boundary নেই — একটা bug মানে সম্পূর্ণ
compromise।
একটা স্তর সবসময় যোগ করুন: যেকোনো কৌশলের সাথে timeout, memory limit (cgroup), আর syscall filter (seccomp) রাখুন। Defence in depth — একটা boundary ভাঙলেও পরেরটা থাকে।
Level 12-এ আমরা এই প্রতিটা কৌশল হাতে-কলমে বানাব।
4KPTI syscall-কে ধীর করে। তাহলে কেন এটা চালু রাখা হয়, আর কখন
বন্ধ করা যুক্তিসঙ্গত?
যুক্তি
কেন চালু: Meltdown একটা সাধারণ unprivileged প্রোগ্রামকে পুরো physical memory পড়তে দেয় — password, key, অন্য VM-এর ডেটা। ৫–৩০% performance-এর বিনিময়ে সেটা ঠেকানো প্রায় সবসময় সঠিক বিনিময়।
কখন বন্ধ করা যুক্তিসঙ্গত — তিনটা ক্ষেত্র:
১. CPU আক্রান্তই নয়। সব AMD, আর Intel Ice Lake ও পরবর্তী প্রজন্মে hardware-এ ঠিক করা। Kernel নিজেই তখন KPTI বন্ধ রাখে — আপনাকে কিছু করতে হয় না।
cat /sys/devices/system/cpu/vulnerabilities/meltdown
# "Not affected" হলে KPTI এমনিতেই বন্ধ২. একক-উদ্দেশ্য, বিশ্বাসযোগ্য workload। একটা HPC cluster node যেখানে শুধু আপনার নিজের গণনা চলে, কোনো untrusted কোড নেই, কোনো নেটওয়ার্ক-মুখী সেবা নেই — সেখানে হুমকি মডেলটাই আলাদা।
৩. Embedded / appliance যেখানে arbitrary কোড চালানোর কোনো পথই নেই।
কখন কখনোই বন্ধ করবেন না:
- Multi-tenant cloud host
- যেকোনো মেশিন যা untrusted কোড চালায় (CI, plugin, user script)
- Browser চালানো desktop — একটা ক্ষতিকর ওয়েবপেজই যথেষ্ট (JavaScript থেকে Meltdown প্রদর্শিত হয়েছে)
কীভাবে বন্ধ করবেন (জেনেশুনে):
# kernel command line-এ
nopti
# অথবা সব speculative প্রশমন বন্ধ — খুবই বিপজ্জনক
mitigations=offআসল পরামর্শ: বন্ধ করার আগে মাপুন। অনেকে ধরে নেয় overhead বেশি, কিন্তু তাদের workload আসলে CPU-bound যেখানে প্রভাব প্রায় শূন্য।
# আপনার নিজের workload-এ syscall হার দেখুন
perf stat -e raw_syscalls:sys_enter ./your_appসেকেন্ডে কয়েক হাজার syscall হলে KPTI-র প্রভাব নগণ্য। কয়েক লক্ষ হলে তখন কথা বলার মতো — আর তখন প্রথম প্রশ্ন হওয়া উচিত “syscall কমানো যায় কি?” (batching, io_uring — লেসন ২১), “নিরাপত্তা বন্ধ করা যায় কি?” নয়।
5একটা প্রোগ্রাম mmap দিয়ে memory নিয়ে সেখানে machine code লিখে
চালাতে চায় (JIT)। কোন permission লাগবে, আর কেন এক ধাপে
PROT_WRITE|PROT_EXEC চাওয়া খারাপ অভ্যাস?
প্রয়োগ
mmap দিয়ে memory নিয়ে সেখানে machine code লিখে
চালাতে চায় (JIT)। কোন permission লাগবে, আর কেন এক ধাপে
PROT_WRITE|PROT_EXEC চাওয়া খারাপ অভ্যাস?সঠিক ক্রম — দুই ধাপে:
/* ১. লেখার জন্য */
void *mem = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memcpy(mem, machine_code, size);
/* ২. চালানোর জন্য — write সরিয়ে exec দিন */
mprotect(mem, size, PROT_READ | PROT_EXEC);
/* ৩. এখন চালান */
int (*fn)(void) = mem;
int result = fn();কেন PROT_WRITE|PROT_EXEC একসাথে খারাপ:
এটা W^X (write xor execute) নীতি লঙ্ঘন করে। যদি কোনো page একই সাথে লেখা যায় আর চালানো যায়, তাহলে একটা buffer overflow-এর মাধ্যমে attacker সেখানে নিজের shellcode লিখে সরাসরি চালাতে পারে।
W^X থাকলে attacker-কে অনেক কঠিন পথে যেতে হয় — ROP (Return-Oriented Programming), অর্থাৎ ইতিমধ্যে বিদ্যমান কোডের টুকরো জোড়া লাগানো। সেটা সম্ভব, কিন্তু অনেক কঠিন আর ASLR-এর সাথে মিলে আরো কঠিন।
যেসব জায়গায় W^X জোর করে প্রয়োগ করা হয়:
| পরিবেশ | আচরণ |
|---|---|
| OpenBSD | সবসময়, mmap ব্যর্থ হবে |
| macOS (Apple Silicon) | Hardened runtime-এ নিষিদ্ধ; MAP_JIT + pthread_jit_write_protect_np() লাগে |
| iOS | সম্পূর্ণ নিষিদ্ধ (JIT-এর বিশেষ entitlement ছাড়া) |
| SELinux | execmem policy দিয়ে ব্লক করা যায় |
| Linux (default) | অনুমোদিত, কিন্তু নিরুৎসাহিত |
আধুনিক JIT-এর কৌশল — dual mapping:
একই physical memory দুইবার ম্যাপ করা হয় — একটা RW,
আরেকটা RX:
int fd = memfd_create("jit", MFD_CLOEXEC);
ftruncate(fd, size);
void *w = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
void *x = mmap(NULL, size, PROT_READ|PROT_EXEC, MAP_SHARED, fd, 0);
/* w দিয়ে লিখুন, x দিয়ে চালান — কোনো একটা pointer-এ দুটো ক্ষমতা নেই */V8, JavaScriptCore আর LuaJIT এই ধরনের কৌশল ব্যবহার করে।
একটা জরুরি বিবরণ যা সবাই ভোলে — instruction cache:
ARM-এ (আর কিছু অন্য architecture-এ) data cache আর instruction cache আলাদা, আর তারা স্বয়ংক্রিয়ভাবে সমন্বিত হয় না। নতুন লেখা কোড চালানোর আগে cache flush করতে হয়:
__builtin___clear_cache((char *)mem, (char *)mem + size);x86-এ এটা লাগে না (coherent i-cache), তাই x86-এ লেখা JIT ARM-এ port করলে রহস্যময়ভাবে ভাঙে — একটা কুখ্যাত bug।
Level 5-এ আমরা একটা JIT compiler বানাব আর এই সব বিবরণ হাতে-কলমে দেখব। Level 11-এ i-cache আর d-cache-এর গঠন।
এরপর কী
পরের লেসন — System call
এই লেসনে আমরা দেয়ালটা দেখলাম, আর দেখলাম তাতে তিনটা দরজা আছে: syscall, interrupt, exception।
পরের লেসনে আমরা প্রথম দরজাটা খুলে দেখব — system call, যেটা একমাত্র দরজা যা user program স্বেচ্ছায় ব্যবহার করে।
আমরা দেখব syscall instruction ঠিক কী করে, syscall number
কোথায় যায়, argument কোন register-এ (আর কেন সেটা C-র calling
convention থেকে আলাদা — rcx কোথায় হারিয়ে গেল?), kernel
কীভাবে sys_call_table থেকে সঠিক handler খুঁজে পায়, আর
ফেরার পথে errno কীভাবে তৈরি হয়।
আর সবচেয়ে গুরুত্বপূর্ণ — আমরা libc সম্পূর্ণ বাদ দিয়ে একটা
write করব, শুধু inline assembly দিয়ে। তারপর strace
দিয়ে পাশাপাশি রেখে দেখব libc ঠিক কী কী বাড়তি কাজ করে।
আজকের experiment-এ আমরা মেপেছি একটা syscall-এর খরচ প্রায় ৪০০ ন্যানোসেকেন্ড। পরের লেসনে দেখব সেই ৪০০ ন্যানোসেকেন্ড ঠিক কোথায় খরচ হয়।
আরও পড়ুন
- Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 3 — Chapter 5: Protection — Intel Corporation · CPL, DPL, RPL আর privilege check-এর প্রামাণ্য বর্ণনা — এই লেসনের প্রতিটা দাবি এখানে যাচাইযোগ্য
- Meltdown: Reading Kernel Memory from User Space — Lipp, Schwarz, Gruss, et al. (USENIX Security 2018) · মূল পেপার; §3-এ আক্রমণের তিনটা ধাপ পরিষ্কারভাবে ব্যাখ্যা করা, আর §6-এ KPTI-র প্রস্তাব
- Linux kernel documentation — x86 memory layout · x86-64-এ কোন virtual address range কী কাজে ব্যবহৃত হয় তার নির্ভুল মানচিত্র
- Operating Systems: Three Easy Pieces — Chapter 6: Limited Direct Execution — Remzi H. Arpaci-Dusseau, Andrea C. Arpaci-Dusseau · কেন OS-কে 'সীমিত' direct execution করতে হয় — এই লেসনের কেন্দ্রীয় যুক্তির সবচেয়ে পরিষ্কার উপস্থাপনা