Skip to content

stage 1 assembly boot

Mohiuddin Khan Inamdar edited this page Jun 21, 2026 · 3 revisions

← Home

Stage 1 — The Assembly Boot Sector

The entire operating system is 512 bytes, including the two-byte signature the firmware looks for.

Stage 1 strips a computer down to the very first thing it runs. There is no kernel, no second stage, no C, and no runtime of any kind — just one 512-byte sector that the BIOS loads to memory and executes in 16-bit real mode. It clears the screen, prints a message using BIOS services, waits for a keypress, and halts. That is the whole program, and it is a complete, bootable operating system.

Two variants ship in this stage: a minimal monochrome version and a larger colorful, interactive version.

  • Directory: helloworld-os-asm/
  • Mode: 16-bit real mode
  • Language: pure NASM assembly
  • Files: main.asm (~69 lines), main_color.asm (~193 lines), Makefile

What's new

This is the first stage, so everything is new. The concepts it introduces are the foundation every later stage builds on:

  • The boot-sector contract: the firmware loads exactly 512 bytes from the first sector of the boot media to physical address 0x7C00, checks that the last two bytes are the signature 0xAA55, and jumps in.
  • 16-bit real mode, the CPU state the machine powers up in — see real-mode.md.
  • The BIOS services that are the only API available before you write your own: video (int 0x10) and keyboard (int 0x16). See bios-services.md.
  • The VGA attribute byte (color variant), encoding background and foreground into a single byte.

For the wider picture of how power-on leads to your code running, read boot-process.md and boot-sector.md.

The files

File Role
main.asm The minimal monochrome OS: clear screen, print two centered strings, wait for q/Q, halt.
main_color.asm The interactive color OS: blue background, colored text, keys 1–5 change the background, SPACE resets, Q quits.
Makefile Assembles both into raw .img images and runs them in QEMU.

Each .asm file is assembled directly into a flat binary image with nasm -f bin. There is no linker and no separate boot sector — the file is the boot sector.

Code walkthrough: the monochrome boot sector

The whole of main.asm is worth reading top to bottom; it is short enough to hold in your head.

Telling the assembler where the code lives

[BITS 16]
[ORG 0x7C00]

[BITS 16] tells NASM to emit 16-bit instructions (the machine is in real mode). [ORG 0x7C00] tells it that the code will run at address 0x7C00, so every absolute reference to a label (msg, sign) is computed relative to that base. This is the address the BIOS always loads the boot sector to.

Clearing the screen and positioning the cursor

start:
    ; clear screen
    mov ah, 0x00
    mov al, 0x03
    int 0x10

int 0x10 is the BIOS video service. With AH = 0x00 it sets the video mode; mode 0x03 is the standard 80×25 16-color text mode. Setting the mode also clears the screen. The cursor is then placed with the same interrupt, AH = 0x02:

    mov ah, 0x02
    mov bh, 0          ; page 0
    mov dh, 12         ; row
    mov dl, 34         ; column
    int 0x10

Printing with teletype output

The print routine is a classic lodsb loop over a null-terminated string:

print_string:
    lodsb              ; AL = [SI], SI++
    or al, al          ; AL == 0 ?
    jz .done
    mov ah, 0x0E       ; teletype output
    int 0x10
    jmp print_string
.done:
    ret

lodsb loads the byte at DS:SI into AL and advances SI. or al, al sets the zero flag if AL is zero, ending the loop at the string terminator. Otherwise int 0x10 with AH = 0x0E (teletype) prints the character and advances the cursor automatically. (main.asm:55)

Waiting for a key, then halting

.wait_key:
    mov ah, 0x00
    int 0x16           ; BIOS: wait for keystroke -> AL = ASCII
    cmp al, 'q'
    je shutdown
    cmp al, 'Q'
    je shutdown
    jmp .wait_key

shutdown:
    cli                ; disable interrupts
    hlt                ; halt the CPU
    jmp $              ; safety net if an NMI wakes it

int 0x16 with AH = 0x00 blocks until a key is pressed and returns its ASCII code in AL. On q/Q the CPU clears interrupts and halts; jmp $ is an infinite self-loop in case a non-maskable interrupt ever resumes execution.

The signature

times 510 - ($ - $$) db 0
dw 0xAA55

