Writing a Minimal OS Loader That Boots into 32-Bit Mode

In this week’s Project52 build, I wrote a 512-byte boot sector that does far more than just boot — it switches the CPU into 32-bit Protected Mode, clears the screen, and prints a message without relying on BIOS or any operating system. This project builds directly on last week’s real-mode bootloader by going deeper into how modern systems actually start up. Instead of using BIOS interrupts like int 0x10, it sets up its own Global Descriptor Table (GDT), flips the protection bit in the CPU’s control register, and writes directly to VGA memory at 0xB8000. It’s a foundational leap from "printing in real mode" to "owning the machine in protected mode."

Last Week, In Week 12 of Project52, I built a minimalist operating system that fit entirely inside a 512-byte boot sector. It was a pure 16-bit real-mode program — no GRUB, no OS, just BIOS, my Assembly code, and a printed message on the screen using BIOS interrupts (int 0x10). That project was all about understanding how a computer starts, how the BIOS loads the boot sector, and what it means to run code directly from 0x7C00.

The code was short, raw, and enlightening:

nasm
mov ah, 0x0E     ; BIOS teletype mode 
mov al, 
'A' int 0x10 

It was a program that lived entirely in the comfort of the BIOS — real mode, interrupts, and all.

But that’s not where real operating systems live.

This week, in Week 13, I took that same 512-byte foundation and used it to enter 32-bit Protected Mode, the execution mode modern operating systems rely on. This meant leaving the BIOS behind. No more interrupts, no more int 0x10, no more handholding.

I wrote code to:

  • Set up the Global Descriptor Table (GDT)

  • Enable the Protection Enable bit in CR0

  • Perform a far jump to 32-bit code

  • And interact with the screen by writing directly to VGA memory (at 0xB8000)

What started as a blinking BIOS-dependent message in Week 12 evolved into an actual bare-metal execution environment this week.

In other words:

Last week, I printed with training wheels.
This week, I took them off — and built my own memory model.

The result is a bootable, BIOS-independent, 32-bit execution environment that clears the screen and prints a message without calling a single BIOS function.

Everything that runs is now mine — the CPU, the screen, the segments, the memory.

This week’s project marks the transformation of a boot sector into something more powerful — a real, memory-protected operating environment. I built a raw bootable binary that:

  • Boots without an operating system

  • Loads and installs a custom Global Descriptor Table (GDT)

  • Switches the CPU from 16-bit to 32-bit mode

  • Clears the VGA text screen

  • And prints a message using direct memory access — without BIOS interrupts

All of this fits in a 512-byte boot sector. Here’s what I built, how it works, and why it matters.

What’s a Boot Sector?

A boot sector is the first 512 bytes of a storage device. When a BIOS-powered PC starts, it looks for a bootable disk. If it finds one, it loads the first 512 bytes into memory address 0x7C00, then jumps to that address and starts executing.

At this point:

  • There is no OS

  • No C standard library

  • No filesystem

  • Just raw machine code and the BIOS

But there’s a rule:

The last two bytes of the boot sector must be 0xAA55 or BIOS will reject it.

BIOS Boot Flow Recap

  1. BIOS runs POST (Power-On Self-Test)

  2. It finds the bootable device and loads the first 512 bytes into 0x7C00

  3. If the last two bytes are 0xAA55, it jumps to 0x7C00

  4. Your bootloader starts executing from there

Real Mode vs Protected Mode

Real Mode (default on boot):

  • 16-bit

  • Max 1 MB memory access

  • Segment:Offset addressing

  • No memory protection

Protected Mode:

  • 32-bit

  • Full access to 4 GB memory

  • Flat memory addressing

  • Enables multitasking, virtual memory, and more

To build a real OS, we need to switch to protected mode. But the CPU starts in real mode, so we must:

  • Set up a GDT (Global Descriptor Table)

  • Enable the PE (Protection Enable) bit in CR0

  • Perform a far jump into a 32-bit code segment

