# Building a Bootloader from Scratch — Just Assembly, No OS

Everything you write at work stands on a tower of things that were already set up for you: a heap, a stack, threads, a filesystem, a C library, an operating system underneath all of it. Writing a bootloader is what it feels like when none of that exists yet. The BIOS copies 512 bytes off the disk to memory address `0x7c00`, jumps to it in 16-bit real mode, and steps back. No OS, no libc, no memory manager. Whatever happens next, you write. This walks a hand-written Stage-1 boot sector line by line — the code from [NanoBoot](https://github.com/harrison001/NanoBoot) — because every constraint it hits is one of those abstractions becoming visible.

%[https://www.youtube.com/watch?v=Ei_ltKTSEfU] 

## The rules the hardware makes for you

A boot sector isn't a program in any normal sense. Three hard constraints define it, and they come from the machine, not from you:

```asm
[bits 16]            ; the CPU starts in 16-bit real mode
[org 0x7c00]         ; BIOS loads this sector to physical 0x7c00
; ... exactly 512 bytes ...
    times 510-($-$$) db 0   ; pad the rest with zeros
    dw 0xAA55               ; the last two bytes MUST be this signature
```

The sector is exactly **512 bytes**. Its final two bytes must be `0xAA55` or the BIOS refuses to boot it — that signature is the entire handshake. It loads at `0x7c00` and runs in **16-bit real mode**, where a physical address is `segment << 4 + offset` and there is no memory protection at all. `[org 0x7c00]` tells the assembler to compute label addresses from that base, because nothing relocates you — the address the BIOS chose is the address you live at.

## First moves: build your own ground

The BIOS hands you the CPU in an indeterminate state. Before anything else, you establish the world your code assumes exists:

```asm
start:
    cli                 ; no interrupts until we have a stack and segments
    cld                 ; string ops count upward
    xor ax, ax
    mov ds, ax          ; data segment = 0
    mov es, ax          ; extra segment = 0
    mov ss, ax          ; stack segment = 0
    mov bp, 0x7c00
    lea sp, [bp-0x20]   ; put a stack just below our own code
    sti
```

Every line here is something an OS would have done for you. There is no stack until you point `SS:SP` at some memory you've decided is free. There's no notion of "the data segment" until you load one. You turn interrupts off (`cli`) precisely because there is no interrupt handling set up yet, do the setup, then turn them back on. This is the layer where "the runtime" is just you, by hand.

## Your only library is the BIOS

With no OS, the one API you have is BIOS interrupt services. Printing a string means calling the teletype service, `int 0x10`, one character at a time:

```asm
print_string:
    mov ah, 0x0E        ; BIOS teletype output
.loop:
    lodsb               ; AL = [SI++]
    test al, al         ; NUL terminator?
    jz .done
    int 0x10            ; print AL
    jmp .loop
.done:
    ret
```

That is the entire implementation of "print." No `printf`, no buffering, no `stdout` — a loop, a register, and a firmware call. Reading from disk is the same shape, through `int 0x13`.

## The 512-byte wall, and the jump that gets you past it

Here's where the constraint bites: 512 bytes is nowhere near enough to do anything real. So a boot sector's actual job is to bootstrap something bigger. Stage-1 uses the BIOS disk service to pull more sectors off the disk into memory, then hands control over:

```asm
    ; load Stage2 to 0x6000 using BIOS int 13h
    mov ax, 0x6000
    mov es, ax
    xor bx, bx
    mov ah, 0x02        ; read sectors
    mov al, 16          ; 16 sectors
    mov ch, 0           ; cylinder 0
    mov cl, 2           ; from sector 2 (sector 1 is us)
    mov dh, 0           ; head 0
    mov dl, [boot_drive]
    int 0x13
    jc  load_error      ; carry set = read failed

    jmp 0x6000:0        ; far jump into Stage2
```

That last instruction, `jmp 0x6000:0`, is a real-mode far jump — it reloads `CS` and continues execution in the freshly loaded second stage. It's also the exact instruction that breaks GDB's symbol resolution when you debug across it, which is its own [field note](https://harrisonsec.com/blog/real-mode-bootloader-gdb-symbols-broken/). And Stage-2, now with room to breathe, is where the switch out of real mode into [protected mode via the GDT and IDT](https://harrisonsec.com/blog/from-real-mode-to-protected-mode-gdt-idt/) happens. The 512-byte sector exists to reach that point.

## Why do this if you'll never ship one

You won't hand-write a boot sector at work. The value is in what it makes visible:

*   **Every abstraction you normally stand on, absent.** No heap, no stack, no library, no scheduler — you build each one or do without. Once you've felt where they *aren't*, you understand what they actually do when they're there.
    
*   **The trust chain starts here.** "Secure boot," firmware trust, and the whole idea of a chain of custody from power-on to kernel begin at this 512-byte sector and its signature. You can't reason clearly about boot-level security if the boot process is a black box.
    
*   **BIOS/firmware is a real attack and reliability surface.** Knowing that the first code to run is unprivileged-by-nothing, in a mode with no memory protection, reframes how you think about what "the bottom of the stack" even is.
    

The deeper payoff is calibration. After you've made a computer print one character with nothing but a firmware call and a byte in a register, "hello world" stops looking simple and starts looking like the top of a very tall stack — which is exactly what it is.

## Related

*   **Video:** [How to Build a Bootloader from Scratch — Just Assembly, No OS](https://www.youtube.com/watch?v=Ei_ltKTSEfU)
    
*   **GitHub:** [NanoBoot](https://github.com/harrison001/NanoBoot) — the two-stage bootloader this Stage-1 is from.
    
*   **Next in the chain:** [Debugging a real-mode bootloader in GDB](https://harrisonsec.com/blog/real-mode-bootloader-gdb-symbols-broken/) and [From real mode to protected mode (GDT/IDT)](https://harrisonsec.com/blog/from-real-mode-to-protected-mode-gdt-idt/).
