KoizOS - Moving to GRUB2
Introduction
I feel like I've made considerable progress on Koiz OS. I built a bootloader, memory management functions, interrupt handlers, and a simple C library. At this point, I thought it would be fun to actually try running the operating system on a bare metal machine. I wanted to take a video of this, but after POST-ing, the machine simply triple faulted and restarted. In fact, it wasn't even making it to the operating system. Even though it worked fine in QEMU, something about it didn't agree with the machine I was using.
Moving my bootloader to GRUB2
There were a few issues in my operating system that I quickly traced back to my bootloader. I couldn't debug exactly what in my bootloader was causing my operating system to fail to load but I knew that at this point, my custom bootloader has become more of a liability. Now came the fun part of moving everything to GRUB2.
Fixing the memory map
So my memory map was originally generated by a neat little piece of code on OSDev. It looked robust enough, but I couldn't verify if it was the cause of the triple-fault. However since I'm using GRUB2, GRUB2 will actually automatically populate a memory map for me as part of the Multiboot specification. I'll need to write a new function to handle it.
/**
* pmem_set_mbd() - Sets the multiboot info and reserved kernel addr
*
* @mbd: This is the address to the mbd as populated by GRUB
* @kernel_reserved_end_addr: Address to end of reserved kernel space
*
* Note that this has to be called BEFORE pmem_initialize()
*/
void pmem_set_mbd(multiboot_info_t* mbd, uint32_t kernel_reserved_end_addr);
Just like before, I'll scan the memory map given to me by GRUB2 for the largest section of free memory and use that as my operating system's memory. The code is extremely similar.
/* Check bit 6 to see if we have a valid memory map */
if(!(pmem_mbd->flags >> 6 & 0x1)) {
printf("mbd flags: %b", pmem_mbd->flags);
panic("invalid memory map given by GRUB bootloader");
}
/*
* We're grabbing the largest region of memory for memory operations
*/
pmem_main_memory_start = 0x00;
pmem_main_memory_length = 0x00;
int i;
for(i = 0; i < pmem_mbd->mmap_length;
i += sizeof(multiboot_memory_map_t))
{
multiboot_memory_map_t* cs =
(multiboot_memory_map_t*) (pmem_mbd->mmap_addr + i);
/* Grab the largest region */
if(cs->type == MULTIBOOT_MEMORY_AVAILABLE &&
cs->len_low > pmem_main_memory_length) {
pmem_main_memory_start = (void*)cs->addr_low;
pmem_main_memory_length = cs->len_low;
}
}
Next is the grub.cfg file. It's refreshingly the least complicated piece of this all:
# Set defaults
set default="koiz-os"
set timeout=0
# Our operating system
menuentry "koiz-os" {
multiboot /boot/koizos.bin
}
And finally, we need to create a modified boot.asm. Since most are in WASM, here's the FASM equivalent version for the GRUB bootloader.
; FASM converted Boot Script off of https://wiki.osdev.org/Bare_Bones
; which was originally written in GNU Assembler
; Multiboot header specification from:
; https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
; Say this is a 32-bit elf file
format ELF
use32
; Magic value that lets bootloader find header
magic_value = 0x1BADB002
; Bit 0 - Align module on page boundary
; Bit 1 - Provide memory map
; Bit 2 - Make information about video mode table availiable
; ...
flags = 00000011b
; Multiboot header that marks program as a kernel
section '.multiboot'
; Align at 32-bit boundary
align 4
; Magic value that lets bootloader find header
dd magic_value
; Flags
dd flags
; Checksum (MagicValue + Flags) - Checksum = 0
dd -(magic_value + flags)
; Multiboot standard does not define a value for the Stack pointer
; register (esp) and it is up to the kernel to provide the stack.
; The following will allocate 16KiB of data for a stack and create a
; symbol on top. The stack grows DOWN on x86 and must be 16-byte aligned
; according to the System V ABI standard. The compiler already assumes
; the stack is properly aligned.
section '.bss' align 16
stack_bottom:
rb 16384
stack_top:
; The linker script we built in linker.ld specifies _start as the entry
; point to the kernel. The bootloader jumps to this once the kernel is loaded.
; We shouldn't return from this function as the bootloader is gone
; at this point.
section '.text'
extrn kernel_main
extrn _init_gdt
extrn kernel_memory_end
public _start
_start:
; At this point, the bootloader has us in 32-bit protected mode
; Interrupts are disabled.
; Paging is disabled
; Processor state is multiboot standard
; The first thing we want to do is set up the stack.
mov [stack_top], esp
; Store the EAX, EBX onto the stack.
; They contain the multiboot info and magic number correspondingly
push kernel_memory_end
push eax
push ebx
; Initialize gdt
call _init_gdt
; Finally, enter the high-level kernel
; The stack should only contain EAX and EBX from the start now
call kernel_main
; Put the system into an infinite loop
cli
jmp $
Conclusion
And now after running it, we're presented with the GRUB2 boot menu before KoizOS boots up. That's pretty awesome!
Next up will be attempting to write the filesystem past of Koiz-OS.
Side Rant
I want to state that I dislike how much of the OS Dev community discourages people from trying to write their own bootloaders. Their responses are usually along the lines of "Don't write your own. Bootloaders like GRUB have decades of experienced programmers working on them and I doubt you'll write a better one."
I believe what many of the community fail to realize is that most of us want to write our own bootloader for the learning experience. I feel writing one has given me more insight and appreciation for bootloaders than I previously had.