$ is the current address and $$ the start of the section, so $ - $$ is the number of bytes emitted so far. times 510 - ($ - $$) db 0 pads with zeros up to byte 510, and dw 0xAA55 writes the two-byte boot signature into bytes 510–511. Without that signature in exactly those bytes the BIOS refuses to boot the disk.

💡 Tidbit: The signature is stored little-endian, so on disk the bytes are 0x55 0xAA, even though the source writes dw 0xAA55. The firmware checks for precisely this pair at offsets 510 and 511 of the first sector.

Code walkthrough: the color variant

main_color.asm keeps the same skeleton but adds attribute-driven color and an interactive loop. The key new mechanism is the VGA attribute byte, computed as (background << 4) | foreground:

%define WHITE_ON_BLUE   0x1F     ; bg=1 (blue), fg=F (white)
%define YELLOW_ON_BLACK 0x0E     ; bg=0 (black), fg=E (yellow)

It paints a full-screen blue window by scrolling a rectangle with int 0x10, AH = 0x06 (scroll up; with AL = 0 it clears the window to the attribute in BH):

    mov ah, 0x06
    mov al, 0          ; AL=0 clears the whole window
    mov bh, WHITE_ON_BLUE
    mov cx, 0x0000     ; top-left = (0,0)
    mov dx, 0x184F     ; bottom-right = (24,79)
    int 0x10

Colored text is drawn with AH = 0x09 (write character and attribute at the cursor) followed by AH = 0x02 to advance the cursor one column, because AH = 0x09 does not move the cursor itself:

.loop:
    lodsb
    or al, al
    jz .done
    mov ah, 0x09       ; write char + attribute
    mov bh, 0
    mov cx, 1          ; repeat count
    int 0x10
    inc dl             ; next column
    mov ah, 0x02       ; reposition cursor
    int 0x10
    jmp .loop

The keyboard loop reads a key with int 0x16 and reacts:

    cmp al, '1'
    jb keyboard_loop
    cmp al, '5'
    ja check_special
    sub al, '0'        ; '1'..'5' -> 1..5
    shl al, 4          ; move into the background nibble
    or al, 0x0F        ; keep a white foreground
    mov bh, al
    ; ... int 0x10 AH=06 to repaint, then redraw the text

Keys 15 select a background color (the digit becomes the high nibble of the attribute), SPACE resets to the default blue, and Q clears the screen, prints "Goodbye!", and halts. (main_color.asm:63)

💡 Tidbit: Because AH = 0x09 writes to the framebuffer but leaves the cursor where it was, the color routine has to issue a separate AH = 0x02 after every character. The simpler AH = 0x0E teletype call used in the monochrome version advances the cursor for free — but can't set a color.

How to build and run

From helloworld-os-asm/:

make            # assemble both helloworld.img and helloworld_color.img
make run        # boot the monochrome image in QEMU
make run-color  # boot the color/keyboard image in QEMU
make clean      # remove the .img files
make help       # list targets

The build is just two NASM invocations — no compiler, no linker:

nasm -f bin main.asm       -o helloworld.img
nasm -f bin main_color.asm -o helloworld_color.img

For the toolchain and how the images are produced across all stages, see toolchain-and-build.md and building-and-running.md.

What it teaches

  • The minimal contract a machine demands before it will run your code: 512 bytes, loaded at 0x7C00, ending in 0xAA55.
  • Real mode and the segmented 16-bit world the CPU boots into.
  • Using BIOS interrupts as the only available system services.
  • VGA text attributes and how a single byte encodes both colors.
  • Why a "Hello, World" with no OS underneath it is a meaningfully different problem from one written against a C library.

Known limits

  • No second stage and no kernel. Everything must fit in 512 bytes, which is why the program is so small.
  • BIOS-only. Every service used here (int 0x10, int 0x16) exists only in real mode. The moment Stage 2 switches to protected mode, all of it disappears and the kernel must talk to hardware directly.
  • 16-bit, 1 MiB addressable, no protection. Real mode offers no memory protection and a 20-bit address space.
  • Halting is final. cli; hlt stops the machine; there is no shutdown or reboot path beyond resetting the emulator.

Next stage

The hardest conceptual leap in the whole tutorial comes next: not learning C, but getting the machine into a state where C can run at all — installing a GDT, flipping the protection-enable bit, far-jumping into 32-bit code, and linking a freestanding kernel to a fixed load address.

Stage 2 — A C Kernel in Protected Mode

See also

Clone this wiki locally