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

System call — user mode থেকে kernel mode

System Calls — Dual-Mode Operation, the Trap into the Kernel, errno and strace

Taught in Bangla, this lesson explains system calls: dual-mode operation and the mode bit, privileged instructions, the journey of write(1, "hi\n", 3) through the library wrapper, registers, trap instruction and syscall table, parameter passing and errno, the six categories with POSIX and Windows examples, library functions vs system calls and buffering, syscall cost and vDSO, and tracing programs with strace, Process Monitor and dtruss.

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

  • Dual-mode operation কেন দরকার, privileged instruction কী, আর user প্রোগ্রাম সেটি চালাতে চাইলে কী ঘটে
  • একটি write(1, "hi\n", 3) কল প্রোগ্রাম থেকে kernel হয়ে টার্মিনাল পর্যন্ত কীভাবে যায় — রেজিস্টার ধরে ধরে
  • System call-এর ছয়টি শ্রেণি, POSIX আর Windows-এর উদাহরণ পাশাপাশি
  • Library ফাংশন বনাম system call, buffering-এর কারণে আউটপুটের ক্রম বদলে যাওয়া, আর errno দিয়ে ত্রুটি বোঝা
  • System call-এর খরচ, vDSO, আর strace / Process Monitor দিয়ে প্রোগ্রামের ভেতরের কথোপকথন শোনা

১ · দুই রকম মোড — user mode আর kernel mode

একটি ব্যাংকের শাখার কথা ভাবুন। গ্রাহকেরা হলে বসতে পারেন, ফর্ম পূরণ করতে পারেন, টোকেন নিতে পারেন। কিন্তু ভল্টে ঢুকে নিজের হাতে টাকা তুলে নেওয়া চলবে না। টাকা তুলতে হলে কাউন্টারে গিয়ে চেক দিতে হয়। কাউন্টারের কর্মকর্তা স্বাক্ষর মেলান, ব্যালান্স দেখেন, তারপর ভল্ট থেকে টাকা এনে দেন। গ্রাহক কখনো ভল্টের চাবি হাতে পান না।

কম্পিউটারও ঠিক এভাবে চলে। আপনার ব্রাউজার, গেম, Word বা নিজের লেখা C প্রোগ্রাম — এরা সবাই “গ্রাহক”। হার্ডওয়্যার (ডিস্ক, নেটওয়ার্ক কার্ড, মেমরির নিয়ন্ত্রণ, টাইমার) হলো “ভল্ট”। আর ভল্টের চাবি থাকে শুধু অপারেটিং সিস্টেমের মূল অংশ kernel-এর হাতে। এই ভাগাভাগি সফটওয়্যারের ভদ্রতা দিয়ে চলে না, চলে হার্ডওয়্যারের জোরে। CPU নিজেই দুই রকম মোডে চলতে পারে:

👤 User mode

সাধারণ প্রোগ্রাম এখানে চলে। যোগ-বিয়োগ, লুপ, নিজের মেমরিতে পড়া-লেখা — সব করা যায়। কিন্তু হার্ডওয়্যার সরাসরি ছোঁয়া, অন্য প্রোগ্রামের মেমরিতে হাত দেওয়া বা interrupt বন্ধ করা নিষেধ।

x86-এ ring 3 · ARM64-এ EL0

🔑 Kernel mode

Kernel এখানে চলে। সব instruction চালানো যায়, সব মেমরি দেখা যায়, যেকোনো ডিভাইসকে আদেশ দেওয়া যায়। এখানে একটি ভুল মানে পুরো সিস্টেম ক্র্যাশ — Windows-এর নীল পর্দা বা Linux-এর kernel panic.

x86-এ ring 0 · ARM64-এ EL1

এই দুই মোডে ভাগ করে চালানোকে বলে dual-mode operation. পাঠ্যবইয়ের ভাষায় CPU-র ভেতরে একটি mode bit থাকে: 0 মানে kernel mode, 1 মানে user mode. বাস্তবের x86 CPU-তে এটি একটি বিট নয়, দুই বিটের একটি সংখ্যা — current privilege level (CPL), যার মান 0 থেকে 3. এই চারটি স্তরকে বলে protection ring. তবে Linux আর Windows কার্যত শুধু ring 0 (kernel) আর ring 3 (user) ব্যবহার করে।

Privileged instruction — শুধু kernel-এর জন্য

কিছু instruction এত ক্ষমতাশালী যে user প্রোগ্রামের হাতে দিলে সিস্টেম আর নিরাপদ থাকে না। এগুলোকে বলে privileged instruction. CPU এগুলো শুধু kernel mode-এ চালায়। কয়েকটি উদাহরণ (x86):

Instructionকাজuser-এর হাতে দিলে বিপদ
cli / stiInterrupt বন্ধ / চালুটাইমার থেমে যায়, একটি প্রোগ্রাম CPU আর ছাড়ে না
hltCPU থামিয়ে রাখাযেকোনো প্রোগ্রাম পুরো কম্পিউটার “ঘুম পাড়িয়ে” দিতে পারত
mov cr3, …Page table বদলানোঅন্য প্রসেসের মেমরি নিজের করে নেওয়া যেত
lidtInterrupt টেবিলের ঠিকানা বসানোসব interrupt নিজের কোডে টেনে নেওয়া যেত
in / outI/O port-এ সরাসরি পড়া-লেখাডিস্ক বা কিবোর্ড কন্ট্রোলারকে সরাসরি আদেশ
wrmsrCPU-র বিশেষ রেজিস্টার লেখাSystem call-এর প্রবেশপথই বদলে দেওয়া যেত

