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"
}

Toggling paging is just like toggling every other CPU feature, 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 of our page tables.

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 guides 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’s initialize our page tables.

pub const IDENTITY_PAGE_TABLE_L4_OFFSET: usize = 0xb000;

pub const IDENTITY_PAGE_TABLE_L3_OFFSET: usize = 0xc000;

pub const IDENTITY_PAGE_TABLE_L2_OFFSET: usize = 0xd000;

fn init_identity_tables() -> Option<&'static mut PageTable> {
    // These tables will hold the initial identity mapping
    let identity_page_table_l4: &mut PageTable = unsafe {
        PageTable::empty_from_ptr(page_table_ptr: IDENTITY_PAGE_TABLE_L4_OFFSET.into())? NonNull<PageTable>
            .as_mut()
    };
    let identity_page_table_l3: &mut PageTable = unsafe {
        PageTable::empty_from_ptr(page_table_ptr: IDENTITY_PAGE_TABLE_L3_OFFSET.into())? NonNull<PageTable>
            .as_mut()
    };
    let identity_page_table_l2: &mut PageTable = unsafe {
        PageTable::empty_from_ptr(page_table_ptr: IDENTITY_PAGE_TABLE_L2_OFFSET.into())? NonNull<PageTable>
            .as_mut()
    };

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

    Some(identity_page_table_l4)
} fn init_identity_tables

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 than the normal 4Kib size, and it is used in the case that we want to map the entire level below 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 set on an L2 or L3 table, and it is not supported on older CPUs. On an L2 table, the resulting page size is 2 MiB (4 KiB × 512 entries), and on an L3 table it is 1 GiB (2 MiB × 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.

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

        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",
        );
    }
} fn set_pae_long_mode

After that, we can finally turn on paging!

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

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

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 {
    /// Creates default global descriptor table for long
    /// mode
    // ANCHOR: gdt_long_default
    pub const fn default() -> Self {
        Self {
            null: GlobalDescriptorTableEntry32::default(),
            kernel_code: GlobalDescriptorTableEntry32::new(
                base: 0,
                limit: 0,
                access_byte: AccessByte::new() AccessByte
                    .segment_type(SegmentDescriptorType::CodeOrData) AccessByte
                    .present(true) AccessByte
                    .dpl(ProtectionLevel::Ring0) AccessByte
                    .readable_writable(true) AccessByte
                    .executable(true),
                flags: LimitFlags::new().long(true),
            ),
            kernel_data: GlobalDescriptorTableEntry32::new(
                base: 0,
                limit: 0,
                access_byte: AccessByte::new() AccessByte
                    .segment_type(SegmentDescriptorType::CodeOrData) AccessByte
                    .present(true) AccessByte
                    .dpl(ProtectionLevel::Ring0) AccessByte
                    .readable_writable(true),
                flags: LimitFlags::new(),
            ),
            user_code: GlobalDescriptorTableEntry32::new(
                base: 0,
                limit: 0,
                access_byte: AccessByte::new() AccessByte
                    .segment_type(SegmentDescriptorType::CodeOrData) AccessByte
                    .present(true) AccessByte
                    .dpl(ProtectionLevel::Ring3) AccessByte
                    .readable_writable(true) AccessByte
                    .executable(true),
                flags: LimitFlags::new().long(true),
            ),
            user_data: GlobalDescriptorTableEntry32::new(
                base: 0,
                limit: 0,
                access_byte: AccessByte::new() AccessByte
                    .segment_type(SegmentDescriptorType::CodeOrData) AccessByte
                    .present(true) AccessByte
                    .dpl(ProtectionLevel::Ring3) AccessByte
                    .readable_writable(true),
                flags: LimitFlags::new(),
            ),
            tss: SystemSegmentDescriptor64::default(),
        }
    } const fn default
} impl GlobalDescriptorTableLong

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 which calls all the functions above sequentially, create and enable paging, load the new long mode GDT, and jump to our kernel.

This can be done with the following code:

#[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")]
    x86::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 {}
} unsafe fn second_stage
  • Enabling memory paging