ACPI and Hardware Description at Boot
When a kernel starts, it knows nothing about the specific machine it is running on — how many CPUs there are, where the interrupt controllers live, where PCI Express configuration space is mapped, which memory belongs to which NUMA node, or even how to turn the machine off. On x86 and modern Arm servers, firmware answers all of this through the Advanced Configuration and Power Interface (ACPI): a set of in-memory tables the firmware builds and leaves in RAM for the kernel to find. The kernel follows a fixed discovery chain — firmware hands it the Root System Description Pointer (RSDP), which points at a table-of-tables (RSDT or XSDT), which points at every other table by its four-character signature. Some of those tables are flat C-style structs of fixed data (the FADT for fixed hardware and power registers, the MADT for interrupt controllers and CPU enumeration, MCFG for PCIe configuration space, SRAT/SLIT for NUMA topology); others — the DSDT and SSDTs — contain ACPI Machine Language (AML), a bytecode the kernel must interpret at runtime to build a namespace of devices and to call methods that compute their answers (Linux table parsing in
tables.c, v6.12; ACPI overview). On Arm and embedded platforms the same role is often filled instead by a Device Tree Blob (DTB) the bootloader passes in. This note traces the table-discovery chain end to end, explains what each key table carries and how the kernel parses it at boot, and contrasts the ACPI path with the Device Tree alternative.
This note is pinned to Linux 6.12 LTS (released 2024-11-17), with the ACPI boot path structurally unchanged in 6.18 LTS (released 2025-11-30); the functions cited were read at git tag v6.12. It owns the boot-time table-discovery chain and what each table describes. Its sibling ACPI Device Enumeration owns the later step — walking the AML namespace to create acpi_device and platform_device objects — and ACPI Power States G S C P and D States owns the runtime power semantics; this note is the layer beneath both, covering how the tables get found in the first place.
Mental Model
Think of ACPI tables as a self-describing tree of structs the firmware drops in memory, rooted at a pointer the firmware tells you where to find. The kernel does not scan all of RAM looking for hardware; it follows one pointer to a directory, reads the directory, and visits each table the directory names. Every table begins with a common header whose first field is a four-character ASCII signature ("FACP" for the FADT, "APIC" for the MADT, "MCFG", "SRAT", "DSDT", …), so the kernel matches tables by signature without parsing their bodies first.
The single most important distinction is fixed-format tables vs. AML tables. The FADT, MADT, MCFG, SRAT, and SLIT are data: flat structures the kernel reads field by field. The DSDT and SSDTs are code: AML bytecode the kernel feeds to an interpreter (ACPICA) to build a live object tree — the ACPI namespace — and to invoke methods that can compute answers dynamically (a _CRS method might read a chip register to report which resources a device is actually using). This is the deep reason ACPI needs a whole bytecode interpreter inside the kernel while Device Tree needs only a parser: Device Tree is pure declarative data, ACPI mixes data tables with a Turing-complete description language.
flowchart TB FW["Firmware (UEFI / BIOS)<br/>builds tables in RAM"] FW -->|"UEFI: EFI config table (ACPI 2.0 GUID)<br/>BIOS: scan EBDA + 0xE0000-0xFFFFF<br/>for 'RSD PTR '"| RSDP["RSDP<br/>Root System Description Pointer<br/>RsdtAddress (32-bit)<br/>XsdtAddress (64-bit, ACPI 2.0+)"] RSDP --> ROOT["RSDT / XSDT<br/>list of physical pointers<br/>to all other tables"] ROOT --> FADT["FADT (sig 'FACP')<br/>fixed HW + power registers<br/>points to DSDT / X_DSDT"] ROOT --> MADT["MADT (sig 'APIC')<br/>interrupt controllers<br/>+ CPU enumeration"] ROOT --> MCFG["MCFG<br/>PCIe ECAM base per segment"] ROOT --> SRAT["SRAT / SLIT<br/>NUMA: domains + distances"] FADT --> DSDT["DSDT + SSDTs<br/>AML bytecode"] DSDT -->|"ACPICA interprets AML"| NS["ACPI namespace<br/>(devices, methods, power)<br/>→ see ACPI Device Enumeration"] MADT -->|"acpi_parse_madt_lapic_entries()"| CPUS["bring up CPUs +<br/>register Local/IO APICs"]
The ACPI table-discovery chain at boot. What it shows: firmware leaves a single root pointer (the RSDP) the kernel finds via the UEFI config table or a BIOS memory scan; that pointer leads to a directory (RSDT/XSDT) naming every other table by signature; fixed tables (FADT/MADT/MCFG/SRAT) are read as data, while the FADT additionally points to the AML tables (DSDT/SSDTs) the kernel must interpret. The insight to take: all of ACPI hangs off one pointer, and the chain cleanly separates “facts the kernel reads early to bring up CPUs and interrupts” (MADT) from “a programmable device namespace the kernel interprets” (DSDT) — the former must work before there’s even a working interrupt setup, the latter can wait until the interpreter is up.
Finding the Root: the RSDP
Everything starts with the Root System Description Pointer (RSDP), a small structure carrying the signature "RSD PTR " (note the trailing space — it is eight bytes), a checksum, an OEM ID, a revision, and the physical address of the root table. In ACPI 1.0 the RSDP held only a 32-bit RsdtAddress; ACPI 2.0 extended it with a Length, a 64-bit XsdtAddress, and an extended checksum, so modern systems with memory above 4 GiB can place tables anywhere.
How the kernel finds the RSDP differs by firmware:
- On UEFI systems, the firmware advertises the RSDP’s physical address through the EFI configuration table. Linux’s
efi_config_parse_tables()matches the configuration-table GUIDs against acommon_tables[]array:ACPI_20_TABLE_GUIDmaps toefi.acpi20and the olderACPI_TABLE_GUIDmaps toefi.acpi(drivers/firmware/efi/efi.c, v6.12). The kernel prefersacpi20(the ACPI 2.0+ RSDP) when present. This is clean and deterministic — no scanning. (The same array also records SMBIOS, ESRT, and TPM-log tables.) - On legacy BIOS systems, there is no configuration table, so the kernel scans memory for the
"RSD PTR "signature on 16-byte boundaries: first the Extended BIOS Data Area (EBDA, whose address is read from a fixed low-memory word), then the read-only BIOS region from0x000E0000to0x000FFFFF. The first 16-byte-aligned match with a valid checksum wins (ACPI overview).
This is exactly why UEFI made ACPI discovery robust: the firmware tells the OS where the tables are instead of the OS guessing.
The Directory: RSDT and XSDT
The address in the RSDP points at the root system description table — either the RSDT (Root System Description Table) or the XSDT (Extended System Description Table). Both are simply a standard ACPI header followed by an array of physical pointers to every other table. The only difference is pointer width: the RSDT’s entries are 32-bit, the XSDT’s are 64-bit. On any system with 64-bit physical addressing the XSDT is authoritative and the RSDT is either absent or a backward-compatibility stub; the Arm64 ACPI guide goes so far as to require the XSDT and forbid the RSDT (arm-acpi.rst, v6.12).
Linux brings the table machinery up in acpi_table_init(), which calls acpi_locate_initial_tables() to find the RSDP, walk the XSDT/RSDT, and populate an initial_tables[] array of table descriptors (capacity ACPI_MAX_TABLES) (drivers/acpi/tables.c, v6.12). Thereafter any subsystem that wants a table calls acpi_table_parse(signature, handler) — “find the table whose four-character signature matches, and run this handler on it” — or acpi_table_parse_entries_array() for tables (like the MADT or SRAT) that are themselves arrays of typed sub-entries. The handler model is how each piece of the kernel grabs exactly the table it cares about.
The Fixed Tables
FADT — Fixed ACPI Description Table
The FADT (signature "FACP") describes the platform’s fixed hardware: the addresses of the power-management event/control registers, the PM timer, the embedded-controller command/data ports, GPE (General-Purpose Event) blocks, and a pile of feature flags (does the platform have a legacy 8042 keyboard controller? is it “hardware-reduced,” i.e. has no legacy ACPI register block at all, as required on Arm64?). Critically, the FADT also carries the physical address of the DSDT: the 32-bit DSDT field and, in ACPI 2.0+, the 64-bit X_DSDT field. So the FADT is both a data table and the link from the fixed-table world into the AML world. Linux parses it in acpi_parse_fadt() on x86, extracting the PM-timer address and legacy-device flags into the platform’s boot state (arch/x86/kernel/acpi/boot.c, v6.12).
MADT — Multiple APIC Description Table
The MADT (signature "APIC") is arguably the most important table at boot, because it both enumerates the CPUs and describes the interrupt controllers. Its body is a variable-length array of typed sub-entries, each tagged with a type byte. Linux iterates them with acpi_table_parse_madt(), dispatching on the type (arch/x86/kernel/acpi/boot.c, v6.12):
ACPI_MADT_TYPE_LOCAL_APICandACPI_MADT_TYPE_LOCAL_X2APIC— one entry per logical CPU, carrying the processor’s APIC ID.acpi_parse_lapic()/acpi_parse_x2apic()extract the APIC ID and processor ID and calltopology_register_apic(); this is how the kernel learns how many CPUs exist and what their APIC IDs are, which is the prerequisite for SMP bring-up.acpi_is_processor_usable()filters on theACPI_MADT_ENABLEDflag so disabled/absent sockets are skipped.ACPI_MADT_TYPE_IO_APIC— each I/O APIC and the Global System Interrupt (GSI) base it owns;acpi_parse_ioapic()→mp_register_ioapic()records them so the kernel can route device interrupts.ACPI_MADT_TYPE_INTERRUPT_OVERRIDE— remaps legacy ISA IRQs to GSIs (e.g. the timer’s IRQ0);acpi_parse_int_src_ovr()applies these so the kernel doesn’t assume the legacy 1:1 IRQ mapping.
On Arm64 the same MADT instead carries GIC (Generic Interrupt Controller) structures — GICC per-CPU interfaces, GICD distributor, redistributors — and the MADT is required to contain only GIC structures (arm-acpi.rst, v6.12). The shape of the table is the same; only the entry types differ by architecture.
MCFG — PCIe Configuration Space
Legacy PCI configuration space is reached through two I/O ports (0xCF8/0xCFC), which only addresses 256 bytes per function and is slow. PCI Express extended config space to 4 KiB per function and made it memory-mapped — the Enhanced Configuration Access Mechanism (ECAM). The MCFG table tells the kernel where that memory-mapped region lives: each MCFG entry gives a (PCI segment group, base physical address, start_bus, end_bus) tuple. Linux parses it in pci_mcfg_parse() (triggered by acpi_table_parse(ACPI_SIG_MCFG, ...)), storing the entries in a pci_mcfg_list; later, pci_mcfg_lookup() matches a PCI root’s segment and bus range and computes the ECAM virtual address as base + (bus << 20) (drivers/acpi/pci_mcfg.c, v6.12). Without the MCFG, the kernel would be stuck with the 256-byte legacy window and could not see PCIe extended capabilities.
SRAT and SLIT — NUMA Topology
On multi-socket and large servers, memory is Non-Uniform Memory Access (NUMA): a CPU reaches its local memory faster than a remote socket’s. Two tables describe this. The SRAT (System Resource Affinity Table) maps CPUs and memory ranges to proximity domains (the firmware’s name for NUMA nodes): acpi_parse_processor_affinity() handles ACPI_SRAT_TYPE_CPU_AFFINITY entries (and x2APIC/GICC variants), and acpi_parse_memory_affinity() handles ACPI_SRAT_TYPE_MEMORY_AFFINITY, converting each proximity domain to a logical node ID and calling numa_add_memblk() to register the memory block with its node (drivers/acpi/numa/srat.c, v6.12). The SLIT (System Locality Information Table) is the node-to-node distance matrix: acpi_parse_slit() reads an N×N matrix of relative distances (10 = local by convention) and feeds it to numa_set_distance(), which the scheduler and page allocator later use to prefer local memory. The kernel’s slit_valid() guards against the common firmware bug of filling the whole matrix with 10 (claiming everything is equidistant), which would defeat NUMA-aware placement. All of this is orchestrated by acpi_numa_init().
The AML Tables: DSDT and SSDTs
The DSDT (Differentiated System Description Table) is “the main AML table” (DSDT overview) — pointed to from the FADT’s DSDT/X_DSDT field. Its body is ACPI Machine Language, a bytecode that, when interpreted, builds the ACPI namespace: a tree of objects describing every non-self-describing device (the embedded controller, LPC/legacy peripherals, I²C/SPI devices, GPIO controllers, batteries, thermal zones) along with methods — small AML programs the OS calls to query or control hardware. A device’s _CRS (Current Resource Settings) method returns its registers and IRQs; _STA reports presence/status; _PRT gives PCI interrupt routing; _DSD carries vendor device properties; power methods like _PS0/_PS3 switch device power states. Because these are methods, not static fields, they can compute their answers — read a register, branch on a condition — which is the whole point of using a bytecode rather than a flat table.
SSDTs (Secondary System Description Tables) are additional AML tables that extend the same namespace. Firmware uses them to keep the DSDT modular (one SSDT per optional feature) and to load device descriptions dynamically — for example, CPU C-state/P-state objects are frequently shipped in SSDTs. The kernel loads the DSDT and all SSDTs into one merged namespace.
The OS’s AML interpreter is ACPICA (ACPI Component Architecture), Intel’s open-source reference interpreter that Linux, FreeBSD, and others embed. Linux brings it up after the early fixed-table parsing; from then on, “the kernel’s AML interpreter can evaluate [a] method, look to see if it supports ‘XYZ’ and answer YES or NO to the BIOS” (osi.rst, v6.12). The actual namespace walk that turns these objects into Linux acpi_devices and platform_devices is the subject of ACPI Device Enumeration — this note stops at “the AML tables are found and handed to the interpreter.”
The Device Tree Alternative
ACPI is dominant on x86 and Arm servers, but most Arm/embedded systems, RISC-V, and older PowerPC/SPARC use a Device Tree instead: the bootloader loads a Device Tree Blob (DTB) — a compiled, declarative description of the board’s hardware — and passes its address to the kernel, which unflattens it into device_node objects (see Device Tree for the full mechanism, and Device Tree Bindings and compatible Strings for the matching contract). The conceptual role is identical — tell the kernel what non-discoverable hardware exists — but the form differs sharply: Device Tree is pure data with no interpreter, while ACPI carries AML bytecode and an interpreter.
On Arm64, where a platform might supply either, the kernel chooses by policy (arm-acpi.rst, v6.12):
- Default (no relevant command-line option): the kernel “will try to use DT for device enumeration; if there is no DT present, the kernel will try to use ACPI tables, but only if they are present.” DT is preferred when both look usable.
acpi=force: the kernel “will attempt to use ACPI tables first, but fall back to DT if there are no ACPI tables present.”acpi=off: ACPI processing is disabled (this is the historical default phrasing on Arm64).
The governing principle the doc states plainly: “the kernel will not fail to boot unless it absolutely has no other choice,” and “the kernel must always be capable of booting with either scheme.” So on Arm64 the bootloader’s choice of what it hands the kernel — a DTB versus a set of ACPI tables — largely decides which path is taken.
Failure Modes and Common Misunderstandings
A wrong or missing table is a silent no-show, not a crash. If the MCFG is absent, PCIe extended config space simply isn’t reachable; if a device’s DSDT entry is missing, the device never appears. The kernel describes only what the firmware asserts.
Buggy SLIT matrices defeat NUMA. As noted, a lot of firmware fills the SLIT with 10s everywhere, telling the kernel every node is equidistant. The kernel’s slit_valid() rejects obviously-bogus matrices, but a plausibly wrong one will quietly mis-place memory and hurt performance.
_OSI and the “everyone pretends to be Windows” problem. AML methods can call _OSI("string") to ask the OS whether it supports an interface, and branch accordingly. In practice firmware vendors test only against Windows and gate working code paths on _OSI("Windows 20xx"), leaving Linux-only paths untested and broken. Linux’s pragmatic response is to answer yes to the relevant Windows _OSI strings so the AML takes the well-tested path — a direct admission that the AML interpreter’s behavior is shaped by firmware bugs, documented in the kernel’s own osi.rst (v6.12).
Overriding tables for debugging. Because the DSDT/SSDT are AML you can decompile (acpidump → iasl), fix, and recompile, Linux supports table upgrades from the initramfs (acpi_table_upgrade() / acpi_table_initrd_override() in tables.c), gated by kernel lockdown so a signed/locked-down kernel cannot have its hardware description silently swapped (drivers/acpi/tables.c, v6.12). This is the standard way to work around a broken vendor DSDT without reflashing firmware.
Production Notes
ACPI table problems are a recurring source of platform bring-up pain. The everyday diagnostic toolkit is dmesg | grep ACPI (which prints each table’s signature, OEM ID, and address as the kernel finds them), the /sys/firmware/acpi/tables/ directory (raw table dumps), and acpidump/iasl to decompile the DSDT. A missing or malformed MADT shows up as the kernel seeing the wrong CPU count; a bad MCFG as PCIe devices missing extended capabilities; a bad SRAT/SLIT as surprising numactl --hardware output and NUMA performance regressions. On Arm64 servers the move to ACPI (over Device Tree) was a deliberate datacenter decision — server vendors wanted one firmware-described model like x86 so a single generic kernel boots any compliant box — and the Arm SBSA/SBBR specifications mandate exactly the table set (RSDP, XSDT, FADT, MADT, GTDT, DSDT, MCFG, SPCR) the kernel’s arm-acpi.rst enumerates. The efivar_ssdt= parameter (see UEFI Boot Services and Runtime Services) even lets an administrator inject an SSDT via an EFI variable, bridging the variable runtime service and ACPI table handling.
Uncertain
Verify: the exact byte-level field layout of the RSDP (offsets of
RsdtAddress/Length/XsdtAddress/ExtendedChecksum) and the FADT’sDSDT/X_DSDTfield offsets. Reason: the authoritative ACPI Specification field tables and the OSDev RSDP page were not directly fetched (uefi.org/related spec hosts returned 403; the field facts here are corroborated from the ACPI/DSDT Wikipedia overviews and the kernel’s table-parsing source, not a direct read of the ACPI spec’s structure tables). To resolve: consult the UEFI Forum’s ACPI Specification (current 6.5/6.6) §5.2 “ACPI System Description Tables” for the exact RSDP and FADT field offsets, or readinclude/acpi/actbl*.hin the kernel tree for the C struct definitions. uncertain
See Also
- ACPI Device Enumeration — the next step: walking the AML namespace this note’s tables expose, to create
acpi_device/platform_deviceobjects - Device Tree — the declarative alternative on Arm/embedded/RISC-V; same role, no AML interpreter
- Device Tree Bindings and compatible Strings — how a DT node is matched to a driver
- ACPI Power States G S C P and D States — the runtime power semantics the FADT/DSDT power objects drive
- UEFI Firmware — how UEFI hands the RSDP to the kernel via the EFI configuration table
- UEFI Boot Services and Runtime Services — the stub records the ACPI config-table address before
ExitBootServices();efivar_ssdt=injects an SSDT via a variable - start_kernel and Early Initialization — where
acpi_table_init()andacpi_boot_init()are called during kernel bring-up - Secure Boot and the Kernel Trust Chain — kernel lockdown gates ACPI table overrides from initramfs
- Linux Boot and Init MOC — the parent map (§1 Firmware)