5.9 KiB
ACPI: finding the tables (RSDP → RSDT/XSDT → SDTs)
ACPI describes the hardware the kernel can't assume — the interrupt controllers, the PCIe config window, the timer, the power registers — in a set of system description tables (SDTs). But before it can read any of them, danos has to find them, and they aren't at a fixed address. Getting there is a short chain of pointers, and this note explains it — in particular the question it's easy to trip on: how does the platform / device module know where the RSDT is?
Short answer: it doesn't receive the RSDT. The firmware hands over the RSDP, and the RSDT's address is a field inside the RSDP. The platform follows that pointer.
The locator chain
UEFI configuration table
│ the loader reads the RSDP's physical address
▼
BootInfo.acpi_rsdp (u64, in the loader↔kernel handoff) system/boot-handoff.zig
│ the kernel forwards the whole BootInfo
▼
platform.discover(boot_info, …) system/devices/platform.zig
│ reads boot_info.acpi_rsdp, hands it to the ACPI backend
▼
acpi.discover(rsdp_phys, …) system/devices/acpi.zig
│ dereferences the RSDP, reads the pointer it contains
▼
RSDP ──(a field in the struct)──► RSDT / XSDT ──► SDTs (MADT, MCFG, FADT, HPET, DSDT…)
The RSDP (Root System Description Pointer) is the root of the whole ACPI tree. Its only job is to point at the root table — the RSDT (ACPI 1.0) or its 64-bit successor the XSDT (ACPI 2.0+) — which in turn lists every other SDT.
Step 1 — the loader finds the RSDP
Only the firmware knows where ACPI lives, so the RSDP must be grabbed while UEFI is
still up. acpiRootSystemDescriptorPointer() in boot/efi.zig walks the UEFI
configuration table for the ACPI GUID and returns the vendor pointer — the same
"grab it before ExitBootServices" pattern as the framebuffer and
the memory map.
Step 2 — the handoff: a physical address in BootInfo
The loader can't just call the device module: the bootloader binary and the kernel
binary are compiled separately, and the loader isn't linked against the platform
module at all (it imports only the boot-handoff contract). So instead of a call, it
deposits a value in the handoff struct:
// boot/efi.zig — while boot services are still up
.acpi_rsdp = if (acpiRootSystemDescriptorPointer()) |p| @intFromPtr(p) else 0,
Two things about what crosses the boundary:
- It's a physical address, not a Zig pointer. The loader and kernel don't share
an address space at the moment of the jump, so a raw
u64physical address is the only thing that survives the handoff.BootInfo.acpi_rsdpis0when the firmware exposed no ACPI (e.g. a future device-tree machine, which would fill a different field instead — the kernel never learns which firmware booted it). - The kernel can dereference it because it identity-maps ACPI memory. The RSDP
lives in ACPI-reclaim memory, which paging.zig identity-maps along with
the rest of RAM, so by the time discovery runs
@ptrFromInt(rsdp_phys)is a valid pointer.
This is the concrete form of the "capture the description pointer" step sketched in
discovery.md — a plain acpi_rsdp: u64 rather than a tagged handle,
since x86 is the only backend wired up so far.
Step 3 — the platform derives the RSDT from the RSDP
acpi.discover reinterprets the physical address as the RSDP struct, validates it,
and then reads the root-table pointer out of it. Which pointer depends on the ACPI
version, because the RSDP carries both:
const rsdp: *const RootSystemDescriptionPointer = @ptrFromInt(rsdp_phys);
if (!std.mem.eql(u8, &rsdp.signature, "RSD PTR ")) return error.BadRsdpSignature;
if (!checksumOk(@ptrFromInt(rsdp_phys), 20)) return error.BadRsdpChecksum;
if (rsdp.revision >= 2) {
// ACPI 2.0+: use the 64-bit XSDT pointer (the 32-bit RSDT is deprecated)
const xsdp: *const ExtendedSystemDescriptorPointer = @ptrFromInt(rsdp_phys);
try walkRoot(u64, xsdp.extended_system_descriptor_table_address, …);
} else {
// ACPI 1.0: use the 32-bit RSDT pointer
try walkRoot(u32, rsdp.root_system_description_table_address, …);
}
- The
revisionbyte selects the root table.root_system_description_table_address(32-bit, → RSDT) andextended_system_descriptor_table_address(64-bit, → XSDT) are ordinary fields of the RSDP/XSDP structs — the platform never receives the RSDT address, it reads it here. On QEMU q35 the RSDP is revision 2, so the XSDT path is taken. - The signature (
"RSD PTR ") and one-byte checksum guard against a bad pointer before anything downstream trusts it.
After the root table
walkRoot treats the RSDT/XSDT as an array of physical pointers — 32-bit entries for
the RSDT, 64-bit for the XSDT — and hands each SDT to handleTable, which dispatches
on its 4-byte signature: MADT (CPUs + IOAPIC), MCFG (PCIe ECAM), FADT
(power registers, and the pointer to the DSDT), HPET (timer). That's where the
firmware-agnostic device model gets populated; this note stops at the
part that answers "where are the tables?" — everything past the RSDP is just following
more pointers the tables themselves provide.
Related
- efi.md — the loader that captures the RSDP before
ExitBootServices. - memory-map.md — the same loader-captures / kernel-consumes seam, and the ACPI-reclaim memory the RSDP lives in.
- discovery.md — the broader (still-evolving) plan for turning these tables into one neutral device model shared with the ARM device-tree path.
- arch.md — why the kernel reaches the device code through a
platformmodule and never names ACPI directly.