How does Misaka boot?
All code samples here are licensed under ToaruOS's license (NCSA/University of Illinois Open Source License), see https://github.com/klange/toaruos/blob/master/LICENSE.
File copyright headers
/**
* @file kernel/arch/x86_64/bootstrap.c
* @brief x86-64 entrypoint from bootloader
*
* This is primarily adapted from Toaru's 32-bit mulitboot bootstrap.
* Instead of jumping straight to our C entry point, however, we need
* to (obviously) get ourselves set up for long mode first by setting
* up initial page tables, etc.
*
* @copyright
* This file is part of ToaruOS and is released under the terms
* of the NCSA / University of Illinois License - see LICENSE.md
* Copyright (C) 2021 K. Lange
*/
How can Misaka boot?
Misaka works on both x86_64 and AARCH64 but doesn't share the same boot path across architectures. This page will explain the Misaka boot process, from assembly to desktop.
x86, Stage 1
On x86_64, Misaka uses both multiboot and multiboot2 to boot using any bootloader that supports those protocols. Using these protocols on x86_64, the kernel starts in kernel/arch/x86_64/bootstrap.S.
The beginning of bootstrap.S sets up the multiboot(2) headers:
.section .multiboot
.code32
.extern bss_start
.extern end
.extern phys
/* Multiboot 1 header */
.set MB_MAGIC, 0x1BADB002
.set MB_FLAG_PAGE_ALIGN, 1 << 0
.set MB_FLAG_MEMORY_INFO, 1 << 1
.set MB_FLAG_GRAPHICS, 1 << 2
.set MB_FLAG_AOUT, 1 << 16
.set MB_FLAGS, MB_FLAG_PAGE_ALIGN | MB_FLAG_MEMORY_INFO | MB_FLAG_GRAPHICS | MB_FLAG_AOUT
.set MB_CHECKSUM, -(MB_MAGIC + MB_FLAGS)
/* Multiboot section */
.align 4
multiboot_header:
.long MB_MAGIC
.long MB_FLAGS
.long MB_CHECKSUM
.long multiboot_header /* header_addr */
.long phys /* load_addr */
.long bss_start /* load_end_addr */
.long end /* bss_end_addr */
.long start /* entry_addr */
/* Request linear graphics mode */
.long 0x00000000
.long 1024
.long 768
.long 32
/* Multiboot 2 header */
.set MB2_MAGIC, 0xe85250d6
.set MB2_ARCH, 0
.set MB2_LENGTH, (multiboot2_header_end - multiboot2_header)
.set MB2_CHECKSUM, -(MB2_MAGIC + MB2_ARCH + MB2_LENGTH)
.align 8
multiboot2_header:
.long MB2_MAGIC
.long MB2_ARCH
.long MB2_LENGTH
.long MB2_CHECKSUM
/* Address tag */
.align 8
mb2_tag_addr:
.word 2
.word 0
.long 24
.long multiboot2_header
.long phys
.long bss_start
.long end
/* Entry tag */
.align 8
mb2_tag_entry:
.word 3
.word 0
.long 12
.long start_mbi2
/* Framebuffer tag */
.align 8
mb2_tag_fb:
.word 5
.word 0
.long 20
.long 1024
.long 768
.long 32
.align 8
.word 4
.word 1
.long 12
.long 2 /* We support EGA text, but don't require it */
/* Modules must be aligned */
.align 8
.word 6
.word 0
.long 8
/* Relocatable tag */
.align 8
.word 10
.word 0
.long 24
.long 0x100000 /* Start */
.long 0x1000000 /* Maximum load address */
.long 4096 /* Request page alignment */
.long 1 /* Load at lowest available */
/* End tag */
.align 8
.word 0
.word 0
.long 8
multiboot2_header_end:
/* .stack resides in .bss */
.section .stack, "aw", @nobits
stack_bottom:
.skip 16384 /* 16KiB */
.global stack_top
stack_top:
start_mbi2:
/* Use reserved 0 space of boot information struct to thunk eip */
movl %ebx, %ecx
addl $8, %ecx
jmp good_to_go
call _forward. Then, we subtract the link-time address of _forward.
%ecx now contains the relocation offset which can be used to adjust all pointers and page tables so they all work no matter where the kernel is loaded in.
The stack pointer is now set to the top of the reserved .stack, then we adjust stack_top by the relocation offset with addl. Other stack alignment is done to ensure 16-bit alignment, required by x86_64 ABI.
The stack pointer and multiboot(2) arguments are then pushed for the next stage when we jump to jmp_to_long.
_forward:
popl %ecx
subl $_forward, %ecx
/* Setup our stack */
mov $stack_top, %esp
addl %ecx, %esp
/* Make sure our stack is 16-byte aligned */
and $-16, %esp
pushl $0
pushl %esp
pushl $0
pushl %eax /* Multiboot header magic */
pushl $0
pushl %ebx /* Multiboot header pointer */
jmp jmp_to_long
We first extern a pointer to a reserved memory region where we can build the initial page tables. This memory was zeroed out by the bootloader, so we can write page tables entries there.
Next, we load the base address of the reserved page table region into %edi and adjust by the relocation offset which ensures the page tables are at the right address. We save %ecx on the stack for later.
%edi will now point to the PML4 table with the 0x1007 flag. We set the entry to point to the Page Directory Pointer and write the first PML4 entry.
Then we move %edi to where the PDP will live, with 0x1003 flags for entry. Setting up the PDP entry to point to the first Page Directory, then write that PDP entry. PDP[0] points to PD[0] with the flags.
%edi is moved to the first PD entry area with a base flag for 2MiB pages. We're going to create a whopping 32 entries in the PD, each mapping 2MiB for a total of 64MiB of low memory.
jmp_to_long:
.extern init_page_region
/* Set up initial page region, which was zero'd for us by the loader */
mov $init_page_region, %edi
addl %ecx, %edi
pushl %ecx
/* PML4[0] = &PDP[0] | (PRESENT, WRITABLE, USER) */
mov $0x1007, %eax
add %edi, %eax
mov %eax, (%edi)
/* PDP[0] = &PD[0] | (PRESENT, WRITABLE, USER) */
add $0x1000, %edi
mov $0x1003, %eax
add %edi, %eax
mov %eax, (%edi)
/* Set 32 2MiB pages to map 64MiB of low memory temporarily, which should
be enough to get us through our C MMU initialization where we then
use 2MiB pages to map all of the 4GiB standard memory space and map
a much more restricted subset of the kernel in the lower address space. */
add $0x1000, %edi
mov $0x87, %ebx
mov $32, %ecx
2MiB is added into %edi and moves to the next physical region. 8 is added into %edi which moves to the next 64-bit entry. loop decrements %ecx and repeats until all 32 entries are filled.
%edi is then reset to the start of the PML4 table. %cr3 is the CPU register that contains PML4's physical address. The CPU is now aware of the new page tables. The %ecx offset is also reapplied to keep memory addresses correct.
PAE mode allows the CPU to use 64-bit page table entries and access >4GiB of memory, required for long mode. or $32 sets bit 5 in CR4 which enables PAE.
EFER is the Extended Feature Enable Register. rdmsr reads the EFER. We need to setting EFER.LME, which is the Long Mode Enable. or $256 is used to set bit 8. wrmsr writes it back to the EFER. We aren't in Long Mode yet, we need to enable paging first.
Now, we check if paging was already enabled using bit 31 from CR0. If so, we just jump to .continue. This is usually true for EFI systems. However, BIOS systems do not have this already enabled.
Paging is then manually enabled. We set bit 31 in CR0 to achieve this. From here, we are technically in 64-bit mode.
.set_entry:
mov %ebx, (%edi)
add $0x200000, %ebx
add $8, %edi
loop .set_entry
mov $init_page_region, %edi
popl %ecx
addl %ecx, %edi
pushl %ecx
mov %edi, %cr3
/* Enable PAE */
mov %cr4, %eax
or $32, %eax
mov %eax, %cr4
/* EFER */
mov $0xC0000080, %ecx
rdmsr
or $256, %eax
wrmsr
/* Check PG */
mov %cr0, %eax
/* If paging was enabled, assume we were already in long mode (eg. booted by EFI) */
test $0x80000000, %eax
jnz .continue
/* Otherwise enable paging */
or $0x80000000, %eax
mov %eax, %cr0