Protected Mode (often abbreviated as PM) is a CPU operating mode on x86 processors that provides advanced features like:

  • Access to 4 GB of memory (vs. 1 MB in Real Mode)

  • Memory protection (so one program can't overwrite another)

  • Multitasking support

  • Privilege levels (kernel vs. user mode)

  • Virtual memory paging

Why Is It Called "Protected" Mode?

Because it protects the system from:

  • Programs accessing memory they don’t own

  • Faulty code crashing the entire OS

  • Unauthorized operations (e.g. user apps modifying kernel memory)

It introduces segment descriptors and page tables that tell the CPU:

“Here’s what memory this code is allowed to access, and how.”

Key Differences from Real Mode:

Feature

Real Mode

Protected Mode

Addressable Memory

1 MB

4 GB (or more)

Addressing Scheme

Segment:Offset

Flat or segmented

Memory Protection

❌ No

✅ Yes

Multitasking Support

❌ No

✅ Yes

Paging Support

❌ No

✅ Yes

Privilege Levels

❌ No

✅ Ring 0–3

Why It Matters

Every modern OS — Linux, Windows, macOS — uses protected mode or its successors (like long mode). Without it, you can’t build:

  • Virtual memory

  • Multi-user systems

  • Kernel vs. user space separation

  • Secure sandboxing

  • Reliable multitasking

Protected mode is the foundation of all modern operating systems.

Final Working Bootloader (Excerpt)

This 512-byte flat binary does all of that.

[org 0x7C00]
[bits 16]

start:
    cli
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00

    lgdt [gdt_descriptor]        ; Load the Global Descriptor Table

    mov eax, cr0
    or eax, 1                    ; Set PE (Protection Enable) bit
    mov cr0, eax

    jmp 0x08:protected_mode_start ; Far jump to 32-bit code segment

Here:

  • cli disables interrupts to prevent unexpected behavior

  • lgdt loads the GDT — which defines the 32-bit memory segments

  • We modify control register CR0 to set the PE bit (bit 0)

  • We then jump to segment 0x08, where our 32-bit code begins

🗂️ The GDT

gdt_start:
    dq 0x0000000000000000         ; Null descriptor
    dq 0x00CF9A000000FFFF         ; Code segment (offset 0, 4GB limit)
    dq 0x00CF92000000FFFF         ; Data segment
gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start - 1    ; GDT size (in bytes - 1)
    dd gdt_start                  ; GDT base address

What’s a GDT?

A Global Descriptor Table defines memory segments for code, data, and stacks. In protected mode, the CPU uses segment selectors to look up addresses in the GDT.

The Global Descriptor Table (GDT) is a critical structure in x86 Protected Mode — it tells the CPU how to interpret memory.

When you switch to Protected Mode, the old real-mode memory model (segment:offset) is no longer valid. Instead, memory access is controlled by segment descriptors, and those descriptors live in the GDT.

In simple terms:

The GDT maps “segment selectors” (like 0x08 or 0x10) to actual memory ranges and access rules.

So when you write:

jmp 0x08:label 

The CPU looks up 0x08 in the GDT to find:

  • Where this segment starts in memory

  • How long it is

  • What kind of access it allows (read? write? code execution?)

  • What privilege level is required (user vs. kernel)

What the GDT Defines

Each GDT entry is 8 bytes long and defines a segment descriptor.

It contains:

Field

Description

Base Address

Where the segment starts in memory

Limit

How big the segment is

Type

Code? Data? Readable? Writable?

Privilege

Which ring (0–3) can access it

Granularity

Byte-sized or 4K pages?

Role of the GDT in Bootloaders

To enter Protected Mode, you must:

  1. Define a GDT

  2. Load it into the GDTR using lgdt [gdt_descriptor]

  3. Use segment selectors from the GDT to set up CS, DS, etc.

Without the GDT, your CPU can’t safely or correctly access memory in Protected Mode.

Printing from Protected Mode (VGA Memory)

Once in protected mode, BIOS interrupts are no longer available. So we write directly to VGA text memory at 0xB8000.

[bits 32]
protected_mode_start:
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov fs, ax
    mov gs, ax

    ; Clear screen (80x25 cells)
    mov edi, 0xB8000
    mov ecx, 80 * 25
    mov ax, 0x1F20              ; ' ' with white on blue background
.clear:
    stosw
    loop .clear

    ; Print message
    mov esi, msg
    mov edi, 0xB8000
.next_char:
    lodsb
    or al, al
    jz .done
    mov ah, 0x1F
    stosw
    jmp .next_char

.done:
    cli
    hlt
    jmp $

msg:
    db "Protected Mode Reached!", 0
  • stosw stores a word (character + color) to memory

  • 0x1F = white on blue (foreground/background)

  • We loop through each byte in the message and print it

What is VGA Memory?

VGA memory is a special region of RAM on your video card, mapped to:

Physical address: 0xB8000

It’s a 16-bit text buffer where:

  • Each character cell takes 2 bytes:

    • 1 byte = ASCII character

    • 1 byte = color attribute (foreground + background)

For example:

nasm
mov ah, 0x0E
mov al, 'A'
int 0x10     ; BIOS teletype output

Prints an 'A' (0x41) in white on blue (0x1F) in the top-left of the screen.

Why Do We Use It in Protected Mode?

Because once you switch to 32-bit Protected Mode, you cannot use BIOS interrupts anymore.

BIOS interrupts (like int 0x10) only work in Real Mode:

nasm
mov ah, 0x0E
mov al, 'A'
int 0x10     ; BIOS teletype output

This won’t work in Protected Mode because:

  • The BIOS lives in memory above 1 MB

  • You can't call int 0x10 unless you're in 16-bit Real Mode

  • Most of the BIOS isn’t even mapped or accessible in Protected Mode

So we write directly to VGA memory instead.

Once you’re in protected mode, the only reliable output mechanism you have (before you write drivers) is:

  • VGA text buffer at 0xB8000

  • Writing 2-byte words directly into that memory

How VGA Memory Works (Text Mode)

Offset (in 0xB8000)

Byte 1 (char)

Byte 2 (color)

0x00

'H'

0x1F

0x02

'e'

0x1F

0x04

'l'

0x1F

...

...

...

The screen is 80×25 by default (2000 character cells). You can write to each one using:

nasm
mov edi, 0xB8000
mov ax, 0x1F20   ; space character ' ' with white on blue
stosw            ; write AX into [EDI], advance EDI

Why Use VGA Memory Instead of BIOS?

BIOS Interrupts

VGA Memory

Only available in Real Mode

Works in Protected Mode

Relies on 16-bit interrupts

Direct memory-mapped output

Can't use after switching CR0

No interrupts, just memory

Easy but limited

Manual but powerful

Once you build your own drivers and kernel later, you can go beyond text mode — but for now, VGA memory is your direct portal to the screen.

Final Output

The entire OS is built as a flat 512-byte binary. The final two lines are:

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

This:

  • Pads the binary to 510 bytes

  • Appends the BIOS-required 0xAA55 signature

The result is a fully bootable OS image that:

  • Sets up protected mode

  • Prints a message without any OS

  • And runs without GRUB, Linux, or BIOS interrupts

Why This Project Matters

This is the line where:

  • Bootloader becomes kernel

  • Startup code becomes OS

  • You become the system

It shows that modern computers still begin from terrifyingly simple logic:

Load 512 bytes, look for 0xAA55, jump to 0x7C00, and execute

Everything else — memory managers, filesystems, GUIs, networks — all build on this.

What’s Next?

Now that protected mode is running:

  • I’ll load a C kernel next (kernel.c)

  • Handle screen I/O in C

  • Write a basic kernel interface with text-based output

This is how you build a full OS — from 512 bytes to multi-tasking kernel, one week at a time.

🚀 Week 13 complete.

– Atul Verma
Creator, Project52