Sitemap
Joshiriya

Joshiriya

I Built My Own Operating System From Scratch — Here’s What I Learned

6 min readJul 6, 2026

--

A journey from “what even is a bootloader?” to a kernel with memory management, interrupts, and a working keyboard driver.

A few weeks ago, I decided I wanted to understand what actually happens between pressing the power button on a computer and seeing a desktop appear. Not the textbook version — the real version, down to the bytes. The only way I knew to really learn that was to build it myself, from absolute zero.

No existing kernel. No copied bootloader. No standard library. Just me, a cross-compiler I built by hand, and a lot of very patient debugging.

Here’s what that looked like.

Step Zero: You Can’t Even Use Your Own Compiler

The first surprise: you can’t build an OS with the GCC that’s already on your machine. Your system’s GCC assumes it’s compiling programs for an operating system — it links against your OS’s standard library, assumes there’s a malloc, assumes there's a printf that knows how to talk to a terminal.

None of that exists yet when you’re building the terminal.

So the very first thing I had to do was build my own cross-compiler — a version of GCC and Binutils that targets i686-elf, a bare-metal target with no underlying OS assumptions at all. That meant compiling GCC from source, which took a genuine chunk of time and taught me more about toolchains than I expected going in.

Once i686-elf-gcc --version finally printed a version number, it felt like I'd built the ground before I could build the house.

Press enter or click to view image in full size

Getting GRUB to Even Look at My Kernel

I didn’t write my own bootloader — that’s its own rabbit hole involving 16-bit real mode, the A20 line, and manually reading disk sectors. Instead, I used GRUB, and made my kernel “multiboot compliant” — which just means embedding a specific magic number and checksum at the start of the binary so GRUB recognizes it as loadable.

The entire bridge between “GRUB has control” and “my code has control” was about 20 lines of x86 assembly: set up a stack, then jump into a C function called kernel_main. That was the entire bootloader-to-kernel handoff. It's almost anticlimactic how small it is — the complexity is all in what comes after.

Writing to the Screen Without an OS

The first real “it’s alive” moment was printing text to the screen — but there’s no printf, no console API, nothing. The trick is that VGA text mode maps directly to a fixed memory address: 0xB8000. Writing bytes there writes directly to the screen. Each character on screen is two bytes: one for the ASCII character, one for its color.

So “Hello, World” for an OS kernel is just:

uint16_t *vga = (uint16_t*) 0xB8000;
vga[0] = 'H' | (0x0F << 8);

No function call, no system, just memory. It’s a strange kind of magic the first time you see it work.

From there I built up a proper terminal driver with newline handling and screen scrolling, and eventually a hand-rolled printf clone (kprintf) supporting %d, %x, %s, and %c — using GCC's built-in variadic argument macros since there's no stdarg.h to lean on.

Press enter or click to view image in full size

Teaching the CPU About Itself: GDT and IDT

This is where things stopped feeling like “programming” and started feeling like operating directly on the processor.

Get Joshiriya’s stories in your inbox

Join Medium for free to get updates from this writer.

The Global Descriptor Table (GDT) tells the CPU how memory is segmented — which regions are kernel code, which are data, what privilege level (ring 0 vs ring 3) each has. GRUB sets up a temporary one just to get you running, but you’re expected to replace it with your own.

The Interrupt Descriptor Table (IDT) was the bigger one. This is what lets the CPU respond to things happening in real time — a divide-by-zero, an invalid instruction, or a key being pressed. Setting this up meant writing 32 assembly stubs for CPU exceptions, plus additional stubs for hardware interrupts, each one saving CPU registers, calling into a C handler, and restoring state before returning with iret instead of a normal ret.

There was a genuinely satisfying moment where I deliberately triggered a divide-by-zero inside the kernel and, instead of the whole system crashing and rebooting, my exception handler caught it and printed:

Exception: Division By Zero (int=0, err=0)

That’s the difference between “code that happens to work” and “an operating system.”

The PIC Trap Everyone Falls Into

Getting keyboard input working exposed a classic gotcha: the hardware chip that manages device interrupts (the PIC) defaults to numbering hardware interrupts 0–15 — which directly collides with the CPU’s own exception numbers 0–31. Every hardware interrupt would get misinterpreted as a CPU exception unless you explicitly reprogram the PIC to remap them higher, into the 32–47 range.

Once that was sorted, wiring up a scancode-to-ASCII table and reading from I/O port 0x60 meant I could finally type into my own OS. Watching characters I typed appear on a screen with no operating system behind them, no drivers I didn't write myself, was the most rewarding checkpoint of the whole project so far.

Memory Management: Where Theory Meets a Lot of Bugs

This was the deepest section, split into three layers:

Physical Memory Manager — GRUB hands you a memory map describing which regions of RAM are actually usable versus reserved for hardware. I built a bitmap where each bit represents one 4KB physical page frame, marked everything used by default, then freed the regions GRUB reported as available, then re-reserved whatever the kernel itself occupies.

Paging — this is where “virtual memory” stops being an abstract concept from a textbook and becomes something you assemble by hand. A Page Directory pointing to Page Tables pointing to physical frames, with the CPU’s MMU translating addresses through them automatically once you load the directory into CR3 and flip the paging bit in CR0. I started with identity mapping — virtual address equals physical address — specifically so I wouldn't have two new sets of bugs at once.

Heap allocation — with physical frames and paging in place, I wrote a kmalloc/kfree pair: a bump allocator for new memory, backed by a free-list so freed blocks get reused rather than wasted. Testing it meant allocating a block, writing a string into it, freeing it, then allocating again and confirming the new pointer reused the exact same address. Small moment, disproportionately satisfying.

What I’d Tell Someone Starting This

You will hit a bug where the C calling convention bites you. Mine was pushing function arguments onto the stack in the wrong order before calling kernel_main — the classic mistake of forgetting that the last pushed argument is the first parameter. The fix was a one-line swap, but finding it required understanding exactly how arguments get read off the stack.

Build your own error signals early. The reason I caught that calling-convention bug quickly is that I’d already built exception handling and kprintf — I could see the multiboot magic number was wrong instead of just getting a silent, confusing crash. Debug visibility is worth building before you think you need it.

The theory only sticks once you’ve broken it. Reading about paging is one thing. Watching your OS instantly triple-fault and reboot because you forgot to identity-map the kernel’s own memory is what actually makes the mental model permanent.

Where This Goes Next

Right now the kernel can boot, print, catch exceptions, read keystrokes, and manage memory dynamically. Next on the list: a basic shell so keystrokes turn into actual commands, a hardware timer for preemptive multitasking, and eventually loading and running separate user-mode programs.

Every step so far has been small enough to understand completely and large enough to be genuinely satisfying when it finally works. That combination is, I think, the whole appeal of doing this from scratch instead of reading about how it works.

If you’re curious about the code, Github link-https://github.com/riyajoshi2004/myos,feel free to poke around, fork it, or tell me what I got wrong.

Press enter or click to view image in full size
Press enter or click to view image in full size

--

--