Getting started
The “Why” behind it.
Writing an OS is hard. Incredibly so.
I’ve learned that the hard way while trying to write this blog.
There are a lot of things can go wrong, and not many ways to figure out what did.
But.. as humans, that is exactly what we like. Things that challenge us, that push us to our limits.
Figuring things out, learning as we encounter new things, and constantly moving forward, that’s what we are.
The purpose of writing this OS kernel (as well as this blog series) is to learn, understand, and explore how computers work and to share what I have learned with the world.
Goals
Let’s be real, we won’t be competing with Windows, Linux etc anytime soon. Maybe in a few decades, sure. For now, we need to figure out what our goals are, what abilities we want out kernel to have.
Here’s a wishlist of items that I want to implement:
- A kernel that can boot (duh!)
- A UI interface (Text or Graphics)
- An input / output system (Using keyboard, serial ports etc)
- A memory management unit
- A CLI shell
- Ability to store things i.e a file system
- Ability to run arbitrary programs in the user space
- Basic Networking (If I see a status 200, I’m happy)
- A few applications that the user can use, such as
- A text editor
- An http client
- Pong? Maybe
That list might not seem a lot right now, but we’re implementing a Kernel which has these abilities, so our main focus will be on that side of the equation.
Some hard choices we need to make
Before you even start writing an OS kernel, you’ll have a bunch of questions in front of you.
Target Architecture? Programming language? How will you boot?
Let’s unpack
What is our target architecture?
There are god knows how many CPU architectures out there. Targeting all of them at once will be a monumental task. We will have to narrow our choices.
Out of the most popular CPU architectures, such as x86-64, ARM, etc. I’ve chosen to target the x86-64 CPU architecture, primarily because of its ubiquity, and ecosystem support. The tools we’ll be using such as debuggers, virtual emulators, libraries etc support x86 VERY well.
Documentation is another factor. Documentation for other architectures is either sparse or too vendor dependent and changes from vendor to vendor. Since x86 provides a stable-er platform, we wouldn’t have to go hunting for our specific spell of magic.
Since this is our proverbial ‘dip in the pond’, I’ll play it on the safe side.
Additionally, I’m running an x86-64 CPU, so if I want to test something isolated from the OS kernel’s context on my own machine, I can do so easily, which is another added benefit.
So it’s settled then. x86-64 it is.
Which programming language to use?
We need a programming language that is very minimal, resource efficient, and gives the ability to arbitrarily perform very low level, close to the hardware stuff.
Most of you already have a bunch of ideas in mind.
Perhaps C, or C++, Zig or Rust. Assembly? Nim?
Java?? Yes, People have built operating systems in Java (Although the HALs were written in C/C++. Anyway, I digress)
Ugghhh. Choices choices!
C or C++ are fantastic options, They have:
- A minimal runtime? Check!
- A high level of control over how instructions are executed? Check!
- Performant? Double Check!
- A stable ABI? C does.
But are they without chinks in their armor? Ehh…
They outsource correctness to the programmer. We are the ones who need to make sure we’re not accidentally introducing UB in our code.
The price of control, is paid with walking on landmines.
All bets are off. Mistakes, turn into invisible bugs.
Uh.. Oh!
Well.. let’s keep exploring.
Zig is interesting. It ticks most of the boxes that C and C++ do but on top of that, it has the added benefit of being very explicit, and having a lot of niceties such as comptime, custom allocators, and state of the art error handling.
What about Rust? It has the same benefits as the other options. But additionally, I like the sound of zero cost abstractions. Of a compiler that gently guides me.
Rust also has a strong ecosystem which on the other hand, Zig does not (not yet at least). That extends to tooling, debugging support, and stability (in terms of breaking changes in the language itself)
Plus, it is the language that I’m most familiar with. It’s better if we stick to things we already know and not try to scale two mountains at once.
Final Verdict? We’ll use Rust.
How do we boot?
This one is surprisingly philosophical.
At a very high level, we have two main options:
- BIOS
- UEFI
BIOS looks simpler at first glance. And in some ways, it is. You get dropped into a tiny environment, load a few sectors from disk, and jump into your code.
That’s it.
But that’s also the problem.
BIOS doesn’t give you much else:
- graphics are awkward
- memory information is fragmented
- modern hardware discovery is painful
- everything feels… old
For what we eventually want to build, BIOS alone won’t cut it.
So why not just use UEFI from day one?
Because UEFI is huge!
More concepts. More moving parts. More things to understand at once. More headaches.
UEFI gives you a lot:
- framebuffer graphics
- clean memory maps
- standardized ACPI tables
- a 64-bit execution environment from the start
All of that is great, but it also adds cognitive load.
The plan
We’ll start with BIOS. Then implement things using UEFI later.
Once we’ve built something that boots with BIOS, prints text, and doesn’t immediately explode, we’ll move on to UEFI.
With that I think we’re all set.
Let’s start by setting up our our project and preparing our kernel.