Skip to content

OS Development Basics Kernel

Từ Power-On đến Hello World trong Kernel

1. Boot Process Overview

BIOS/UEFI → Kernel

                         BOOT SEQUENCE
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   Power On                                                      │
│      │                                                          │
│      ▼                                                          │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                    BIOS / UEFI                          │   │
│   │  • POST (Power-On Self-Test)                            │   │
│   │  • Initialize hardware                                  │   │
│   │  • Find boot device                                     │   │
│   │  • Load bootloader to 0x7C00                            │   │
│   └───────────────────────────┬─────────────────────────────┘   │
│                               │                                 │
│                               ▼                                 │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                    Bootloader                           │   │
│   │  • Switch to 32-bit Protected Mode                      │   │
│   │  • Enable paging (optional)                             │   │
│   │  • Load kernel to memory                                │   │
│   │  • Jump to kernel entry point                           │   │
│   └───────────────────────────┬─────────────────────────────┘   │
│                               │                                 │
│                               ▼                                 │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                    Kernel (Rust)                        │   │
│   │  • Setup stack                                          │   │
│   │  • Initialize IDT                                       │   │
│   │  • Setup paging                                         │   │
│   │  • Initialize drivers                                   │   │
│   │  • Start scheduler                                      │   │
│   └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Project Structure

my_os/
├── Cargo.toml
├── x86_64-my_os.json      # Custom target
├── .cargo/
│   └── config.toml        # Build settings
├── src/
│   ├── main.rs            # Kernel entry
│   ├── vga_buffer.rs      # VGA text mode
│   └── interrupts.rs      # IDT setup
└── bootloader/            # Or use bootloader crate

2. Inline Assembly

Syntax cơ bản

rust
use core::arch::asm;

/// Read from I/O port
unsafe fn inb(port: u16) -> u8 {
    let value: u8;
    asm!(
        "in al, dx",
        in("dx") port,
        out("al") value,
        options(nomem, nostack)
    );
    value
}

/// Write to I/O port
unsafe fn outb(port: u16, value: u8) {
    asm!(
        "out dx, al",
        in("dx") port,
        in("al") value,
        options(nomem, nostack)
    );
}

CPU Control Instructions

rust
/// Halt CPU until next interrupt
pub fn hlt() {
    unsafe {
        asm!("hlt", options(nomem, nostack));
    }
}

/// Disable interrupts
pub fn cli() {
    unsafe {
        asm!("cli", options(nomem, nostack));
    }
}

/// Enable interrupts
pub fn sti() {
    unsafe {
        asm!("sti", options(nomem, nostack));
    }
}

/// Read CR3 (page table base)
pub fn read_cr3() -> u64 {
    let value: u64;
    unsafe {
        asm!("mov {}, cr3", out(reg) value, options(nomem, nostack));
    }
    value
}

/// Load IDT
pub unsafe fn lidt(idtr: &DescriptorTablePointer) {
    asm!("lidt [{}]", in(reg) idtr, options(readonly, nostack));
}

Stack Setup

rust
// Trong assembly boot code
core::arch::global_asm!(r#"
.section .bss
.align 16
stack_bottom:
    .space 16384    # 16 KB stack
stack_top:

.section .text
.global _start
_start:
    # Setup stack
    mov rsp, offset stack_top
    
    # Call Rust entry point
    call kernel_main
    
    # Should never return
1:  hlt
    jmp 1b
"#);
)

3. Interrupt Descriptor Table (IDT)

