This is an operating system project for the x86-64 architecture built from the ground up for learning purposes.
The project is still in its early stages. Right now, it consists of a legacy BIOS bootloader that starts in 16-bit real mode, enters 32-bit protected mode, and verifies that the processor supports x86-64 long mode.
The long-term goal is to build a stable operating system that runs on real x86-64 hardware, continuously improve its performance, and learn how each major part of an operating system works along the way.
The name is just for fun. I like the word Elite.
I am learning OS development as I build this, so expect mistakes, experiments, refactoring, and plenty of changes along the way.
If the required tools are already installed, skip this step.
On Debian, Ubuntu, or WSL, run:
sudo apt update
sudo apt install nasm qemu-system-x86 git fdiskIf the repository is already cloned, skip this step.
Clone the project and move into the new directory:
git clone https://github.com/e7ite/EliteOS.git
cd EliteOS Where do you want to run EliteOS?
|
+---------------------------------+
| |
QEMU Physical hardware
a quick test on your PC boot a real PC from
in a VM a USB drive
Jump to QEMU or Physical hardware.
Run this command:
./scripts/run-qemu.shThat is the whole development loop. The script assembles the bootloader, rebuilds and verifies the disk image, and boots it, so every run starts from freshly built code and the same disk layout used on physical hardware.
If you get an error like this:
bash: ./scripts/run-qemu.sh: Permission denied
the scripts have lost their executable permission. This usually happens when the project is downloaded as a ZIP archive rather than cloned with git. Fix it with:
chmod +x scripts/*.shTo boot an image that has already been built, without rebuilding it first, run QEMU directly:
qemu-system-x86_64 -drive format=raw,file=build/eliteos.imgRun both of these commands. The first assembles the bootloader, and the second packages it into the bootable disk image:
./scripts/build-bootloader.sh
./scripts/build-disk-image.shThese are the same two commands ./scripts/run-qemu.sh runs before it starts a virtual machine. They are run directly here because this path ends with a file to write to a USB drive rather than a running QEMU window.
Write the resulting build/eliteos.img to a USB drive using a raw-image writing tool such as Rufus.
The current legacy BIOS boot path requires firmware that supports Legacy BIOS or Compatibility Support Module (CSM) booting.
Enable Legacy BIOS/CSM support if necessary, then use the system's one-time boot menu or firmware boot order to boot from the USB drive.
The legacy BIOS path has been successfully tested using CSM booting on a PC with the following parts:
- ASRock X470 Master SLI/ac motherboard
- AMD Ryzen 7 2700X
- NVIDIA GeForce GTX 1080 Ti
This does not imply driver support for every device. I plan to expand real-hardware testing to additional systems and hardware configurations as the project grows.
The need for this particular MBR layout was observed on the tested physical machine. Other firmware implementations may accept different disk layouts.
- Boot from legacy BIOS
- Execute in 16-bit real mode
- Set up a Global Descriptor Table (GDT)
- Enter 32-bit protected mode
- Detect x86-64 long mode support
- Enter 64-bit long mode
- Load a separate x86-64 kernel
- Add a UEFI boot path
- Boot the same kernel through both BIOS and UEFI
- Add GRUB/Multiboot2 compatibility
The immediate goal is to enter 64-bit long mode. After that, the next major milestone is loading a separate x86-64 kernel and establishing a clean boundary between the bootloader and kernel.
The build produces two files, and the distinction between them matters:
build/boot.bin assembled BIOS boot sector, an internal build artifact
build/eliteos.img the canonical bootable disk image
build/eliteos.img is what gets booted, by QEMU and by physical hardware alike. build/boot.bin is never booted directly. Using one image for both keeps the disk layout tested under emulation identical to the one written to real hardware.
scripts/build-bootloader.sh assembles src/boot.asm into build/boot.bin with a single nasm call. The -f bin output format emits a flat binary with no object headers, because the processor executes the boot sector directly and there is no loader to interpret a file format.
scripts/build-disk-image.sh turns that boot sector into the disk image. It removes any previous image, creates a fresh 32 MiB image, writes a conventional MBR partition table containing one bootable partition of type 0x0c starting at sector 2048, installs the boot code, and then verifies the result. Recreating the image instead of overwriting one in place prevents bytes written by an earlier build from surviving somewhere the current build does not touch.
A traditional 512-byte MBR is laid out as:
Bytes 0-445 Bootloader area
Bytes 446-509 Partition table
Bytes 510-511 0x55AA boot signature
Only the first 446 bytes of build/boot.bin are installed into the image. The partition table begins at byte 446, so copying the whole sector would destroy it. src/boot.asm fails to assemble if the MBR-resident bootloader grows past that 446-byte area.
scripts/build-disk-image.sh expects build/boot.bin to already exist and will not build it. Keeping compilation separate means building the bootloader does not require disk-image tooling such as sfdisk, and lets the image be built without launching QEMU. scripts/run-qemu.sh composes both steps for the normal development loop.
- NASM — x86 assembler
sfdisk— creates the MBR partition table used by the bootable disk image (provided by thefdiskpackage on Debian/Ubuntu)- QEMU — virtual x86-64 machine used for development and automated testing
- Git — source control
- Build a stable operating system that runs on real x86-64 hardware.
- Continuously improve the performance and efficiency of the system as it develops.
- Implement components myself when doing so is important to understanding how operating systems and the underlying hardware work.
- Build each subsystem deeply enough to understand its important concepts, tradeoffs, and failure modes.
- Build implementations that are stable and testable enough to be used as real parts of the operating system.
- Understand the full path from firmware execution through the kernel and eventually into userspace.
- Gain hands-on experience with memory management, interrupts, scheduling, drivers, storage, networking, graphics, and other major OS subsystems.
- Expand hardware support and system capabilities over time as the project grows.
- Maintain clean subsystem boundaries so implementations can evolve without unnecessarily coupling the entire system together.
- Use QEMU for fast iteration, debugging, and automated testing while treating physical hardware as the primary target.
- Introduce unit, integration, and end-to-end testing alongside the systems they validate.
- Document important architectural decisions when they become relevant to active development.
- Competing with production operating systems such as Linux, Windows, or BSD.
- Requiring every subsystem to reach production-level sophistication before moving on to other parts of the operating system, that can be revisited later.
- Making broad hardware support or feature completeness a prerequisite for progress elsewhere.
- Reimplementing a component when an existing implementation can be reused or ported without meaningfully reducing what I would learn about how the operating system works.
- Designing the entire operating system before implementation begins.
- Preserving early design decisions when implementation shows that they were wrong.
The goal is not to make every subsystem as sophisticated as its equivalent in a mature operating system before moving forward. A scheduler, virtual memory manager, filesystem, network stack, GPU driver, or any other subsystem can be stable and useful without being finished forever.
These systems can continue to be expanded, optimized, and redesigned as the project grows.
- Implementation-driven: Get hands-on with a subsystem early instead of spending weeks designing code that does not exist yet.
- Design important boundaries early: Decisions that could create major coupling or be especially expensive to reverse should still be considered before implementation.
- Design details when they become relevant: More detailed design documents can be written alongside the subsystem being implemented.
- Keep documentation grounded in reality: Document what has actually been implemented, learned, and decided.
- Accept refactoring: Some early assumptions will be wrong. Finding those mistakes and understanding how to correct them is part of the project.
- Keep improving: Moving on from a subsystem does not mean it is finished forever. It can be revisited as new requirements, performance problems, hardware, or better designs become relevant.
- Choose implementation depth based on what I want to learn: If building something myself is important to understanding how the OS works, I will build it. If reusing or porting an existing implementation preserves that understanding, I can spend that time learning another part of the system instead.
- More time is spent implementing and debugging real systems.
- Design decisions can be informed by actual implementation experience.
- Documentation stays connected to code that exists.
- Mistakes provide experience diagnosing and correcting bad assumptions.
- The project can make progress across many areas without requiring any one subsystem to be perfected first.
- Subsystems can evolve as the requirements of the operating system become clearer.
- Some early decisions may require significant refactoring.
- Problems may appear later that better upfront planning could have prevented.
- Interfaces may evolve as later subsystems expose requirements that were not obvious earlier.
- Implementing important components myself will often take longer than relying entirely on mature existing implementations.
These tradeoffs are intentional. EliteOS is primarily a learning project, and learning how to recognize, understand, and correct bad assumptions is part of the experience.
Physical x86-64 hardware is the primary testing target for EliteOS. QEMU is used alongside it for faster iteration, debugging, and automated testing.
QEMU and physical hardware use the same build/eliteos.img disk image so both environments exercise the same bootloader and disk layout.
The goal is for implemented subsystems and features to work correctly on real hardware, not only under emulation.
As the project grows, testing will include:
- Unit tests for individual components and algorithms
- Integration tests for interactions between subsystems and drivers
- End-to-end tests that boot complete EliteOS images in QEMU
- Real-hardware validation of implemented subsystems and features
Automated tests will be added to CI alongside the features they validate rather than being deferred until later in development.
QEMU is a development and testing tool, not the final target. EliteOS is intended to run on actual x86-64 hardware.
EliteOS will support multiple ways of reaching the same x86-64 kernel rather than tying the kernel to a specific bootloader.
Legacy BIOS ---> EliteOS BIOS bootloader ---┐
├---> EliteOS x86-64 kernel
UEFI ----------> EliteOS UEFI bootloader ---┤
│
GRUB / Multiboot2 ---------------------------┘
The BIOS path provides experience with the traditional x86 boot process, including real mode, protected mode, paging, and the transition into 64-bit long mode.
The UEFI path will provide experience with the firmware environment used by modern x86-64 systems.
GRUB/Multiboot2 compatibility will provide a standardized alternate boot path and help verify that the kernel remains independent from the custom bootloaders.
Longer-term areas I want to explore include:
- Memory management
- Interrupts
- Multitasking and scheduling
- Userspace
- System calls
- Filesystems
- Hardware drivers
- Networking
- Graphics
- GPU drivers
- A graphical user interface
These areas are directions rather than a fixed design. Individual goals can be expanded as I reach them and understand their requirements better.
Using something like Limine would be a practical way to reach kernel development much faster. If the main objective were simply getting an operating system running as quickly as possible, that would often be the better engineering choice.
For this project, understanding how execution gets from firmware into the kernel is itself an important part of what I want to learn.
A production-quality bootloader can be a substantial project on its own. I want to implement enough of the boot process to understand it deeply and provide this project with reliable boot paths, without turning the bootloader into the entire project.
The exact implementation will evolve, but the responsibility of the bootloader is roughly:
Firmware
↓
EliteOS bootloader
↓
collect required firmware and hardware information
↓
load the kernel
↓
establish the required CPU state and memory mappings
↓
construct common boot information
↓
enter the x86-64 kernel
For the legacy BIOS path, reaching that point requires learning and implementing the transitions through real mode, protected mode, paging, and 64-bit long mode.
The UEFI loader will reach the same kernel through a different firmware environment. It will eventually be responsible for tasks such as:
- Loading the same kernel
- Obtaining the system memory map
- Obtaining graphics information when available
- Exiting UEFI boot services
- Constructing the common boot information
- Transferring control to the kernel
Once a boot path can reliably perform those responsibilities, adding unrelated bootloader features is not automatically a priority.
They expose different parts of x86 system development.
BIOS provides experience with:
- 16-bit real mode
- Legacy PC firmware interfaces
- Protected mode
- Paging setup
- The transition into 64-bit long mode
UEFI provides experience with:
- Modern x86-64 firmware
- EFI applications and protocols
- Firmware memory maps
- Graphics Output Protocol
- Boot services
ExitBootServices()- Booting on modern physical hardware
Supporting both also creates a useful architectural constraint: both paths should eventually reach the same kernel through a common boot interface.
The two serve different purposes.
Custom bootloaders:
- Exist primarily for learning.
- Give me direct experience with firmware, processor state, and the bootloader-to-kernel transition.
GRUB/Multiboot2:
- Provides a standardized alternate boot path.
- Helps verify that the kernel does not depend on assumptions made by my custom loaders.
- Provides another way to exercise the bootloader/kernel boundary.
The same x86-64 kernel should eventually work regardless of which supported boot path reaches it.
Yes. That is expected.
The goal is not to avoid thinking ahead. The goal is to avoid becoming stuck trying to predict every future problem before writing code.
If an early decision turns out to be wrong, I want to understand:
- Why it failed
- Which assumption was incorrect
- What symptoms the design caused
- How the design can be improved
- How I could recognize the same problem earlier in the future
Learning how to identify and correct those decisions is part of the project.
"From the ground up" does not mean that every existing component has to be reinvented.
The boundary is whether implementing something myself is important to understanding how the operating system works.
If implementing a component exposes concepts or behavior that I specifically want to understand, I want to build it myself. That is why areas such as booting, memory management, scheduling, interrupts, and other fundamental OS mechanisms are important parts of this project.
If an existing component can be reused or ported without taking away much of that understanding, I am comfortable using it and spending that time exploring another part of the operating system instead.
The goal is to maximize what I learn about operating systems, not maximize the amount of code I personally rewrite.
| Resource | Reason |
|---|---|
| Daniel McCarthy | Course on developing a multithreaded kernel from scratch |
| Intel Software Developer Manuals | Intel 64 and IA-32 architecture documentation |
| AMD Developer Documentation | AMD64 architecture and system programming documentation |
| OSDev Wiki | Reference for operating-system development |
| AI agents (Claude Code, Codex) | Explaining operating-system and hardware concepts, reviewing changes, and working through problems while building this |