User mode-এ থাকা কোনো প্রোগ্রাম যদি এর একটি চালাতে চায়? CPU সেটি চালায় না। বরং একটি exception তোলে — x86-এ general protection fault (#GP) — আর নিয়ন্ত্রণ সঙ্গে সঙ্গে kernel-এর হাতে চলে যায়। Kernel দেখে, অপরাধী একটি সাধারণ প্রোগ্রাম। Linux তখন সেই প্রসেসকে SIGSEGV signal পাঠায়, সাধারণত প্রসেসটি বন্ধ হয়ে যায় (signal নিয়ে বিস্তারিত পর্ব ১৩-এ)। Windows-এ একই ঘটনায় প্রোগ্রাম STATUS_PRIVILEGED_INSTRUCTION exception পায়। কম্পিউটারের বাকি সবকিছু চলতে থাকে — ঠিক যেমন ভল্টে ঢুকতে চাওয়া গ্রাহককে দারোয়ান বের করে দিলেও ব্যাংকের কাজ থামে না।

নিচের ডায়াগ্রামে এক প্রোগ্রামের জীবনের কয়েকটি মুহূর্ত দেখুন। প্রতিটি ধাপে mode bit কীভাবে বদলায়, আর কে বদলায়, তা খেয়াল করুন। একটি বিষয় মনে রাখার মতো: user থেকে kernel-এ যাওয়া ঘটে শুধু তিনটি পথে — system call, interrupt আর exception. আর kernel থেকে user-এ ফেরে kernel নিজেই, একটি বিশেষ return instruction দিয়ে।

1boot: kernel sets up hardware2kernel starts a user program3user: add, mov, call ...4user: read(0, buf, 64) -> syscall5kernel: sys_read done, return6timer interrupt fires7kernel: timer handled, return8user: hlt   ; privileged!9CPU: #GP exception -> kernel10kernel: send SIGSEGV, end it
CPU-র অবস্থা
mode bit
x86 CPL
কে চলছে
কারণ
  1. চালু হওয়ার সময় সবকিছু kernel-এর। কম্পিউটার চালু হলে CPU kernel mode-এ থাকে (mode bit 0, CPL 0). Kernel মেমরি, interrupt টেবিল আর টাইমার সাজায়। এখনো কোনো user প্রোগ্রাম নেই।
  2. Kernel নিজেই মোড নামিয়ে দেয়। প্রথম প্রোগ্রাম চালানোর সময় kernel একটি বিশেষ return instruction (sysret বা iret) দিয়ে লাফ দেয়, সঙ্গে mode bit হয় 1, CPL 3. মোড নামানো সহজ — ওঠানো কঠিন।
  3. সাধারণ কাজ user mode-এই। যোগ, তুলনা, লুপ, ফাংশন কল — এগুলোর জন্য kernel লাগে না। প্রোগ্রামের সময়ের বড় অংশ এভাবেই কাটে। Mode bit 1-ই থাকে।
  4. পথ ১ — system call. প্রোগ্রাম কিবোর্ড থেকে পড়তে চায়, তাই syscall instruction চালায়। CPU নিজে mode bit 0 করে, আর kernel-এর আগে থেকে ঠিক করা প্রবেশপথে লাফ দেয়। প্রোগ্রাম নিজে কোথায় লাফ দেবে বেছে নিতে পারে না — এটিই নিরাপত্তার মূল।
  5. কাজ শেষে ফেরা। Kernel ডেটা প্রোগ্রামের buffer-এ রেখে sysret দিয়ে ফেরে। Mode bit আবার 1. প্রোগ্রামের মনে হয় একটি সাধারণ ফাংশন ফিরে এল।
  6. পথ ২ — interrupt. প্রোগ্রাম কিছু না চাইলেও হার্ডওয়্যার টাইমার নির্দিষ্ট সময় পরপর interrupt দেয় (পর্ব ৪)। CPU মোড বদলে kernel-এর handler চালায়। এভাবেই kernel নিয়মিত নিয়ন্ত্রণ ফিরে পায়, আর কোনো প্রোগ্রাম CPU আটকে রাখতে পারে না।
  7. আবার user mode. Handler শেষে kernel চাইলে অন্য প্রোগ্রামকে CPU দিতে পারত (context switch, পর্ব ৯)। এখানে একই প্রোগ্রামে ফিরল, mode bit 1.
  8. নিষিদ্ধ কাজের চেষ্টা। প্রোগ্রামটি hlt চালাতে চাইল। User mode-এ এটি privileged instruction — CPU এটি চালাবে না। Mode bit এখনো 1, কিন্তু পরের মুহূর্তেই বদলাবে।
  9. পথ ৩ — exception. CPU general protection fault তোলে, mode bit 0, kernel-এর exception handler চলে। Kernel জানে, দোষ এই প্রোগ্রামের — হার্ডওয়্যারের নয়।
  10. শাস্তি শুধু অপরাধীর। Linux প্রসেসটিকে SIGSEGV পাঠায়; প্রসেস শেষ, আর shell লেখে “Segmentation fault”. বাকি সব প্রোগ্রাম আর kernel আগের মতোই চলছে। Dual mode না থাকলে এই এক লাইনেই পুরো কম্পিউটার থেমে যেত।

সুরক্ষা-স্তরের ধারণা পুরোনো। ১৯৬০-এর দশকে MIT, Bell Labs আর General Electric-এর যৌথ OS Multics-এ “ring” ধারণা গড়ে ওঠে; পরে Honeywell 6180 মেশিনে হার্ডওয়্যারেই আটটি ring দেওয়া হয়। Intel-এর জনপ্রিয় CPU-তে চার ring আসে ১৯৮২ সালের 80286-এর protected mode-এ। এর আগের 8086-এ চলা MS-DOS-এ কোনো সুরক্ষা ছিল না — যেকোনো প্রোগ্রাম যেকোনো মেমরিতে লিখতে পারত, আর একটি বাগওয়ালা প্রোগ্রাম পুরো কম্পিউটার আটকে দিতে পারত।

২ · System call কী, আর কেন লাগে

User mode এত সীমিত হলে প্রোগ্রাম কাজ করে কীভাবে? একটি “Hello” লিখতেও তো স্ক্রিন বা টার্মিনাল লাগে, যা হার্ডওয়্যার। ফাইল পড়তে ডিস্ক লাগে। উত্তর: প্রোগ্রাম kernel-কে অনুরোধ করে। সেই নির্দিষ্ট, নিয়ন্ত্রিত অনুরোধের নামই system call — kernel-এর দেওয়া একটি সেবা, যা প্রোগ্রাম একটি বিশেষ instruction দিয়ে ডাকে।

রেস্তোরাঁর রান্নাঘরে একটিই চুলা, আর সেখানে শুধু বাবুর্চি ঢোকেন। ওয়েটার (আপনার প্রোগ্রাম) সরাসরি চুলায় হাঁড়ি বসান না; তিনি একটি অর্ডার স্লিপ লেখেন — “টেবিল ৩, এক প্লেট খিচুড়ি” — আর কাউন্টারের জানালা দিয়ে দেন। বাবুর্চি (kernel) রান্না করে জানালা দিয়েই প্লেট ফেরত দেন, অথবা বলেন “খিচুড়ি শেষ”। স্লিপের নির্দিষ্ট ফর্ম্যাট হলো system call-এর নিয়ম, জানালাটি হলো trap instruction, আর “খিচুড়ি শেষ” হলো error code.

এই ব্যবস্থার তিনটি বড় লাভ:

  • সুরক্ষা: Kernel প্রতিটি অনুরোধ যাচাই করে — এই ফাইল পড়ার অনুমতি আছে কি? এই মেমরির ঠিকানা কি সত্যিই এই প্রসেসের? ভুল হলে অনুরোধ ফিরিয়ে দেয়।
  • সরলতা (abstraction): প্রোগ্রামকে জানতে হয় না ডিস্কটি SSD না হার্ডডিস্ক, বা টার্মিনালটি আসল স্ক্রিন না একটি pipe. সবার জন্য একই write().
  • ভাগাভাগি: একটি ডিস্ক বা নেটওয়ার্ক কার্ড শত শত প্রোগ্রাম একসাথে ব্যবহার করে। Kernel মাঝখানে থেকে ক্রম ঠিক রাখে, যাতে একজনের লেখা আরেকজনের লেখায় মিশে না যায়।

প্রতিটি OS-এর system call-এর একটি নির্দিষ্ট তালিকা আছে। Linux-এ x86-64-এর জন্য এমন call কয়েকশো (৩৫০-এর বেশি), প্রতিটির একটি নম্বর — syscall number. যেমন x86-64 Linux-এ read = 0, write = 1, getpid = 39. তবে প্রোগ্রামাররা নম্বর মুখস্থ করেন না; তাঁরা ডাকেন C library-র ফাংশন, যেমন write() বা open(). Linux-এ এই library সাধারণত glibc (GNU C Library); Android-এ Bionic, ছোট container-এ প্রায়ই musl.

মূল ধারণা

তিনটি স্তর আলাদা করে চিনে রাখুন। API হলো প্রোগ্রামার যা ডাকেন — যেমন POSIX-এর write() বা Windows-এর WriteFile(). System call হলো kernel-এর আসল প্রবেশপথ, নম্বর দিয়ে চেনা। মাঝখানে থাকে wrapper function — library-র ছোট একটি ফাংশন, যা API-কে system call-এ অনুবাদ করে। আর রেজিস্টারে কী কোথায় বসবে, সেই নিয়মকে বলে ABI.

এখানে Linux আর Windows-এর একটি বড় পার্থক্য আছে। Linux-এ write()-এর wrapper প্রায় সরাসরি একটি system call করে। Windows-এ WriteFile() হলো Win32 API-র ফাংশন; এটি ভেতরে ntdll.dll-এর NtWriteFile ডাকে, আর আসল system call করে সেই ntdll. Microsoft Win32 API স্থির রাখে, কিন্তু system call-এর নম্বর Windows-এর সংস্করণভেদে বদলায় — তাই Windows-এ সরাসরি syscall নম্বর ব্যবহার করা অসমর্থিত। Linux উল্টো: kernel ডেভেলপাররা প্রতিশ্রুতি দেন যে একবার যোগ হওয়া syscall-এর নম্বর ও আচরণ বদলানো হবে না (“don't break userspace”). macOS-ও নিজের syscall ABI স্থির রাখার প্রতিশ্রুতি দেয় না; সেখানে প্রোগ্রাম libSystem দিয়ে যায়।

৩ · একটি system call-এর যাত্রাপথ

এবার সবচেয়ে গুরুত্বপূর্ণ অংশ। আপনার C প্রোগ্রামে একটি লাইন আছে: n = write(1, "hi\n", 3); — অর্থাৎ file descriptor 1 (সাধারণত stdout, মানে টার্মিনাল)-এ ৩ বাইট লেখার অনুরোধ। এই এক লাইনের পেছনে CPU-তে কী কী ঘটে, x86-64 Linux ধরে ধাপে ধাপে দেখি। ডান দিকে CPU-র register-এর মান খেয়াল করুন।

1// user program   (user mode)2n = write(1, "hi\n", 3);3// glibc: write() wrapper4mov  eax, 1        ; SYS_write5mov  edi, 1        ; arg1 fd6lea  rsi, [buf]    ; arg2 "hi\n"7mov  edx, 3        ; arg3 count8syscall            ; trap!9// kernel          (kernel mode)10entry_SYSCALL_64: save regs11sys_call_table[rax] -> write12fd 1 -> file -> tty driver13rax = 3; sysret14// glibc again    (user mode)15if -4095 <= rax <= -1: errno16return rax;   // n = 3
Register আর অবস্থা
mode
rax
rdi
rsi
rdx
n
  1. আপনার কোড একটি সাধারণ ফাংশন ডাকে। write দেখতে আর দশটা C ফাংশনের মতোই। CPU এখনো user mode-এ; কোনো register-এ syscall-এর কিছু বসেনি। আসল কাজ শুরু হবে glibc-র ভেতরে।
  2. Wrapper syscall নম্বর বসায়। glibc-র write wrapper rax-এ রাখে 1 — x86-64 Linux-এ SYS_write-এর নম্বর। এই নম্বর দেখেই kernel বুঝবে কোন সেবা চাওয়া হচ্ছে।
  3. Argument যায় নির্দিষ্ট register-এ। x86-64 Linux-এর নিয়ম: প্রথম argument rdi, দ্বিতীয় rsi, তৃতীয় rdx (তারপর r10, r8, r9). তাই rdi = 1 (fd), rsi = "hi\n"-এর ঠিকানা, rdx = 3. লেখাটি নিজে কপি হয়নি — শুধু তার ঠিকানা গেছে।
  4. syscall — জানালায় স্লিপ জমা। এই একটি instruction-এ CPU তিনটি কাজ করে: ফেরার ঠিকানা রাখে rcx-এ, flags রাখে r11-এ, আর মোড kernel-এ তুলে kernel-এর আগে থেকে ঠিক করা ঠিকানায় (একটি বিশেষ রেজিস্টার LSTAR-এ লেখা) লাফ দেয়। প্রোগ্রাম কোথায় ঢুকবে বেছে নিতে পারে না।
  5. Kernel-এর প্রবেশদ্বার। Linux-এর entry_SYSCALL_64 কোড user-এর stack ছেড়ে kernel-এর নিজস্ব stack নেয়, আর user-এর সব register সেখানে সংরক্ষণ করে — যাতে ফেরার সময় প্রোগ্রাম সবকিছু আগের মতো পায়।
  6. Syscall table থেকে handler খোঁজা। Kernel rax-এর মান (1) দিয়ে syscall table-এ খোঁজে — ঠিক যেন ব্যাংকের টোকেন নম্বর দেখে সঠিক কাউন্টারে পাঠানো। ১ নম্বরে আছে write-এর handler. নম্বর অজানা হলে kernel সঙ্গে সঙ্গে -ENOSYS ফেরত দিত।
  7. আসল কাজ। Handler প্রসেসের file descriptor টেবিলে 1 নম্বর খোঁজে, পায় টার্মিনালের সাথে যুক্ত একটি file. ঠিকানাটি সত্যিই এই প্রসেসের কি না যাচাই করে user মেমরি থেকে ৩ বাইট নিরাপদে কপি করে (copy_from_user), আর tty driver সেটি টার্মিনালে পাঠায়। স্ক্রিনে এল “hi”.
  8. ফল নিয়ে ফেরা। Kernel লেখা বাইটের সংখ্যা 3 রাখে rax-এ, সংরক্ষিত register ফিরিয়ে দেয়, আর sysret চালায়। CPU আবার user mode-এ, ঠিক syscall-এর পরের instruction থেকে চলা শুরু।
  9. glibc ফল যাচাই করে। Linux-এর নিয়ম: ব্যর্থ হলে kernel একটি ঋণাত্মক error code (−4095 থেকে −1) দেয়। 3 সেই সীমায় পড়ে না, তাই এটি সফল ফল। errno ছোঁয়া হলো না।
  10. আপনার কোডে ফেরত। write ফিরল 3, অর্থাৎ n = 3 — তিনটি বাইটই লেখা হয়েছে। আপনার চোখে এটি ছিল একটি ফাংশন কল; ভেতরে ঘটেছে দুবার মোড বদল, register সংরক্ষণ, টেবিল খোঁজা আর অনুমতি যাচাই।

Trap instruction — CPU-ভেদে আলাদা নাম

“জানালা” প্রতিটি CPU-তে আছে, শুধু নাম আর নিয়ম আলাদা। পরীক্ষায় এবং বাস্তব debugging-এ এই টেবিলটি কাজে লাগে:

প্ল্যাটফর্মTrap instructionSyscall নম্বরArgumentফল
Linux x86-64syscallraxrdi, rsi, rdx, r10, r8, r9rax
Linux ARM64svc #0x8x0–x5x0
Linux 32-bit x86 (পুরোনো)int 0x80eaxebx, ecx, edx, esi, edi, ebpeax

খেয়াল করুন: একই কাজের syscall নম্বর CPU-ভেদে আলাদা। write x86-64-এ 1, কিন্তু ARM64-এ 64. পুরোনো int 0x80 আসলে একটি software interrupt — interrupt টেবিলের ১২৮ নম্বর ঘর দিয়ে kernel-এ ঢোকা। এটি ধীর ছিল বলে পরে Intel sysenter আর AMD syscall নামে দ্রুত, বিশেষ instruction আনে। এজন্যই অনেক বইয়ে system call-কে “software interrupt” বলা হয় — ইতিহাসের দিক থেকে কথাটি ঠিক, তবে আধুনিক x86-64-এ এটি আলাদা instruction.

নিজের হাতে syscall নম্বর দিয়ে ডাকা

glibc একটি সাধারণ ফাংশন syscall() দেয়, যেখানে নম্বরটি আপনি নিজে বলে দেন। নিচের প্রোগ্রাম একই কাজ দুইভাবে করে — wrapper দিয়ে আর সরাসরি নম্বর দিয়ে — এবং ফল মেলায়:

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>

int main(void) {
    pid_t via_wrapper = getpid();                 /* glibc wrapper */
    long  via_raw     = syscall(SYS_getpid);      /* number by hand */
    printf("SYS_getpid = %d, SYS_write = %d on this machine\n",
           SYS_getpid, SYS_write);
    printf("wrapper and raw syscall agree: %s\n",
           via_raw == via_wrapper ? "yes" : "no");
    fflush(stdout);

    const char *msg = "hello from syscall(SYS_write, ...)\n";
    long n1 = syscall(SYS_write, 1, msg, strlen(msg));
    long n2 = write(1, "hello from write()\n", 19);
    printf("returned bytes: raw = %ld, wrapper = %ld\n", n1, n2);
    return 0;
}

একটি আসল রানের আউটপুট:

SYS_getpid = 39, SYS_write = 1 on this machine
wrapper and raw syscall agree: yes
hello from syscall(SYS_write, ...)
hello from write()
returned bytes: raw = 35, wrapper = 19

প্রথম লাইন দেখায়, এই x86-64 Linux মেশিনে getpid-এর নম্বর 39 আর write-এর 1 — ARM64 মেশিনে চালালে অন্য সংখ্যা আসত। PID নিজে প্রতিবার আলাদা হতে পারে, তাই সেটি না ছেপে শুধু “দুটো মিলল কি না” ছাপা হয়েছে: yes. শেষ লাইনে দুই পথেই ফেরত এসেছে লেখা বাইটের সংখ্যা — বার্তার দৈর্ঘ্য ৩৫ আর ১৯. মাঝখানের fflush(stdout) কেন দরকার ছিল, তা অংশ ৬-এ পরিষ্কার হবে। বাস্তব কোডে syscall() লাগে কেবল তখন, যখন নতুন কোনো syscall-এর wrapper এখনো glibc-তে আসেনি।

৪ · Parameter পাঠানো, ফল ফেরত আর errno

Kernel-কে তথ্য দেওয়ার তিন উপায়

System call-এর সাথে তথ্য পাঠাতে হয় — কোন ফাইল, কত বাইট, কোন buffer. পাঠ্যবইয়ে তিনটি উপায় বলা হয়:

১. Register-এ

সবচেয়ে দ্রুত। Linux x86-64 সর্বোচ্চ ছয়টি argument register-এ নেয়। প্রায় সব syscall এতেই চলে যায়।

Linux, Windows, macOS

২. মেমরিতে একটি block

অনেক তথ্য হলে একটি struct-এ সাজিয়ে শুধু তার ঠিকানা register-এ পাঠানো হয়। যেমন Linux-এর clone3 একটি struct clone_args-এর ঠিকানা নেয়।

বেশি argument হলে

৩. Stack-এ

প্রোগ্রাম argument stack-এ push করে, kernel সেখান থেকে পড়ে। কিছু পুরোনো সিস্টেম, যেমন পুরোনো BSD-র 32-bit x86, এভাবে চলত।

মূলত ঐতিহাসিক

দ্বিতীয় আর তৃতীয় উপায়ে kernel-কে user-এর মেমরি থেকে পড়তে হয়। Kernel কখনো ধরে নেয় না যে ঠিকানাটি সৎ। প্রতিটি পয়েন্টার যাচাই করে, তারপর বিশেষ ফাংশন দিয়ে কপি করে। কারণ একটি দুষ্ট প্রোগ্রাম kernel-এর নিজের মেমরির ঠিকানা দিয়ে বলতে পারে, “এখানে লিখুন”।

ফল আর ত্রুটি — errno

System call ব্যর্থ হতেই পারে: ফাইল নেই, অনুমতি নেই, ডিস্ক ভর্তি। Linux kernel তখন rax-এ একটি ঋণাত্মক সংখ্যা ফেরত দেয় — যেমন −2 মানে ENOENT (“এমন ফাইল নেই”)। কিন্তু C-র POSIX নিয়ম ভিন্ন: ফাংশন ফেরত দেবে −1, আর কারণটি থাকবে একটি আলাদা ভ্যারিয়েবল errno-তে। এই অনুবাদের কাজ করে glibc wrapper. নিচের ডায়াগ্রামে একটি ব্যর্থ open()-এর পথ দেখুন:

1fd = open("/no/such/file.txt", O_RDONLY);2// glibc: open() -> openat(AT_FDCWD, ...)3mov eax, 257 ; SYS_openat ; syscall4// kernel: look up "/" -> "no" ... missing5rax = -2  (-ENOENT); sysret6// glibc: is -4095 <= rax <= -1 ?7errno = -rax;  return -1;8if (fd == -1) perror("open");
মানগুলো
mode
rax
errno
fd
  1. প্রোগ্রাম একটি ফাইল খুলতে চায়। ফাইলটি আসলে নেই, কিন্তু প্রোগ্রাম তা জানে না। errno এখন 0, fd-এর মান এখনো ঠিক হয়নি।
  2. glibc আসলে openat ডাকে। আধুনিক glibc x86-64-এ open()-কে openat(AT_FDCWD, …) দিয়ে করে, যার নম্বর 257. syscall চলল, মোড kernel. (strace-এ তাই open নয়, openat দেখবেন।)
  3. Kernel পথ ধরে খোঁজে। “/” থেকে শুরু করে “no” নামের directory খোঁজে — পায় না। কাজ এখানেই শেষ। Kernel কোনো লেখা ছাপে না; শুধু একটি সংখ্যা দিয়ে কারণ জানায়।
  4. Kernel ফেরত দেয় −2. rax = −2, অর্থাৎ −ENOENT. মোড আবার user. লক্ষ করুন, errno এখনো বদলায়নি — errno kernel-এর জিনিস নয়, user-space library-র ভ্যারিয়েবল।
  5. glibc সীমা যাচাই করে। −2 পড়েছে −4095 থেকে −1-এর মধ্যে, তাই এটি ত্রুটি। (সফল ফল, যেমন fd 3, কখনো এই সীমায় পড়ে না।)
  6. POSIX রূপে অনুবাদ। glibc errno-তে রাখে 2 (চিহ্ন উল্টে), আর ফেরত দেয় −1. এখন fd = −1. multi-thread প্রোগ্রামে প্রতিটি থ্রেডের errno আলাদা, তাই একজনের ত্রুটি আরেকজনের errno নষ্ট করে না।
  7. মানুষের পড়ার মতো বার্তা। perror("open") errno = 2 দেখে stderr-এ লেখে “open: No such file or directory”. কাজের নিয়ম: −1 পেলেই সঙ্গে সঙ্গে errno পড়ুন বা কপি করুন, কারণ পরের যেকোনো library কল সেটি বদলে দিতে পারে।

এবার আসল মেশিনে কয়েক রকম ত্রুটি দেখি। perror লেখে stderr-এ; এখানে প্রথম লাইনেই dup2 দিয়ে stderr-কে stdout-এ জুড়ে দেওয়া হয়েছে (shell-এর 2>&1-এর মতো), যাতে দুটোই এক জায়গায় দেখা যায়:

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

static void try_open(const char *path) {
    errno = 0;
    int fd = open(path, O_RDONLY);
    int saved = errno;                  /* copy errno at once */
    printf("open(\"%s\") = %d, errno = %d (%s)\n",
           path, fd, saved, strerror(saved));
    fflush(stdout);
    if (fd == -1) perror("  perror says");
    else close(fd);
}

int main(void) {
    dup2(STDOUT_FILENO, STDERR_FILENO); /* like 2>&1: show stderr here */
    try_open("/no/such/file.txt");
    try_open("/etc/shadow");
    try_open("/etc/hostname");
    errno = 0;
    long r = write(99, "x", 1);         /* fd 99 was never opened */
    printf("write(99, ...) = %ld, errno = %d (%s)\n", r, errno, strerror(errno));
    return 0;
}

একটি আসল রানের আউটপুট:

open("/no/such/file.txt") = -1, errno = 2 (No such file or directory)
  perror says: No such file or directory
open("/etc/shadow") = -1, errno = 13 (Permission denied)
  perror says: Permission denied
open("/etc/hostname") = 3, errno = 0 (Success)
write(99, ...) = -1, errno = 9 (Bad file descriptor)

তিন রকম ব্যর্থতা, তিনটি আলাদা errno: ২ (ENOENT, ফাইল নেই), ১৩ (EACCES, ফাইল আছে কিন্তু সাধারণ ব্যবহারকারীর পড়ার অনুমতি নেই — /etc/shadow-এ পাসওয়ার্ডের hash থাকে), আর ৯ (EBADF, ৯৯ নম্বর fd কখনো খোলাই হয়নি)। সফল open ফেরত দিয়েছে 3 — কারণ 0, 1, 2 আগে থেকেই stdin, stdout, stderr-এর দখলে, আর kernel সবচেয়ে ছোট খালি নম্বরটি দেয়। এখানে “errno = 0 (Success)” এসেছে শুধু কারণ আমরা আগে নিজেরা 0 বসিয়েছিলাম। নিয়মটি মনে রাখুন: সফল কলের পর errno-র মানের কোনো অর্থ নেই; শুধু −1 পেলে errno দেখবেন। এই container-এ প্রোগ্রাম root হিসেবে চলেনি বলেই /etc/shadow আটকেছে; root হলে ফল ভিন্ন হতো।

৫ · System call-এর ছয়টি শ্রেণি — POSIX আর Windows পাশাপাশি

কয়েকশো system call মনে রাখা অসম্ভব, দরকারও নেই। পাঠ্যবই (যেমন Silberschatz-দের Operating System Concepts) এগুলোকে ছয়টি শ্রেণিতে ভাগ করে। অফিসের ফাইল ক্যাবিনেটের ড্রয়ারের মতো ভাবুন — কোন অনুরোধ কোন ড্রয়ারে যায়, জানলেই নতুন call দেখে তার কাজ আন্দাজ করা যায়।

শ্রেণিকাজWindows (Win32 API)Unix / Linux (POSIX)
Process controlপ্রসেস তৈরি, শেষ, অপেক্ষা, অন্য প্রোগ্রাম চালানোCreateProcess()
ExitProcess()
WaitForSingleObject()
fork(), exec()
exit()
wait(), waitpid()
File managementফাইল খোলা, পড়া, লেখা, বন্ধ, মুছে ফেলাCreateFile()
ReadFile(), WriteFile()
CloseHandle()
open()
read(), write()
close(), unlink()
Device managementডিভাইসের সাথে কাজ, বিশেষ নিয়ন্ত্রণSetConsoleMode()
ReadConsole(), WriteConsole()
DeviceIoControl()
ioctl()
read(), write()
Information maintenanceসময়, প্রসেসের পরিচয়, সিস্টেমের তথ্যGetCurrentProcessId()
SetTimer()
Sleep()
getpid()
alarm()
sleep()
Communicationপ্রসেসে প্রসেসে কথা — pipe, shared memory, socketCreatePipe()
CreateFileMapping()
MapViewOfFile()
pipe()
shm_open()
mmap()
Protectionঅনুমতি দেখা ও বদলানোSetFileSecurity()
InitializeSecurityDescriptor()
SetSecurityDescriptorGroup()
chmod()
umask()
chown()

দুটি সূক্ষ্ম কথা, পরীক্ষাতেও কাজে লাগে। প্রথমত, Windows কলামের নামগুলো Win32 API — এরা নিজেরা system call নয়; ভেতরে ntdll-এর NtCreateFile, NtReadFile ইত্যাদি আসল syscall করে। দ্বিতীয়ত, Unix কলামেরও সবকিছু সরাসরি syscall নয়: sleep() glibc-তে nanosleep বা clock_nanosleep দিয়ে তৈরি, shm_open() আসলে /dev/shm-এ একটি ফাইল খোলে, আর exec() একটি পরিবার, যার kernel-এর অংশ execve. আরও লক্ষ করুন, fork আর CreateProcess সমান নয় — fork নিজের কপি বানায়, CreateProcess সরাসরি নতুন প্রোগ্রাম চালু করে (বিস্তারিত পর্ব ১০-এ)।

Unix-এর একটি সুন্দর নকশা-চিন্তা এখানে চোখে পড়ে: “সবকিছুই ফাইল”। কিবোর্ড, টার্মিনাল, pipe, এমনকি অনেক ডিভাইস — সবার সাথেই read আর write চলে। তাই device management-এর আলাদা call খুব কম; যা ফাইলের মতো করা যায় না, তার জন্য একটি সাধারণ “বাকি সব” call আছে: ioctl. আর এই একরূপ interface-এর মানদণ্ড হলো POSIX — IEEE-র একটি মানদণ্ড, যার প্রথম সংস্করণ ১৯৮৮ সালে। Linux, macOS আর BSD বেশিরভাগ ক্ষেত্রে POSIX মেনে চলে, তাই একই C কোড এদের সবখানে চলে; Windows-এ আলাদা API.

৬ · Library ফাংশন বনাম system call — printf আর write

নতুনদের একটি সাধারণ ভুল ধারণা: “printf একটি system call”. না। printf হলো C standard library-র ফাংশন, পুরোটা চলে user mode-এ। এটি সংখ্যাকে লেখায় রূপ দেয় (%d → "42"), ফলাফল একটি buffer-এ জমায়, আর সুবিধামতো সময়ে একবার write system call ডেকে পুরো buffer kernel-কে দেয়।

পাড়ার ডাকবাক্সের কথা ভাবুন। আপনি প্রতিটি চিঠি হাতে নিয়ে প্রধান ডাকঘরে দৌড়ান না — বাক্সে ফেলেন, আর পিয়ন দিনে একবার সব চিঠি একসাথে নিয়ে যান। প্রতিটি দৌড় হলো একটি system call; বাক্স হলো stdio buffer. এতে দৌড় অনেক কমে। কিন্তু একটি ঝামেলা: আপনি যদি কিছু চিঠি বাক্সে ফেলেন আর কিছু নিজে হাতে ডাকঘরে দিয়ে আসেন, তাহলে হাতে দেওয়াগুলো আগে পৌঁছাবে।

stdio library তিন রকম buffering ব্যবহার করে, আর কোনটি হবে তা নির্ভর করে stdout কোথায় যাচ্ছে তার ওপর:

  • Line buffered: stdout যখন টার্মিনাল। প্রতিটি \n-এ buffer খালি হয়। তাই টার্মিনালে সবকিছু “স্বাভাবিক” দেখায়।
  • Fully buffered: stdout যখন ফাইল বা pipe. Buffer ভরলে (সাধারণত কয়েক KB), fflush ডাকলে, বা প্রোগ্রাম স্বাভাবিকভাবে শেষ হলে লেখা যায়।
  • Unbuffered: stderr. ত্রুটির বার্তা সঙ্গে সঙ্গে যায়, যাতে crash-এর আগেও দেখা যায়।

এই পাতার কোড-রানারে stdout একটি pipe (পরের অংশের bash আউটপুটে নিজের চোখেই দেখবেন), তাই printf fully buffered. নিচের প্রোগ্রাম printf আর সরাসরি write মিশিয়ে লেখে। কোড পড়ে আগে অনুমান করুন আউটপুট কোন ক্রমে আসবে, তারপর চালান:

#include <stdio.h>
#include <string.h>
#include <unistd.h>

static void raw(const char *s) {          /* straight to the kernel */
    write(STDOUT_FILENO, s, strlen(s));
}

int main(void) {
    printf("stdout is a terminal? %s\n", isatty(STDOUT_FILENO) ? "yes" : "no");
    printf("1. printf  (goes into the stdio buffer)\n");
    raw("2. write   (one system call, printed now)\n");
    printf("3. printf  (still waiting in the buffer)\n");
    raw("4. write   (printed now)\n");
    fflush(stdout);                       /* buffer -> one write() call */
    raw("5. write   (after fflush)\n");
    printf("6. printf  (flushed by exit)\n");
    return 0;
}

একটি আসল রানের আউটপুট (stdout একটি pipe):

2. write   (one system call, printed now)
4. write   (printed now)
stdout is a terminal? no
1. printf  (goes into the stdio buffer)
3. printf  (still waiting in the buffer)
5. write   (after fflush)
6. printf  (flushed by exit)

কোডে প্রথম লাইন printf, কিন্তু আউটপুটে প্রথমে এসেছে ২ আর ৪ — কারণ write সরাসরি kernel-এ গেছে, আর তিনটি printf-এর লেখা তখনো buffer-এ বসে ছিল (প্রথম লাইনটিই বলছে stdout টার্মিনাল নয়, তাই fully buffered). fflush(stdout) সেই তিন লাইন একটিমাত্র write call-এ পাঠিয়েছে, তাই এরপর ৫ ঠিক জায়গায় এসেছে। শেষ printf বের হয়েছে return 0-এর পর, যখন exit buffer খালি করে। একই প্রোগ্রাম নিজের টার্মিনালে চালালে stdout line buffered হতো, তাই আউটপুট ১ থেকে ৬ ক্রমে আসত, আর প্রথম লাইনে থাকত yes. শিক্ষা: একই stream-এ stdio আর সরাসরি write মেশাবেন না; মেশাতেই হলে মাঝে fflush. আর _exit() বা crash-এ buffer খালিই হয় না — লগ হারিয়ে যায়। fork-এর আগে fflush না করলে buffer দুই প্রসেসেই কপি হয়ে লেখা দুবার আসে; সেটি পর্ব ১০-এ দেখবেন।

Buffering শুধু user-space-এ নয়। write ফিরে আসা মানে লেখা kernel-এর হাতে পৌঁছেছে, ডিস্কে নয় — kernel-ও লেখা কিছুক্ষণ নিজের মেমরিতে (page cache) রাখে। ডিস্কে নিশ্চিতভাবে পৌঁছাতে আলাদা call fsync লাগে। ডেটাবেসগুলো এ কারণেই fsync নিয়ে এত সাবধান (দেখুন DBMS কোর্সের recovery পর্ব)।

৭ · System call-এর খরচ, vDSO আর /proc

প্রতিটি system call-এ মোড বদল, register সংরক্ষণ আর ফেরত, অনুমতি যাচাই — সব মিলিয়ে খরচ আছে। আধুনিক x86 CPU-তে প্রায় কিছু না করা একটি syscall-এর খরচ সাধারণত মোটামুটি একশো থেকে কয়েকশো ন্যানোসেকেন্ড; সাধারণ একটি function call তার চেয়ে কয়েক ডজন গুণ সস্তা। সংখ্যাটি CPU, kernel-এর সংস্করণ আর নিরাপত্তা-ব্যবস্থার ওপর অনেক নির্ভর করে — ২০১৮ সালে Meltdown নিরাপত্তা-ত্রুটির পর চালু হওয়া KPTI-এর মতো সুরক্ষায় অনেক মেশিনে খরচ উল্লেখযোগ্যভাবে বেড়েছে। তাই নির্দিষ্ট সংখ্যা মুখস্থ না করে নিজের মেশিনে মেপে দেখাই ঠিক।

একটি syscall-এর খরচ ছোট, কিন্তু লাখ লাখ হলে বড়। ধরুন একটি লগ-প্রোগ্রাম ১০ লাখ লাইন লিখবে, প্রতিটি ২০ বাইট, আর ধরে নিই প্রতিটি syscall-এ ২০০ ন্যানোসেকেন্ড লাগে:

পদ্ধতিSyscall-এর সংখ্যাশুধু syscall-এর সময়
প্রতি লাইনে একটি write১০,০০,০০০১০৬ × ২০০ ns = ০.২ সেকেন্ড
৪০৯৬ বাইটের buffer (stdio-র মতো)⌈২,০০,০০,০০০ ÷ ৪০৯৬⌉ = ৪,৮৮৩৪,৮৮৩ × ২০০ ns ≈ ০.৯৮ মিলিসেকেন্ড

প্রায় ২০০ গুণ কম syscall! এজন্যই stdio buffer করে, আর এজন্যই বড় সার্ভার সফটওয়্যার syscall কমানোর নানা কৌশল খোঁজে।

vDSO — kernel-এ না গিয়েই kernel-এর উত্তর

কিছু প্রশ্ন প্রোগ্রাম সেকেন্ডে হাজারবার করে — “এখন কয়টা বাজে?” (clock_gettime, gettimeofday). ওয়েব সার্ভার প্রতিটি অনুরোধের লগে সময় লেখে, গেম প্রতিটি ফ্রেমে সময় দেখে। প্রতিবার kernel-এ ঢোকা অপচয়। Linux-এর সমাধান: kernel প্রতিটি প্রসেসের মেমরিতে একটি ছোট, kernel-এর তৈরি library বসিয়ে দেয় — vDSO (virtual dynamic shared object). সঙ্গে থাকে একটি পড়ার-অনুমতি-মাত্র ডেটা পাতা ([vvar]), যেখানে kernel নিয়মিত সময়ের তথ্য হালনাগাদ রাখে। glibc-র clock_gettime তখন user mode-এই সেই পাতা পড়ে উত্তর দেয় — কোনো মোড বদল নেই।

অফিসের রিসেপশনে প্রতিবার “ক'টা বাজে?” জিজ্ঞেস করতে ম্যানেজারের রুমে ঢুকতে হলে কাজ থেমে যেত। তাই দেয়ালে একটি ঘড়ি টাঙানো থাকে, যা ম্যানেজারের দপ্তর চালু রাখে, আর সবাই তাকিয়েই সময় জেনে নেন। vDSO সেই দেয়ালঘড়ি।

vDSO সব কাজে চলে না — শুধু এমন কাজে, যেখানে উত্তর নিরাপদে পড়ার-অনুমতি-মাত্র মেমরি থেকে দেওয়া যায় (x86-64-এ মূলত clock_gettime, gettimeofday, time, getcpu). ফাইল লেখা বা প্রসেস তৈরির মতো কাজে kernel-এ যেতেই হয়। আর সময় যাচাইয়ের উৎস যদি এমন হার্ডওয়্যার ঘড়ি হয় যা user mode থেকে পড়া যায় না (কিছু virtual machine-এ এমন হয়), তাহলে vDSO চুপচাপ আসল syscall-এ ফিরে যায়।

/proc — kernel-এর জানালা দিয়ে নিজেকে দেখা

Linux-এ /proc একটি বিশেষ ফাইল সিস্টেম: এখানের “ফাইল” ডিস্কে থাকে না, পড়ার মুহূর্তে kernel তৈরি করে দেয় (প্রসেস আর /proc নিয়ে বিস্তারিত পর্ব ১১-এ)। আর /proc/self সবসময় বোঝায় “যে প্রসেস এখন পড়ছে, সে নিজে”। এই পাতার bash রানারে strace নেই, তাই syscall গোনা যাবে না; তবে kernel প্রসেস সম্পর্কে যা জানে, তার অনেকটাই দেখা যায়:

echo "== files inside /proc/self (here 'self' is the ls process) =="
ls /proc/self/ | head -n 12 | tr '\n' ' '; echo
echo
echo "== open file descriptors of this shell =="
ls -l /proc/$$/fd | awk 'NR>1 {print $9, $10, $11}'
echo
echo "== selected fields of /proc/self/status (self = grep) =="
grep -E '^(Name|State|Pid|PPid|Uid|Threads|voluntary_ctxt_switches|nonvoluntary_ctxt_switches)' /proc/self/status
echo
echo "== the vDSO is mapped into every process =="
grep -E 'vdso|vvar' /proc/self/maps | awk '{print $2, $6}'

একটি আসল রানের আউটপুট (pipe নম্বর আর PID প্রতিবার বদলায়):

== files inside /proc/self (here 'self' is the ls process) ==
arch_status attr autogroup auxv cgroup clear_refs cmdline comm coredump_filter cpu_resctrl_groups cpuset cwd

== open file descriptors of this shell ==
0 -> pipe:[918826095]
1 -> pipe:[918826096]
2 -> pipe:[918826097]
255 -> /home/wandbox/prog.sh

== selected fields of /proc/self/status (self = grep) ==
Name:	grep
State:	R (running)
Pid:	8
PPid:	2
Uid:	999	999	999	999
Threads:	1
voluntary_ctxt_switches:	0
nonvoluntary_ctxt_switches:	1

== the vDSO is mapped into every process ==
r--p [vvar]
r-xp [vdso]

প্রথম অংশে প্রতিটি নাম একটি “ফাইল” — cmdline-এ প্রসেসের কমান্ড, cwd তার বর্তমান directory. দ্বিতীয় অংশটি অংশ ৬-এর রহস্য খুলে দেয়: shell-এর fd 0, 1, 2 তিনটিই pipe, টার্মিনাল নয় — তাই এই রানারে printf fully buffered ছিল। (255 হলো bash-এর নিজের খোলা স্ক্রিপ্ট ফাইল।) তৃতীয় অংশে Name: grep, কারণ এখানে /proc/self পড়ছে grep নিজে। Uid: 999 বলছে প্রসেসটি root (0) নয় — এজন্যই আগের প্রোগ্রামে /etc/shadow খোলা যায়নি। voluntary_ctxt_switches গোনে প্রসেস কতবার নিজে থেকে CPU ছেড়েছে (প্রায়ই কোনো blocking syscall-এ অপেক্ষা করতে গিয়ে), আর nonvoluntary গোনে কতবার kernel জোর করে সরিয়েছে। শেষ অংশে দেখা যাচ্ছে vDSO (r-xp, অর্থাৎ পড়া ও চালানো যায়) আর তার ডেটা পাতা vvar (r--p, শুধু পড়া) — এই grep প্রসেসের ভেতরেও kernel সেগুলো বসিয়ে রেখেছে।

৮ · System call চোখে দেখা — strace, Process Monitor, dtruss

একটি প্রোগ্রাম kernel-এর সাথে কী কী কথা বলছে, তা শোনার টুল আছে। এটি debugging-এর অন্যতম শক্তিশালী কৌশল: “প্রোগ্রাম কোন ফাইল খুঁজছে পাচ্ছে না?”, “কোথায় আটকে আছে?” — সোর্স কোড না দেখেও উত্তর মেলে।

OSটুলকী দেখায়
Linuxstrace (আর ltrace)প্রতিটি syscall, তার argument আর ফল; ltrace দেখায় library ফাংশনের কল
WindowsProcess Monitor (Sysinternals)ফাইল, registry, প্রসেস আর নেটওয়ার্কের ঘটনা — কাঁচা syscall নয়, তবে প্রায় একই উদ্দেশ্য পূরণ করে
macOSdtruss (DTrace-ভিত্তিক)syscall-এর তালিকা; root লাগে, আর System Integrity Protection চালু থাকলে অনেক ক্ষেত্রে সীমিত

Linux-এ cat notes.txt চালালে strace কী দেখায়, তার একটি উদাহরণ নিচে। (এটি একটি নমুনা আউটপুট, এই পাতায় চালানো যাবে না; লম্বা অংশ “…” দিয়ে ছোট করা। আপনার মেশিনে ঠিকানা আর কিছু লাইন ভিন্ন হবে।)

$ strace cat notes.txt
execve("/usr/bin/cat", ["cat", "notes.txt"], 0x7ffc... /* 24 vars */) = 0
brk(NULL)                               = 0x5581f3a4e000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=27133, ...}) = 0
mmap(NULL, 27133, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f2c1a9b4000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0"..., 832) = 832
...
openat(AT_FDCWD, "notes.txt", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=12, ...}) = 0
read(3, "Hello Dhaka\n", 131072)        = 12
write(1, "Hello Dhaka\n", 12Hello Dhaka
)           = 12
read(3, "", 131072)                     = 0
close(3)                                = 0
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

