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 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 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.