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 shared `danos` module) src/root.zig
│ the kernel forwards the whole BootInfo
▼
platform.discover(boot_info, …) src/device/platform.zig
│ reads boot_info.acpi_rsdp, hands it to the ACPI backend
▼
acpi.discover(rsdp_phys, …) src/device/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 src/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 shared danos module). So instead of a call, it
deposits a value in the handoff struct:
// src/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.