প্রতিটি লাইনের গঠন একই: নাম(argument…) = ফল. নিচের ডায়াগ্রামে একটি লাইন টুকরো করে পড়ি:

openat (AT_FDCWD, "notes.txt", O_RDONLY) = 3
  1. openat — syscall-এর নাম। প্রোগ্রাম C কোডে হয়তো open() বা fopen() লিখেছে, কিন্তু kernel-এ গেছে openat. strace সবসময় kernel-এর আসল নাম দেখায়, library-র নাম নয়।
  2. AT_FDCWD — কোথা থেকে পথ ধরা হবে। এর মানে “বর্তমান working directory থেকে”। openat অন্য কোনো খোলা directory-র fd থেকেও পথ ধরতে পারে; সেটিই এর বাড়তি সুবিধা।
  3. "notes.txt" — পথ। strace পয়েন্টারের বদলে তার ভেতরের লেখা দেখিয়ে দেয়, যাতে আপনি পড়তে পারেন। কোনো প্রোগ্রাম “ফাইল পাচ্ছে না” বললে এখানেই দেখবেন সে আসলে কোন পথে খুঁজছে।
  4. O_RDONLY — flag. শুধু পড়ার জন্য খোলা। লেখার অনুমতি চাইলে O_WRONLY বা O_RDWR, নতুন ফাইল বানাতে O_CREAT দেখা যেত।
  5. = 3 — ফল। নতুন file descriptor 3 (0, 1, 2 আগেই দখলে)। ব্যর্থ হলে এখানে দেখাত = -1 ENOENT (No such file or directory) — অর্থাৎ strace kernel-এর −2-কে glibc-র মতো করেই সুন্দর করে লিখে দেয়।

