Skip to content

Bootloader

Galfurian edited this page Aug 19, 2026 · 4 revisions

The first MentOS code executed when the system starts.

What this page is (and is not) about

It is worth being precise about the word "bootloader", because two different things are usually called that:

Who What it does
External boot loader GRUB, QEMU's -kernel, or any Multiboot-compliant loader Runs from the firmware, starts in 16-bit real mode, switches the CPU to 32-bit protected mode, loads the MentOS image, and jumps to it
MentOS bootstrap (boot/) This page Runs already in 32-bit protected mode, sets up bootstrap paging, relocates the kernel, and hands over to kmain()

MentOS does not ship a real-mode boot sector. boot/src/boot.S opens with bits 32 and never executes a single real-mode instruction.

Overview

The MentOS bootstrap is responsible for:

  1. Minimal CPU setup - Establish a stack and enter C code
  2. Multiboot Handling - Use the Multiboot header/info provided by GRUB/QEMU
  3. Kernel Relocation & Paging - Map low memory and relocate the embedded kernel ELF
  4. Kernel Transfer - Jump to the kernel entry point (kmain())

Location

boot/
├── src/
│   ├── boot.S         ← Assembly entry point
│   ├── boot.c         ← Main bootloader logic
│   └── multiboot.c    ← Multiboot spec handling
└── linker/
    ├── boot.lds       ← Bootloader linker script
    └── kernel.lds     ← Kernel linker script

Boot Sequence

1. A Multiboot Loader Loads bootloader.bin

  • The firmware starts GRUB (or QEMU loads the image directly); GRUB does the real-mode → protected-mode transition
  • GRUB loads bootloader.bin at the address given by boot/linker/boot.lds (0x00100000)
  • Execution begins at boot_entry (defined in boot.S) with the CPU in 32-bit protected mode, EAX = 0x2BADB002 and EBX pointing at the Multiboot information structure

2. Assembly Entry (boot.S)

; boot/src/boot.S
bits 32             ; <- everything below is 32-bit code; no real mode anywhere

section .text
global boot_entry
boot_entry:
    ; Disable interrupts
    cli
    
    ; Set up stack
    mov esp, stack_top

    ; Pass arguments to boot_main(magic, header, esp)
    push esp  ; initial stack pointer
    push ebx  ; multiboot info structure
    push eax  ; multiboot magic number
    
    ; Call C code
    call boot_main
    
    ; Halt if boot_main returns
    cli
.hang:
    hlt
    jmp .hang

Actions:

  • Disables interrupts (cli)
  • Sets esp to stack_top. The stack is reserved in .bss by KERNEL_STACK_SIZE equ 0x400000, i.e. 4 MiB
  • Pushes boot parameters (magic, header, initial stack)
  • Calls boot_main()

3. Bootloader Main (boot.c)

In boot/src/boot.c, boot_main() performs the boot-time setup needed to transfer control to the kernel:

  • Parses the header of the embedded kernel ELF (kernel.bin is linked into bootloader.bin as a binary blob) and walks its PT_LOAD program headers to find the kernel's lowest and highest virtual address (__get_kernel_low_high())
  • Builds a boot_info_t structure (kernel bounds, modules end, memory layout)
  • Sets up bootstrap paging mappings (identity-maps low memory and maps the kernel at its virtual base)
  • Loads CR3 and sets CR0.PGpaging is on before the kernel gets control
  • Relocates the embedded kernel ELF to its virtual addresses
  • Calls boot_kernel(stack, entry, &boot_info) to jump to the kernel

Note: GDT/IDT setup happens later inside the kernel (see kernel/src/descriptor_tables/). The GDT in effect while the bootstrap runs is the one GRUB left behind.

A MentOS-specific bound to be aware of: boot_main() sets

boot_info.lowmem_phy_end = 896 * 1024 * 1024; // 896 MB of low memory max

The 896 MiB figure is a MentOS implementation limit (it matches the 896 MiB KERNEL_LOWMEM region declared in boot/linker/kernel.lds), not an architectural property of x86. Physical memory above it is tracked as "high memory".

4. Transfer to Kernel

// In boot.S: boot_kernel(stack, entry, boot_info)
// Sets ESP, pushes boot_info, and calls kernel entry

Multiboot Specification

MentOS uses the Multiboot specification to be compatible with GRUB and other bootloaders.

Multiboot Header

boot.S is assembled with NASM, so the header is written in NASM syntax:

; boot/src/boot.S
section .multiboot_header
align 4
multiboot_header:
    dd MULTIBOOT_HEADER_MAGIC
    dd MULTIBOOT_HEADER_FLAGS
    dd MULTIBOOT_CHECKSUM

