PHASE I · IGNITION / MODULE 02 ·

Memory & Interrupts

The firmware handed you a running CPU and a map of RAM. Now you take ownership: build a physical memory manager, install your own interrupt table, and make the timer and keyboard fire into your code.

Module 02 — walkthrough
module introcoming soon

In Module 01 you drew to a screen the firmware owned. From here, you own the machine. That means three things the firmware was doing for you become your job: managing memory, handling interrupts, and keeping time. This module builds all three.

Lesson 2.1

2.1 The UEFI memory map

the firmware tells you exactly what RAM you have
2.1 — reading the memory map
~14 mincoming soon

Before you can hand out memory, you need to know what exists. UEFI gives you a memory map: an array of descriptors, each saying "this physical range, this many pages, this type". You ask for it with GetMemoryMap. The catch: the call tells you how big the buffer needs to be by failing the first time, so you call it twice.

memory.c — get the mapcopy
UINTN map_size = 0, map_key, desc_size;
UINT32 desc_ver;
EFI_MEMORY_DESCRIPTOR *map = 0;

// first call: fails with BUFFER_TOO_SMALL but fills map_size
BS->GetMemoryMap(&map_size, map, &map_key, &desc_size, &desc_ver);

// allocate a little extra — the map can grow between calls
map_size += 2 * desc_size;
BS->AllocatePool(EfiLoaderData, map_size, (void**)&map);

// second call: actually fills the map
BS->GetMemoryMap(&map_size, map, &map_key, &desc_size, &desc_ver);
Why desc_size and not sizeof

Never index the map with sizeof(EFI_MEMORY_DESCRIPTOR). The firmware may use a larger descriptor than your headers define, and it tells you the real size in desc_size. Walk the array by adding desc_size bytes each step, or you'll desync and read garbage — a classic first-map bug.

Walk the descriptors and sum every range marked EfiConventionalMemory — that's the free RAM you're allowed to manage. Everything else (firmware code, ACPI tables, MMIO) is off-limits.

Lesson 2.2

2.2 A physical memory manager

a bitmap allocator — the simplest thing that works
2.2 — building the PMM
~20 mincoming soon

A physical memory manager hands out 4 KB pages. The simplest correct design is a bitmap: one bit per page, 0=free, 1=used. It's O(n) to find a free page, which is fine at this stage and trivial to get right — and "trivial to get right" beats "clever" for the layer everything else depends on.

pmm.c — bitmap allocatorcopy
static UINT8 *bitmap;       // one bit per 4KB page
static UINT64 total_pages;

static int bit_get(UINT64 i){ return (bitmap[i>>3] >> (i&7)) & 1; }
static void bit_set(UINT64 i){ bitmap[i>>3] |= (1 << (i&7)); }
static void bit_clr(UINT64 i){ bitmap[i>>3] &= ~(1 << (i&7)); }

void *pmm_alloc_page(void) {
    for (UINT64 i = 0; i < total_pages; i++) {
        if (!bit_get(i)) {          // first free page
            bit_set(i);
            return (void*)(i * 4096);
        }
    }
    return 0;                       // out of memory
}

void pmm_free_page(void *p){ bit_clr((UINT64)p / 4096); }
Mark the firmware's regions used FIRST

Before handing out a single page, walk the memory map again and bit_set every page that isn't EfiConventionalMemory. If you skip this, your allocator will happily hand out the firmware's own tables, the framebuffer, or ACPI data — and the machine dies in a way that's miserable to debug. Reserve first, allocate second.

Lesson 2.3

2.3 ExitBootServices — cutting the cord

the point of no return
2.3 — leaving the firmware behind
~16 mincoming soon

So far the firmware's boot services are still running underneath you. To become a real OS you call ExitBootServices — after this, GetMemoryMap, AllocatePool, and the firmware console are gone. You're on your own. This is the single most important handoff in the boot, and it has one infamous gotcha.