এবার পুরো নমুনাটি লাইন ধরে বুঝে নিন:

  • execve — shell-এর তৈরি নতুন প্রসেস নিজের জায়গায় /usr/bin/cat প্রোগ্রামটি বসাল। ফল 0 মানে সফল।
  • brk, mmap — মেমরি চাওয়া (পর্ব ৩৮-এ বিস্তারিত)।
  • access("/etc/ld.so.preload") = −1 ENOENT — dynamic linker দেখছে আগে থেকে কোনো library জোর করে লোড করার নির্দেশ আছে কি না। নেই — এবং এটি স্বাভাবিক। strace-এ ENOENT দেখলেই ভয় পাবেন না; অনেক প্রোগ্রাম ইচ্ছে করেই “আছে কি?” যাচাই করে।
  • openat … ld.so.cache, তারপর libc.so.6 — dynamic linker libc খুঁজে খুলছে, ELF header পড়ছে (\177ELF), মেমরিতে map করছে। আপনার প্রোগ্রামের main শুরুর আগেই এত কাজ!
  • openat(…"notes.txt"…) = 3 — এবার cat-এর আসল কাজ শুরু।
  • read(3, …, 131072) = 12 — ১২৮ KB পর্যন্ত পড়তে চেয়েছিল, ফাইলে ছিল ১২ বাইট। read কম ফেরত দিতেই পারে; প্রোগ্রামকে তা সামলাতে হয়।
  • write(1, …, 12) — stdout-এ লেখা। খেয়াল করুন, “Hello Dhaka” লেখাটি strace-এর লাইনের মাঝখানে ঢুকে গেছে: strace নিজে লেখে stderr-এ, আর cat লেখে stdout-এ, দুটোই একই টার্মিনালে।
  • read(3, "", …) = 0 — 0 মানে ফাইলের শেষ (end of file). cat থামে।
  • close, exit_group(0) — সব fd বন্ধ, প্রসেসের সব থ্রেড শেষ। = ? কারণ এই call কখনো ফেরে না।
