- Atul for Marketing
- Posts
- Writing a Minimal OS Loader That Boots into 32-Bit Mode
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
BIOS runs POST (Power-On Self-Test)
It finds the bootable device and loads the first 512 bytes into
0x7C00
If the last two bytes are
0xAA55
, it jumps to0x7C00
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 behaviorlgdt
loads the GDT — which defines the 32-bit memory segmentsWe 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:
Define a GDT
Load it into the GDTR using
lgdt [gdt_descriptor]
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 memory0x1F
= 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 ModeMost 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) |
---|---|---|
|
|
|
|
|
|
|
|
|
... | ... | ... |
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