Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Booting the Kernel

"A small thing. Yet it holds everything together." - J.R.R. Tolkien, paraphrased


In the previous section we talked about memory paging, what it is, and how to initialize page tables. So, logically the only thing that is left to do, is to toggle on paging.

After that we can also toggle long mode, which is another mode in the CPU, just like protected mode which will let us run 64-bit instructions.

Initializing Paging

The code below assumes the following target and linker script
OUTPUT_FORMAT(binary)
ENTRY(second_stage)

SECTIONS {
    . = 0x7c00 + 512;

    .start : { *(.start) }

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

    .fill_32 : {
        FILL(0)
        . = 0x10000;
    }
}

I leave the starting address of the next stage as an exercise for the reader (There is a really good reason to use that address).

Note: The code for using the linker script in the build script is the same as in stage one.

{
  "arch": "x86",
  "cpu": "i686",
  "data-layout": "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-i128:128-f64:32:64-f80:32-n8:16:32-S128",
  "dynamic-linking": false,
  "executables": true,
  "linker-flavor": "ld.lld",
  "linker": "rust-lld",
  "llvm-target": "i686-unknown-none",
  "max-atomic-width": 64,
  "position-independent-executables": false,
  "disable-redzone": true,
  "target-c-int-width": 32,
  "target-pointer-width": 32,
  "target-endian": "little",
  "panic-strategy": "abort",
  "os": "none",
  "vendor": "unknown",
  "relocation-model": "static",
  "features": "+soft-float,-sse,-mmx",
  "rustc-abi": "x86-softfloat"
}

Like every feature of the CPU, to toggle it we just need to flip some bits on some control registers. But, in this case if we were to just toggle paging, our computer will crash instantly because of the following reasons:

  1. Our cr3 register doesn't hold a meaningful address of a valid page table.
  2. Our current addressing assumes addresses are physical, continuous and starting at 0.
  3. We didn't set up any page table.

Problems 1 and 3 are almost the same, because after we set up a page table, we can just set cr3 to hold it's address. But how should we set our initial table? This is where problem 2 helps us. Because until now we used physical address, we want to continue doing that at least until we can create processes. So, with that said, we want to map the start of our virtual address space, to the start of the physical address space, thus creating what is called identity paging.

So firstly, let initialize our page tables.

#[cfg(target_arch = "x86")]
pub fn enable() -> Option<()> {

    // These tables will hold the initial identity mapping
    let identity_page_table_l4 = unsafe {
        PageTable::empty_from_ptr(IDENTITY_PAGE_TABLE_L4_OFFSET.into())?
    };
    let identity_page_table_l3 = unsafe {
        PageTable::empty_from_ptr(IDENTITY_PAGE_TABLE_L3_OFFSET.into())?
    };
    let identity_page_table_l2 = unsafe {
        PageTable::empty_from_ptr(IDENTITY_PAGE_TABLE_L2_OFFSET.into())?
    };

    unsafe {
        // Setup identity paging Mapping address virtual addresses
        // 0x000000-0x1fffff to the same physical addresses.
        identity_page_table_l4.entries[0].map_unchecked(
            PhysicalAddress::new_unchecked(IDENTITY_PAGE_TABLE_L3_OFFSET),
            PageEntryFlags::table_flags(),
        );
        identity_page_table_l3.entries[0].map_unchecked(
            PhysicalAddress::new_unchecked(IDENTITY_PAGE_TABLE_L2_OFFSET),
            PageEntryFlags::table_flags(),
        );
        identity_page_table_l2.entries[0].map_unchecked(
            PhysicalAddress::new_unchecked(0),
            PageEntryFlags::huge_page_flags(),
        );
    }

    unsafe {
        // Set the page table at cr3 register
        asm!(
            // load the address of the 4th page table to cr3
            // so the cpu can access it
            "mov eax, {0}",
            "mov cr3, eax",
            const IDENTITY_PAGE_TABLE_L4_OFFSET
        );
    }

}

After we initialize the table, notice we set the L2 table to hold huge page offset for address 0.

Huge page means it is bigger then the normal 4Kib size, and it is used in the case that we want to map the entire level bellow this table contiguously (eg map 0->0, 4096->4096, 8192->8192 etc..)

Instead of creating multiple tables, and wasting precious memory, we can flag the entry as huge page. which says to the mmu "This entry points to a contiguous memory block and not to a table".

This flag can only be put on a L2 or L3 table and it is not support on older cpu's, on L2 table the resulting page size is 2Mib (4Kib x 512 entries) and on L3 table 1Gib (2Mib * 512 entries)

