Files
LithosAnanake/docs/lithosananke/amd64-isr-fix/README.md
T

98 lines
3.5 KiB
Markdown

# APIC Timer ISR Fix — amd64
**Date**: 2026-06-03
**Branch**: `claude/starforth-architecture-review-oubN0`
**Commits**: `fb0c256`, `901f827`
---
## Problem
The LithosAnanke amd64 kernel was triple-faulting on the first APIC timer interrupt,
causing QEMU to exit immediately after printing `ok>`. The adaptive heartbeat never
fired and `Heartbeat: N ticks` never appeared in serial logs.
## Root Cause
The kernel is compiled with `-fPIC -fvisibility=hidden` (required for UEFI PE
relocation). Assembly symbols `isr_stub_table` and `isr_stub0` in `isr.S` were
declared `.global` with **default visibility**. The C compiler therefore accessed
them through GOT indirection. However, the linker — building a final non-shared
binary — collapsed the GOT references to point directly at the symbols themselves.
The net effect: "load from GOT[`isr_stub_table`]" loaded the 8-byte instruction
bytes of `isr_stub0` (`0x006a90eb006a006a`) rather than its address. Both
`link_time_addr` and `runtime_addr` in `arch_interrupts_init()` became the same
wrong value (instruction bytes), so `reloc_offset = 0` and all 256 IDT entries
received garbage handler addresses. IDT[32] was set to `0x6a006afffffe91e9`
(non-canonical), which triggered #GP → double fault → triple fault on the first
timer delivery.
The bug was confirmed by disassembling `arch_interrupts_init()` in the loader PE:
the compiler emitted a `mov` (load from memory) for `&isr_stub0` instead of a
`lea` (compute address), reading the function's instruction bytes.
## Fix
Two changes, one line each:
**`src/starkernel/arch/amd64/isr.S`** — Added `.hidden` directives for all symbols
referenced from C:
```asm
.hidden isr_stub_table
.hidden isr_stub0
.hidden isr_stub32
.hidden isr_stub_default
.hidden isr_common_entry
```
**`src/starkernel/arch/amd64/interrupts.c`** — Added visibility attributes to the
`extern` declarations:
```c
extern void *isr_stub_table[IDT_ENTRIES] __attribute__((visibility("hidden")));
extern void isr_stub0(void) __attribute__((visibility("hidden")));
```
With hidden visibility, the compiler uses direct RIP-relative `lea` instead of GOT
indirection, yielding correct runtime addresses for all 256 IDT entries.
## Result
| Criterion | Before | After |
|---|---|---|
| IDT[32] handler | `0x6a006afffffe91e9` (corrupt) | `0x3d90251c` (valid canonical address) |
| First timer interrupt | #GP → triple fault → QEMU exit | ISR fires cleanly, returns via `iretq` |
| `PARITY:OK` | ✅ (unaffected) | ✅ |
| `Init: init.4th OK` | ✅ (unaffected) | ✅ |
| `ok>` REPL | ✅ reached, then immediate crash | ✅ runs indefinitely |
| `Heartbeat: N ticks` | Never | Every second at 100 Hz, TIME-TRUST ≈ 0.999 |
## Secondary Fix
`repl_print_dec32`, `repl_lapic_ccr`, `repl_print_hex32`, and `idle_iters` in
`repl.c` are x86-64 specific (LAPIC MMIO, `pushfq`). They were unguarded, causing
`-Werror=unused-function` / `unused-variable` failures on aarch64 (clang) and
riscv64 (gcc). All moved inside `#ifdef ARCH_AMD64`.
## Acceptance Testing
All three architectures must boot to `ok>` with `PARITY:OK` and `Init: init.4th OK`.
amd64 additionally shows `Heartbeat: N ticks TIME-TRUST≈0.999` repeating at 1 Hz.
```bash
make -f Makefile.starkernel ARCH=amd64 clean qemu
make -f Makefile.starkernel ARCH=aarch64 clean qemu
make -f Makefile.starkernel ARCH=riscv64 clean qemu
```
Verify each log:
```bash
grep "PARITY:OK" logs/qemu-<arch>-*.log
grep "Init: init.4th OK" logs/qemu-<arch>-*.log
grep "ok>" logs/qemu-<arch>-*.log
grep "Heartbeat:" logs/qemu-amd64-*.log | head -5
```