boot.c — the handoffcopy
// You must pass the CURRENT map_key. If the map changed since you
// last fetched it, ExitBootServices FAILS — and the act of allocating
// the map buffer can itself change the map. So: fetch, exit, and if it
// fails, re-fetch and retry.
for (int tries = 0; tries < 2; tries++) {
    get_memory_map(&map, &map_size, &map_key);
    if (BS->ExitBootServices(ImageHandle, map_key) == EFI_SUCCESS)
        break;   // success — firmware is now out of the picture
}
// From here: no firmware services. Your PMM is the only allocator.
// Your framebuffer pointer still works — it is just memory now.
The map_key gotcha ends more first OSes than any other line

ExitBootServices refuses to run with a stale map_key, and allocating the buffer to hold the map can change the map, invalidating the key you just got. The fix is the retry loop above: fetch the map immediately before the call, and if it fails, fetch again and retry. Miss this and your OS hangs at the exact moment it's supposed to come alive.

Lesson 2.4

2.4 The IDT and handling interrupts

make the CPU call your code on events
2.4 — installing an interrupt table
~22 mincoming soon

An Interrupt Descriptor Table is 256 entries telling the CPU where to jump when something happens — a divide-by-zero (vector 0), a page fault (14), a timer tick, a keystroke. You build the table, point each entry at a small assembly stub that saves registers and calls your C handler, then load it with lidt.

idt.c — one entrycopy
struct idt_entry {
    UINT16 off_low;    // handler address, bits 0-15
    UINT16 selector;   // code segment (0x08)
    UINT8  ist;
    UINT8  flags;       // 0x8E = present, ring 0, interrupt gate
    UINT16 off_mid;    // bits 16-31
    UINT32 off_high;   // bits 32-63
    UINT32 zero;
} __attribute__((packed));

void idt_set(int vec, void *handler) {
    UINT64 a = (UINT64)handler;
    idt[vec].off_low  = a & 0xFFFF;
    idt[vec].selector = 0x08;
    idt[vec].flags    = 0x8E;
    idt[vec].off_mid  = (a >> 16) & 0xFFFF;
    idt[vec].off_high = (a >> 32);
}
ForgeOS ships this behind a safety switch

Installing an IDT is the one step that can wedge a machine at boot if a stub is wrong. ForgeOS makes the whole interrupt foundation opt-out (a SAFEMODE marker file disables it) precisely because a bad IDT is unrecoverable without it. When you build yours, keep a known-good build on a second stick — you will fault the CPU at least once, and that's how you learn.

Lesson 2.5

2.5 The timer and keyboard

the machine finally responds to you
2.5 — timer ticks and keystrokes
~18 mincoming soon

With an IDT in place, wire two interrupts. The LAPIC timer fires at a fixed rate — your heartbeat for animation, timeouts, and scheduling later. The keyboard fires on every key. Both work the same way: the device raises an interrupt, your stub runs, you read the device, you acknowledge the interrupt.

handlers.ccopy
volatile UINT64 ticks = 0;

void timer_handler(void) {
    ticks++;                    // your sense of time
    lapic_eoi();               // acknowledge — or it never fires again
}

void keyboard_handler(void) {
    UINT8 scancode = inb(0x60);   // read the byte the keyboard latched
    key_queue_push(scancode);      // hand it to your input layer
    lapic_eoi();
}
Always acknowledge the interrupt

If you forget lapic_eoi() (or the PIC's EOI), that interrupt line never fires again — your timer stops, or the keyboard goes dead after exactly one key. It's the most common "why did everything freeze" bug in a new interrupt setup. Every handler ends by acknowledging.

Module 02 checkpoint
  • The UEFI memory map, parsed by desc_size
  • A working bitmap PMM that hands out and frees 4 KB pages
  • A clean ExitBootServices with the map_key retry loop
  • An IDT installed, with stubs calling your C handlers
  • A ticking timer and a keyboard that fills an input queue

The machine is now yours — you own memory, you own time, and it responds to the keyboard. Everything from here is building upward. Next: turn that framebuffer into a real compositor that can draw windows without flicker.

next module

Module 03 — A Software Compositor

Double buffering, dirty rectangles, and the write-combined framebuffer that turns the slideshow from Module 01 into a smooth 60-fps surface. The primitives every app is built on.

Get lifetime access — $149 →
or see all plans →