What is Long Mode?

Just before we will toggle paging on our cpu, we should enter protected mode, to do that, we need to toggle 2 things, the first is called the physical address extension (PAE) which is an extension for protected mode paging, which allows 32bit paging entries to be 64bit, which results in a way to access addresses above 32bit because the page table walker can access the 64bit address on the entries. This extension must be activated to access long mode, which also allows us to have 64bit instructions.

To activate PAE and Long mode, we can use this inline assembly.

#[cfg(target_arch = "x86")]
pub fn enable() -> Option<()> {

    unsafe {
        asm!(
            // Enable Physical Address Extension (number 5) in cr4
            "mov eax, cr4",
            "or eax, 1 << 5",
            "mov cr4, eax",
        );
    }

    unsafe {
        asm!(
            // set long mode bit (number 8) in the Extended Feature
            // Enable Register Model Specific Register
            // (EFER MSR) This register became
            // architectural from amd64 and also adopted by
            // intel, it's number is 0xC0000080
            "mov ecx, 0xC0000080",
            // read the MSR specified in ecx into eax
            "rdmsr",
            "or eax, 1 << 8",
            // write what's in eax to the MSR specified in ecx
            "wrmsr",
        );
    }

}

After that, we can finally turn on paging!

Like the previous features, this also it toggled by a control register, and done via inline assembly

#[cfg(target_arch = "x86")]
pub fn enable() -> Option<()> {

    unsafe {
        // Toggle the paging bit (number 31) in cr0
        asm!("mov eax, cr0", "or eax, 1 << 31", "mov cr0, eax");
    }
    Some(())

}   

Now, to go into long mode, we need to far jump just like in protected mode, with a special global descriptor table. This table will look almost the same as our previous table, the key differences are that the long mode flag replaces the protected mode flag, and that most of the flags are not used because in this mode they are irrelevant.

For now ignore the tss entry, it will be relevant on later chapters

So after the changes the table will look like this:

impl GlobalDescriptorTableLong {

    pub const fn default() -> Self {
        Self {
            null: GlobalDescriptorTableEntry32::empty(),
            kernel_code: GlobalDescriptorTableEntry32::new(
                0,
                0,
                AccessByte::new()
                    .code_or_data()
                    .present()
                    .dpl(ProtectionLevel::Ring0)
                    .writable()
                    .executable(),
                LimitFlags::new().long(),
            ),
            kernel_data: GlobalDescriptorTableEntry32::new(
                0,
                0,
                AccessByte::new()
                    .code_or_data()
                    .present()
                    .dpl(ProtectionLevel::Ring0)
                    .writable(),
                LimitFlags::new(),
            ),
            user_code: GlobalDescriptorTableEntry32::new(
                0,
                0,
                AccessByte::new()
                    .code_or_data()
                    .present()
                    .dpl(ProtectionLevel::Ring3)
                    .writable()
                    .executable(),
                LimitFlags::new().long(),
            ),
            user_data: GlobalDescriptorTableEntry32::new(
                0,
                0,
                AccessByte::new()
                    .code_or_data()
                    .present()
                    .dpl(ProtectionLevel::Ring3)
                    .writable(),
                LimitFlags::new(),
            ),
            tss: SystemSegmentDescriptor64::empty(),
        }
    }

}

Hello Kernel!

After all that initialization we can jump to our kernel main!

All that is left to do is to call the enable function we created to enable paging, load the new long mode GDT, and jump to our kernel.

This can be done with the following code:

static GLOBAL_DESCRIPTOR_TABLE_LONG_MODE: GlobalDescriptorTableLong =
    GlobalDescriptorTableLong::default();


#[unsafe(no_mangle)]
#[unsafe(link_section = ".start")]
#[allow(unsafe_op_in_unsafe_fn)]
#[allow(clippy::missing_safety_doc)]
pub unsafe extern "C" fn second_stage() -> ! {
    // Set data segment register
    asm!("mov eax, 0x10", "mov ds, eax",);
    // Enable paging and load page tables with an identity
    // mapping
    #[cfg(target_arch = "x86")]
    cpu_utils::structures::paging::enable();
    // Load the global descriptor table for long mode
    GLOBAL_DESCRIPTOR_TABLE_LONG_MODE.load();
    // Update global descriptor table to enable long mode
    // and jump to kernel code
    asm!(
        "ljmp ${section}, ${next_stage}",
        section = const Sections::KernelCode as u8,
        next_stage = const KERNEL_OFFSET,
        options(att_syntax)
    );

    #[allow(clippy::all)]
    loop {}
}
  • Enabling memory paging