-
Notifications
You must be signed in to change notification settings - Fork 0
protected mode
The 32-bit operating mode the kernel runs in — flat memory, ring 0, and no BIOS safety net.
After the BIOS finishes and the bootloader takes over, the CPU is still running in
real mode: a 16-bit, 1 MiB-addressable compatibility mode that has been kept
alive on every x86 chip since the 8086. To run a modern C kernel with a flat
4 GiB address space, MyOS-Simple switches the processor into protected mode.
This page explains what protected mode is, why the project needs it, and walks
through the exact switch performed in boot.asm.
Real mode is convenient at boot — the BIOS services
are available, and segment:offset addressing matches what the firmware expects —
but it is also extremely limiting:
- Addresses are formed as
segment * 16 + offset, capping reach at ~1 MiB. - Registers and the default operand size are 16-bit.
- There is no memory protection, no privilege separation, and no clean 32-bit model.
Protected mode lifts these limits. It gives the CPU:
-
32-bit registers and operands by default, so a C compiler targeting
-m32produces code that just works. - A 4 GiB linear address space addressable with plain 32-bit pointers.
-
Segment descriptors (held in the Global Descriptor Table)
that define each segment's base, limit, and access rights, instead of the fixed
*16arithmetic of real mode. - Privilege rings (0–3), though this project only ever uses ring 0.
💡 Tidbit: "Protected" refers to memory protection — the descriptor system can restrict access by privilege level and segment limit. MyOS-Simple does not actually use those protections for isolation; it sets up a single flat ring-0 model and runs everything there. The name is historical baggage from the 286.
MyOS-Simple uses the simplest possible protected-mode layout: the flat model.
Every segment (code and data) is configured with base 0 and limit 4 GiB, so
each one spans the entire address space 0x00000000 .. 0xFFFFFFFF. Because the
base is zero and the limit covers everything, the segment:offset translation
collapses: the effective address is simply the offset, i.e. a plain 32-bit
linear address.
This project never enables paging, so there is no page-table translation between linear and physical addresses. The result is the simplest mental model you can have:
logical (segment:offset) -> linear address -> physical address
offset == offset == offset
💡 Tidbit: Because paging is off, linear address == physical address throughout the entire kernel. When the C code writes to
0xB8000(seekernel.c:52), it is touching that exact physical byte of VGA text memory — no translation layer in between.
The two segment descriptors that establish this model are defined in the
GDT; the exhaustive bit-by-bit breakdown lives in
reference/gdt-descriptor-format.md.
The transition happens in boot.asm:39-63.
Here is the real code from the bootloader:
; Switch to protected mode
cli
lgdt [gdt_descriptor]
mov eax, cr0
or eax, 0x1
mov cr0, eax
jmp CODE_SEG:init_pm
[BITS 32]
init_pm:
; Set up protected mode segments
mov ax, DATA_SEG
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ebp, 0x90000
mov esp, ebp
; Call C kernel
call KERNEL_OFFSET
jmp $cliReal-mode interrupt handlers live in the BIOS-provided Interrupt Vector Table at
the bottom of memory. Once we change the CPU's operating mode, those handlers are
no longer valid, and this project never installs an IDT to replace them. So we
clear the interrupt flag and simply leave hardware interrupts off for the entire
life of the kernel. Input is handled by polling instead — see how
shell.c:145 busy-waits on the keyboard status port.
⚠️ Caveat: Because interrupts are never re-enabled, the kernel cannot be driven by a timer interrupt. The cooperative scheduler in stage 4 works around this by having tasks voluntarily yield, rather than being preempted by a clock tick.
lgdt [gdt_descriptor]lgdt loads the GDT register (GDTR) from a 6-byte structure describing where
the table is and how big it is. The CPU needs valid segment descriptors before
we enter protected mode, because the very next selector load must resolve against
the GDT. The structure is:
gdt_descriptor:
dw gdt_end - gdt_start - 1 ; limit (size in bytes, minus one)
dd gdt_start ; base (linear address of the table)The full table is covered in global-descriptor-table.md.
mov eax, cr0
or eax, 0x1
mov cr0, eaxBit 0 of control register CR0 is the Protection Enable (PE) flag.
or eax, 0x1 sets exactly that bit, then writes it back. The instant this write
completes, the CPU is technically in protected mode — segment registers now
hold selectors that index the GDT, rather than the shifted base values of real
mode.
But there is a subtlety: the CS register still holds its real-mode value,
and the CPU is still executing the instruction stream that follows. The processor
keeps interpreting code with the old code-segment until CS is reloaded.
jmp CODE_SEG:init_pmThis is the linchpin of the whole switch, and the one step beginners most often
get wrong. A far jump specifies both a segment selector and an offset:
CODE_SEG here is the value 0x08 (the byte offset of the code descriptor inside
the GDT). The far jump does two essential things at once:
-
Reloads
CSwith the protected-mode code selector, so the CPU finally starts decoding instructions using the 4 GiB flat code segment. - Flushes the instruction prefetch/pipeline, discarding any instructions the CPU fetched and decoded under the old 16-bit assumptions.
You cannot simply fall through into the 32-bit code — CS would never be reloaded
and the pipeline could contain stale, mis-decoded bytes. The far jump is what
makes the mode change actually take effect.
💡 Tidbit: Notice the
[BITS 32]directive right atinit_pm. This is an assembler directive telling NASM to emit 32-bit encodings from that point on; it does not change the CPU at runtime. The code before the far jump is still assembled[BITS 16]. Getting these directives to line up with the actual mode the CPU will be in is essential — a 32-bit encoding executed in 16-bit mode (or vice versa) decodes into garbage.
Now executing genuine 32-bit code, the bootloader loads every data segment
register (DS, SS, ES, FS, GS) with DATA_SEG (0x10, the data
descriptor's offset in the GDT). It then points the stack at 0x90000:
mov ebp, 0x90000
mov esp, ebpThis puts a comfortable stack well above both the bootloader (0x7C00) and the
kernel (0x1000), with plenty of room to grow downward. See the
memory map for the full picture of where everything
lives.
call KERNEL_OFFSET ; KERNEL_OFFSET equ 0x1000Finally the bootloader calls 0x1000, the address where the kernel was loaded
from disk. The first byte there is _start (from
kernel_entry.asm), which calls into C. The whole reason the
kernel is built as a flat binary linked at 0x1000 is so that
this single call lands exactly on the entry stub.
There is one historical gotcha that this project quietly relies on the emulator to
handle. On the original PC, address line A20 was forced low for 8086
compatibility, causing addresses at and above 1 MiB to wrap around to low
memory. To address all 4 GiB in protected mode you normally have to explicitly
enable the A20 line (commonly via the keyboard controller or fast-A20 port 0x92).
MyOS-Simple never enables A20 explicitly.
⚠️ Caveat: This works under QEMU because the emulator enables A20 by default. On some real hardware, with A20 left disabled, any address with bit 20 set would wrap at the 1 MiB boundary, corrupting memory accesses above that line. A production bootloader would enable A20 before relying on high memory. For a tutorial that boots in an emulator, it is omitted for simplicity.
- Global Descriptor Table — the segment table this mode requires
- Real mode — the 16-bit mode we switch out of
- Freestanding C — what the kernel can and cannot assume once we arrive here
- Boot process — the full firmware-to-kernel journey
- Stage 2: C in protected mode — the stage that introduces this switch
-
reference/gdt-descriptor-format.md— exhaustive descriptor bit tables - Memory map — where the GDT, stack, and kernel sit
- Home
Stages
- 1 · Assembly boot
- 2 · C protected mode
- 3 · Interactive shell
- 4 · Clock / processes / calc
- 5 · Stabilized release
Concepts — boot
Concepts — protected mode
Concepts — hardware
Concepts — OS services
Reference
- Memory map
- I/O ports
- GDT descriptor format
- Scancode tables
- Command reference
- Toolchain & build
- Glossary
Guides