strace -c ls                  # summary: which syscalls, how many, how long
strace -e trace=openat,read ls  # show only these syscalls
strace -f -o log.txt ./server # follow child processes, write to a file
strace -p 1234                # attach to a running process (needs permission)

strace প্রতিটি syscall-এ প্রোগ্রাম থামিয়ে তথ্য নেয় (ptrace ব্যবহার করে), তাই প্রোগ্রাম অনেক ধীর হয়ে যায় — ব্যস্ত production সার্ভারে সাবধানে, অল্প সময়ের জন্য ব্যবহার করুন। অন্যের প্রসেসে attach করতে অনুমতি লাগে; অনেক Linux-এ নিরাপত্তার জন্য ptrace সীমিত করা থাকে, সেটি নিজের সুবিধার জন্য বন্ধ করবেন না।

আরও গভীরে: Linux-এ syscall-এর C-অংশের প্রবেশপথ do_syscall_64(); ঐতিহ্যগতভাবে এটি sys_call_table নামের ফাংশন-পয়েন্টার অ্যারে দিয়ে handler খুঁজত, তবে Spectre-ধরনের আক্রমণ ঠেকাতে সাম্প্রতিক কিছু kernel-এ x86-এ অ্যারের বদলে একটি বড় switch ব্যবহার হয়। Syscall-এর খরচ কমাতে ২০১৯ সালে Linux 5.1-এ এসেছে io_uring: প্রোগ্রাম আর kernel একটি shared memory ring-এ অনেক I/O অনুরোধ জমা ও ফল নেয়, ফলে প্রতি অনুরোধে আলাদা syscall লাগে না। আর syscall-কে নিরাপত্তার দেয়াল হিসেবে ব্যবহার করে seccomp — একটি প্রসেস কোন কোন syscall করতে পারবে তা আগেই সীমিত করা যায়; Chrome-এর sandbox আর Docker-এর ডিফল্ট profile এটি ব্যবহার করে (পর্ব ৪৯)। strace-এর চেয়ে অনেক কম খরচে syscall দেখতে আধুনিক Linux-এ perf trace বা eBPF-ভিত্তিক টুল (পর্ব ৫৫) ব্যবহার হয়।

