OS Dev Log I: The Road to a Real Kernel

In the previous posts, we successfully set up our development environment, wrote a minimal kernel to handle booting, and initialized the UART to get a “Hello, world!” message printing to a serial console. This was a critical milestone, proving that our code can boot and run correctly within the QEMU emulator on the virt AArch64 platform.

However, our current kernel is still very rudimentary. It relies on hardcoded memory addresses, it’s unaware of multiple CPU cores, and it lacks any form of memory protection. To move forward, we need to build a more robust and dynamic foundation.

This post is a development log outlining the next major goals for the OS. These steps are about transforming our proof-of-concept into the beginnings of a modern kernel.

From Hardcoded Addresses to Dynamic Discovery

Currently, the kernel knows the memory address of the UART controller because I looked it up in the hardware documentation and wrote it directly into the code. This approach is inflexible and not portable; running this OS on any other AArch64 platform, or even a different QEMU machine configuration, would require changing the source code and recompiling.

The Next Step: Parse the Device Tree Blob

A Device Tree Blob (DTB) is a data structure passed to our kernel by the bootloader or firmware. It describes the hardware layout of the system, including memory maps, the number of CPU cores, and the memory-mapped addresses of peripherals like our UART controller.

By parsing the DTB at boot time, our kernel can discover the hardware it’s running on dynamically. The immediate goal is to implement or integrate a DTB parser that can read this structure. This will allow the kernel to ask for the UART address instead of having it hardcoded, a fundamental step towards portability.

Preparing for a Multi-Core World: Per-CPU Variables

Our OS operates as if it’s on a single-core processor. If we were to enable multiple CPU cores in our QEMU environment, they would all share the same global variables. This would create race conditions and unpredictable behavior for core-specific information, such as which task is currently running or the state of an exception handler.

The Next Step: Implement Per-CPU Variables

To properly support a multi-core environment, we need a way to create per-CPU variables, where each core has its own private instance of that variable. On AArch64, a standard way to achieve this is by using the TPIDR_EL1 (Thread Pointer ID Register, Exception Level 1).

The plan is to make this register on each core point to a unique, core-local data structure. When the kernel needs to access a piece of per-CPU data, it will first read TPIDR_EL1 to find the base address of its local data area. This will allow each core to safely manage its own state.

The Foundation of a Modern OS: Virtual Memory

This is the most significant challenge yet. The kernel currently runs using physical addresses, meaning a pointer in our code corresponds directly to a location in RAM. This has two critical security and stability flaws:

  1. No Protection: A bug anywhere in the kernel could corrupt other parts of the kernel or its data, leading to a system crash.
  2. No Isolation: It’s impossible to run multiple processes (like user-space applications) because there is nothing to stop them from interfering with each other’s memory or with the kernel itself.

The Next Step: Implement Virtual Memory

By enabling the hardware’s Memory Management Unit (MMU), we can create virtual address spaces. The MMU translates the virtual addresses used by the CPU into physical addresses in RAM. This system is essential for any modern OS. It will allow us to:

  • Isolate the kernel: We can map the kernel into a protected region of the virtual address space, making it inaccessible to other code.
  • Create process address spaces: Each future process will get its own private virtual address space, making it impossible for it to access memory outside of its sandbox without explicit permission from the kernel.

Summary

These three objectives—DTB parsing, per-CPU variables, and virtual memory—are the architectural pillars for our OS. They will move us from a simple “hello world” program to a system capable of managing hardware dynamically, scaling across multiple cores, and providing memory protection.

The source code for this project is available on GitHub for anyone interested in following along or exploring the implementation.

In the next post, I will focus on the first goal: implementing the DTB parser to eliminate our reliance on hardcoded addresses.

WordPress Cookie Plugin by Real Cookie Banner