Advanced OS Customizations & Bootloaders

Building a Micro-Kernel: How to Hand-Off Control from Your x86 Bootloader

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction: The Bootloader’s Critical Role

In the intricate world of operating system development, the bootloader stands as the unsung hero, the very first piece of software to execute and set the stage for your kernel. For x86 systems, this process involves a carefully choreographed dance from the BIOS to a minimal boot sector, ultimately transitioning the CPU from a restricted real mode into the powerful protected mode required by modern kernels. This article will guide you through building a basic x86 bootloader from scratch, focusing specifically on the crucial steps of setting up protected mode and handing off control to your micro-kernel.

Understanding the x86 Boot Process

When an x86 machine powers on, the BIOS (Basic Input/Output System) performs POST (Power-On Self-Test) and then searches for a bootable device. Upon finding one, it loads the first 512 bytes of that device (the Master Boot Record or MBR, or a Volume Boot Record for partitions) into memory address 0x7c00 and jumps to it. This 512-byte sector is our bootloader’s initial entry point, executing in 16-bit real mode. Real mode offers direct access to physical memory but is limited to 1MB and lacks crucial features like memory protection and paging. To build a robust micro-kernel, we must transition to 32-bit protected mode.

Phase 1: The Real Mode Boot Sector

Our boot sector’s primary responsibilities in real mode are to initialize basic registers, enable the A20 line (to access memory beyond 1MB), load the kernel into memory, and prepare for the mode switch. Here’s a simplified assembly snippet for a boot sector:

org 0x7c00                ; Bootloader starts at 0x7c00
bits 16                   ; We are in 16-bit real mode

jmp short start           ; Jump to actual code
nop

start:
    mov ax, 0x0000        ; Set up data segments
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7c00        ; Stack grows downwards from bootloader start

    ; --- Enable A20 Line (various methods, here using BIOS) ---
    ; A more robust method would involve Keyboard Controller
    mov ax, 0x2401        ; AX = Enable A20
    int 0x15
    jc .a20_error

    ; --- Load Kernel (example: loading from disk) ---
    ; For simplicity, assume kernel is 1 sector after bootloader (LBA 1)
    ; And it's loaded to address 0x100000 (1MB)
    mov ah, 0x42          ; AH = Extended Read
    mov dl, 0x80          ; Drive number (e.g., first hard disk)
    mov si, boot_packet   ; DS:SI points to Disk Address Packet
    int 0x13              ; Call BIOS disk service
    jc .disk_error

    ; --- Setup GDT and Switch to Protected Mode ---
    cli                   ; Disable interrupts
    lgdt [gdt_descriptor] ; Load GDT register
    mov eax, cr0          ; Enable protected mode bit
    or eax, 0x1
    mov cr0, eax

    ; Far jump to flush instruction pipeline and enter protected mode
    jmp 0x08:protected_mode_start ; Selector 0x08 (code segment) to protected_mode_start

.a20_error:
    ; Handle A20 error
    jmp $
.disk_error:
    ; Handle disk read error
    jmp $

; --- Data Structures ---
boot_packet:
    dw 16                   ; Size of DAP (16 bytes)
    dw 1                    ; Number of sectors to read
    dd 0x100000             ; Target memory address (1MB)
    dd 1                    ; LBA start address (sector 1)

; --- GDT Setup (defined later) ---
gdt_start:
    dq 0x0                  ; Null descriptor
gdt_code:
    dw 0xffff               ; Limit 0-1MB (low)
    dw 0x0000               ; Base 0-1MB (low)
    db 0x00                 ; Base (middle)
    db 10011010b            ; Access: P=1, DPL=0, S=1, Type=Code, R/W=1, A=0
    db 11001111b            ; Granularity=4K, Size=32bit, Limit (high), Base (high)
gdt_data:
    dw 0xffff
    dw 0x0000
    db 0x00
    db 10010010b            ; Access: P=1, DPL=0, S=1, Type=Data, R/W=1, A=0
    db 11001111b
gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start - 1 ; GDT Limit
    dd gdt_start               ; GDT Base Address

    times 510 - ($ - $$) db 0  ; Fill with zeros until 510 bytes
    dw 0xaa55                 ; Boot signature

Phase 2: Entering Protected Mode

