From 7d0155d8ce1eba54c10f3ab3bc73836ce95cf5ae Mon Sep 17 00:00:00 2001 From: Katharina Heidenreich Date: Thu, 2 Jul 2026 19:33:19 +0200 Subject: [PATCH] feat: madt parsing --- kernel/include/init/acpi.h | 35 ++++--- kernel/include/init/apic.h | 193 +++++++++++++++++++++++++++++++++++++ kernel/src/init/init.cpp | 15 ++- kernel/src/init/init.o | Bin 36704 -> 0 bytes 4 files changed, 227 insertions(+), 16 deletions(-) create mode 100644 kernel/include/init/apic.h delete mode 100644 kernel/src/init/init.o diff --git a/kernel/include/init/acpi.h b/kernel/include/init/acpi.h index 39675b1..9561506 100644 --- a/kernel/include/init/acpi.h +++ b/kernel/include/init/acpi.h @@ -12,9 +12,6 @@ constexpr uint32_t fold_signature(const char (&text)[5]) { | static_cast(static_cast(text[3])) << 24; } -// Values match the folded signature stored in sdt_header_t::signature. DSDT and -// FACS are absent on purpose: they are reached through the FADT, not listed in -// the RSDT/XSDT. enum class signature : uint32_t { fadt = fold_signature("FACP"), madt = fold_signature("APIC"), @@ -24,7 +21,6 @@ enum class signature : uint32_t { ssdt = fold_signature("SSDT"), }; -// A folded signature, unfolded back into a null-terminated 4-character string. struct signature_text_t { char text[5]; }; @@ -63,6 +59,8 @@ struct rsdp_t { } __attribute__((packed)); static_assert(sizeof(rsdp_t) == 36); +struct madt_t; + struct Rsdp { explicit Rsdp(paddr_t rsdp_physical) : physical(rsdp_physical) {} @@ -75,20 +73,33 @@ struct Rsdp { for (auto index = 0u; index < count; index++) { uint64_t table_physical = 0; memcpy(&table_physical, entries + index * entry_size, entry_size); - callback(paddr_t{table_physical}); + dispatch(paddr_t{table_physical}.access(), callback); } } +private: template - void find_table(signature sig, Fn&& callback) { - for_each_table([&](paddr_t table) { - if (table.access()->signature == static_cast(sig)) { - callback(table); - } - }); + static void dispatch(const sdt_header_t* header, Fn&& fn) { +#define ACPI_DISPATCH(signature_enum, TableType) \ + case signature::signature_enum: \ + if constexpr (requires { fn(static_cast(nullptr)); }) { \ + fn(reinterpret_cast(header)); \ + return; \ + } \ + break; + + switch (static_cast(header->signature)) { + ACPI_DISPATCH(madt, madt_t) + default: break; + } + +#undef ACPI_DISPATCH + + if constexpr (requires { fn(static_cast(nullptr)); }) { + fn(header); + } } -private: bool uses_xsdt() { return physical.access()->revision >= 2; } diff --git a/kernel/include/init/apic.h b/kernel/include/init/apic.h new file mode 100644 index 0000000..d969639 --- /dev/null +++ b/kernel/include/init/apic.h @@ -0,0 +1,193 @@ +#pragma once + +#include "init/acpi.h" +#include "memory/pointer.h" +#include "util/number.h" + +namespace acpi { + +enum class madt_entry_type : uint8_t { + local_apic = 0, + io_apic = 1, + interrupt_source_override = 2, + nmi_source = 3, + local_apic_nmi = 4, + local_apic_address_override = 5, +}; + +inline constexpr const char *get_madt_entry_type_name(madt_entry_type type) { + switch (type) { + case madt_entry_type::local_apic: + return "local_apic"; + case madt_entry_type::io_apic: + return "io_apic"; + case madt_entry_type::interrupt_source_override: + return "interrupt_source_override"; + case madt_entry_type::nmi_source: + return "nmi_source"; + case madt_entry_type::local_apic_nmi: + return "local_apic_nmi"; + case madt_entry_type::local_apic_address_override: + return "local_apic_address_override"; + } + return "unknown"; +} + +struct madt_flags_t { + uint32_t dual_8259_present : 1; + uint32_t reserved : 31; +}; +static_assert(sizeof(madt_flags_t) == 4); + +struct local_apic_flags_t { + uint32_t enabled : 1; + uint32_t online_capable : 1; + uint32_t reserved : 30; +}; +static_assert(sizeof(local_apic_flags_t) == 4); + +enum class inti_polarity : uint16_t { + bus_default = 0, + active_high = 1, + active_low = 3, +}; + +enum class inti_trigger_mode : uint16_t { + bus_default = 0, + edge = 1, + level = 3, +}; + +struct inti_flags_t { + inti_polarity polarity : 2; + inti_trigger_mode trigger_mode : 2; + uint16_t reserved : 12; +}; +static_assert(sizeof(inti_flags_t) == 2); + +struct madt_t { + sdt_header_t header; + uint32_t local_apic_address; + madt_flags_t flags; + uint8_t entries[0]; +} __attribute__((packed)); +static_assert(sizeof(madt_t) == 44); + +struct madt_entry_t { + madt_entry_type type; + uint8_t length; +} __attribute__((packed)); +static_assert(sizeof(madt_entry_t) == 2); + +struct madt_local_apic_t { + madt_entry_type type; + uint8_t length; + uint8_t acpi_processor_uid; + uint8_t apic_id; + local_apic_flags_t flags; +} __attribute__((packed)); +static_assert(sizeof(madt_local_apic_t) == 8); + +struct madt_io_apic_t { + madt_entry_type type; + uint8_t length; + uint8_t io_apic_id; + uint8_t reserved; + uint32_t io_apic_address; + uint32_t global_system_interrupt_base; +} __attribute__((packed)); +static_assert(sizeof(madt_io_apic_t) == 12); + +struct madt_interrupt_source_override_t { + madt_entry_type type; + uint8_t length; + uint8_t bus; // always 0 (ISA) + uint8_t source; // ISA IRQ number + uint32_t global_system_interrupt; + inti_flags_t flags; +} __attribute__((packed)); +static_assert(sizeof(madt_interrupt_source_override_t) == 10); + +struct madt_nmi_source_t { + madt_entry_type type; + uint8_t length; + inti_flags_t flags; + uint32_t global_system_interrupt; +} __attribute__((packed)); +static_assert(sizeof(madt_nmi_source_t) == 8); + +struct madt_local_apic_nmi_t { + madt_entry_type type; + uint8_t length; + uint8_t acpi_processor_uid; // 0xFF: applies to all processors + inti_flags_t flags; + uint8_t lint; // local APIC LINT# the NMI is wired to +} __attribute__((packed)); +static_assert(sizeof(madt_local_apic_nmi_t) == 6); + +struct madt_local_apic_address_override_t { + madt_entry_type type; + uint8_t length; + uint16_t reserved; + uint64_t local_apic_address; +} __attribute__((packed)); +static_assert(sizeof(madt_local_apic_address_override_t) == 12); + +struct Madt { + explicit Madt(paddr_t madt_physical) + : table(madt_physical.access()) {} + explicit Madt(const madt_t *madt_table) : table(madt_table) {} + + paddr_t local_apic_address() { + uint64_t address = table->local_apic_address; + for_each_entry( + [&](const madt_local_apic_address_override_t *override_entry) { + address = override_entry->local_apic_address; + }); + return paddr_t{address}; + } + + bool has_legacy_pic() { return table->flags.dual_8259_present != 0; } + + template void for_each_entry(Fn &&callback) { + auto ptr = table->entries; + const auto end = + reinterpret_cast(table) + table->header.length; + while (ptr < end) { + auto entry = reinterpret_cast(ptr); + if (entry->length < sizeof(madt_entry_t)) { + break; + } + dispatch(entry, callback); + ptr += entry->length; + } + } + +private: + template + static void dispatch(const madt_entry_t *entry, Fn &&fn) { +#define MADT_DISPATCH(entry_enum, EntryType) \ + case madt_entry_type::entry_enum: \ + if constexpr (requires { fn(static_cast(nullptr)); }) \ + fn(reinterpret_cast(entry)); \ + break; + + switch (entry->type) { + MADT_DISPATCH(local_apic, madt_local_apic_t) + MADT_DISPATCH(io_apic, madt_io_apic_t) + MADT_DISPATCH(interrupt_source_override, madt_interrupt_source_override_t) + MADT_DISPATCH(nmi_source, madt_nmi_source_t) + MADT_DISPATCH(local_apic_nmi, madt_local_apic_nmi_t) + MADT_DISPATCH(local_apic_address_override, + madt_local_apic_address_override_t) + default: + break; + } + +#undef MADT_DISPATCH + } + + const madt_t *table; +}; + +} // namespace acpi diff --git a/kernel/src/init/init.cpp b/kernel/src/init/init.cpp index 07d2ed6..b4839dd 100644 --- a/kernel/src/init/init.cpp +++ b/kernel/src/init/init.cpp @@ -4,6 +4,7 @@ #include "init/multiboot2.h" #include "init/print.h" #include "init/acpi.h" +#include "init/apic.h" #include "memory/allocator.h" [[maybe_unused]] [[noreturn]] __attribute__((used)) static void halt() { @@ -179,12 +180,18 @@ static acpi::Rsdp findRsdp() { return acpi::Rsdp(*rsdp); } +struct AcpiDiscoveryResult { + optional rsdp; + optional madt; +}; + static void setupAcpi() { auto rsdp = findRsdp(); - rsdp.for_each_table([](paddr_t table) { - auto header = table.access(); - print(acpi::unfold_signature(header->signature).text); - print("\n"); + rsdp.for_each_table(overloaded{ + [&](const acpi::sdt_header_t* header) { + print(acpi::unfold_signature(header->signature).text); + print("\n"); + } }); } diff --git a/kernel/src/init/init.o b/kernel/src/init/init.o deleted file mode 100644 index 5387e41fa2404c2a0af1e0db491d17ad24b0f182..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36704 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 Rpbb3Ocs)oTsEv;9egOGVbJ+j@