৯ · AI-এর যুগে system call

“Linux-এ একটি ফাইল কপি করার C প্রোগ্রাম লিখে দাও” — AI কয়েক সেকেন্ডে দেবে, আর সেটি সাধারণত চলবেও। সমস্যা লুকিয়ে থাকে কিনারায়: আংশিক write, errno ঠিকমতো না দেখা, ভুল OS-এর API. System call বোঝেন বলে আপনি এগুলো ধরতে পারবেন। AI-এর সাধারণ ভুলগুলো:

AI-এর ভুলফলকীভাবে ধরবেন
write-এর ফল যাচাই না করা, ধরে নেওয়া পুরোটা লেখা হয়েছেpipe বা socket-এ ডেটা হারায়ফেরত সংখ্যা < চাওয়া সংখ্যা হলে বাকিটা আবার লেখার লুপ আছে কি না দেখুন
ব্যর্থতায় errno পড়ার আগে অন্য library কল (যেমন printf)ভুল ত্রুটির কারণ দেখায়−1 পাওয়ার সঙ্গে সঙ্গে errno কপি হচ্ছে কি না দেখুন
Linux-এর syscall দিয়ে Windows-এর কোড, বা উল্টোunistd.h পাওয়া যায় না, বা ভুল APIপ্রম্পটে OS, compiler আর সংস্করণ স্পষ্ট লিখুন
x86-64-এর syscall নম্বর ARM64-এ ব্যবহারসম্পূর্ণ অন্য syscall চলেনম্বর নয়, SYS_write-এর মতো macro ব্যবহার হচ্ছে কি না দেখুন
printf আর write মিশিয়ে, fflush ছাড়াফাইলে বা pipe-এ আউটপুটের ক্রম উল্টে যায়আউটপুট ফাইলে redirect করে চালিয়ে দেখুন
“Permission denied” সারাতে sudo বা chmod 777 পরামর্শবড় নিরাপত্তা-ঝুঁকিকোন ফাইলে কার কী অনুমতি দরকার, ন্যূনতম সেটুকুই দিন (পর্ব ৪৭)
পুরোনো তথ্য: “getpid সবসময় cache হয়” বা “syscall মানেই int 0x80”ভুল ব্যাখ্যা, পরীক্ষায় নম্বর কাটাman page (man 2 syscall) আর kernel-এর ডকুমেন্টেশন মিলিয়ে নিন

AI-কে systems কোড লিখতে দিলে প্রেক্ষাপট আর যাচাইয়ের শর্ত দুটোই দিন:

আমার পরিবেশ: Ubuntu 24.04, x86-64, gcc 13, glibc. শুধু POSIX system call (open, read, write, close) দিয়ে একটি C প্রোগ্রাম লিখে দাও, যা একটি ফাইল আরেকটিতে কপি করবে। শর্ত: ১. read আর write-এর প্রতিটি ফেরত মান যাচাই করো; write কম বাইট লিখলে বাকিটা লুপে আবার লেখো। ২. EINTR এলে আবার চেষ্টা করো; অন্য ত্রুটিতে perror দিয়ে কারণ দেখিয়ে exit(1) করো, errno অন্য কলের আগে কপি করো। ৩. stdio (printf, fopen) ব্যবহার করবে না, শুধু ত্রুটির বার্তায় perror চলবে। ৪. gcc -Wall -Wextra-এ কোনো warning যেন না আসে। শেষে বলো: strace -e trace=openat,read,write দিয়ে চালালে কোন কোন লাইন দেখার কথা, আর ফাইল না থাকলে কোন errno আসবে।

AI-এর লেখা systems কোড বা কমান্ড চালানোর আগে — চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • Dual-mode operation কী, কেন দরকার? — CPU-র user ও kernel দুই মোড; privileged instruction শুধু kernel mode-এ চলে, তাই ভুল বা দুষ্ট প্রোগ্রাম হার্ডওয়্যার বা অন্য প্রসেসের ক্ষতি করতে পারে না।
  • System call আর সাধারণ function call-এর পার্থক্য? — system call trap instruction দিয়ে মোড বদলে kernel-এর নির্দিষ্ট প্রবেশপথে যায়, kernel argument যাচাই করে; function call একই মোডে যেকোনো ঠিকানায় লাফ, অনেক সস্তা।
  • Parameter পাঠানোর তিন উপায়? — register, মেমরির block-এর ঠিকানা register-এ, আর stack.
  • printf কি system call? — না; এটি library ফাংশন, buffer করে শেষে write system call ডাকে।
  • System call-এর শ্রেণিগুলো? — process control, file management, device management, information maintenance, communication, protection.

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১হার্ডওয়্যারে dual mode না থাকলে কি নিরাপদ OS বানানো সম্ভব? ইতিহাস কী বলে?

ইতিহাস প্রথমে একটি কঠিন শিক্ষা দেয়। ১৯৮০-র দশকের MS-DOS চলত Intel 8086-এর মতো CPU-তে, যেখানে কোনো সুরক্ষা-মোড ছিল না। যেকোনো প্রোগ্রাম interrupt টেবিল বদলাতে পারত, OS-এর নিজের মেমরিতে লিখতে পারত, সরাসরি ডিস্ক কন্ট্রোলারকে আদেশ দিতে পারত। ফল: একটি বাগওয়ালা প্রোগ্রাম মানেই পুরো কম্পিউটার আটকে যাওয়া, আর ভাইরাসের জন্য খোলা মাঠ। পুরোনো Mac OS (Classic, ১৯৮৪ থেকে প্রায় ২০০১) দীর্ঘদিন প্রসেসগুলোর মধ্যে মেমরি-সুরক্ষা ছাড়াই চলেছে; একটি অ্যাপ ক্র্যাশ করলে প্রায়ই পুরো মেশিন restart করতে হতো। Apple শেষ পর্যন্ত Unix-ভিত্তিক Mac OS X-এ চলে যায়, যেখানে আসল user/kernel বিভাজন আছে।

তবে হার্ডওয়্যার ছাড়াও সুরক্ষার আরেকটি পথ গবেষণায় দেখা গেছে: ভাষা-ভিত্তিক সুরক্ষা। Microsoft Research-এর গবেষণা-OS Singularity (২০০০-এর দশক) প্রায় সব কোড type-safe ভাষায় লিখে, compiler আর যাচাইকারী দিয়ে নিশ্চিত করত যে কোনো প্রোগ্রাম অন্যের মেমরিতে হাত দিতে পারবে না — তাই আলাদা হার্ডওয়্যার-মোড ছাড়াও প্রসেসগুলো আলাদা থাকত। ব্রাউজারের WebAssembly-ও একরকম সফটওয়্যার-সুরক্ষা: কোডটি নিজের নির্দিষ্ট মেমরির বাইরে যেতে পারে না, যা runtime যাচাই করে।

কিন্তু এ পথের দাম আছে: সব কোডকে নির্দিষ্ট ভাষায় লিখতে হয়, আর compiler বা যাচাইকারীর একটি বাগই পুরো সুরক্ষা ভেঙে দিতে পারে। C বা assembly-তে লেখা পুরোনো সফটওয়্যার চালানো যায় না। এজন্য বাস্তবের প্রায় সব সাধারণ-উদ্দেশ্যের OS হার্ডওয়্যার-মোডের ওপরই দাঁড়িয়ে, আর ভাষা-ভিত্তিক সুরক্ষা ব্যবহার হয় বাড়তি স্তর হিসেবে। আজকের CPU আরও স্তর যোগ করেছে — ARM64-এর EL2 hypervisor-এর জন্য, x86-এর virtualization mode (পর্ব ৫০)। ছোট microcontroller-এও সাধারণত একটি memory protection unit থাকে।

মূল উপলব্ধি: সুরক্ষা আসে এমন এক জায়গা থেকে যাকে সাধারণ প্রোগ্রাম বদলাতে পারে না। হার্ডওয়্যারের mode bit সেই জায়গার সবচেয়ে সরল, দ্রুত আর ভাষা-নিরপেক্ষ রূপ; বাকি সব স্তর তার ওপর বসে।

প্র ০২Linux syscall নম্বর কখনো বদলায় না, অথচ Windows আর macOS বদলায় — এই নীতিগত পার্থক্যের ফল কী?