Protected mode provides 32-bit (or 64-bit) addressing, memory segmentation and paging, and hardware-enforced protection. The transition primarily involves setting up a Global Descriptor Table (GDT) and manipulating the `CR0` control register. The GDT defines memory segments, including their base address, limit, and access rights. Our example GDT defines two segments: a 32-bit code segment and a 32-bit data segment, both covering the entire 4GB address space for simplicity.

bits 32                   ; Now in 32-bit protected mode
protected_mode_start:
    mov ax, 0x10          ; Load data segment selector (0x10 for gdt_data)
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x90000      ; Setup a new stack in high memory

    ; --- Jump to Kernel Entry Point ---
    ; The kernel is assumed to be loaded at 0x100000
    call KERNEL_ENTRY_POINT ; Call the kernel's main function

    ; If kernel returns, halt system
    hlt

In this 32-bit segment, we load the segment registers with our data segment selector (0x10 refers to the 2nd entry in the GDT, `gdt_data`). We also set up a new stack in a higher memory region, crucial since the real mode stack at 0x7c00 will be overwritten or too low. Finally, we make a `call` to our kernel’s entry point, which we’ve arbitrarily named `KERNEL_ENTRY_POINT` for now.

Phase 3: Loading Your Micro-Kernel

The `int 0x13` BIOS call in the real mode boot sector is used to load our kernel. In a more advanced bootloader, you’d parse a file system (like FAT32 or Ext2) to find and load your kernel binary. For this example, we’re assuming the kernel is a raw binary (`.bin` file) placed immediately after the bootloader on the disk, loaded to a specific high memory address (e.g., 0x100000 or 1MB).

Your kernel’s entry point should be designed to handle the state of the CPU after the hand-off:

  • CPU is in 32-bit protected mode.
  • GDT is loaded.
  • Segment registers (CS, DS, ES, FS, GS, SS) are configured.
  • Stack pointer (ESP) is initialized.
  • Interrupts are disabled (`cli` was used before GDT load).

A minimal C kernel might look like this:

// kernel.c
void kmain(void)
{
    char* video_memory = (char*)0xb8000; // Text mode video memory
    *video_memory = 'H';
    *(video_memory + 1) = 0x0F; // White on black
    video_memory += 2;
    *video_memory = 'e';
    *(video_memory + 1) = 0x0F;
    video_memory += 2;
    *video_memory = 'l';
    *(video_memory + 1) = 0x0F;
    video_memory += 2;
    *video_memory = 'l';
    *(video_memory + 1) = 0x0F;
    video_memory += 2;
    *video_memory = 'o';
    *(video_memory + 1) = 0x0F;

    while(1) { ; } // Kernel halts
}

To make this C function the `KERNEL_ENTRY_POINT`, you need to compile it and link it at the specified load address. The `call KERNEL_ENTRY_POINT` in assembly should refer to the address of `kmain` in your kernel binary.

Phase 4: The Hand-off

The `call KERNEL_ENTRY_POINT` instruction is the ultimate hand-off. It pushes the return address onto the stack (the address of the `hlt` instruction in our bootloader) and jumps to the kernel’s entry point. From this moment, your kernel code is executing, taking full control of the system. The kernel can then initialize its own data structures, set up an Interrupt Descriptor Table (IDT), configure paging, and begin managing system resources.

Building and Testing Your OS

Here’s how you can compile and link these components:

  1. **Compile Bootloader**: Use NASM to assemble the bootloader.nasm -f bin bootloader.asm -o bootloader.bin
  2. **Compile Kernel**: Use GCC (cross-compiler recommended) for your kernel.i686-elf-gcc -m32 -ffreestanding -c kernel.c -o kernel.o
  3. **Link Kernel**: Link the kernel object file. Ensure the entry point and base address are correct.i686-elf-ld -m elf_i386 -Ttext 0x100000 --oformat binary kernel.o -o kernel.bin (The `0x100000` matches our load address).
  4. **Create Disk Image**: Combine the bootloader and kernel.cat bootloader.bin kernel.bin > os.img
  5. **Run in QEMU**: Test your creation in a virtual machine.qemu-system-i386 -fda os.img

If all goes well, QEMU will launch, and you should see

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner