danos/docs/acpi.md

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 u64 physical address is the only thing that survives the handoff. BootInfo.acpi_rsdp is 0 when 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 revision byte selects the root table. root_system_description_table_address (32-bit, → RSDT) and extended_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.

  • 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 platform module and never names ACPI directly.