Linux-এ “kernel” আর “C library” আলাদা দল বানায়: kernel আসে kernel.org থেকে, আর library আসে glibc, musl বা Android-এর Bionic থেকে। তাই kernel-এর বাইরের জগতের সাথে চুক্তিটি হলো syscall ABI নিজেই। Linus Torvalds বহু বছর ধরে নিয়মটি কড়াভাবে মানিয়েছেন — “we do not break userspace”: পুরোনো প্রোগ্রাম নতুন kernel-এ ভেঙে গেলে দোষ kernel-এর। ফলে বহু বছর আগে compile করা একটি static প্রোগ্রামও আজকের kernel-এ চলে, আর Go-র মতো ভাষা Linux-এ libc ছাড়াই সরাসরি syscall করতে পারে।

Windows আর macOS-এ kernel আর মূল library একই কোম্পানির, একসাথে আসে। তাই সেখানে চুক্তিটি এক স্তর উপরে: Windows-এ Win32 API আর ntdll, macOS-এ libSystem. ভেতরের syscall নম্বর ইচ্ছেমতো সাজানো যায়। এর বাস্তব প্রভাব দেখা গেছে Go ভাষায়: শুরুর দিকে Go macOS-এও সরাসরি syscall করত, কিন্তু macOS-এর আপডেটে কিছু প্রোগ্রাম ভেঙে যায়। Go 1.11 (২০১৮) থেকে macOS-এ Go runtime libSystem দিয়ে যায়। OpenBSD আরও এক ধাপ এগিয়েছে: সাম্প্রতিক সংস্করণে kernel যাচাই করে syscall instruction-টি অনুমোদিত জায়গা (মূলত libc) থেকে এসেছে কি না — আক্রমণকারী যাতে নিজের কোড থেকে সরাসরি syscall না করতে পারে।

দুই নীতিরই যুক্তি আছে। Linux-এর স্থিরতা ভিন্ন ভিন্ন libc, container আর পুরোনো binary-র জন্য দারুণ, কিন্তু একবার ভুল নকশার syscall যোগ হলে তা চিরকাল রাখতে হয় — তাই Linux-এ open-এর পাশে openat, clone-এর পাশে clone3-এর মতো “নতুন সংস্করণ” যোগ হয়। Windows/macOS-এর পথে kernel ভেতরে ভেতরে বদলানো সহজ, তবে অ্যাপকে library-র ওপর নির্ভর করতেই হয়। এমনকি Windows-এ Linux চালানোর WSL1 Linux-এর syscall-কে NT kernel-এর ভেতরে অনুবাদ করত — Linux-এর ABI স্থির বলেই সেটি সম্ভব হয়েছিল।

মূল উপলব্ধি: প্রতিটি সিস্টেমের একটি “স্থির সীমানা” থাকে, যেখানে প্রতিশ্রুতি দেওয়া হয়। সেটি কোথায় — syscall-এ, না library-তে — জানলে বোঝা যায় কোন কোড কতদিন টিকবে, আর কোথায় সরাসরি হাত দেওয়া ঝুঁকিপূর্ণ।

প্র ০৩System call দামি হলে কেন সব কিছু user mode-এ করে ফেলা হয় না? খরচ কমানোর কৌশলগুলো কী কী?

কারণ কিছু কাজের জন্য kernel-এর সিদ্ধান্ত লাগেই — কে কোন ফাইল পড়তে পারবে, কোন প্রসেস কতটা মেমরি পাবে, কোন প্যাকেট কার কাছে যাবে। এগুলো user mode-এ ছেড়ে দিলে সুরক্ষার পুরো ভিত্তি ভেঙে যায়। তাই প্রশ্নটি দাঁড়ায়: দরকারি নিরাপত্তা রেখে kernel-এ যাওয়ার সংখ্যা বা খরচ কীভাবে কমানো যায়? প্রকৌশলীরা কয়েকটি পথ বের করেছেন:

  • Batching (একসাথে পাঠানো): stdio buffer অনেক ছোট লেখা জমিয়ে এক write-এ দেয়। readv/writev একবারে অনেক buffer নেয়, sendmmsg অনেক নেটওয়ার্ক বার্তা।
  • Kernel-এর তথ্য user-এর কাছে আনা: vDSO সময়ের মতো পড়ার-অনুমতি-মাত্র তথ্য user mode-এই দেয়। Linux-এর পুরোনো “vsyscall” পাতা একটি স্থির ঠিকানায় থাকত, যা আক্রমণকারীর জন্য সুবিধাজনক ছিল; তাই পরে ঠিকানা-বদলানো (ASLR-যুক্ত) vDSO তার জায়গা নেয়, আর vsyscall এখন শুধু পুরোনো প্রোগ্রামের জন্য সীমিতভাবে রাখা হয়।
  • Shared ring: io_uring (Linux 5.1, ২০১৯) প্রোগ্রাম আর kernel-এর মাঝে দুটি ring buffer রাখে — অনুরোধ জমা আর ফল নেওয়ার জন্য। অনেক অনুরোধ একটি syscall-এ পাঠানো যায়; আর একটি বিশেষ ব্যবস্থায় (SQPOLL) kernel-এর নিজস্ব থ্রেড ring দেখে কাজ তুলে নেয়, তখন প্রায় কোনো syscall-ই লাগে না।
  • কাজটিই kernel-এর ভেতরে নিয়ে যাওয়া: sendfile ফাইল থেকে socket-এ ডেটা kernel-এর ভেতরেই পাঠায়, user-এ কপি আনতে হয় না। eBPF ছোট যাচাই-করা প্রোগ্রাম kernel-এ চালায়।
  • Kernel-কেই এড়িয়ে যাওয়া: উচ্চগতির নেটওয়ার্কে DPDK-র মতো kernel-bypass কাঠামো নেটওয়ার্ক কার্ড সরাসরি একটি প্রসেসকে দিয়ে দেয় — বিনিময়ে kernel-এর সুরক্ষা আর সুবিধা অনেকটা হারায়।

২০১৮ সালে Meltdown প্রকাশের পর KPTI (kernel page-table isolation) চালু হলে প্রতিটি syscall-এ page table বদলাতে হয়, ফলে অনেক syscall-নির্ভর কাজের গতি লক্ষণীয়ভাবে কমে যায় — এটি দেখিয়েছে নিরাপত্তা আর গতির টানাপোড়েন কতটা বাস্তব। নতুন CPU-তে হার্ডওয়্যার-সমাধান আসায় অনেক ক্ষেত্রে সেই খরচ আবার কমেছে।

মূল উপলব্ধি: System call-এর খরচ কমানোর প্রতিটি কৌশল আসলে একটি প্রশ্নের উত্তর — “নিরাপত্তার সীমানা কতবার পার হতে হবে?” উত্তর কমানো যায়, কিন্তু সীমানা তুলে দিলে সুরক্ষাও যায়।

প্র ০৪Buffering কি আশীর্বাদ, না অভিশাপ? কোন পরিস্থিতিতে buffer আপনাকে বিপদে ফেলে?

দুটোই। আশীর্বাদ, কারণ অংশ ৭-এর হিসাবেই দেখেছেন — buffer syscall প্রায় দুশো গুণ কমাতে পারে। কিন্তু buffer মানে “লেখা হয়েছে” আর “আসলে পৌঁছেছে”-র মাঝে একটি ফাঁক। সেই ফাঁকেই বাগ জন্মায়:

  • ক্র্যাশে লগ হারানো: প্রোগ্রাম segmentation fault-এ মারা গেলে stdio buffer খালি হয় না। শেষ কয়েকটি লগ লাইন — যেগুলোতে ক্র্যাশের ঠিক আগের ঘটনা ছিল — কখনো ফাইলে পৌঁছায় না। এজন্যই stderr unbuffered, আর গুরুত্বপূর্ণ লগে প্রায়ই প্রতিটি লাইনের পর flush করা হয়।
  • fork-এ দ্বিগুণ লেখা: fork প্রসেসের পুরো মেমরি কপি করে — buffer-এ বসে থাকা লেখাসহ। পরে দুই প্রসেসই নিজের buffer flush করলে একই লাইন দুবার আসে। টার্মিনালে ঠিক দেখায় (line buffered), ফাইলে redirect করলে দ্বিগুণ — এমন বাগ খুঁজে পেতে নতুনদের অনেক সময় লাগে।
  • Pipe-এ “আটকে থাকা” আউটপুট: ./prog | grep error চালালে prog-এর stdout fully buffered হয়ে যায়; ধীরে ধীরে চলা প্রোগ্রামের লাইন কয়েক KB না জমা পর্যন্ত grep কিছুই দেখে না। মনে হয় প্রোগ্রাম থেমে আছে। সমাধান হিসেবে প্রোগ্রামে setvbuf বা fflush, অথবা GNU coreutils-এর stdbuf -oL ব্যবহার হয়।
  • Interactive prompt: “Enter your name: ” লেখার শেষে newline না থাকলে কিছু পরিবেশে লেখাটি স্ক্রিনে ওঠার আগেই প্রোগ্রাম ইনপুটের অপেক্ষায় বসে থাকে।

আরেকটি স্তর আছে: kernel-এর page cache. write সফল মানে ডেটা kernel-এর মেমরিতে, ডিস্কে নয়। বিদ্যুৎ চলে গেলে — বাংলাদেশে লোডশেডিং যার পরিচিত অভিজ্ঞতা — fsync না করা ডেটা হারাতে পারে। এজন্যই ডেটাবেস commit-এর সময় fsync করে, আর এজন্যই USB ড্রাইভ “safely remove” করার আগে OS অপেক্ষা করে।

মূল উপলব্ধি: প্রতিটি buffer একটি প্রতিশ্রুতি স্থগিত রাখে। কোথায় কোথায় buffer আছে (stdio, kernel, ডিস্কের নিজস্ব cache) আর কে কখন তা খালি করে — এই মানচিত্র মাথায় থাকলে “আমার লেখা গেল কোথায়?” ধরনের রহস্য আর রহস্য থাকে না।

প্র ০৫System call-ই যদি kernel-এ ঢোকার একমাত্র দরজা হয়, তাহলে নিরাপত্তার দৃষ্টিতে সেই দরজা কীভাবে পাহারা দেওয়া হয়?

