find_rsdp() scanned the EBDA and 0xE0000-0x100000. That is a BIOS convention, and UEFI firmware is under no obligation to leave a copy there -- OVMF does not. So the kernel found no RSDP, no MADT and no IOAPIC, and nothing routed IRQ 4. Serial output is polled, so the machine booted all the way to a shell prompt and then ignored every key, which reads as a broken console rather than a missing table. The UEFI loader reads the RSDP out of the EFI configuration table, preferring ACPI 2.0 for its XSDT, and leaves it at SCHBOOT_RSDP_ADDR -- the same low-memory hand-off block the e820 map already uses, now described in include/schboot.h so the two sides cannot disagree. The kernel validates it rather than trusting it: the BIOS path never writes that slot, so there it holds whatever was in memory, and following a wild pointer faults before there is anything to report the fault with. Signature and checksum do that. It deliberately does not require 16-byte alignment -- that rule is about searching the BIOS area, and OVMF's RSDP sits at 0x3f77e014, so demanding it threw away the one address this code exists to use. UEFI now reports "acpi: 1 cpu, lapic at ..., ioapic id 0" and the ISO boots to an interactive shell: uname, ls /bin (241), writing and reading a file, ps, /proc/schfs -- all driven over serial. BIOS unchanged: bootcheck 108 [ok], same ACPI line as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
287 lines
9 KiB
C
287 lines
9 KiB
C
// Find the processors, the way firmware from this century describes them.
|
|
//
|
|
// mpinit() walks the Intel MP table, which is from 1994. Modern firmware fills
|
|
// it in minimally or omits it: measured 2026-08-08, QEMU/SeaBIOS with -smp 4
|
|
// publishes a 208-byte MP table containing exactly *one* processor entry, then
|
|
// buses and interrupt assignments. Everything since has expected the operating
|
|
// system to read the ACPI MADT instead.
|
|
//
|
|
// The consequence was not that SMP was slow, it was that ncpu was 1 and SMP was
|
|
// silently never exercised. Machines whose firmware does publish a full MP
|
|
// table -- VMware, older hardware -- took the other path, started their APs,
|
|
// and triple faulted. So this file is a prerequisite for debugging that: until
|
|
// the CPUs are visible under QEMU there is no way to reproduce the crash except
|
|
// on the hardware it happens on.
|
|
//
|
|
// Written from the ACPI specification. No table layout here is copied from
|
|
// another kernel; they are all in chapter 5 of the published standard.
|
|
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#include "schboot.h" // SCHBOOT_RSDP_ADDR: the loader/kernel hand-off
|
|
#include "param.h"
|
|
#include "memlayout.h"
|
|
#include "mmu.h"
|
|
#include "proc.h"
|
|
#include "x86.h"
|
|
|
|
extern struct cpu cpus[NCPU];
|
|
extern int ncpu;
|
|
extern uchar ioapicid;
|
|
extern volatile uint *lapic;
|
|
|
|
// The Root System Description Pointer. Found by signature, not by being told
|
|
// where it is -- it sits either in the Extended BIOS Data Area or in the BIOS
|
|
// read-only region, on a 16-byte boundary.
|
|
struct acpi_rsdp {
|
|
char sig[8]; // "RSD PTR "
|
|
uint8 checksum; // over the first 20 bytes
|
|
char oemid[6];
|
|
uint8 revision; // 0 = ACPI 1.0 (RSDT), 2+ = 2.0 (XSDT)
|
|
uint32 rsdt_addr;
|
|
// ACPI 2.0 and later only, and only valid when revision >= 2.
|
|
uint32 length;
|
|
uint64 xsdt_addr;
|
|
uint8 ext_checksum; // over `length` bytes
|
|
uint8 reserved[3];
|
|
} __attribute__((packed));
|
|
|
|
// Every ACPI table starts with this.
|
|
struct acpi_sdt {
|
|
char sig[4];
|
|
uint32 length;
|
|
uint8 revision;
|
|
uint8 checksum;
|
|
char oemid[6];
|
|
char oem_table_id[8];
|
|
uint32 oem_revision;
|
|
uint32 creator_id;
|
|
uint32 creator_revision;
|
|
} __attribute__((packed));
|
|
|
|
// The Multiple APIC Description Table: signature "APIC".
|
|
struct acpi_madt {
|
|
struct acpi_sdt h;
|
|
uint32 lapic_addr;
|
|
uint32 flags; // bit 0: legacy 8259 PICs are present
|
|
} __attribute__((packed));
|
|
|
|
#define MADT_PCAT_COMPAT 1
|
|
|
|
// MADT entry types. Only these three are read; the rest are interrupt
|
|
// routing overrides that ioapic.c does not consult yet.
|
|
#define MADT_LAPIC 0
|
|
#define MADT_IOAPIC 1
|
|
#define MADT_LAPIC_OVERRIDE 5
|
|
|
|
struct madt_entry {
|
|
uint8 type;
|
|
uint8 length;
|
|
} __attribute__((packed));
|
|
|
|
struct madt_lapic {
|
|
struct madt_entry e;
|
|
uint8 acpi_id;
|
|
uint8 apic_id;
|
|
uint32 flags; // bit 0: enabled, bit 1: online-capable
|
|
} __attribute__((packed));
|
|
|
|
#define LAPIC_ENABLED 1
|
|
#define LAPIC_ONLINE_CAPABLE 2
|
|
|
|
struct madt_ioapic {
|
|
struct madt_entry e;
|
|
uint8 id;
|
|
uint8 reserved;
|
|
uint32 addr;
|
|
uint32 gsi_base;
|
|
} __attribute__((packed));
|
|
|
|
struct madt_lapic_override {
|
|
struct madt_entry e;
|
|
uint16 reserved;
|
|
uint64 addr;
|
|
} __attribute__((packed));
|
|
|
|
static int
|
|
checksum_ok(const void *p, uint32 len)
|
|
{
|
|
const uint8 *b = p;
|
|
uint8 sum = 0;
|
|
for (uint32 i = 0; i < len; i++)
|
|
sum += b[i];
|
|
return sum == 0;
|
|
}
|
|
|
|
// Scan a physical range for the RSDP signature. It is 16-byte aligned by
|
|
// specification, which makes this cheap enough to do twice.
|
|
static struct acpi_rsdp *
|
|
scan_rsdp(uint64 base, uint64 len)
|
|
{
|
|
for (uint64 off = 0; off + sizeof(struct acpi_rsdp) <= len; off += 16) {
|
|
struct acpi_rsdp *r = (struct acpi_rsdp *)P2V(base + off);
|
|
if (memcmp(r->sig, "RSD PTR ", 8) != 0)
|
|
continue;
|
|
// Signature alone is not enough: the string can appear in unrelated data,
|
|
// and following a bad pointer here means faulting before there is anything
|
|
// to report the fault with.
|
|
if (!checksum_ok(r, 20))
|
|
continue;
|
|
return r;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static struct acpi_rsdp *
|
|
find_rsdp(void)
|
|
{
|
|
// A UEFI loader hands the address over directly, because the scan below is a
|
|
// BIOS convention that UEFI firmware need not honour and OVMF does not: the
|
|
// kernel found no RSDP, so no MADT, so no IOAPIC, and nothing routed IRQ 4 --
|
|
// it reached a shell prompt on polled serial output and then ignored every
|
|
// key.
|
|
//
|
|
// Validated, never trusted. The BIOS path does not write this slot, so there
|
|
// it holds whatever happened to be in memory, and following a wild pointer
|
|
// faults before there is anything to report the fault with. The bound is what
|
|
// the direct map covers; the signature and checksum below do the rest.
|
|
//
|
|
// Deliberately *not* requiring 16-byte alignment. That rule applies to
|
|
// searching the BIOS area, not to a pointer handed over by firmware -- OVMF's
|
|
// sits at 0x3f77e014, and demanding alignment threw away the very address
|
|
// this code was added to use.
|
|
uint64 handed = *(uint64 *)P2V(SCHBOOT_RSDP_ADDR);
|
|
if (handed && handed < 0x100000000ULL) {
|
|
struct acpi_rsdp *r = (struct acpi_rsdp *)P2V(handed);
|
|
if (memcmp(r->sig, "RSD PTR ", 8) == 0 && checksum_ok(r, 20))
|
|
return r;
|
|
}
|
|
|
|
// The EBDA, whose segment address the BIOS leaves at 0x40E.
|
|
uint16 ebda_seg = *(uint16 *)P2V(0x40E);
|
|
if (ebda_seg) {
|
|
struct acpi_rsdp *r = scan_rsdp((uint64)ebda_seg << 4, 1024);
|
|
if (r)
|
|
return r;
|
|
}
|
|
return scan_rsdp(0xE0000, 0x20000);
|
|
}
|
|
|
|
// Walk the RSDT or XSDT looking for a table by signature.
|
|
//
|
|
// Two nearly identical loops rather than one clever one: the only difference is
|
|
// the pointer width -- 32-bit entries in the RSDT, 64-bit in the XSDT -- and
|
|
// reading a 64-bit pointer out of a 32-bit array is exactly the sort of bug
|
|
// that produces a plausible address pointing at nothing.
|
|
static struct acpi_sdt *
|
|
find_table(struct acpi_rsdp *rsdp, const char *sig)
|
|
{
|
|
struct acpi_sdt *root;
|
|
int wide;
|
|
|
|
if (rsdp->revision >= 2 && rsdp->xsdt_addr) {
|
|
root = (struct acpi_sdt *)P2V(rsdp->xsdt_addr);
|
|
wide = 1;
|
|
} else {
|
|
root = (struct acpi_sdt *)P2V((uint64)rsdp->rsdt_addr);
|
|
wide = 0;
|
|
}
|
|
|
|
if (!checksum_ok(root, root->length))
|
|
return 0;
|
|
|
|
uint32 n = (root->length - sizeof(struct acpi_sdt)) / (wide ? 8 : 4);
|
|
for (uint32 i = 0; i < n; i++) {
|
|
uint64 pa;
|
|
if (wide) {
|
|
// Byte-wise, because the array is packed and 8-byte entries in it are
|
|
// not guaranteed to be 8-byte aligned.
|
|
uint8 *p = (uint8 *)(root + 1) + i * 8;
|
|
pa = 0;
|
|
for (int b = 7; b >= 0; b--)
|
|
pa = (pa << 8) | p[b];
|
|
} else {
|
|
pa = ((uint32 *)(root + 1))[i];
|
|
}
|
|
struct acpi_sdt *t = (struct acpi_sdt *)P2V(pa);
|
|
if (memcmp(t->sig, sig, 4) == 0)
|
|
return t;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Returns 1 if the processors were enumerated, 0 to fall back to mpinit().
|
|
int
|
|
acpi_init(void)
|
|
{
|
|
struct acpi_rsdp *rsdp = find_rsdp();
|
|
if (!rsdp)
|
|
return 0;
|
|
|
|
struct acpi_madt *madt = (struct acpi_madt *)find_table(rsdp, "APIC");
|
|
if (!madt || !checksum_ok(madt, madt->h.length))
|
|
return 0;
|
|
|
|
lapic = (uint *)P2V((uint64)madt->lapic_addr);
|
|
|
|
uint8 *p = (uint8 *)madt + sizeof(struct acpi_madt);
|
|
uint8 *e = (uint8 *)madt + madt->h.length;
|
|
int found_ioapic = 0;
|
|
|
|
while (p + sizeof(struct madt_entry) <= e) {
|
|
struct madt_entry *ent = (struct madt_entry *)p;
|
|
// A zero length would spin here forever. Firmware tables are not always
|
|
// well formed, and an infinite loop this early is indistinguishable from a
|
|
// hang with no output at all.
|
|
if (ent->length < sizeof(struct madt_entry))
|
|
break;
|
|
|
|
switch (ent->type) {
|
|
case MADT_LAPIC: {
|
|
struct madt_lapic *l = (struct madt_lapic *)p;
|
|
// Skip processors that are neither enabled nor online-capable: those are
|
|
// sockets the firmware is describing but not offering.
|
|
if (!(l->flags & (LAPIC_ENABLED | LAPIC_ONLINE_CAPABLE)))
|
|
break;
|
|
if (ncpu < NCPU) {
|
|
cpus[ncpu].apicid = l->apic_id;
|
|
ncpu++;
|
|
}
|
|
break;
|
|
}
|
|
case MADT_IOAPIC: {
|
|
struct madt_ioapic *io = (struct madt_ioapic *)p;
|
|
// The first one only. Multi-I/O-APIC routing needs the interrupt source
|
|
// overrides too, which nothing here reads yet, so claiming to support it
|
|
// would be worse than not.
|
|
if (!found_ioapic) {
|
|
ioapicid = io->id;
|
|
found_ioapic = 1;
|
|
}
|
|
break;
|
|
}
|
|
case MADT_LAPIC_OVERRIDE: {
|
|
struct madt_lapic_override *o = (struct madt_lapic_override *)p;
|
|
lapic = (uint *)P2V(o->addr);
|
|
break;
|
|
}
|
|
}
|
|
p += ent->length;
|
|
}
|
|
|
|
if (ncpu == 0)
|
|
return 0;
|
|
|
|
// The 8259s are still there and still wired to the CPU. On a system with an
|
|
// IMCR, this switches interrupt delivery from the PICs to the APIC; on one
|
|
// without, the ports are unclaimed and the writes go nowhere. mpinit() does
|
|
// the same thing guarded by the MP table's imcrp flag.
|
|
if (madt->flags & MADT_PCAT_COMPAT) {
|
|
outb(0x22, 0x70);
|
|
outb(0x23, inb(0x23) | 1);
|
|
}
|
|
|
|
cprintf("acpi: %d cpu%s, lapic at %p, ioapic id %d\n",
|
|
ncpu, ncpu == 1 ? "" : "s", lapic, ioapicid);
|
|
return 1;
|
|
}
|