There’s something almost magical about those first 512 bytes that run when a computer powers on. Think about it: your entire operating system, with all its complexity, all its billions of lines of code, all its sophisticated abstractions—it all starts with a tiny chunk of assembly code that has to fit in a single disk sector. That’s the bootloader, and if you’ve never written one, you’re missing out on one of the most satisfying “hello world” moments in systems programming.
I remember the first time I wrote a bootloader. I was expecting something complicated, something that would require deep kernel knowledge and years of experience. Instead, I discovered that a bootloader is actually one of the simplest pieces of system software you can write—precisely because it’s so constrained. When you only have 512 bytes to work with, you don’t have room for complexity. You don’t have room for abstraction layers, or object-oriented design patterns, or any of that high-level nonsense. You have 512 bytes of raw assembly, and every single instruction has to earn its place.
In this article, we’re going to dive deep into bootloader development. We’ll start with the absolute basics—what happens when you press the power button—and work our way up to writing a complete two-stage bootloader that can load a real kernel. By the time we’re done, you’ll understand not just how to write a bootloader, but why they work the way they do, and you’ll have a deep appreciation for the ingenious tricks that systems programmers have developed to work around the 512-byte constraint.
Let’s get started.
What is a Bootloader? (The First Code That Runs—And Why It’s 512 Bytes of Magic)
When you press the power button on your computer, a complex choreographed dance begins. The CPU wakes up, the BIOS (Basic Input/Output System) or UEFI firmware takes control, hardware gets initialized, and eventually—if everything goes right—your operating system kernel gets loaded into memory and starts running. But right there in the middle of that process, sandwiched between the firmware initialization and the kernel launch, is the bootloader.
The bootloader is the first custom code that runs on your computer. Everything before it—the POST (Power-On Self-Test), the hardware enumeration, the initial hardware configuration—that’s all firmware code that came with your motherboard. Everything after it—the kernel initialization, the user space startup, the login prompt—that’s all part of your operating system. The bootloader is the bridge between those two worlds.
But here’s the thing that makes bootloaders so interesting: the very first stage of the bootloader has to fit in exactly 512 bytes. Not 513 bytes. Not 511 bytes. Exactly 512 bytes. This isn’t an arbitrary choice—it’s a constraint baked into the x86 architecture by decades of backward compatibility.
To understand why it’s 512 bytes, we have to go back to the early 1980s and the original IBM PC. The engineers designing the PC needed a way for the system to load an operating system from a floppy disk. They chose the simplest possible approach: the BIOS would read the very first sector of the disk—sector 1, head 0, cylinder 0—into memory at a fixed address, and then jump to that address to start executing. That first sector is 512 bytes, because that’s the standard size of a disk sector.
Now, you might think that 512 bytes isn’t much space—and you’d be right. But the original IBM PC had only 16 bytes of code in its bootloader! It was just enough to load the next stage from disk. The constraint of 512 bytes has persisted through decades of computing history, from the original 8088 processor all the way to modern x86-64 systems. Even today, if you’re writing a bootloader for a legacy BIOS system, you still get exactly 512 bytes.
(If you’re booting with UEFI, the rules are different—UEFI can load larger files directly—but legacy BIOS boot is still widely used, and understanding it is essential for understanding how bootloaders work.)
So what exactly does a bootloader do? At its most basic level, a bootloader has one job: load the operating system kernel into memory and jump to it. That’s it. But when you’re working with only 512 bytes, even that simple job requires some careful engineering.
Here’s what a minimal bootloader needs to accomplish:
- Set up a minimal execution environment: Configure segment registers, set up a stack, maybe switch to a known video mode.
- Load additional code from disk: 512 bytes isn’t enough to load a real kernel, so the bootloader has to load a “stage 2” bootloader (or directly load the kernel, if it’s small enough).
- Transition to the loaded code: Jump to the code that was just loaded, passing control to the next stage.
Some bootloaders have to do more than this. If you’re writing a bootloader for a modern operating system, you might also need to:
- Switch from real mode to protected mode (or even long mode)
- Parse a filesystem to find the kernel on disk
- Load an initial RAM disk (initrd) for the kernel
- Parse kernel command-line arguments
- Set up a video mode for the kernel
- Perform basic hardware detection
But all of that has to happen in stage 2, because it sure as hell won’t fit in 512 bytes.
The magic of a bootloader is that it’s a self-contained, standalone piece of code that runs directly on the hardware with no operating system support. There’s no standard library, no malloc(), no printf(). There’s not even a C runtime—you’re writing pure assembly (or very careful C that doesn’t use any standard library functions). You’re talking directly to the hardware, using BIOS interrupts or direct I/O port access to get things done.
And yet, with all those constraints, a bootloader is remarkably simple. It’s just a tiny program that loads a bigger program. If you can understand how the BIOS loads that first sector, and how to use BIOS interrupts to read more sectors from disk, you can write a bootloader.
Let me show you just how simple the very simplest bootloader can be. Here’s a complete, working bootloader that does nothing but print the letter ‘A’ to the screen and then hangs:
; boot.asm - The world's simplest bootloader
; Assemble with: nasm -f bin boot.asm -o boot.bin
; Run with: qemu-system-i386 -fda boot.bin
bits 16 ; We start in 16-bit real mode
org 0x7C00 ; BIOS loads us at 0x7C00
start:
mov ax, 0xB800 ; Video memory segment
mov es, ax ; ES = 0xB800
mov byte [es:0], 'A' ; Put 'A' at first character cell
mov byte [es:1], 0x0F ; Put attribute (white on black)
jmp $ ; Hang forever
times 510-($-$$) db 0 ; Pad to 510 bytes
dw 0xAA55 ; Boot signature (must be at offset 510)
That’s it. That’s a complete, bootable program. When the BIOS loads this 512-byte sector, it gets:
- A few instructions that print ‘A’ to the screen
- 510 bytes of padding (zeros)
- The magic number 0xAA55 at the last two bytes
The BIOS checks for that 0xAA55 signature at the end of the sector. If it’s there, the BIOS knows this is a bootable sector, and it jumps to 0x7C00 to start executing. If it’s not there, the BIOS moves on to the next boot device.
This is the essence of bootloader development: you have 512 bytes, you have to end with 0xAA55, and you have to make something useful happen in between. Everything else is just figuring out how to be useful in 510 bytes of code.
In the sections that follow, we’re going to take this minimal example and build it up into a real, working bootloader that can load a kernel from disk. We’ll cover the BIOS boot process in detail, the 512-byte constraint and how to work around it, how to read from disk using BIOS interrupts, how to switch to protected mode, and finally how to write a complete two-stage bootloader.
But before we get to any of that, there’s something important you need to understand about the execution environment that the BIOS gives you. And that brings us to…