প্রতিটি syscall আসলে kernel-এর একটি “পাবলিক ফাংশন”, যা যেকোনো প্রোগ্রাম — দুষ্ট প্রোগ্রামও — ডাকতে পারে। তাই নিরাপত্তা প্রকৌশলীরা পুরো syscall interface-কে kernel-এর attack surface বলেন, আর সেটিকে কয়েক স্তরে রক্ষা করা হয়:

  • প্রতিটি argument অবিশ্বাস করা: user-এর দেওয়া পয়েন্টার আসলে user মেমরির কি না যাচাই হয়, আর কপি হয় বিশেষ ফাংশন দিয়ে। আধুনিক x86-এ SMEP আর SMAP নামের হার্ডওয়্যার সুবিধা kernel-কে ভুল করেও user মেমরির কোড চালাতে বা অনুমতি ছাড়া user মেমরি পড়তে দেয় না; ARM-এ PXN/PAN একই কাজ করে।
  • পড়া আর ব্যবহারের মাঝের ফাঁক বন্ধ করা: kernel একবার user মেমরির মান যাচাই করে পরে আবার পড়লে, মাঝখানে অন্য থ্রেড মানটি বদলে দিতে পারে — একে বলে TOCTOU বা “double fetch” বাগ। নিয়ম হলো, একবার kernel-এ কপি করে সেই কপির ওপরই কাজ করা।
  • দরজার সংখ্যা কমানো: seccomp দিয়ে প্রসেস নিজেকে কয়েকটি syscall-এ সীমিত করতে পারে। ব্রাউজারের renderer প্রসেস বা container-এর ভেতরের অ্যাপের ফাইল সিস্টেম মাউন্ট বা kernel module লোড করার দরকার নেই — তাই সেগুলোর দরজা আগেই বন্ধ। Docker-এর ডিফল্ট seccomp profile কয়েক ডজন বিপজ্জনক বা কম-ব্যবহৃত syscall আটকায়।
  • অনুমতির যাচাই: প্রতিটি ফাইল খোলায় user/group অনুমতি, প্রয়োজনে SELinux বা AppArmor-এর নিয়ম (পর্ব ৪৭ ও ৪৯)।
  • ক্রমাগত পরীক্ষা: Google-এর syzkaller-এর মতো fuzzer এলোমেলো argument দিয়ে কোটি কোটি syscall ডেকে kernel-এর বাগ খোঁজে; এভাবে বহু বাগ আক্রমণকারীর আগেই ধরা পড়েছে।

Windows-এও একই চিন্তা আছে: যেমন কিছু প্রক্রিয়ার জন্য win32k-এর graphics syscall বন্ধ রাখার নীতি, যাতে ব্রাউজারের sandbox থেকে সেই বড় kernel অংশে পৌঁছানো না যায়।

মূল উপলব্ধি: নিরাপত্তার মূল নিয়ম — “সীমানা পার হওয়া প্রতিটি তথ্য অবিশ্বাস্য”। Syscall হলো OS-এর সবচেয়ে গুরুত্বপূর্ণ সীমানা, তাই সেখানে যাচাই, সংকোচন আর পরীক্ষা — তিনটিই চলে।

অনুশীলন

হাতে হিসাব, আউটপুট অনুমান আর রেজিস্টার ট্রেস — আগে নিজে চেষ্টা করুন, তারপর উত্তর মেলান।

  1. আউটপুট অনুমান: নিচের কোড (১) টার্মিনালে আর (২) ./prog > out.txt করে চালালে কী আসবে?
    printf("A");
    write(1, "B", 1);
    printf("C\n");
    write(1, "D\n", 2);

    (১) টার্মিনাল (line buffered): printf("A")-এ newline নেই, তাই A buffer-এ থাকে। write("B") সঙ্গে সঙ্গে B লেখে। printf("C\n")-এ newline আসায় buffer-এর "AC\n" বেরিয়ে যায়। তারপর write("D\n"). ফল:

    BAC
    D

    (২) ফাইল (fully buffered): B আর D\n সরাসরি যায়; "AC\n" বসে থাকে প্রোগ্রাম শেষ হওয়া পর্যন্ত। ফল:

    BD
    AC

    একই কোড, দুই ক্রম — পার্থক্য শুধু stdout কোথায় যাচ্ছে তাতে।

  2. হাতে হিসাব: একটি প্রোগ্রাম ৫ লাখ রেকর্ড লিখবে, প্রতিটি ৪০ বাইট। ধরে নিন প্রতিটি syscall-এ ২৫০ ন্যানোসেকেন্ড লাগে। (ক) প্রতি রেকর্ডে একটি write করলে শুধু syscall-এ কত সময় যাবে? (খ) ৮১৯২ বাইটের buffer দিয়ে লিখলে কতটি write লাগবে, আর কত সময়?

    (ক) ৫,০০,০০০ × ২৫০ ns = ১২,৫০,০০,০০০ ns = ০.১২৫ সেকেন্ড.

    (খ) মোট ডেটা = ৫,০০,০০০ × ৪০ = ২,০০,০০,০০০ বাইট। write-এর সংখ্যা = ⌈২,০০,০০,০০০ ÷ ৮১৯২⌉ = ⌈২৪৪১.৪…⌉ = ২,৪৪২টি (শেষটি আংশিক buffer). সময় = ২,৪৪২ × ২৫০ ns = ৬,১০,৫০০ ns ≈ ০.৬১ মিলিসেকেন্ড. অনুপাত ৫,০০,০০০ ÷ ২,৪৪২ ≈ ২০৫ গুণ কম syscall. (বাস্তবে kernel-এর ভেতরে ডেটা কপির খরচ দুই ক্ষেত্রেই থাকে, তাই মোট সময়ের পার্থক্য এর চেয়ে কম হবে।)

  3. রেজিস্টার ট্রেস: write(2, "err\n", 4) সফলভাবে চললে x86-64 Linux-এ syscall instruction-এর ঠিক আগে rax, rdi, rsi, rdx-এ কী থাকবে, আর ফেরার পর rax-এ? ARM64 Linux-এ কোন রেজিস্টারে কী যাবে?

    x86-64: rax = 1 (SYS_write), rdi = 2 (fd, stderr), rsi = "err\n" লেখাটির ঠিকানা, rdx = 4. ফেরার পর rax = 4 (লেখা বাইট)। rcx আর r11-এর আগের মান নষ্ট হয়, কারণ syscall সেখানে ফেরার ঠিকানা আর flags রাখে।

    ARM64: x8 = 64 (ARM64-এ write-এর নম্বর), x0 = 2, x1 = ঠিকানা, x2 = 4; তারপর svc #0; ফল আসে x0-তে = 4. ব্যর্থ হলে, যেমন fd 2 বন্ধ থাকলে, x86-64-এ rax = −9 (−EBADF), আর glibc ফেরত দিত −1 ও errno = 9.

  4. শ্রেণি নির্ণয় ও Windows-এ খোঁজা: নিচের POSIX call-গুলো ছয় শ্রেণির কোনটিতে পড়ে বলুন, আর প্রতিটির কাছাকাছি একটি Win32 API নাম লিখুন: waitpid, unlink, ioctl, getpid, pipe, chmod.
    • waitpid — process control; Windows-এ WaitForSingleObject (প্রসেসের handle-এ)।
    • unlink — file management; DeleteFile.
    • ioctl — device management; DeviceIoControl.
    • getpid — information maintenance; GetCurrentProcessId.
    • pipe — communication; CreatePipe.
    • chmod — protection; SetFileSecurity (Windows-এ অনুমতি ACL দিয়ে, তাই মিল পুরোপুরি এক নয়)।

    মনে রাখুন, Win32 নামগুলো API; আসল syscall ntdll-এর Nt… ফাংশনে।

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

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

ক্রমx86-64 Linux-এ একটি write() কলের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. প্রোগ্রাম glibc-র write() wrapper ডাকে
  2. Wrapper rax-এ syscall নম্বর আর rdi, rsi, rdx-এ argument রাখে
  3. syscall instruction মোড বদলে kernel-এর প্রবেশপথে নিয়ে যায়
  4. Kernel syscall table থেকে handler খুঁজে কাজটি করে
  5. ফল rax-এ রেখে user mode-এ ফেরা; ঋণাত্মক হলে glibc errno বসায়

নিচের কোন কাজটি user mode-এ চলা প্রোগ্রাম সরাসরি করতে পারে না (privileged)?

  • দুটি register-এর মান যোগ করা
  • নিজের stack-এ একটি মান রাখা
  • Interrupt বন্ধ করা (x86-এ cli)
  • নিজের প্রোগ্রামের একটি ফাংশন ডাকা

x86-64 Linux-এ syscall instruction চালানোর আগে syscall নম্বর কোন register-এ রাখা হয়?

  • rdi
  • rax
  • stack-এর ওপরে
  • rcx

একটি C প্রোগ্রাম পরপর printf("A\n"); আর write(1, "B\n", 2); চালায়, আর stdout একটি ফাইলে redirect করা। ফাইলে কোন ক্রমে লেখা থাকবে?

  • A, তারপর B
  • শুধু B, A হারিয়ে যায়
  • A আর B-র অক্ষর মিশে যাবে
  • B, তারপর A

নিচের কোনটি নিজে system call নয়, বরং একটি library ফাংশন যা ভেতরে system call ব্যবহার করে?

  • read()
  • fork()
  • printf()
  • write()

Linux-এ clock_gettime সাধারণত একটি পূর্ণ system call-এর চেয়ে অনেক দ্রুত হয় কেন?

  • vDSO-র মাধ্যমে user mode-এই kernel-এর হালনাগাদ রাখা সময় পড়া যায়
  • CPU নিজেই সেকেন্ড আর ন্যানোসেকেন্ড হিসাব করে দেয়, OS-এর কিছু লাগে না
  • glibc প্রোগ্রাম শুরুর সময় একবার সময় পড়ে রেখে দেয়
  • clock_gettime ডাকা প্রোগ্রাম স্থায়ীভাবে kernel mode-এ চলে

Linux-এ open() একটি না-থাকা ফাইলের জন্য ব্যর্থ হলে প্রোগ্রাম কী পায়?

  • ফেরত মান −2, errno অপরিবর্তিত
  • ফেরত মান −1, আর errno = ENOENT (2)
  • ফেরত মান 0
  • প্রোগ্রাম সঙ্গে সঙ্গে SIGSEGV পেয়ে বন্ধ হয়

শব্দকোষ

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

মনে রাখুন

  1. CPU-র user mode আর kernel mode; privileged instruction শুধু kernel mode-এ, user চালাতে চাইলে exception আর প্রসেস বন্ধ।
  2. User থেকে kernel-এ যাওয়ার তিন পথ: system call, interrupt, exception; ফেরা হয় kernel-এর return instruction দিয়ে।
  3. যাত্রাপথ: wrapper → register-এ নম্বর ও argument → trap (syscall / svc) → syscall table → ফল; Linux-এ ব্যর্থতা মানে −1 আর errno.
  4. ছয় শ্রেণি: process, file, device, information, communication, protection; Win32 নামগুলো API, আসল syscall ntdll-এ।
  5. printf ≠ system call — buffer করে write ডাকে; syscall দামি, তাই buffering আর vDSO; কী ঘটছে দেখতে strace.

পরের পর্বে: Kernel-এর দরজা চিনলেন; এবার ভেতরের ঘর। পুরো OS কি একটি বিশাল প্রোগ্রাম (monolithic), নাকি ছোট ছোট আলাদা সেবার সমষ্টি (microkernel)? Linux, Windows আর macOS কোন পথে গেছে, আর কেন।

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

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