IDT Structure

                    INTERRUPT DESCRIPTOR TABLE
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   Entry 0:  Division Error (#DE)                                │
│   Entry 1:  Debug (#DB)                                         │
│   Entry 2:  NMI                                                 │
│   Entry 3:  Breakpoint (#BP)                                    │
│   ...                                                           │
│   Entry 13: General Protection Fault (#GP)                      │
│   Entry 14: Page Fault (#PF)                                    │
│   ...                                                           │
│   Entry 32-255: User-defined interrupts                         │
│                                                                 │
│   Each entry (16 bytes in x86_64):                              │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │ Offset 0:15 │ Selector │ IST │ Type│ Offset 16:31      │   │
│   ├─────────────────────────────────────────────────────────┤   │
│   │ Offset 32:63                │ Reserved                  │   │
│   └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

IDT Implementation

rust
use core::arch::asm;

/// IDT Entry (Gate Descriptor)
#[repr(C, packed)]
#[derive(Clone, Copy)]
pub struct IdtEntry {
    offset_low: u16,
    selector: u16,
    ist: u8,
    type_attr: u8,
    offset_mid: u16,
    offset_high: u32,
    reserved: u32,
}

impl IdtEntry {
    pub const fn missing() -> Self {
        Self {
            offset_low: 0,
            selector: 0,
            ist: 0,
            type_attr: 0,
            offset_mid: 0,
            offset_high: 0,
            reserved: 0,
        }
    }
    
    pub fn set_handler(&mut self, handler: extern "x86-interrupt" fn()) {
        let addr = handler as u64;
        self.offset_low = addr as u16;
        self.offset_mid = (addr >> 16) as u16;
        self.offset_high = (addr >> 32) as u32;
        self.selector = 0x08; // Code segment
        self.type_attr = 0x8E; // Present, DPL=0, Interrupt Gate
        self.ist = 0;
        self.reserved = 0;
    }
}

/// IDT Table
#[repr(C, align(16))]
pub struct Idt {
    entries: [IdtEntry; 256],
}

impl Idt {
    pub const fn new() -> Self {
        Self {
            entries: [IdtEntry::missing(); 256],
        }
    }
    
    pub fn load(&'static self) {
        let ptr = DescriptorTablePointer {
            limit: (core::mem::size_of::<Self>() - 1) as u16,
            base: self as *const _ as u64,
        };
        
        unsafe {
            asm!("lidt [{}]", in(reg) &ptr, options(readonly, nostack));
        }
    }
}

#[repr(C, packed)]
struct DescriptorTablePointer {
    limit: u16,
    base: u64,
}

// Interrupt handlers
extern "x86-interrupt" fn divide_error_handler() {
    panic!("EXCEPTION: Divide Error");
}

extern "x86-interrupt" fn page_fault_handler() {
    panic!("EXCEPTION: Page Fault");
}

// Setup IDT
static mut IDT: Idt = Idt::new();

pub fn init_idt() {
    unsafe {
        IDT.entries[0].set_handler(divide_error_handler);
        IDT.entries[14].set_handler(page_fault_handler);
        IDT.load();
    }
}

4. VGA Text Mode

Memory Layout

                    VGA TEXT BUFFER (0xB8000)
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   Address: 0xB8000 - 0xB8FA0 (80 × 25 × 2 bytes = 4000 bytes)  │
│                                                                 │
│   Each character cell (2 bytes):                                │
│   ┌────────────┬────────────┐                                   │
│   │  Character │  Attribute │                                   │
│   │   (ASCII)  │  (color)   │                                   │
│   └────────────┴────────────┘                                   │
│       Byte 0       Byte 1                                       │
│                                                                 │
│   Attribute byte:                                               │
│   ┌───┬───┬───┬───┬───┬───┬───┬───┐                             │
│   │ 7 │ 6 │ 5 │ 4 │ 3 │ 2 │ 1 │ 0 │                             │
│   └───┴───┴───┴───┴───┴───┴───┴───┘                             │
│   │   └─────┬─────┘ └─────┬─────┘                               │
│   │      Background    Foreground                               │
│   └── Blink                                                     │
│                                                                 │
│   Colors: 0=Black 1=Blue 2=Green 3=Cyan 4=Red 5=Magenta        │
│           6=Brown 7=LightGray 8-15=Bright versions              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

VGA Writer Implementation

rust
use core::fmt;
use core::ptr::write_volatile;

const VGA_BUFFER: *mut u8 = 0xB8000 as *mut u8;
const VGA_WIDTH: usize = 80;
const VGA_HEIGHT: usize = 25;

#[repr(u8)]
#[derive(Clone, Copy)]
pub enum Color {
    Black = 0,
    Blue = 1,
    Green = 2,
    Cyan = 3,
    Red = 4,
    Magenta = 5,
    Brown = 6,
    LightGray = 7,
    DarkGray = 8,
    LightBlue = 9,
    LightGreen = 10,
    LightCyan = 11,
    LightRed = 12,
    Pink = 13,
    Yellow = 14,
    White = 15,
}

pub struct VgaWriter {
    col: usize,
    row: usize,
    color: u8,
}

impl VgaWriter {
    pub const fn new() -> Self {
        Self {
            col: 0,
            row: 0,
            color: (Color::Black as u8) << 4 | (Color::LightGreen as u8),
        }
    }
    
    pub fn write_byte(&mut self, byte: u8) {
        match byte {
            b'\n' => self.new_line(),
            byte => {
                if self.col >= VGA_WIDTH {
                    self.new_line();
                }
                
                let offset = (self.row * VGA_WIDTH + self.col) * 2;
                unsafe {
                    write_volatile(VGA_BUFFER.add(offset), byte);
                    write_volatile(VGA_BUFFER.add(offset + 1), self.color);
                }
                self.col += 1;
            }
        }
    }
    
    fn new_line(&mut self) {
        self.col = 0;
        if self.row < VGA_HEIGHT - 1 {
            self.row += 1;
        } else {
            self.scroll();
        }
    }
    
    fn scroll(&mut self) {
        // Move all lines up by 1
        for row in 1..VGA_HEIGHT {
            for col in 0..VGA_WIDTH {
                let src = (row * VGA_WIDTH + col) * 2;
                let dst = ((row - 1) * VGA_WIDTH + col) * 2;
                unsafe {
                    let char = *VGA_BUFFER.add(src);
                    let attr = *VGA_BUFFER.add(src + 1);
                    write_volatile(VGA_BUFFER.add(dst), char);
                    write_volatile(VGA_BUFFER.add(dst + 1), attr);
                }
            }
        }
        
        // Clear last line
        self.clear_row(VGA_HEIGHT - 1);
    }
    
    fn clear_row(&mut self, row: usize) {
        for col in 0..VGA_WIDTH {
            let offset = (row * VGA_WIDTH + col) * 2;
            unsafe {
                write_volatile(VGA_BUFFER.add(offset), b' ');
                write_volatile(VGA_BUFFER.add(offset + 1), self.color);
            }
        }
    }
    
    pub fn clear(&mut self) {
        for row in 0..VGA_HEIGHT {
            self.clear_row(row);
        }
        self.col = 0;
        self.row = 0;
    }
}

impl fmt::Write for VgaWriter {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        for byte in s.bytes() {
            self.write_byte(byte);
        }
        Ok(())
    }
}

// Global writer
use spin::Mutex;
pub static WRITER: Mutex<VgaWriter> = Mutex::new(VgaWriter::new());

// print! macro
#[macro_export]
macro_rules! print {
    ($($arg:tt)*) => {{
        use core::fmt::Write;
        WRITER.lock().write_fmt(format_args!($($arg)*)).unwrap();
    }};
}

#[macro_export]
macro_rules! println {
    () => (print!("\n"));
    ($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*)));
}

5. Complete Minimal Kernel

rust
#![no_std]
#![no_main]
#![feature(abi_x86_interrupt)]

use core::panic::PanicInfo;

mod vga_buffer;
mod interrupts;

#[no_mangle]
pub extern "C" fn kernel_main() -> ! {
    // Clear screen
    vga_buffer::WRITER.lock().clear();
    
    // Print hello
    println!("Hello from Rust Kernel!");
    println!("=====================================");
    
    // Initialize IDT
    interrupts::init_idt();
    println!("[OK] IDT initialized");
    
    // Enable interrupts
    unsafe { core::arch::asm!("sti"); }
    println!("[OK] Interrupts enabled");
    
    println!("\nKernel running. Halting CPU...");
    
    loop {
        unsafe { core::arch::asm!("hlt"); }
    }
}

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    println!("\n!!! KERNEL PANIC !!!");
    println!("{}", info);
    loop {
        unsafe { core::arch::asm!("hlt"); }
    }
}

Build & Run

toml
# Cargo.toml
[package]
name = "my_os"
version = "0.1.0"
edition = "2021"

[dependencies]
bootloader = "0.9"
spin = "0.9"

[profile.release]
panic = "abort"
lto = true
bash
# Build
cargo build --release --target x86_64-unknown-none

# Run with QEMU
qemu-system-x86_64 -drive format=raw,file=target/x86_64-unknown-none/release/bootimage-my_os.bin

🎯 OS Dev Checklist

StepComponent
1Bootloader (use bootloader crate)
2VGA text mode output
3Serial port debugging
4IDT for CPU exceptions
5Keyboard driver
6Memory management
7Process scheduling

💡 RESOURCE

Xem thêm: Writing an OS in Rust - Tutorial series xuất sắc.