Constants (verbatim from boot/src/boot.S):

  • MULTIBOOT_HEADER_MAGIC = 0x1BADB002 — what the image contains, so the loader can find it
  • MULTIBOOT_HEADER_FLAGS = MULTIBOOT_PAGE_ALIGN | MULTIBOOT_MEMORY_INFO = 0x00000003 (4 KiB module alignment + memory information). MULTIBOOT_VIDEO_MODE (0x4) is defined in the file but deliberately not requested.
  • MULTIBOOT_CHECKSUM = -(MAGIC + FLAGS), so the three fields sum to zero mod 2³²
  • MULTIBOOT_BOOTLOADER_MAGIC = 0x2BADB002 — what the loader leaves in EAX. Do not confuse the two: kmain() checks this second value and refuses to boot if it differs.

Multiboot Information

The bootloader receives a multiboot_info_t from GRUB (see kernel/inc/multiboot.h):

typedef struct multiboot_info {
    uint32_t flags;           // Available info
    uint32_t mem_lower;       // Lower memory (KB)
    uint32_t mem_upper;       // Upper memory (KB)
    uint32_t boot_device;     // Boot device
    uint32_t cmdline;         // Kernel command line
    uint32_t mods_count;      // Number of modules
    uint32_t mods_addr;       // Modules address
    // ... more fields
} multiboot_info_t;

Information Provided:

  • Memory map (physical RAM available)
  • Boot device
  • Kernel command line
  • Loaded modules (if any)
  • Framebuffer information

Boot-Time Paging

__setup_boot_paging() in boot/src/boot.c builds a page directory with two mappings of the same low physical memory:

  • an identity mapping (virtual == physical), so the bootstrap code — which is linked at 0x00100000 — keeps executing correctly the instant CR0.PG is set;
  • a high mapping at the kernel's virtual base (0xC0000000), so the kernel's link-time addresses become valid.

Paging is then enabled (boot_paging_switch_pgd() loads CR3, boot_paging_enable() clears CR4.PSE and sets CR0.PG) before the kernel is entered.

Do not confuse this with the kernel's paging subsystem. These bootstrap tables are a throwaway, statically allocated scaffold. Much later, kmain() calls paging_init(), which builds the kernel's real, allocator-backed page directory and takes over. Both are "paging", but they are different code owning different data structures — see Kernel.

Build Process

Compilation

Use the CMake targets:

cd build
make bootloader.bin

Output

build/mentos/bootloader.bin contains:

  • Bootloader code
  • Embedded kernel.bin (as a linked binary blob)

Linker Scripts

Boot Linker Script (boot.lds)

Key excerpts (see boot/linker/boot.lds for full script):

ENTRY(boot_entry)

. = 0x00100000;
_bootloader_start = .;

.multiboot : {
    *(.multiboot_header)
}

.text : { *(.text) }
.rodata : { *(.rodata*) }
.data : { *(.data) }
.bss  : { *(.bss*) }

_bootloader_end = .;

Kernel Linker Script (kernel.lds)

Links the standalone kernel.bin (used by kernel compilation).

Key Functions

boot_main()

Main bootloader entry point from assembly.

Parameters:

  • magic: Multiboot bootloader magic (0x2BADB002 expected)
  • header: Pointer to multiboot information structure
  • esp: Initial stack pointer

Actions:

  1. Build boot_info_t (kernel bounds, modules end, memory layout)
  2. Set up boot-time paging
  3. Relocate the kernel ELF image
  4. Call boot_kernel()

__setup_boot_paging()

Creates the bootstrap page tables used during early boot (identity mapping + kernel high mapping).

__relocate_kernel_image()

For each PT_LOAD program header, copies min(filesz, memsz) bytes from the embedded image to the segment's virtual address, then writes zeroes over the remaining memsz - filesz bytes. That second step is what makes the kernel's .bss start out zeroed — .bss occupies space in memory (memsz) but stores nothing in the file (filesz).

boot_kernel()

Assembly helper that switches the stack pointer and calls the kernel entry.

Debugging Bootloader

Using GDB

# Terminal 1
make qemu-gdb

# Terminal 2 (from build/)
gdb --quiet --command=gdb.run

Debug Output

Add debug prints (serial console):

// In boot.c
__debug_puts("[bootloader] Start...\n");

Inspect with objdump

objdump -d build/mentos/bootloader.bin | less

Common Issues

"Multiboot magic invalid"

  • GRUB didn't load properly
  • Multiboot header is missing or incorrect
  • Check .multiboot_header section in assembly

"Kernel not found"

  • Kernel binary wasn't embedded correctly
  • Check linker script
  • Verify kernel.bin.o creation

"Page fault during boot"

  • Paging setup incorrect
  • Identity mapping missing
  • Check page table entries

"Triple fault" (QEMU resets)

  • CPU exception during boot
  • Usually GDT or paging issue
  • Use make qemu-gdb to catch early

References

Next Steps


Previous: Architecture | Next: Kernel

Clone this wiki locally