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.
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.
2.1 The UEFI memory map
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.
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);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.
2.2 A physical memory manager
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.
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); }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.
2.3 ExitBootServices — cutting the cord
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.
// 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.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.
2.4 The IDT and handling interrupts
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.
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);
}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.
2.5 The timer and keyboard
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.
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();
}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.
- The UEFI memory map, parsed by
desc_size - A working bitmap PMM that hands out and frees 4 KB pages
- A clean
ExitBootServiceswith 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.
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 →