Some time ago, I started a hobby project: writing a new operating system. I’m not trying to create a concurrent to Linux, I’m just trying to learn some more stuff about operating systems. I’m gonna try to write some posts about this kernel on this blog.
In this post, I’ll describe the boot process I’ve written for this operating system.
The first step is of course the bootloader. The bootloader is in the MBR and is loaded by the system at 0x7C00.
I’m doing the bootloading in two stages. The first stage (one sector) print some messages and then load the second stage (one sector) from floppy at 0×900. The goal of doing it in two stages is just to be able to overwrite the bootloader memory by the stage. The second stage loads the kernel into memory from floppy. The kernel is loaded at 0×1000 and then run directly.
The bootloader stages are written in assembly.
When the processor, it boots in real mode (16 bits) and you have to setup plenty of things before you can go into long mode (64 bits). So the first steps of the kernel are running in 16 bits. The kernel is mostly written in C++ with some inline assembly.
Here are all the things that are done in this mode:
- The memory is inspected using BIOS E820 function. It is necessary to do that at this point since BIOS function calls are not available after going to protected mode. This function gives a map of the available memory. The map is used later by the dynamic memory allocator.
- The interrupts are disabled and a fake Interrupt Descriptor Table is configured to make sure no interrupt are thrown in protected mode
- The Global Descriptor Table is setup. This table describes the different portion of the memory and what each process can do with each portion of the memory. I have three descriptors: a 32bit code segment, a data segment and a 64bit code segment.
- Protected mode is activated by setting PE bit of CR0 control register.
- Disable paging
- Jump to the next step. It is necessary to use a far jump so that the code segment is changed.
At this point, the processor is running in protected mode (32 bits). BIOS interrupts are not available anymore.
Again, several steps are necessary:
- To be able to use all memory, Physical Address Extensions are activated.
- Long Mode is enabled by setting the EFER.LME bit.
- Paging is setup, the first MiB of memory is mapped to the exact same virtual addresses.
- The address of the Page-Map Level 4 Table is set in the CR0 register.
- Finally paging is activated.
- Jump to the real mode kernel, again by using a far jump to change code segment.
The kernel finally runs in 64 bits.
There are still some initialization steps that needs to be done:
- SSE extensions are enabled.
- The final Interrupt Descriptor Table is setup.
- ISRs are created for each possible processor exception
- The IRQs are installed in the IDT
- Interrupts are enabled
At this point, is kernel is fully loaded and starts initialization stuff like loading drivers, preparing memory, setting up timers…
If you want more information about this process, you can read the different source files involved (stage1.asm, stage2.asm, boot_16.cpp, boot_32.cpp and kernel.cpp) and if you have any question, you can comment on this post.