Toy Kernel
Toy Kernel
শূন্য থেকে একটা bootable kernel — bootloader, protected/long mode-এ ঢোকা, VGA text output, IDT আর interrupt handler, timer, physical page allocator, আর শেষে দুইটা task-এর মধ্যে context switch। QEMU-তে চলবে, আর প্রতিটা ধাপে আপনি দেখবেন OS-এর কোন abstraction কোথা থেকে জন্মায়।
কেন এই প্রজেক্ট
এই কারিকুলামের সবচেয়ে কঠিন প্রজেক্ট, আর সবচেয়ে বেশি বদলে দেওয়া প্রজেক্ট।
Level 4-এর প্রতিটা লেসনে আমরা বলেছি “kernel এটা করে”, “OS ওটা
সেটআপ করে”। এই প্রজেক্টে আপনিই সেই kernel। কেউ আপনার জন্য
stack সেটআপ করে দেবে না, কোনো printf নেই, কোনো malloc নেই,
crash করলে কোনো error message নেই — শুধু QEMU রিবুট হতে থাকবে।
আর যখন প্রথমবার আপনার নিজের timer interrupt থেকে দুইটা task-এর মধ্যে switch হবে, তখন “multitasking” শব্দটা আর কখনো বিমূর্ত মনে হবে না।
| এই প্রজেক্টে যা নেই | কারণ |
|---|---|
| libc | আপনি এখনো OS লেখেননি |
printf | কোনো stdout নেই, VGA memory আছে |
malloc | heap কে? আপনি বানাবেন |
| Stack | bootloader যা দিয়েছে, তার বেশি নয় |
| Debugger | QEMU + gdb stub — শিখতে হবে |
Toolchain
# Linux
sudo apt install qemu-system-x86 nasm xorriso grub-pc-bin grub-common
# একটা cross-compiler সবচেয়ে নিরাপদ, কিন্তু শুরুতে
# -ffreestanding সহ host gcc-ও চলে যায়
gcc -ffreestanding -fno-stack-protector -fno-pic -mno-red-zone \
-mno-mmx -mno-sse -mno-sse2 -c kernel.c -o kernel.o
-mno-red-zone অপরিহার্য x86-64 kernel-এ। System V ABI
বলে stack pointer-এর নিচে ১২৮ byte “red zone” leaf function
ব্যবহার করতে পারে — কিন্তু interrupt সেই জায়গাতেই frame push
করে, তাই আপনার ডেটা নষ্ট হবে। এটা একটা কুখ্যাত bug যা ধরতে
দিন লেগে যায়।
ধাপে ধাপে
১. Boot — প্রথম output
সবচেয়ে সরল পথ GRUB + multiboot2। GRUB আপনাকে protected mode-এ নামিয়ে দেবে, তাই real mode-এর ঝামেলা এড়ানো যায়।
volatile uint16_t *vga = (uint16_t *)0xB8000;
void putc_at(char c, uint8_t color, int x, int y) {
vga[y * 80 + x] = (uint16_t)c | ((uint16_t)color \<\< 8);
}
প্রতিটা cell ২ byte — নিচের byte অক্ষর, উপরের byte রঙ। QEMU-তে
Hello দেখতে পাওয়া মানে আপনার কোড সত্যিই CPU-তে চলছে,
কোনো OS ছাড়াই।
২. GDT আর long mode
64-bit-এ যেতে হলে: PAE চালু, একটা identity-mapped page table
বানানো, EFER.LME সেট, তারপর CR0.PG — আর একটা far jump।
সরল রাখতে চাইলে 32-bit protected mode-এই থাকুন। শেখার উদ্দেশ্য একই, ঝামেলা অর্ধেক।
৩. IDT আর প্রথম interrupt
struct idt_entry {
uint16_t offset_low;
uint16_t selector;
uint8_t ist;
uint8_t type_attr;
uint16_t offset_mid;
uint32_t offset_high;
uint32_t zero;
} __attribute__((packed));
__attribute__((packed)) ভুলবেন না — compiler padding ঢোকালে
CPU ভুল জায়গা পড়বে আর triple fault হবে।
Handler-এ assembly stub লাগে, কারণ C function interrupt-এর জন্য সঠিক prologue/epilogue বানায় না:
isr0:
push 0 ; কিছু exception error code push করে, কিছু করে না
push 0 ; interrupt number
jmp isr_common
প্রথম মাইলফলক: ইচ্ছে করে int x = 1/0; লিখুন আর আপনার
divide-by-zero handler থেকে "Divide by zero!" ছাপা দেখুন।
সেই মুহূর্তে আপনি hardware exception ধরেছেন — নিজের কোড দিয়ে।
৪. Timer — preemption-এর জন্ম
PIT (channel 0) configure করে ১০০ Hz-এ interrupt নিন:
#define PIT_FREQ 1193182
void timer_init(uint32_t hz) {
uint32_t divisor = PIT_FREQ / hz;
outb(0x43, 0x36);
outb(0x40, divisor & 0xFF);
outb(0x40, (divisor >> 8) & 0xFF);
}
প্রতিটা tick-এ একটা counter বাড়িয়ে screen-এ দেখান। এটাই সেই ঘড়ি যা preemptive multitasking সম্ভব করে — এর আগে পর্যন্ত একটা task স্বেচ্ছায় না ছাড়লে CPU ছাড়ত না।
৫. Physical page allocator
Multiboot memory map থেকে usable region বের করে একটা bitmap রাখুন — প্রতি bit একটা 4KB frame।
uint8_t *frame_bitmap;
void *alloc_frame(void) {
for (size_t i = 0; i \< total_frames; i++)
if (!bit_test(frame_bitmap, i)) {
bit_set(frame_bitmap, i);
return (void *)(i * 4096);
}
return NULL; /* out of memory */
}
আপনার প্রথম allocator — আর এটা malloc-এর চেয়ে অনেক নিচের স্তরে।
৬. Context switch — চূড়ান্ত মাইলফলক
দুইটা task, প্রতিটার নিজের stack:
switch_task: ; void switch_task(uint64_t **old_sp, uint64_t *new_sp)
push rbp
push rbx
push r12
push r13
push r14
push r15
mov [rdi], rsp ; পুরনো stack pointer সংরক্ষণ
mov rsp, rsi ; নতুনটায় switch
pop r15
pop r14
pop r13
pop r12
pop rbx
pop rbp
ret ; নতুন task-এর return address-এ যাবে
মাত্র ১৪টা instruction — আর এটাই multitasking-এর পুরো রহস্য।
ret যখন নতুন stack থেকে address তুলবে, CPU অন্য task-এ
চলে যাবে, আর তার কোনো ধারণাই থাকবে না যে কিছু ঘটেছে।
Timer handler থেকে এটা ডাকলে preemptive scheduling হয়ে গেল।
Debugging
qemu-system-x86_64 -cdrom kernel.iso -s -S &
gdb kernel.elf
(gdb) target remote :1234
(gdb) break kmain
(gdb) continue
আর QEMU-র নিজের tool গুলো অপরিহার্য:
qemu-system-x86_64 -cdrom kernel.iso -d int,cpu_reset -no-reboot -no-shutdown
-d int প্রতিটা interrupt ছাপে, -no-reboot triple fault-এ
রিবুট না করে থামে — নাহলে আপনি শুধু অসীম রিবুট দেখবেন আর
কারণ জানবেন না।
| লক্ষণ | সাধারণ কারণ |
|---|---|
| অসীম রিবুট | Triple fault — সাধারণত GDT/IDT ভুল |
| কিছুই হয় না | Multiboot header ভুল বা ভুল জায়গায় |
| র্যান্ডম corruption | red zone (-mno-red-zone দিন) |
| Interrupt আসে না | PIC mask, বা sti করেননি |
| প্রথম interrupt-এর পর crash | stub-এ register সংরক্ষণ করেননি |
নিজেকে চ্যালেঞ্জ করুন
- User mode — ring 3-এ একটা program চালান, আর একটা সত্যিকারের syscall interface বানান
- ELF loader — একটা compiled binary memory-তে load করে চালান
- একটা kernel heap (
kmalloc) আপনার page allocator-এর উপর - Filesystem — একটা RAM disk, বা FAT12 পড়া
- SMP — অন্য CPU core জাগান (APIC IPI দিয়ে)
যেখানে সাহায্য পাবেন
- OSDev Wiki (
wiki.osdev.org) — অপরিহার্য, প্রায় প্রতিটা সমস্যার উত্তর সেখানে আছে - Philipp Oppermann-এর “Writing an OS in Rust” — Rust-এ, কিন্তু ধারণাগুলো একই আর ব্যাখ্যা অসাধারণ
- xv6 — MIT-র teaching OS, ছোট আর পড়ার যোগ্য। আটকে গেলে দেখুন তারা কীভাবে করেছে
- Intel SDM Volume 3 — protected mode, paging আর interrupt-এর প্রামাণ্য উৎস
এরপর কোথায়
এই প্রজেক্টের পর Level 12-এর Container from Scratch প্রজেক্টটা আশ্চর্যজনকভাবে সহজ মনে হবে — কারণ আপনি জানবেন namespace আর cgroup আসলে সেই kernel-এর ভেতরে কী কী বদলাচ্ছে যেটা আপনি নিজে হাতে বানিয়েছেন।