SchOS/kernel/arch/x86_64/lapic.c
Nexoniarz 38c89b1a9a panic stops every other processor
A panic used to stop only the processor it happened on. The rest carried
on with a dead kernel: on the ASUS laptop the report flashed up and was
drawn over, and the fans stayed loud until the power button was held.

Now the panicking processor sends every other one an NMI, which arrives
even with interrupts off, and trap() halts a processor that takes one
while panic_stop is set -- first thing, before the canary check, since
that stack may be what broke. NMI gets its own per-CPU stack (IST 1):
for a few instructions at the top of syscall_entry the live stack is the
user's, at ring 0, and a fault pushing the frame there would be a double
fault on the same stack and a reset instead of a report.

A second processor that panics while one is already reporting halts
instead of drawing over it; the owner's own nested panic still gets
through.

Checked under QEMU with -smp 4: after a sysrq crash CPUs 1-3 sit in hlt
with IF clear and CPU 0 is in the report's blink loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 00:50:50 +02:00

583 lines
19 KiB
C

// The local APIC manages internal (non-I/O) interrupts.
// See Chapter 8 & Appendix C of Intel processor manual volume 3.
#include "core/param.h"
#include "core/types.h"
#include "core/defs.h"
#include "core/date.h"
#include "arch/memlayout.h"
#include "arch/traps.h"
#include "arch/mmu.h"
#include "arch/x86.h"
#include "core/proc.h" // ncpu
// Local APIC registers, divided by 4 for use as uint[] indices.
#define ID (0x0020/4) // ID
#define VER (0x0030/4) // Version
#define TPR (0x0080/4) // Task Priority
#define EOI (0x00B0/4) // EOI
#define SVR (0x00F0/4) // Spurious Interrupt Vector
#define ENABLE 0x00000100 // Unit Enable
#define ESR (0x0280/4) // Error Status
#define ICRLO (0x0300/4) // Interrupt Command
#define SINIT 0x00000500 // INIT/RESET
#define STARTUP 0x00000600 // Startup IPI
#define DELIVS 0x00001000 // Delivery status
#define ASSERT 0x00004000 // Assert interrupt (vs deassert)
#define LEVEL 0x00008000 // Level triggered
#define BCAST 0x00080000 // Send to all APICs, including self.
#define BUSY 0x00001000
#define ICRHI (0x0310/4) // Interrupt Command [63:32]
#define TIMER (0x0320/4) // Local Vector Table 0 (TIMER)
#define X1 0x0000000B // divide counts by 1
#define PERIODIC 0x00020000 // Periodic
#define PCINT (0x0340/4) // Performance Counter LVT
#define LINT0 (0x0350/4) // Local Vector Table 1 (LINT0)
#define LINT1 (0x0360/4) // Local Vector Table 2 (LINT1)
#define ERROR (0x0370/4) // Local Vector Table 3 (ERROR)
#define MASKED 0x00010000 // Interrupt masked
#define TICR (0x0380/4) // Timer Initial Count
#define TCCR (0x0390/4) // Timer Current Count
#define TDCR (0x03E0/4) // Timer Divide Configuration
volatile uint *lapic; // Initialized in mp.c
static void
lapicw(int index, int value)
{
lapic[index] = value;
lapic[ID]; // wait for write to finish, by reading
}
// ---------------------------------------------------------------------
// Timer calibration
// ---------------------------------------------------------------------
// The LAPIC timer counts down at the APIC bus frequency, and nothing in the
// architecture tells you what that frequency is. xv6 loaded TICR with
// 10000000 and hoped; its own comment admitted the gap -- "if xv6 cared more
// about precise timekeeping, TICR would be calibrated using an external time
// source." That value yields the intended 100 Hz only if the bus happens to
// run at exactly 1 GHz.
//
// Under KVM it does not, and the error is not subtle. The kernel believed
// ticks arrived 100 times a second while they were arriving roughly 18 times
// a second, so every number derived from ticks was wrong by that same factor,
// in the same direction, everywhere at once: uptime ran slow, `time` reported
// three seconds for twenty, ping's one-second interval stretched to six. It
// read as three unrelated bugs in three unrelated programs. It was this line.
//
// So measure it. The 8254 PIT is exactly the external time source the comment
// asked for: its input clock is fixed at 1193182 Hz by the hardware, it is
// always present, and channel 2 is the one whose gate software can raise and
// whose output software can poll -- a stopwatch that needs no interrupt, which
// matters because this runs before the IDT is ready to take one.
#define PIT_HZ 1193182 // 8254 input clock, fixed by definition
#define PIT_CH2 0x42
#define PIT_CMD 0x43
#define PIT_GATE 0x61 // bit 0: ch2 gate, bit 1: speaker, bit 5: ch2 output
uint lapic_timer_hz; // measured LAPIC timer counts per second
uint64 tsc_hz; // measured TSC cycles per second
uint64 boot_tsc; // the TSC when calibration finished
// The TSC's frequency from the processor itself, for when there is no PIT to
// time it against. 0 if the processor does not say.
//
// Leaf 0x15 is the ratio of the TSC to the core crystal, and the crystal's
// frequency when the part reports it; leaf 0x16 is the base frequency in MHz,
// which is the TSC's on every part that has an invariant one. The first is
// exact and the second is a nominal figure, so the first is preferred.
static uint64
tsc_hz_from_cpuid(void)
{
uint32 max, a, b, c, d;
cpuid_leaf(0, &max, 0, 0, 0);
if (max >= 0x15) {
cpuid_leaf(0x15, &a, &b, &c, &d);
if (a && b && c)
return (uint64)c * b / a;
}
if (max >= 0x16) {
cpuid_leaf(0x16, &a, 0, 0, 0);
if (a & 0xFFFF)
return (uint64)(a & 0xFFFF) * 1000000;
}
return 0;
}
// The fallback when the PIT does not count.
//
// Something else has to be the stopwatch. In order: the ACPI PM timer, which
// is a real clock at a fixed 3.579545 MHz on every PC that is not
// hardware-reduced; then the TSC frequency the processor states in CPUID,
// with the TSC itself as the stopwatch; and only then a guess.
extern uint32 acpi_pmtmr_port;
extern int acpi_pmtmr_32;
#define PMTMR_HZ 3579545
// 50 ms of the PM timer, with the LAPIC and the TSC counted across it.
// Returns 0 if the timer did not move, which is a port that is not one.
static int
calibrate_pmtimer(void)
{
uint32 mask = acpi_pmtmr_32 ? 0xFFFFFFFFu : 0x00FFFFFFu;
uint32 want = PMTMR_HZ / 20, t0, t;
lapicw(TDCR, X1);
lapicw(TIMER, MASKED);
lapicw(TICR, 0xFFFFFFFF);
t0 = inl(acpi_pmtmr_port) & mask;
uint64 tsc0 = rdtsc();
uint lap0 = lapic[TCCR];
do {
t = inl(acpi_pmtmr_port) & mask;
if (rdtsc() - tsc0 > 4000000000ULL) {
lapicw(TICR, 0);
return 0;
}
} while (((t - t0) & mask) < want);
uint lap1 = lapic[TCCR];
uint64 tsc1 = rdtsc();
lapicw(TICR, 0);
uint32 ticks = (t - t0) & mask;
tsc_hz = (tsc1 - tsc0) * PMTMR_HZ / ticks;
lapic_timer_hz = (uint)((uint64)(lap0 - lap1) * PMTMR_HZ / ticks);
boot_tsc = tsc1;
return tsc_hz >= 20000000ULL;
}
static void
calibrate_without_pit(void)
{
if (acpi_pmtmr_port && calibrate_pmtimer()) {
kinfo("cpu0: the PIT does not count; timed against the ACPI PM timer\n");
} else {
tsc_hz = tsc_hz_from_cpuid();
if (tsc_hz == 0) {
// Nothing left to measure against. Wrong-but-running, as below: time
// will run at the wrong speed and the line says that it may.
tsc_hz = 2000000000ULL;
cprintf("cpu0: no PIT, no PM timer and no TSC frequency from CPUID; "
"assuming 2 GHz\n");
} else {
kinfo("cpu0: the PIT does not count; TSC frequency from CPUID\n");
}
lapicw(TDCR, X1);
lapicw(TIMER, MASKED);
lapicw(TICR, 0xFFFFFFFF);
uint64 tsc0 = rdtsc();
uint lap0 = lapic[TCCR];
uint64 until = tsc0 + tsc_hz / 20; // 50 ms
while (rdtsc() < until)
asm volatile("pause");
uint lap1 = lapic[TCCR];
uint64 tsc1 = rdtsc();
lapicw(TICR, 0);
lapic_timer_hz = (uint)((uint64)(lap0 - lap1) * tsc_hz / (tsc1 - tsc0));
boot_tsc = tsc1;
}
if (lapic_timer_hz < HZ * 1000u) {
cprintf("cpu0: lapic calibration implausible (%u Hz), assuming 1 GHz bus\n",
lapic_timer_hz);
lapic_timer_hz = 1000000000u;
}
}
static void
calibrate_timers(void)
{
// 50 ms: long enough that the port I/O around the measurement is noise,
// short enough that nobody notices it in boot time. The count fits in the
// 16 bits the PIT has (59659), which is the real upper bound here.
const uint us = 50000;
uint count = (uint)(((uint64)PIT_HZ * us) / 1000000);
// Channel 2, mode 0 (interrupt on terminal count), lobyte then hibyte.
// Gate high so it counts; the speaker bit stays clear so the box is silent.
outb(PIT_GATE, (inb(PIT_GATE) & ~0x02) | 0x01);
outb(PIT_CMD, 0xB0);
outb(PIT_CH2, count & 0xFF);
outb(PIT_CH2, (count >> 8) & 0xFF);
// A free-running LAPIC counter for the duration: one-shot, masked, started
// at the largest count there is so it cannot wrap inside 50 ms.
lapicw(TDCR, X1);
lapicw(TIMER, MASKED);
lapicw(TICR, 0xFFFFFFFF);
uint64 tsc0 = rdtsc();
uint lap0 = lapic[TCCR];
// Bounded. "Always present" stopped being true: recent Intel boards
// clock-gate the 8254 to save power, and then the output never rises and
// this loop was the whole of the boot -- a machine stopped at the line
// before "lapic timer", with nothing to say why. Four billion TSC cycles is
// over a second on anything that exists and twenty times the window on a
// 4 GHz part, so a working PIT never comes near it.
//
// And checked at the start: in mode 0 the output goes low the moment the
// count is written, so a bit that is already high is no PIT at all. An
// absent one floats -- port 0x61 reads 0xFF -- and then the loop ended at
// once, the TSC was "measured" at 0 MHz, and every clock in the system ran
// thousands of times fast. Under QEMU with pit=off uptime read four days
// thirty seconds after boot.
int pit_ok = !(inb(PIT_GATE) & 0x20);
while (pit_ok && !(inb(PIT_GATE) & 0x20)) { // ch2 output rises at terminal count
if (rdtsc() - tsc0 > 4000000000ULL) {
pit_ok = 0;
break;
}
}
uint lap1 = lapic[TCCR];
uint64 tsc1 = rdtsc();
// 50 ms of a TSC slower than 20 MHz is no processor this runs on: the
// window was not 50 ms, so the PIT was not counting it.
if (pit_ok && tsc1 - tsc0 < 1000000)
pit_ok = 0;
if (!pit_ok) {
lapicw(TICR, 0);
calibrate_without_pit();
return;
}
lapicw(TICR, 0); // stop counting
// TCCR counts down, so elapsed counts are start minus end.
lapic_timer_hz = (uint)(((uint64)(lap0 - lap1) * 1000000) / us);
tsc_hz = ((tsc1 - tsc0) * 1000000) / us;
// Where the clock starts. Everything the system reports as elapsed time is
// measured from here, against the TSC, rather than by counting timer
// interrupts -- see kernel/core/time.c.
boot_tsc = tsc1;
// If the measurement came back implausible -- no LAPIC, a hypervisor that
// stops the counter, a PIT that never raises its output -- fall back to the
// old assumption rather than dividing by zero and taking the machine down.
// Wrong-but-running beats a triple fault, and the printed value says which
// case this is.
if (lapic_timer_hz < HZ * 1000u) {
cprintf("cpu0: lapic calibration implausible (%u Hz), assuming 1 GHz bus\n",
lapic_timer_hz);
lapic_timer_hz = 1000000000u; // the 1 GHz that TICR=10000000 implied
}
}
#define MSR_APIC_BASE 0x1B
#define APIC_BASE_EN (1ULL << 11) // globally enabled
#define APIC_BASE_EXTD (1ULL << 10) // x2APIC mode
// Put this processor's local APIC in xAPIC mode, which is the mode everything
// here speaks: registers at an MMIO address, read and written as memory.
//
// Firmware is allowed to hand over in x2APIC mode, and on recent Intel
// laptops it usually does. In that mode the MMIO page is not the APIC any
// more -- the registers are MSRs -- so every lapic[] access read nothing
// useful: the ID came back wrong, cpunum() panicked "unknown apicid", and the
// timer was never started. A hang or a panic within the first second, on the
// newest machines and on no emulator, because none of them hand over that way.
//
// The architecture allows exactly one way back: x2APIC to disabled, disabled
// to xAPIC. Disabling discards the APIC's state, which costs nothing here
// because lapicinit() is about to set all of it.
//
// It cannot be done when the processor has locked xAPIC out (some server
// parts, which report it in IA32_XAPIC_DISABLE_STATUS), or when its APIC ID
// does not fit in eight bits. Both get said plainly rather than left to fail
// at the first register access.
void
lapic_xapic_mode(void)
{
uint32 d;
uint64 base = rdmsr(MSR_APIC_BASE);
if ((base & (APIC_BASE_EN | APIC_BASE_EXTD)) == APIC_BASE_EN)
return; // xAPIC already: the usual case
if (base & APIC_BASE_EXTD) {
cpuid_leaf(7, 0, 0, 0, &d);
if (d & (1u << 29)) { // IA32_ARCH_CAPABILITIES exists
uint64 caps = rdmsr(0x10A);
if ((caps & (1ULL << 21)) && (rdmsr(0xBD) & 1))
panic("lapic: firmware has locked the APIC in x2APIC mode");
}
if (rdmsr(0x802) > 254) // x2APIC ID register
panic("lapic: APIC id too large for xAPIC mode");
wrmsr(MSR_APIC_BASE, base & ~(APIC_BASE_EN | APIC_BASE_EXTD));
}
// From disabled (whether firmware left it so or the line above did) to
// xAPIC. The base address bits are kept: they are where lapic[] points.
wrmsr(MSR_APIC_BASE, (base & ~APIC_BASE_EXTD) | APIC_BASE_EN);
}
void
lapicinit(void)
{
if(!lapic)
return;
lapic_xapic_mode();
// Enable local APIC; set spurious interrupt vector.
lapicw(SVR, ENABLE | (T_IRQ0 + IRQ_SPURIOUS));
// The timer counts down at the APIC bus frequency and interrupts at zero.
// What that frequency *is*, calibrate_timers() had to go and measure --
// see the comment above it for why loading a constant here does not work.
if (lapic_timer_hz == 0) {
calibrate_timers();
kinfo("cpu0: lapic timer %u Hz, tsc %u MHz, tick = %u counts at %d Hz\n",
lapic_timer_hz, (uint)(tsc_hz / 1000000),
lapic_timer_hz / HZ, HZ);
}
lapicw(TDCR, X1);
lapicw(TIMER, PERIODIC | (T_IRQ0 + IRQ_TIMER));
lapicw(TICR, lapic_timer_hz / HZ);
// Disable logical interrupt lines.
lapicw(LINT0, MASKED);
lapicw(LINT1, MASKED);
// Disable performance counter overflow interrupts
// on machines that provide that interrupt entry.
if(((lapic[VER]>>16) & 0xFF) >= 4)
lapicw(PCINT, MASKED);
// Map error interrupt to IRQ_ERROR.
lapicw(ERROR, T_IRQ0 + IRQ_ERROR);
// Clear error status register (requires back-to-back writes).
lapicw(ESR, 0);
lapicw(ESR, 0);
// Ack any outstanding interrupts.
lapicw(EOI, 0);
// Send an Init Level De-Assert to synchronise arbitration ID's.
lapicw(ICRHI, 0);
lapicw(ICRLO, BCAST | SINIT | LEVEL);
while(lapic[ICRLO] & DELIVS)
;
// Enable interrupts on the APIC (but not on the processor).
lapicw(TPR, 0);
}
int
cpunum(void)
{
int apicid, i;
// Cannot call cpu when interrupts are enabled:
// result not guaranteed to last long enough to be used!
// Would prefer to panic but even printing is chancy here:
// almost everything, including cprintf and panic, calls cpu,
// often indirectly through acquire and release.
if(readeflags()&FL_IF){
static int n;
if(n++ == 0)
cprintf("cpu called from %x with interrupts enabled\n",
__builtin_return_address(0));
}
if (!lapic)
return 0;
apicid = lapic[ID] >> 24;
for (i = 0; i < ncpu; ++i) {
if (cpus[i].apicid == apicid)
return i;
}
panic("unknown apicid\n");
}
// An NMI to every processor but this one: how a panic stops the others. An
// NMI is delivered with interrupts off too, so it reaches a processor spinning
// on a lock with cli as surely as one running a program; trap() halts any that
// takes one while panic_stop is set. Bounded, because the caller is dying.
void
lapic_nmi_others(void)
{
if (!lapic)
return;
lapicw(ICRHI, 0);
lapicw(ICRLO, 0x000C0000 | 0x00000400 | ASSERT); // all but self, NMI
for (int i = 0; i < 100000 && (lapic[ICRLO] & DELIVS); i++)
microdelay(1);
}
// Acknowledge interrupt.
void
lapiceoi(void)
{
if(lapic)
lapicw(EOI, 0);
}
// Spin for a given number of microseconds.
//
// This was an empty function body -- xv6's stub, carrying a comment saying
// real hardware would want to tune it dynamically. Nothing ever did, so every
// microdelay() in the tree returned immediately: the xHCI reset sequence, the
// USB hub power-on wait, the UART drain. Those delays are written into the
// specifications because the hardware genuinely needs the time, and a port
// that "works anyway" is one that is racing and winning.
//
// The TSC is the right clock for this: calibrate_timers() measured it in the
// same pass as the LAPIC, and unlike the LAPIC timer it is not already busy
// driving the tick.
void
microdelay(int us)
{
if (us <= 0)
return;
// Before calibration (early boot, and the AP trampoline) there is no
// measured frequency to work from. Spin on a rough count instead: the
// figure is wrong, but being approximately late is the entire point of the
// call, and being instantly early is what broke it.
if (tsc_hz == 0) {
for (volatile int i = 0; i < us * 100; i++)
;
return;
}
uint64 end = rdtsc() + ((uint64)us * tsc_hz) / 1000000;
while (rdtsc() < end)
asm volatile("pause");
}
#define CMOS_PORT 0x70
#define CMOS_RETURN 0x71
// Start additional processor running entry code at addr.
// See Appendix B of MultiProcessor Specification.
void
lapicstartap(uchar apicid, uint addr)
{
int i;
ushort *wrv;
// "The BSP must initialize CMOS shutdown code to 0AH
// and the warm reset vector (DWORD based at 40:67) to point at
// the AP startup code prior to the [universal startup algorithm]."
outb(CMOS_PORT, 0xF); // offset 0xF is shutdown code
outb(CMOS_PORT+1, 0x0A);
wrv = (ushort*)P2V((0x40<<4 | 0x67)); // Warm reset vector
wrv[0] = 0;
wrv[1] = addr >> 4;
// "Universal startup algorithm."
// Send INIT (level-triggered) interrupt to reset other CPU.
lapicw(ICRHI, apicid<<24);
lapicw(ICRLO, SINIT | LEVEL | ASSERT);
microdelay(200);
lapicw(ICRLO, SINIT | LEVEL);
// The MP specification's 10 ms. This was 100 us, "because Bochs is slow",
// which is a figure for an emulator: older real processors want the full
// wait between INIT and STARTUP, and starting one too early is an AP that
// does not come up.
microdelay(10000);
// Send startup IPI (twice!) to enter code.
// Regular hardware is supposed to only accept a STARTUP
// when it is in the halted state due to an INIT. So the second
// should be ignored, but it is part of the official Intel algorithm.
// Bochs complains about the second one. Too bad for Bochs.
for(i = 0; i < 2; i++){
lapicw(ICRHI, apicid<<24);
lapicw(ICRLO, STARTUP | (addr>>12));
microdelay(200);
}
}
// Hold a processor in wait-for-SIPI. For an AP that did not report in on
// time: if it is merely slow, this stops it from waking later and reading the
// trampoline parameters startothers() has since written for the next one --
// which would be two processors on one stack.
void
lapic_park(uchar apicid)
{
lapicw(ICRHI, apicid<<24);
lapicw(ICRLO, SINIT | LEVEL | ASSERT);
microdelay(200);
lapicw(ICRLO, SINIT | LEVEL);
microdelay(100);
}
#define CMOS_STATA 0x0a
#define CMOS_STATB 0x0b
#define CMOS_UIP (1 << 7) // RTC update in progress
#define SECS 0x00
#define MINS 0x02
#define HOURS 0x04
#define DAY 0x07
#define MONTH 0x08
#define YEAR 0x09
static uint cmos_read(uint reg)
{
outb(CMOS_PORT, reg);
microdelay(200);
return inb(CMOS_RETURN);
}
static void fill_rtcdate(struct rtcdate *r)
{
r->second = cmos_read(SECS);
r->minute = cmos_read(MINS);
r->hour = cmos_read(HOURS);
r->day = cmos_read(DAY);
r->month = cmos_read(MONTH);
r->year = cmos_read(YEAR);
}
//PAGEBREAK!
// qemu seems to use 24-hour GWT and the values are BCD encoded
void cmostime(struct rtcdate *r)
{
struct rtcdate t1, t2;
int sb, bcd;
sb = cmos_read(CMOS_STATB);
bcd = (sb & (1 << 2)) == 0;
// make sure CMOS doesn't modify time while we read it
for(;;) {
fill_rtcdate(&t1);
if(cmos_read(CMOS_STATA) & CMOS_UIP)
continue;
fill_rtcdate(&t2);
if(memcmp(&t1, &t2, sizeof(t1)) == 0)
break;
}
// convert
if(bcd) {
#define CONV(x) (t1.x = ((t1.x >> 4) * 10) + (t1.x & 0xf))
CONV(second);
CONV(minute);
CONV(hour );
CONV(day );
CONV(month );
CONV(year );
#undef CONV
}
*r = t1;
r->year += 2000;
}