feat: make kernel architecture-agnostic; primitive work on arm port

This commit is contained in:
daniel committed 2026-10-24 21:25:18 +02:00
1 parent dab7d5427b
commit 1bae77ec84
164 files changed
+3664 -1641

No files matched your search

+10 -4
View File
@@ -70,10 +70,16 @@ sudo apt install build-essential nasm git curl xorriso mtools gdisk qemu-system-
### Cross-Architecture Builds
To build for a different architecture, pass `ARCH=`:
The kernel is split into architecture-neutral code and per-architecture
ports under `kernel/src/Arch/` (see [porting.md](porting.md)). x86_64 is the
only complete port. `aarch64` is a skeleton: the whole kernel compiles and
links for it, but paging, exception vectors, the GIC and the timer are not
written yet, so it does not boot. Userspace is x86_64-only.
Non-x86 kernels are built with LLVM (`clang`, plus `ld.lld` for the link):
```bash
make ARCH=aarch64
make ARCH=riscv64
make ARCH=loongarch64
make kernel ARCH=aarch64 # kernel/bin-aarch64/kernel
make kernel ARCH=aarch64 KERNEL_LD=/path/to/ld.lld
./scripts/check-arch-neutral.sh # boundary check + aarch64 compile
```
+117
View File
@@ -0,0 +1,117 @@
# Porting the Kernel to a New Architecture
The kernel is written so that everything outside `kernel/src/Arch/` is
architecture-neutral. A port is one directory, `kernel/src/Arch/<arch>/`,
implementing the interfaces declared in `kernel/src/Arch/*.hpp` (plus a few
interface headers elsewhere, listed below). The build compiles the generic
tree plus `Arch/$(ARCH)/` only.
`scripts/check-arch-neutral.sh` enforces the boundary and should pass before
every commit that touches the kernel:
- no inline assembly, assembly sources or architecture `#if`s outside
`src/Arch/` (the Limine adapter is the one exception: `limine.h` has
per-architecture SMP structures);
- no generic file includes a port's private headers (`<Arch/x86_64/...>`);
- with clang installed, the kernel compiles for `aarch64` and every symbol
the generic code references is defined by the port.
## Layout
```
kernel/src/Arch/
Arch.hpp target selection (MONTAUK_ARCH_*), MONTAUK_ARCH_NAME
Cpu.hpp inline CPU primitives (interrupt state, halt, barriers,
timestamp counter, TLB, page-table root)
Interrupts.hpp IRQ slots, handler registration, legacy line routing,
MSI message composition, IPIs
Smp.hpp Smp::CpuData (generic fields + arch sub-structs),
per-CPU base access, InitBsp / BootAPs
Context.hpp thread context: FPU area, initial kernel stack,
SwitchContext, ActivateThread, EnterUserMode
Abi.hpp user ABI: ELF machine, relocation kinds, TLS variant,
exit stub, initial user stack, EFI calling convention
Syscall.hpp SyscallFrame contract and entry-point installation
Timer.hpp per-CPU tick timer hardware
Platform.hpp boot-sequence hooks, platform device list, reboot
CpuPower.hpp frequency scaling / thermal governor (may be no-ops)
x86_64/ the complete port
aarch64/ skeleton port
```
Other interfaces a port implements:
| Header | What the port provides |
|---|---|
| `Memory/Paging.hpp` | the whole `Memory::VMM::Paging` class: page-table format, kernel and user mappings, address-space creation and teardown |
| `Io/IoPort.hpp` | port I/O (`Io::In8`...), or the absent-device fallback with `HasPortIo = false` |
| `ACPI/AcpiSleep.hpp` | `Initialize` / `IsS3Available` / `Suspend` (S3 via the FACS waking vector is an x86 mechanism) |
| `Common/Panic.hpp` | `Arch::PanicFrame` and `Arch::PrintPanicFrame` |
Interface headers that need inline definitions include the port's header of
the same name at their end (`Arch/Cpu.hpp` -> `Arch/<arch>/Cpu.hpp`); those
are `Cpu.hpp`, `IoPort.hpp`, `PerCpu.hpp`, `Context.hpp`, `Abi.hpp` and
`SyscallFrame.hpp`. Everything else is an ordinary out-of-line function.
## Boot sequence
`kmain` (`Boot/Main.cpp`) owns the order and calls the port at fixed points:
1. `Arch::EarlyInitialize()` - descriptor tables, FPU/SIMD enable. No allocator.
2. Frame allocator and heap come up (generic).
3. `Arch::InitializeTraps()` - exception vectors.
4. `Paging::Init()` switches to the kernel's own page tables, then
`Arch::InitializeMemoryAttributes()` (x86: PAT for write-combining).
5. ACPI tables are parsed (generic). `Arch::InitializeInterrupts(xsdt)` brings
up interrupt controllers, calls `Smp::InitBsp()`, idle and power
management; kmain then calls `Arch::EnableInterrupts()`.
6. PCI, AML, drivers (generic); `Timekeeping::TimerInitialize()` registers
`IRQ_TIMER` and calls `Arch::Timer::InitializeBsp(1)`.
7. `Arch::InitializePlatformDevices()` (x86: PS/2).
8. `Arch::PrepareUserMode()`, `montauk::abi::InitializeSyscalls()` (calls
`Arch::InitializeSyscallEntry()`), scheduler, IPC.
9. `Smp::BootAPs()`, `Arch::LateInitialize()`, first process, idle loop.
## Assumptions the generic kernel makes
- 64-bit, little-endian, 4 KiB pages.
- A higher-half direct map of physical memory (`Memory::HHDM`), as Limine
provides on every architecture.
- User space in the lower half of each address space, kernel in the upper
half; an address space is identified by the physical address of its root
table (on ARM, the TTBR0 table; the kernel half lives in TTBR1).
- Firmware tables via ACPI (`Hal::ACPI`); device-tree-only platforms would
need a discovery path first.
- Interrupts are numbered IRQ slots (`Arch::IRQ_COUNT`); slot numbers for
MSI-capable drivers are fixed in the drivers.
## The aarch64 skeleton
Real: CPU primitives (DAIF, WFI, `CNTVCT_EL0`, TLBI, TTBR0), per-CPU data via
`TPIDR_EL1`, the context switch and EL0 entry (`Context.S`), TLS through
`TPIDR_EL0`, the ELF/TLS/exit-stub ABI (syscall number in x8), the IRQ slot
dispatch table (`ArchIrqDispatch`), panic-frame printing.
Not yet written (each is marked `TODO(aarch64)`):
- `Paging.cpp`: translation tables and MAIR (every method panics).
- Exception vectors (`VBAR_EL1`): synchronous faults, `svc`, IRQs.
- GIC driver: SPI routing, SGIs for IPIs, MSIs via ITS/GICv2m
(`ComposeMsi` returns false, so drivers fall back to INTx/polling).
- Generic timer tick (`Arch::Timer::InitializeBsp` returns false).
- Secondary CPU start-up, PSCI reset and suspend.
Building it needs `clang` and `ld.lld` (`make kernel ARCH=aarch64`;
`KERNEL_LD=` points at a different `ld.lld`, e.g. rustup's
`lib/rustlib/x86_64-unknown-linux-gnu/bin/gcc-ld/ld.lld`).
## Adding another architecture
1. Create `kernel/src/Arch/<arch>/` with the six inline headers listed above
and stubs for the out-of-line functions (copy `aarch64/` as a template).
2. Add the target to `Arch/Arch.hpp` and to the `#if` chains at the end of
the dispatching interface headers.
3. Add `KernelStartSymbol` / `KernelEndSymbol` to `kernel/linker-<arch>.ld`,
and the `ld.lld` emulation to `LD_EMULATION_<arch>` in `kernel/GNUmakefile`.
4. Teach `Boot/Protocols/LimineProtocol.cpp` the architecture's MP fields.
5. Run `./scripts/check-arch-neutral.sh <arch>` until it passes.
+48 -1
View File
@@ -27,6 +27,20 @@ ifeq ($(ARCH),x86_64)
endif
endif
# The other architectures have no in-tree cross GCC: default to LLVM, which
# cross-compiles to every target (linking needs ld.lld; see docs/porting.md).
ifneq ($(ARCH),x86_64)
ifeq ($(filter command line environment,$(origin CC)),)
CC := clang
endif
ifeq ($(filter command line environment,$(origin CXX)),)
CXX := clang++
endif
ifeq ($(filter command line environment,$(origin AR)),)
AR := ar
endif
endif
# User controllable C compiler command.
CC ?= cc
@@ -197,8 +211,10 @@ override CXXFLAGS += \
-std=gnu++20 \
-fno-rtti \
-fno-exceptions \
-fmodules-ts \
$(SHARED_FLAGS)
ifneq ($(CXX_IS_CLANG),1)
override CXXFLAGS += -fmodules-ts
endif
# Internal linker flags that should not be changed by the user.
override LDFLAGS += \
@@ -212,6 +228,9 @@ override LDFLAGS += \
# Use "find" to glob all *.c, *.cpp, *.S, and *.asm files in the tree and obtain the
# object and header dependency file names.
override SRCFILES := $(shell cd src && find -L * -type f | LC_ALL=C sort)
# Architecture-specific code lives in src/Arch/<arch>/; only the target's
# directory is built. Everything else in src/ is architecture-neutral.
override SRCFILES := $(filter-out Arch/%,$(SRCFILES)) $(filter Arch/$(ARCH)/%,$(SRCFILES))
override CFILES := $(filter %.c,$(SRCFILES))
override CXXFILES := $(filter %.cpp,$(SRCFILES))
override OBJCFILES := $(filter %.mm,$(SRCFILES))
@@ -253,6 +272,11 @@ $(BUILDNO_HDR): $(filter-out $(BUILDNO_OBJ),$(OBJ)) $(PROG_SRC)
# on a clean build (where no .d dependency exists yet).
$(BUILDNO_OBJ): $(BUILDNO_HDR)
# Compile everything without linking (used by scripts/check-arch-neutral.sh
# to verify a port's objects before a cross linker is available).
.PHONY: objects
objects: $(OBJ) cc-runtime-$(ARCH)/cc-runtime.a
# Link rules for building the C compiler runtime.
cc-runtime-$(ARCH)/cc-runtime.a: GNUmakefile cc-runtime/*
rm -rf cc-runtime-$(ARCH)
@@ -263,10 +287,33 @@ cc-runtime-$(ARCH)/cc-runtime.a: GNUmakefile cc-runtime/*
CFLAGS="$(CFLAGS)" \
CPPFLAGS='-isystem ../freestnd-c-hdrs/$(ARCH)/include -DCC_RUNTIME_NO_FLOAT'
# Clang's generic-ELF driver links cross targets through the host's GCC,
# so a clang-built non-x86 kernel is linked by ld.lld directly.
ifneq ($(ARCH),x86_64)
ifeq ($(CXX_IS_CLANG),1)
KERNEL_LD ?= ld.lld
override LD_EMULATION_aarch64 := aarch64elf
override LD_EMULATION_riscv64 := elf64lriscv
override LD_EMULATION_loongarch64 := elf64loongarch
override KERNEL_LDFLAGS := \
-m $(LD_EMULATION_$(ARCH)) \
--build-id=none \
--no-relax \
-static \
-z max-page-size=0x1000 \
--gc-sections \
-T linker-$(ARCH).ld
endif
endif
# Link rules for the final executable.
bin-$(ARCH)/$(OUTPUT): GNUmakefile linker-$(ARCH).ld $(OBJ) cc-runtime-$(ARCH)/cc-runtime.a
mkdir -p "$$(dirname $@)"
ifeq ($(KERNEL_LD),)
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJ) cc-runtime-$(ARCH)/cc-runtime.a -o $@
else
$(KERNEL_LD) $(KERNEL_LDFLAGS) $(OBJ) cc-runtime-$(ARCH)/cc-runtime.a -o $@
endif
# Compilation rules for *.c files.
obj-$(ARCH)/%.c.o: src/%.c GNUmakefile
+3
View File
@@ -22,6 +22,7 @@ SECTIONS
/* Any address in this region will do, but often 0xffffffff80000000 is chosen as */
/* that is the beginning of the region. */
. = 0xffffffff80000000;
KernelStartSymbol = .;
/* Define a section to contain the Limine requests and assign it to its own PHDR */
.limine_requests : {
@@ -74,4 +75,6 @@ SECTIONS
*(.eh_frame*)
*(.note .note.*)
}
KernelEndSymbol = .;
}
+1 -1
View File
@@ -16,7 +16,7 @@
#include <Memory/Paging.hpp>
#include <Memory/Heap.hpp>
#include <Libraries/Memory.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Timekeeping/Timer.hpp>
using namespace Kt;
+7 -12
View File
@@ -7,9 +7,7 @@
#include "AcpiEvents.hpp"
#include <ACPI/AcpiSleep.hpp>
#include <Io/IoPort.hpp>
#include <Hal/Apic/Apic.hpp>
#include <Hal/Apic/IoApic.hpp>
#include <Hal/Apic/Interrupts.hpp>
#include <Arch/Interrupts.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
@@ -191,24 +189,21 @@ namespace Hal {
// routed. Clear stale status only after the enables are down.
DisableAndClearGpes();
// Route SCI to an IRQ vector. The SCI is level-triggered,
// Route the SCI to its IRQ slot. The SCI is level-triggered,
// active-low (ACPI spec requirement). The MADT may have an
// Interrupt Source Override for this IRQ that already sets
// these flags; RouteIrq applies overrides automatically.
uint8_t sciVector = IRQ_VECTOR_BASE + (uint8_t)g_sciIrq;
uint8_t bspApicId = (uint8_t)LocalApic::GetId();
// these flags; RouteLegacyIrq applies overrides automatically.
// Register the handler before routing to avoid missing events
RegisterIrqHandler((uint8_t)g_sciIrq, SciHandler);
Arch::RegisterIrqHandler((uint8_t)g_sciIrq, SciHandler);
// Route via IoApic (applies MADT overrides for polarity/trigger)
IoApic::RouteIrq((uint8_t)g_sciIrq, sciVector, bspApicId);
// Route to the boot CPU (applies firmware polarity/trigger overrides)
Arch::RouteLegacyIrq((uint8_t)g_sciIrq);
g_initialized = true;
KernelLogStream(OK, "ACPI") << "ACPI events initialized (SCI IRQ "
<< base::dec << (uint64_t)g_sciIrq << ", vector "
<< (uint64_t)sciVector << ")";
<< base::dec << (uint64_t)g_sciIrq << ")";
}
void Reinitialize() {
+4 -3
View File
@@ -5,6 +5,7 @@
*/
#include "AcpiShutdown.hpp"
#include <Arch/Cpu.hpp>
#include "AcpiSleep.hpp"
#include "AcpiEvents.hpp"
#include <ACPI/FADT.hpp>
@@ -95,7 +96,7 @@ namespace Hal {
}
}
asm volatile("cli");
Arch::DisableInterrupts();
// Phase 1: Write SLP_TYP to PM1x_CNT without SLP_EN,
// preserving existing register bits
@@ -110,7 +111,7 @@ namespace Hal {
}
// Flush all caches before entering sleep state
asm volatile("wbinvd");
Arch::WritebackInvalidateCaches();
// Phase 2: Assert SLP_EN to trigger the transition
Io::Out16(pm1a | SLP_EN, (uint16_t)g_pm1aControlBlock);
@@ -122,7 +123,7 @@ namespace Hal {
// Halt and wait for the hardware to power off
for (;;) {
asm volatile("hlt");
Arch::Halt();
}
}
};
+4 -15
View File
@@ -75,21 +75,6 @@ namespace Hal {
// CPU State (saved across S3)
// ============================================================================
// Layout must match S3Wake.asm offsets exactly.
// No FPU/SSE state — the kernel is compiled with -mno-sse.
struct CpuState {
uint64_t Rax, Rbx, Rcx, Rdx; // 0x00 - 0x18
uint64_t Rsi, Rdi, Rbp, Rsp; // 0x20 - 0x38
uint64_t R8, R9, R10, R11; // 0x40 - 0x58
uint64_t R12, R13, R14, R15; // 0x60 - 0x78
uint64_t Rflags; // 0x80
uint64_t Rip; // 0x88
uint64_t Cr3; // 0x90
uint64_t Cr0, Cr4; // 0x98, 0xA0
uint8_t GdtPtr[10]; // 0xA8
uint8_t IdtPtr[10]; // 0xB2
} __attribute__((aligned(16)));
// ============================================================================
@@ -109,5 +94,9 @@ namespace Hal {
// Returns 0 on successful resume, or -1 if S3 is unavailable.
int Suspend();
// Initialize/IsS3Available/Suspend are implemented by the
// architecture (the FACS waking-vector path is an x86 platform
// mechanism); other targets report S3 as unavailable.
};
};
+1 -1
View File
@@ -9,7 +9,7 @@
#include "AcpiDevices.hpp"
#include <ACPI/AML/AmlInterpreter.hpp>
#include <CppLib/Spinlock.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Timekeeping/Timer.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
+10 -16
View File
@@ -6,8 +6,6 @@
#pragma once
#include <cstdint>
#include <Hal/Apic/ApicInit.hpp>
#include <Drivers/PS2/PS2Controller.hpp>
#include <Drivers/USB/Xhci.hpp>
#include <Drivers/USB/UsbDevice.hpp>
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
@@ -18,6 +16,9 @@
#include <Drivers/Storage/Ahci.hpp>
#include <Drivers/Audio/IntelHda.hpp>
#include <Pci/Pci.hpp>
#include <Arch/Arch.hpp>
#include <Arch/Platform.hpp>
#include <Arch/Smp.hpp>
#include "Syscall.hpp"
#include <Drivers/Storage/BlockDevice.hpp>
@@ -69,28 +70,21 @@ namespace montauk::abi {
};
// CPU cores (category 0)
int cpuCount = Hal::GetDetectedCpuCount();
int cpuCount = Smp::GetDetectedCpuCount();
if (cpuCount > 0) {
char detail[48];
int p = 0;
p = dl_append(detail, p, "x86_64, ", 48);
p = dl_append(detail, p, MONTAUK_ARCH_NAME ", ", 48);
p = dl_append_dec(detail, p, cpuCount, 48);
p = dl_append(detail, p, " core(s)", 48);
add(0, "Processor", detail);
}
// Interrupt controllers (category 1)
add(1, "Local APIC", "");
add(1, "I/O APIC", "");
// Timer (category 2)
add(2, "LAPIC Timer", "Local APIC periodic timer");
// PS/2 Input (category 3)
add(3, "PS/2 Keyboard", "IRQ 1");
if (Drivers::PS2::IsDualChannel()) {
add(3, "PS/2 Mouse", "IRQ 12");
}
// Interrupt controllers (category 1), timer (2), platform input (3)
Arch::ListPlatformDevices([](void* ctx, uint8_t category,
const char* name, const char* detail) {
(*(decltype(add)*)ctx)(category, name, detail);
}, &add);
// USB devices (category 4)
if (Drivers::USB::Xhci::IsInitialized()) {
+2 -2
View File
@@ -94,14 +94,14 @@ namespace montauk::abi {
proc->readdirCursor = (slot + 1) % Sched::UserReadDirSlots;
uint64_t userVa = Sched::UserReadDirBase + (uint64_t)slot * 0x1000ULL;
uint64_t physAddr = Memory::VMM::Paging::GetPhysAddr(proc->pml4Phys, userVa);
uint64_t physAddr = Memory::VMM::Paging::GetPhysAddr(proc->pageTableRoot, userVa);
uint8_t* pageBuf = nullptr;
if (physAddr == 0) {
void* page = Memory::g_pfa->AllocateZeroed();
if (page == nullptr) return OutOfMemory;
physAddr = Memory::SubHHDM((uint64_t)page);
if (!Memory::VMM::Paging::MapUserIn(proc->pml4Phys, physAddr, userVa)) {
if (!Memory::VMM::Paging::MapUserIn(proc->pageTableRoot, physAddr, userVa)) {
Memory::g_pfa->Free(page);
return OutOfMemory;
}
+2 -2
View File
@@ -83,7 +83,7 @@ namespace montauk::abi {
for (uint64_t i = 0; i < numPages; i++) {
if (!Memory::VMM::Paging::MapUserInWC(
proc->pml4Phys,
proc->pageTableRoot,
fbPhys + i * 0x1000,
userVa + i * 0x1000
)) {
@@ -98,7 +98,7 @@ namespace montauk::abi {
uint64_t buf1Phys = Drivers::Graphics::IntelGPU::GetBufferPhys(1);
for (uint64_t i = 0; i < numPages; i++) {
if (!Memory::VMM::Paging::MapUserInWC(
proc->pml4Phys,
proc->pageTableRoot,
buf1Phys + i * 0x1000,
userVa + (numPages + i) * 0x1000
)) {
+15 -15
View File
@@ -107,7 +107,7 @@ namespace montauk::abi {
if (page == nullptr) break;
uint64_t phys = Memory::SubHHDM((uint64_t)page);
if (!Memory::VMM::Paging::MapUserInPermissions(
proc->pml4Phys, phys, pageVa, writable, executable)) {
proc->pageTableRoot, phys, pageVa, writable, executable)) {
Memory::g_pfa->Free(page);
break;
}
@@ -131,7 +131,7 @@ namespace montauk::abi {
// Reset heap allocation tracking for a process slot.
// The actual physical pages are freed by Paging::FreeUserHalf() during process cleanup.
inline void CleanupHeapForSlot(int slot, uint64_t /*pml4Phys*/) {
inline void CleanupHeapForSlot(int slot, uint64_t /*pageTableRoot*/) {
if (slot < 0 || slot >= Sched::MaxProcesses) return;
g_heapLocks[slot].Acquire();
HeapAlloc* area = g_heapAllocs[slot];
@@ -182,7 +182,7 @@ namespace montauk::abi {
}
for (uint64_t i = 0; i < area->numPages; i++)
if (Memory::VMM::Paging::GetPhysAddr(
proc->pml4Phys, area->va + i * 0x1000ULL) != 0)
proc->pageTableRoot, area->va + i * 0x1000ULL) != 0)
resident++;
*link = area->next;
area->next = released;
@@ -194,7 +194,7 @@ namespace montauk::abi {
// user TLB entry can otherwise corrupt the frame's next owner.
while (released != nullptr) {
HeapAlloc* next = released->next;
Memory::UnmapAndFreeUserRange(proc->pml4Phys, released->va,
Memory::UnmapAndFreeUserRange(proc->pageTableRoot, released->va,
released->numPages);
Sched::ReleaseUserHeapRange(slot, released->va,
released->numPages * 0x1000ULL);
@@ -253,11 +253,11 @@ namespace montauk::abi {
uint64_t resident = 0;
for (uint64_t i = 0; i < pages; i++)
if (Memory::VMM::Paging::GetPhysAddr(proc->pml4Phys, addr + i * 0x1000ULL) != 0)
if (Memory::VMM::Paging::GetPhysAddr(proc->pageTableRoot, addr + i * 0x1000ULL) != 0)
resident++;
g_heapLocks[slot].Release();
Memory::UnmapAndFreeUserRange(proc->pml4Phys, addr, pages);
Memory::UnmapAndFreeUserRange(proc->pageTableRoot, addr, pages);
Sched::ReleaseUserHeapRange(slot, addr, size);
g_heapLocks[slot].Acquire();
Sched::g_allocatedPages[slot] -= resident;
@@ -336,10 +336,10 @@ namespace montauk::abi {
bool writable = (prot & VmProtWrite) != 0;
bool executable = (prot & VmProtExec) != 0;
for (uint64_t i = 0; i < pages; i++) {
if (Memory::VMM::Paging::GetPhysAddr(proc->pml4Phys,
if (Memory::VMM::Paging::GetPhysAddr(proc->pageTableRoot,
addr + i * 0x1000ULL) == 0)
continue;
if (!Memory::VMM::Paging::ProtectUserIn(proc->pml4Phys,
if (!Memory::VMM::Paging::ProtectUserIn(proc->pageTableRoot,
addr + i * 0x1000ULL,
writable, executable)) {
g_heapLocks[slot].Release();
@@ -347,17 +347,17 @@ namespace montauk::abi {
}
}
g_heapLocks[slot].Release();
Memory::ShootdownUserRange(proc->pml4Phys, addr, (uint32_t)pages);
Memory::ShootdownUserRange(proc->pageTableRoot, addr, (uint32_t)pages);
return Success;
}
// Resolve a non-present access inside an anonymous VMA. This is used for
// both ring-3 faults and kernel copies to untouched userspace buffers.
inline bool TryHandleAnonymousPageFault(uint64_t faultAddr, uint64_t errorCode) {
inline bool TryHandleAnonymousPageFault(uint64_t faultAddr, const Memory::VMM::PageFault& fault) {
auto* proc = Sched::GetCurrentProcessPtr();
int slot = GetCurrentSlot();
if (proc == nullptr || slot < 0 || faultAddr < Sched::UserHeapBase ||
faultAddr >= Sched::UserHeapLimit || (errorCode & 1) != 0) return false;
faultAddr >= Sched::UserHeapLimit || fault.present) return false;
uint64_t pageVa = faultAddr & ~0xFFFULL;
g_heapLocks[slot].Acquire();
@@ -370,14 +370,14 @@ namespace montauk::abi {
}
}
if (found == nullptr ||
((errorCode & (1ULL << 1)) != 0 && (found->prot & VmProtWrite) == 0) ||
((errorCode & (1ULL << 4)) != 0 && (found->prot & VmProtExec) == 0)) {
(fault.write && (found->prot & VmProtWrite) == 0) ||
(fault.execute && (found->prot & VmProtExec) == 0)) {
g_heapLocks[slot].Release();
return false;
}
// A sibling may have resolved the same fault while this CPU waited.
if (Memory::VMM::Paging::GetPhysAddr(proc->pml4Phys, pageVa) != 0) {
if (Memory::VMM::Paging::GetPhysAddr(proc->pageTableRoot, pageVa) != 0) {
g_heapLocks[slot].Release();
return true;
}
@@ -389,7 +389,7 @@ namespace montauk::abi {
}
uint64_t phys = Memory::SubHHDM((uint64_t)page);
bool mapped = Memory::VMM::Paging::MapUserInPermissions(
proc->pml4Phys, phys, pageVa,
proc->pageTableRoot, phys, pageVa,
(found->prot & VmProtWrite) != 0, (found->prot & VmProtExec) != 0);
if (!mapped) Memory::g_pfa->Free(page);
else Sched::g_allocatedPages[slot]++;
+3 -3
View File
@@ -91,7 +91,7 @@ namespace montauk::abi {
}
// Load the library into the process's address space
uint64_t loadBias = Sched::ElfLoadLib(path, proc->pml4Phys, libSlot);
uint64_t loadBias = Sched::ElfLoadLib(path, proc->pageTableRoot, libSlot);
if (loadBias == 0) {
Kt::KernelLogStream(Kt::ERROR, "Lib") << "Failed to load library: " << path;
g_libLocks[slot].Release();
@@ -134,7 +134,7 @@ namespace montauk::abi {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc != nullptr) {
Memory::UnmapAndFreeUserRange(proc->pml4Phys, libBase,
Memory::UnmapAndFreeUserRange(proc->pageTableRoot, libBase,
(libEnd - libBase) / 0x1000ULL);
}
@@ -203,7 +203,7 @@ namespace montauk::abi {
uint64_t libBase = GetLibSlotBase(i);
uint64_t libEnd = libBase + Sched::LIB_MAX_SIZE;
Memory::UnmapAndFreeUserRange(proc->pml4Phys, libBase,
Memory::UnmapAndFreeUserRange(proc->pageTableRoot, libBase,
(libEnd - libBase) / 0x1000ULL);
g_libTable[slot][i].inUse = false;
+1 -1
View File
@@ -9,7 +9,7 @@
#pragma once
#include <cstdint>
#include <Sched/Scheduler.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Timekeeping/Timer.hpp>
#include <Net/Icmp.hpp>
#include <Net/Dns.hpp>
#include <Net/Ipv4.hpp>
+12 -15
View File
@@ -8,13 +8,13 @@
#include <cstdint>
#include <Efi/UEFI.hpp>
#include <Memory/Paging.hpp>
#include <Io/IoPort.hpp>
#include <Graphics/Framebuffer.hpp>
#include <ACPI/AcpiShutdown.hpp>
#include <ACPI/AcpiSleep.hpp>
#include <ACPI/CpuIdle.hpp>
#include <ACPI/Battery.hpp>
#include <Hal/CpuPower.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/CpuPower.hpp>
#include <Arch/Platform.hpp>
#include <Sched/Scheduler.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
@@ -56,15 +56,13 @@ namespace montauk::abi {
Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec
<< (uint64_t)Sched::GetCurrentPid() << " entered final reboot stage";
if (Efi::g_ResetSystem) {
/* Switch to kernel PML4 which has identity-mapped UEFI runtime regions */
Memory::VMM::LoadCR3(Memory::VMM::g_paging->PML4);
/* Switch to the kernel address space, which has identity-mapped UEFI runtime regions */
Memory::VMM::ActivateKernelAddressSpace();
Efi::g_ResetSystem(Efi::EfiResetCold, 0, 0, nullptr);
}
/* Fallback: triple fault via null IDT */
struct [[gnu::packed]] { uint16_t limit; uint64_t base; } nullIdt = {0, 0};
asm volatile("lidt %0; int $0x03" :: "m"(nullIdt));
__builtin_unreachable();
/* Fallback: architecture-specific reset */
Arch::Reboot();
}
static void Sys_Shutdown() {
@@ -77,13 +75,12 @@ namespace montauk::abi {
/* Fallback: EFI ResetSystem */
if (Efi::g_ResetSystem) {
Memory::VMM::LoadCR3(Memory::VMM::g_paging->PML4);
Memory::VMM::ActivateKernelAddressSpace();
Efi::g_ResetSystem(Efi::EfiResetShutdown, 0, 0, nullptr);
}
/* Last resort: halt the CPU */
asm volatile("cli; hlt");
__builtin_unreachable();
Arch::HaltForever();
}
static int64_t Sys_Suspend() {
@@ -96,8 +93,8 @@ namespace montauk::abi {
// SYS_POWERINFO. Fills a CPU power/thermal snapshot. Returns -1 when the
// CPU exposes no power management features (e.g. under QEMU).
static int64_t Sys_PowerInfo(PowerInfo* out) {
Hal::CpuPower::Snapshot s{};
if (!Hal::CpuPower::GetSnapshot(s)) {
Arch::CpuPower::Snapshot s{};
if (!Arch::CpuPower::GetSnapshot(s)) {
return NoDevice;
}
@@ -112,7 +109,7 @@ namespace montauk::abi {
out->baseMHz = s.BaseMHz;
out->maxMHz = s.MaxMHz;
out->effMHz = s.EffMHz;
out->apIdleHint = Hal::CpuIdle::GetApIdleHint();
out->apIdleHint = s.ApIdleHint;
return Success;
}
+1 -1
View File
@@ -7,7 +7,7 @@
#pragma once
#include <Sched/Scheduler.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Timekeeping/Timer.hpp>
#include <Memory/Paging.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Memory/HHDM.hpp>
+8 -8
View File
@@ -6,23 +6,23 @@
#pragma once
#include <cstdint>
#include <Arch/Cpu.hpp>
namespace montauk::abi {
// ---- Random number generation ----
// Uses RDTSC mixed with xorshift64* PRNG for entropy.
// RDRAND is intentionally avoided: some firmware disables the RDRAND
// hardware unit while CPUID still advertises support (bit 30 of ECX),
// causing #UD on real hardware. RDTSC-based entropy is sufficient for
// seeding BearSSL's PRNG for TLS session keys.
// Uses the CPU timestamp counter mixed with xorshift64* PRNG for entropy.
// RDRAND is intentionally avoided on x86: some firmware disables the
// RDRAND hardware unit while CPUID still advertises support (bit 30 of
// ECX), causing #UD on real hardware. Timestamp-based entropy is
// sufficient for seeding BearSSL's PRNG for TLS session keys.
static int64_t Sys_GetRandom(uint8_t* buf, uint64_t len) {
uint64_t tsc;
asm volatile("rdtsc; shl $32, %%rdx; or %%rdx, %%rax" : "=a"(tsc) :: "rdx");
uint64_t tsc = Arch::ReadTimestamp();
uint64_t state = tsc;
for (uint64_t i = 0; i < len; i += 8) {
asm volatile("rdtsc; shl $32, %%rdx; or %%rdx, %%rax" : "=a"(tsc) :: "rdx");
tsc = Arch::ReadTimestamp();
state ^= tsc;
state ^= state >> 12;
state ^= state << 25;
+4 -25
View File
@@ -6,8 +6,6 @@
#include "Syscall.hpp"
#include <Terminal/Terminal.hpp>
#include <Hal/MSR.hpp>
#include <Hal/GDT.hpp>
/* For common functions used by multiple syscall implementations*/
#include "Common.hpp"
@@ -42,9 +40,6 @@
#include "Clipboard.hpp" // SYS_CLIPBOARD_* (text and multi-format)
#include "UserMemory.hpp"
// Assembly entry point
extern "C" void SyscallEntry();
namespace montauk::abi {
static constexpr uint64_t kMaxPrintableStringBytes = 4096;
@@ -796,29 +791,13 @@ namespace montauk::abi {
}
}
// ---- SYSCALL MSR initialization ----
// ---- Entry point initialization ----
void InitializeSyscalls() {
// Enable SYSCALL/SYSRET and no-execute page permissions in EFER.
uint64_t efer = Hal::ReadMSR(Hal::IA32_EFER);
efer |= 1 | (1ULL << 11); // SCE | NXE
Hal::WriteMSR(Hal::IA32_EFER, efer);
Arch::InitializeSyscallEntry();
// STAR: kernel CS in [47:32], sysret base in [63:48]
// SYSCALL: CS=0x08, SS=0x10
// SYSRET: CS=0x10+16=0x20|RPL3=0x23, SS=0x10+8=0x18|RPL3=0x1B
uint64_t star = (0x0010ULL << 48) | (0x0008ULL << 32);
Hal::WriteMSR(Hal::IA32_STAR, star);
// LSTAR: SYSCALL entry point
Hal::WriteMSR(Hal::IA32_LSTAR, (uint64_t)SyscallEntry);
// FMASK: mask IF on SYSCALL entry (bit 9 = IF)
Hal::WriteMSR(Hal::IA32_FMASK, 0x200);
Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR="
<< kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", "
<< (SYS_SPAWN_REDIR_CAPS + 1) << " syscall slots)";
Kt::KernelLogStream(Kt::OK, "Syscall") << "System call entry installed ("
<< kcp::dec << (SYS_SPAWN_REDIR_CAPS + 1) << " syscall slots)";
}
}
+5 -8
View File
@@ -8,6 +8,7 @@
#include <cstdint>
#include <cstddef>
#include <Common/StatusCodes.hpp>
#include <Arch/Syscall.hpp>
namespace montauk::abi {
@@ -1229,15 +1230,11 @@ namespace montauk::abi {
uint64_t timestampTick;
};
// Stack frame pushed by SyscallEntry.asm
struct SyscallFrame {
uint64_t r15, r14, r13, r12, rbp, rbx; // callee-saved
uint64_t arg6, arg5, arg4, arg3, arg2, arg1;
uint64_t syscall_nr;
uint64_t user_rflags, user_rip, user_rsp;
};
// Saved user context of a system call; its layout is the
// architecture's (see Arch/Syscall.hpp).
using SyscallFrame = Arch::SyscallFrame;
// Kernel-only: set up SYSCALL MSRs and initialize dispatch
// Kernel-only: install the system-call entry point and initialize dispatch
void InitializeSyscalls();
}
+1 -1
View File
@@ -5,7 +5,7 @@
*/
#pragma once
#include <Timekeeping/ApicTimer.hpp>
#include <Timekeeping/Timer.hpp>
#include <Timekeeping/Time.hpp>
#include "Syscall.hpp"
+12 -12
View File
@@ -19,9 +19,9 @@ namespace montauk::abi::UserMemory {
return addr != 0 && addr < USER_SPACE_END;
}
inline uint64_t CurrentPml4() {
inline uint64_t CurrentRoot() {
auto* proc = Sched::GetCurrentProcessPtr();
return proc ? proc->pml4Phys : 0;
return proc ? proc->pageTableRoot : 0;
}
inline bool Range(uint64_t addr, uint64_t size, bool requireWrite = false) {
@@ -30,10 +30,10 @@ namespace montauk::abi::UserMemory {
if (addr + size < addr) return false;
if (addr + size > USER_SPACE_END) return false;
uint64_t pml4Phys = CurrentPml4();
if (pml4Phys == 0) return false;
uint64_t pageTableRoot = CurrentRoot();
if (pageTableRoot == 0) return false;
if (Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, addr, size, requireWrite))
if (Memory::VMM::Paging::IsUserRangeAccessible(pageTableRoot, addr, size, requireWrite))
return true;
// Anonymous VMAs are demand-committed. Validation precedes the actual
@@ -42,13 +42,13 @@ namespace montauk::abi::UserMemory {
uint64_t page = addr & ~0xFFFULL;
uint64_t last = (addr + size - 1) & ~0xFFFULL;
for (;;) {
if (!Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, page, 1, requireWrite) &&
!TryHandleAnonymousPageFault(page, requireWrite ? 2ULL : 0ULL))
if (!Memory::VMM::Paging::IsUserRangeAccessible(pageTableRoot, page, 1, requireWrite) &&
!TryHandleAnonymousPageFault(page, {false, requireWrite, false}))
return false;
if (page == last) break;
page += 0x1000ULL;
}
return Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, addr, size, requireWrite);
return Memory::VMM::Paging::IsUserRangeAccessible(pageTableRoot, addr, size, requireWrite);
}
inline bool OptionalRange(uint64_t addr, uint64_t size, bool requireWrite = false) {
@@ -68,13 +68,13 @@ namespace montauk::abi::UserMemory {
inline bool String(uint64_t addr, uint64_t maxLen) {
if (!ValidUserPtr(addr) || maxLen == 0) return false;
uint64_t pml4Phys = CurrentPml4();
if (pml4Phys == 0) return false;
uint64_t pageTableRoot = CurrentRoot();
if (pageTableRoot == 0) return false;
const char* str = (const char*)addr;
for (uint64_t i = 0; i < maxLen; i++) {
if (!Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, addr + i, 1, false)) {
if (!TryHandleAnonymousPageFault(addr + i, 0)) return false;
if (!Memory::VMM::Paging::IsUserRangeAccessible(pageTableRoot, addr + i, 1, false)) {
if (!TryHandleAnonymousPageFault(addr + i, {false, false, false})) return false;
}
if (str[i] == '\0') return true;
}
+14 -14
View File
@@ -54,13 +54,13 @@ namespace WinServer {
Ipc::ReleaseSurface(surface);
}
static int UnmapRetiredSnapshotsLocked(int windowId, int callerPid, uint64_t callerPml4) {
static int UnmapRetiredSnapshotsLocked(int windowId, int callerPid, uint64_t callerRoot) {
int result = InvalidArgument;
for (int i = 0; i < MaxRetiredSnapshots; i++) {
if (!g_retiredSnapshots[i].used || g_retiredSnapshots[i].windowId != windowId) continue;
if (g_retiredSnapshots[i].surface != nullptr) {
if (Ipc::UnmapSurfaceForPid(g_retiredSnapshots[i].surface, callerPid, callerPml4) == 0) {
if (Ipc::UnmapSurfaceForPid(g_retiredSnapshots[i].surface, callerPid, callerRoot) == 0) {
result = Success;
}
Ipc::ReleaseSurface(g_retiredSnapshots[i].surface);
@@ -84,7 +84,7 @@ namespace WinServer {
if (unmapOwner && slot.liveSurface != nullptr) {
auto* owner = Sched::GetProcessByPid(slot.ownerPid);
if (owner != nullptr) {
Ipc::UnmapSurfaceForPid(slot.liveSurface, slot.ownerPid, owner->pml4Phys);
Ipc::UnmapSurfaceForPid(slot.liveSurface, slot.ownerPid, owner->pageTableRoot);
}
}
@@ -157,7 +157,7 @@ namespace WinServer {
return rc > 0 ? Success : Busy;
}
int Create(int ownerPid, uint64_t ownerPml4, const char* title, int w, int h,
int Create(int ownerPid, uint64_t ownerRoot, const char* title, int w, int h,
uint64_t& heapNext, uint64_t& outVa) {
WsGuard guard;
@@ -194,7 +194,7 @@ namespace WinServer {
FillSurfaceLocked(snapshot, w, h, InitialWindowPixel);
uint64_t userVa = 0;
if (Ipc::MapSurfaceForPid(live, ownerPid, ownerPml4, heapNext, userVa, false) != 0) {
if (Ipc::MapSurfaceForPid(live, ownerPid, ownerRoot, heapNext, userVa, false) != 0) {
Ipc::ReleaseMailbox(mailbox, true, true);
Ipc::ReleaseSurface(snapshot);
Ipc::ReleaseSurface(live);
@@ -287,7 +287,7 @@ namespace WinServer {
return count;
}
uint64_t Map(int windowId, int callerPid, uint64_t callerPml4, uint64_t& heapNext) {
uint64_t Map(int windowId, int callerPid, uint64_t callerRoot, uint64_t& heapNext) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) return 0;
@@ -295,23 +295,23 @@ namespace WinServer {
if (!slot.used || slot.snapshotSurface == nullptr) return 0;
uint64_t outVa = 0;
if (Ipc::MapSurfaceForPid(slot.snapshotSurface, callerPid, callerPml4, heapNext, outVa, false) != 0) {
if (Ipc::MapSurfaceForPid(slot.snapshotSurface, callerPid, callerRoot, heapNext, outVa, false) != 0) {
return 0;
}
return outVa;
}
int Unmap(int windowId, int callerPid, uint64_t callerPml4) {
int Unmap(int windowId, int callerPid, uint64_t callerRoot) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) {
return UnmapRetiredSnapshotsLocked(windowId, callerPid, callerPml4);
return UnmapRetiredSnapshotsLocked(windowId, callerPid, callerRoot);
}
int result = UnmapRetiredSnapshotsLocked(windowId, callerPid, callerPml4);
int result = UnmapRetiredSnapshotsLocked(windowId, callerPid, callerRoot);
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.snapshotSurface == nullptr) return result;
int current = Ipc::UnmapSurfaceForPid(slot.snapshotSurface, callerPid, callerPml4);
int current = Ipc::UnmapSurfaceForPid(slot.snapshotSurface, callerPid, callerRoot);
if (current == 0) return Success;
return result;
}
@@ -321,7 +321,7 @@ namespace WinServer {
return SendEventLocked(windowId, event);
}
int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH,
int Resize(int windowId, int callerPid, uint64_t ownerRoot, int newW, int newH,
uint64_t& heapNext, uint64_t& outVa) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) return InvalidArgument;
@@ -348,7 +348,7 @@ namespace WinServer {
Ipc::RetainSurface(newSnapshot);
uint64_t newOwnerVa = 0;
if (Ipc::MapSurfaceForPid(newLive, callerPid, ownerPml4, heapNext, newOwnerVa, false) != 0) {
if (Ipc::MapSurfaceForPid(newLive, callerPid, ownerRoot, heapNext, newOwnerVa, false) != 0) {
Ipc::ReleaseSurface(newSnapshot);
Ipc::ReleaseSurface(newLive);
return GenericFailure;
@@ -366,7 +366,7 @@ namespace WinServer {
}
if (slot.liveSurface != nullptr) {
Ipc::UnmapSurfaceForPid(slot.liveSurface, callerPid, ownerPml4);
Ipc::UnmapSurfaceForPid(slot.liveSurface, callerPid, ownerRoot);
Ipc::ReleaseSurface(slot.liveSurface);
}
if (slot.snapshotSurface != nullptr) {
+4 -4
View File
@@ -28,16 +28,16 @@ namespace WinServer {
uint32_t flags;
};
int Create(int ownerPid, uint64_t ownerPml4, const char* title, int w, int h,
int Create(int ownerPid, uint64_t ownerRoot, const char* title, int w, int h,
uint64_t& heapNext, uint64_t& outVa);
int Destroy(int windowId, int callerPid);
int Present(int windowId, int callerPid);
int Poll(int windowId, int callerPid, montauk::abi::WinEvent* outEvent);
int Enumerate(montauk::abi::WinInfo* outArray, int maxCount);
uint64_t Map(int windowId, int callerPid, uint64_t callerPml4, uint64_t& heapNext);
int Unmap(int windowId, int callerPid, uint64_t callerPml4);
uint64_t Map(int windowId, int callerPid, uint64_t callerRoot, uint64_t& heapNext);
int Unmap(int windowId, int callerPid, uint64_t callerRoot);
int SendEvent(int windowId, const montauk::abi::WinEvent* event);
int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH,
int Resize(int windowId, int callerPid, uint64_t ownerRoot, int newW, int newH,
uint64_t& heapNext, uint64_t& outVa);
void CleanupProcess(int pid);
int SetCursor(int windowId, int callerPid, int cursor);
+4 -4
View File
@@ -22,7 +22,7 @@ namespace montauk::abi {
if (proc == nullptr) return GenericFailure;
uint64_t outVa = 0;
int id = WinServer::Create(proc->pid, proc->pml4Phys, title, w, h,
int id = WinServer::Create(proc->pid, proc->pageTableRoot, title, w, h,
proc->heapNext, outVa);
result->id = id;
result->pixelVa = (id >= 0) ? outVa : 0;
@@ -55,13 +55,13 @@ namespace montauk::abi {
if (!Sched::HasCapability(CAP_COMPOSITOR)) return 0;
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return 0;
return WinServer::Map(windowId, proc->pid, proc->pml4Phys, proc->heapNext);
return WinServer::Map(windowId, proc->pid, proc->pageTableRoot, proc->heapNext);
}
static int Sys_WinUnmap(int windowId) {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return GenericFailure;
return WinServer::Unmap(windowId, proc->pid, proc->pml4Phys);
return WinServer::Unmap(windowId, proc->pid, proc->pageTableRoot);
}
static int Sys_WinSendEvent(int windowId, const WinEvent* event) {
@@ -74,7 +74,7 @@ namespace montauk::abi {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return 0;
uint64_t outVa = 0;
int r = WinServer::Resize(windowId, proc->pid, proc->pml4Phys, newW, newH,
int r = WinServer::Resize(windowId, proc->pid, proc->pageTableRoot, newW, newH,
proc->heapNext, outVa);
return (r == 0) ? outVa : 0;
}
+64
View File
@@ -0,0 +1,64 @@
/*
* Abi.hpp
* Architecture-specific parts of the user-mode ABI
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Arch/Arch.hpp>
// What the ELF loader and the process/thread setup code need to know about
// the user-mode binary interface of the target. Defined inline by
// Arch/<arch>/Abi.hpp.
namespace Arch {
// ELF e_machine value accepted for user programs and libraries, and a
// name for diagnostics.
// constexpr uint16_t ElfMachine;
// constexpr const char* ElfMachineName;
// Calling-convention attribute for UEFI runtime services:
// #define ARCH_EFIAPI
// How the loader must treat a dynamic relocation type.
enum class RelocationKind {
None, // no-op (type 0)
Relative, // *target = loadBias + addend
Symbol, // *target = symbol address + addend (64-bit absolute)
Unsupported,
};
inline RelocationKind ClassifyRelocation(uint32_t type);
// Geometry of one thread's TLS block, as offsets from its page-aligned
// base. The variant (I: TLS above the thread pointer, II: below it) is
// fixed by the architecture's ELF TLS ABI.
struct TlsLayout {
uint64_t templateOffset; // where .tdata/.tbss start
uint64_t tpOffset; // thread pointer; [TP] holds the self-pointer
uint64_t totalSize; // bytes to map, incl. the TCB
};
inline TlsLayout ComputeTlsLayout(uint64_t memSize, uint64_t pAlign);
// Emit the user-mode code that calls SYS_EXIT(0). A process's entry
// function returns into it.
inline void WriteExitStub(uint8_t* page);
// Prepare the main thread's initial user stack so that returning from
// the entry function lands at `returnAddress`. `topPage` is the kernel
// mapping of the page just below `stackTop`. Returns the initial user SP.
inline uint64_t SetupUserEntryStack(uint8_t* topPage, uint64_t stackTop,
uint64_t returnAddress);
// Initial user SP for a sibling thread whose stack ends at `stackTop`
// (the thread trampoline never returns, so no return address is set).
inline uint64_t ThreadEntryStackPointer(uint64_t stackTop);
}
#if MONTAUK_ARCH_X86_64
#include <Arch/x86_64/Abi.hpp>
#elif MONTAUK_ARCH_AARCH64
#include <Arch/aarch64/Abi.hpp>
#endif
+25
View File
@@ -0,0 +1,25 @@
/*
* Arch.hpp
* Target architecture selection
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
// Everything outside src/Arch/ is architecture-neutral and talks to the CPU
// only through the interfaces in src/Arch/*.hpp. Each supported architecture
// implements them under src/Arch/<arch>/; the build compiles only the
// directory matching ARCH. See docs/porting.md.
//
// Interface headers that need inline (per-arch) definitions include the
// matching implementation header at their end, selected below.
#if defined(__x86_64__)
#define MONTAUK_ARCH_X86_64 1
#define MONTAUK_ARCH_NAME "x86_64"
#elif defined(__aarch64__)
#define MONTAUK_ARCH_AARCH64 1
#define MONTAUK_ARCH_NAME "aarch64"
#else
#error "MontaukOS: unsupported target architecture (add one under src/Arch/)"
#endif
+49
View File
@@ -0,0 +1,49 @@
/*
* Context.hpp
* Architecture-neutral thread context interface
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Arch/Arch.hpp>
#include <Arch/Smp.hpp>
// The scheduler's view of a thread's CPU context: a kernel stack pointer
// saved by SwitchContext, an FPU/SIMD save area, and a page-table root.
#if MONTAUK_ARCH_X86_64
#include <Arch/x86_64/Context.hpp>
#elif MONTAUK_ARCH_AARCH64
#include <Arch/aarch64/Context.hpp>
#endif
// The architecture provides (in Arch/<arch>/Context.hpp):
// constexpr uint64_t Arch::FpuStateSize, Arch::FpuStateAlign
namespace Arch {
// Reset an FPU/SIMD save area to the state a new thread starts with.
void InitFpuState(uint8_t* area);
// Build the initial kernel stack of a new thread so that the first
// SwitchContext to it "returns" into `entry`. Returns the saved SP.
uint64_t PrepareThreadStack(uint64_t kernelStackTop, void (*entry)());
// Save the running context (stack pointer to *oldSp, FPU state to
// oldFpu when non-null), switch to page-table root `newRoot` if it is
// not already active, and resume the context saved at newSp (restoring
// newFpu when non-null). Returns when the old context is resumed.
void SwitchContext(uint64_t* oldSp, uint64_t newSp, uint64_t newRoot,
uint8_t* oldFpu, uint8_t* newFpu);
// Point this CPU at the thread about to run: the kernel stack used when
// it enters the kernel from user mode, and its TLS thread pointer.
void ActivateThread(Smp::CpuData* cpu, uint64_t kernelStackTop,
uint64_t threadPointer);
// Drop to user mode at `entry` with stack `userSp`; `arg` is the first
// argument register. Interrupts are enabled in user mode.
[[noreturn]] void EnterUserMode(uint64_t entry, uint64_t userSp, uint64_t arg);
}
+100
View File
@@ -0,0 +1,100 @@
/*
* Cpu.hpp
* Architecture-neutral CPU primitives
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Arch/Arch.hpp>
// Every function here is defined inline by the architecture
// (Arch/<arch>/Cpu.hpp), so the primitives cost what the raw instruction
// costs. Generic kernel code must use these instead of inline assembly.
namespace Arch {
// Saved interrupt-enable state, as returned by SaveAndDisableInterrupts.
// Opaque to generic code: only hand it back to RestoreInterrupts.
using IrqFlags = uint64_t;
// ---- Spinning and barriers ----
// Spin-loop hint (x86 PAUSE, ARM YIELD).
inline void Pause();
// Stop the compiler from caching memory across this point. Emits no
// instruction; use it in polling loops on device-written memory.
inline void CompilerBarrier() {
asm volatile("" ::: "memory");
}
// Order all earlier stores before all later stores, including stores to
// write-combining memory (framebuffers) that must become visible.
inline void StoreFence();
// Full read/write memory barrier.
inline void MemoryFence();
// ---- Interrupt state ----
inline void EnableInterrupts();
inline void DisableInterrupts();
inline bool InterruptsEnabled();
// Disable interrupts and return the previous state.
inline IrqFlags SaveAndDisableInterrupts();
// Restore an interrupt state returned by SaveAndDisableInterrupts.
inline void RestoreInterrupts(IrqFlags flags);
// ---- Halting ----
// Wait for the next interrupt. Interrupt state is left as it was; with
// interrupts disabled this only returns on an NMI-class event.
inline void Halt();
// Called with interrupts disabled: atomically enable them and wait for
// an interrupt, then disable them again before returning. The atomicity
// closes the race where a wakeup IRQ lands between the readiness check
// and the wait, leaving the CPU asleep until the next timer deadline.
inline void HaltWithInterruptsDisabled();
// Disable interrupts and stop this CPU for good.
[[noreturn]] inline void HaltForever();
// ---- Time and delays ----
// A free-running, monotonically increasing cycle counter (x86 TSC, ARM
// CNTVCT). Its rate is architecture-defined; use it for entropy and
// relative timing, not wall-clock time.
inline uint64_t ReadTimestamp();
// Delay for at least about one microsecond. Works with interrupts
// disabled and before any clock is calibrated; errs long.
inline void IoDelay();
// ---- Caches and translation ----
// Write back and invalidate all CPU caches (before power state changes).
inline void WritebackInvalidateCaches();
// Invalidate this CPU's translation for one virtual page.
inline void FlushTlbPage(uint64_t virtualAddress);
// Invalidate this CPU's non-global translations.
inline void FlushTlbAll();
// Physical address of the active page-table root (x86 CR3, ARM TTBR0).
inline uint64_t ReadPageTableRoot();
// Switch to the page-table root at the given physical address.
inline void WritePageTableRoot(uint64_t rootPhys);
}
#if MONTAUK_ARCH_X86_64
#include <Arch/x86_64/Cpu.hpp>
#elif MONTAUK_ARCH_AARCH64
#include <Arch/aarch64/Cpu.hpp>
#endif
@@ -1,14 +1,18 @@
/*
* CpuPower.hpp
* Intel CPU power management: HWP frequency scaling, C1E promotion,
* and the package thermal governor.
* Architecture-neutral CPU power/thermal management interface
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Hal {
// Frequency scaling, idle-state policy and the thermal governor are CPU
// specific (x86_64: Intel HWP, C1E promotion and a package-temperature
// governor driven through MSRs). An architecture without them implements
// these as no-ops and returns false from GetSnapshot.
namespace Arch {
namespace CpuPower {
// Snapshot of the power/thermal state for SYS_POWERINFO and logging.
@@ -24,6 +28,7 @@ namespace Hal {
uint32_t BaseMHz; // CPUID base frequency (0 = unknown)
uint32_t MaxMHz; // CPUID max turbo frequency
uint32_t EffMHz; // measured average active frequency (BSP)
uint8_t ApIdleHint; // idle state secondary CPUs enter (arch-defined)
};
// BSP: detect features, enable HWP with a balanced energy preference,
+70
View File
@@ -0,0 +1,70 @@
/*
* Interrupts.hpp
* Architecture-neutral hardware interrupt interface
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
// Devices and kernel services see interrupts as numbered IRQ slots. The
// architecture maps each slot onto its interrupt controller (x86: an IDT
// vector fed by the I/O APIC, MSIs and local APIC IPIs; ARM: a GIC INTID)
// and calls the registered handler.
namespace Arch {
// IRQ handler function type. The parameter is the IRQ slot number.
// `fromUser` is derived directly from the interrupted context. Do not
// cache it in per-CPU data: an IRQ handler may context-switch and later
// resume on another CPU, making per-CPU saved-origin state stale.
using IrqHandler = void(*)(uint8_t irq, bool fromUser);
// Number of IRQ slots: 0-23 platform interrupt lines, 24-45 message-
// signalled interrupts (one slot per MSI-capable driver), 46-47 IPIs.
constexpr int IRQ_COUNT = 48;
// The architecture's periodic scheduler/timekeeping timer.
constexpr uint8_t IRQ_TIMER = 0;
// Platform interrupt lines with PC/ISA meaning. Only drivers for PC
// devices (PS/2, legacy PCI INTx through the I/O APIC) use these.
constexpr uint8_t IRQ_KEYBOARD = 1;
constexpr uint8_t IRQ_CASCADE = 2;
constexpr uint8_t IRQ_COM2 = 3;
constexpr uint8_t IRQ_COM1 = 4;
constexpr uint8_t IRQ_FLOPPY = 6;
constexpr uint8_t IRQ_RTC = 8;
constexpr uint8_t IRQ_MOUSE = 12;
constexpr uint8_t IRQ_ATA1 = 14;
constexpr uint8_t IRQ_ATA2 = 15;
// Inter-processor interrupts.
constexpr uint8_t IRQ_TLB_SHOOTDOWN = 46;
constexpr uint8_t IRQ_RESCHEDULE = 47;
// Register the handler for an IRQ slot.
void RegisterIrqHandler(uint8_t irq, IrqHandler handler);
// Route platform interrupt line `irq` to IRQ slot `irq` on the boot CPU,
// applying the firmware's polarity/trigger overrides, and unmask it.
void RouteLegacyIrq(uint8_t irq);
// Unmask platform interrupt line `irq` (already routed at boot).
void EnableLegacyIrq(uint8_t irq);
// A message-signalled interrupt targeting IRQ slot `irq` on the boot
// CPU: the device writes `data` to `address` to raise it. Program these
// into a PCI MSI capability or MSI-X table entry. Returns false when the
// platform cannot receive MSIs (drivers then fall back to INTx/polling).
struct MsiMessage {
uint64_t address;
uint32_t data;
};
bool ComposeMsi(uint8_t irq, MsiMessage& out);
// Raise IPI slot `irq` (IRQ_RESCHEDULE / IRQ_TLB_SHOOTDOWN) on the CPU
// with the given Smp index. Returns false if delivery did not complete.
bool SendIpi(int cpuIndex, uint8_t irq);
}
+54
View File
@@ -0,0 +1,54 @@
/*
* Platform.hpp
* Architecture hooks for kernel bring-up and power-off paths
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <ACPI/ACPI.hpp>
// kmain (Boot/Main.cpp) owns the boot sequence and calls into the
// architecture at these points, in this order.
namespace Arch {
// First thing after the boot console exists: CPU descriptor tables,
// FPU/SIMD enablement. No memory allocation is available yet.
void EarlyInitialize();
// The frame allocator and heap exist: install exception/trap vectors.
void InitializeTraps();
// The kernel page tables are live: program memory-type attributes that
// MapWC/MapMMIO rely on (x86 PAT).
void InitializeMemoryAttributes();
// ACPI tables are available: bring up interrupt controllers, the boot
// CPU's per-CPU data (Smp::InitBsp), idle-state and power management.
// Interrupts stay disabled; kmain enables them afterwards.
void InitializeInterrupts(Hal::ACPI::CommonSDTHeader* xsdt);
// Platform devices that are not discovered on PCI (x86: the PS/2
// controller, keyboard and mouse). Called once the timer is running.
void InitializePlatformDevices();
// Right before the first user process is created: anything user-mode
// entry needs (x86: load the task register for ring-3 interrupt stacks).
void PrepareUserMode();
// Secondary CPUs are up and the first process is about to be spawned.
void LateInitialize();
// Report devices that belong to the platform rather than a bus (for
// SYS_DEVLIST): interrupt controllers (category 1), timers (2) and
// platform input devices (3).
using PlatformDeviceSink = void (*)(void* context, uint8_t category,
const char* name, const char* detail);
void ListPlatformDevices(PlatformDeviceSink sink, void* context);
// Reset the machine without firmware help (last resort after EFI
// ResetSystem). Never returns.
[[noreturn]] void Reboot();
}
+94
View File
@@ -0,0 +1,94 @@
/*
* Smp.hpp
* Architecture-neutral per-CPU data and SMP bring-up interface
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Arch/Arch.hpp>
#include <Boot/BootInfo.hpp>
#if MONTAUK_ARCH_X86_64
#include <Arch/x86_64/PerCpu.hpp>
#elif MONTAUK_ARCH_AARCH64
#include <Arch/aarch64/PerCpu.hpp>
#endif
// The architecture provides (in Arch/<arch>/PerCpu.hpp):
// struct Arch::CpuEntryState -- fields kernel-entry assembly reads at a
// fixed offset (8) from the per-CPU base
// struct Arch::CpuState -- everything else the port keeps per CPU
// void* Arch::CurrentCpuLocal() -- this CPU's CpuData (fast path)
// void* Arch::TryCurrentCpuLocal() -- same, or nullptr before InitBsp
namespace Smp {
static constexpr int MaxCPUs = 64;
struct CpuData {
// === Fixed layout: read by low-level entry code ===
uint64_t selfPtr; // offset 0: pointer to self
Arch::CpuEntryState entry; // offset 8: arch kernel-entry fields
// === Generic fields ===
int32_t currentSlot; // process table slot, -1 = idle
int cpuIndex; // 0-based CPU index (0 = boot CPU)
uint64_t idleSavedSp; // idle context's stack pointer while a process runs
volatile bool started; // set by an AP once its init is complete
// When set, the scheduler tick must NOT pull this (idle) CPU away to
// run a ready process: its idle context is busy with long-running
// kernel work whose wall-clock-bounded waits would spuriously expire
// if the context only ran when nothing else is ready (observed: the
// deferred Bluetooth firmware download racing login-screen startup).
// Other CPUs schedule normally while this is held.
volatile bool reservedForKernelWork = false;
// Set by the BSP timer when an IRQ bottom half needs process-safe
// execution even though runnable user processes keep this CPU busy.
// Schedule() consumes it by yielding once to the idle kernel context.
bool kernelWorkPending = false;
// === Architecture-private state ===
Arch::CpuState arch;
};
static_assert(__builtin_offsetof(CpuData, selfPtr) == 0,
"CpuData: per-CPU base must point at selfPtr");
static_assert(__builtin_offsetof(CpuData, entry) == 8,
"CpuData: entry-assembly fields must start at offset 8");
// The current CPU's data.
inline CpuData* GetCurrentCpuData() {
return (CpuData*)Arch::CurrentCpuLocal();
}
// Exception paths may run before InitBsp() has installed the per-CPU
// base. This variant returns nullptr in that state instead of faulting
// recursively on a bogus base.
inline CpuData* TryGetCurrentCpuData() {
return (CpuData*)Arch::TryCurrentCpuLocal();
}
// Get CPU data by index
CpuData* GetCpuData(int index);
// Number of online CPUs
int GetCpuCount();
// Number of CPUs the firmware describes (available once interrupts are
// initialized; may exceed GetCpuCount if some failed to start).
// Implemented by the architecture.
int GetDetectedCpuCount();
// Initialize the boot CPU's per-CPU data (call before interrupts are
// enabled). Implemented by the architecture.
void InitBsp();
// Boot all secondary CPUs (call after all subsystems are ready).
// `smp` is the boot contract's SMP block; if FeatureSmp was not
// advertised the caller may still pass it (cpuCount<=1 is a no-op).
// Implemented by the architecture.
void BootAPs(const montauk::boot::SmpInfo& smp);
}
+38
View File
@@ -0,0 +1,38 @@
/*
* Syscall.hpp
* Architecture-neutral system-call entry interface
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Arch/Arch.hpp>
#if MONTAUK_ARCH_X86_64
#include <Arch/x86_64/SyscallFrame.hpp>
#elif MONTAUK_ARCH_AARCH64
#include <Arch/aarch64/SyscallFrame.hpp>
#endif
// The architecture's system-call entry code saves the user context into an
// Arch::SyscallFrame on the thread's kernel stack (Arch/<arch>/
// SyscallFrame.hpp defines it, exposing at least `syscall_nr` and `arg1` ..
// `arg6`), enables interrupts, and calls
//
// extern "C" int64_t SyscallDispatch(Arch::SyscallFrame* frame);
//
// whose result it returns to user mode. Before returning it calls
//
// extern "C" void SchedExitIfKilled();
//
// so a kill that arrived during the syscall takes effect at a point where
// the syscall holds no locks.
namespace Arch {
// Install the system-call entry point on the calling CPU. Called on the
// boot CPU by montauk::abi::InitializeSyscalls and by every secondary
// CPU during its bring-up.
void InitializeSyscallEntry();
}
+32
View File
@@ -0,0 +1,32 @@
/*
* Timer.hpp
* Architecture-neutral per-CPU tick timer interface
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
// The hardware behind Timekeeping (Timekeeping/Timer.hpp): a per-CPU timer
// that raises Arch::IRQ_TIMER periodically. Timekeeping registers the IRQ
// handler and owns all tick accounting; the architecture only calibrates
// and programs the hardware (x86: local APIC timer calibrated against the
// PIT; ARM: the generic timer).
namespace Arch::Timer {
// Calibrate the timer and start the calling (boot) CPU's periodic tick
// with the given period. Returns false if no usable timer was found.
bool InitializeBsp(uint32_t periodMs);
// (Re)start the calling CPU's periodic tick, reusing the boot CPU's
// calibration: secondary CPU bring-up, leaving idle, and the boot CPU
// after S3 resume (timer state does not survive suspend). Only called
// after InitializeBsp succeeded.
void StartPeriodic(uint32_t periodMs);
// Mask the calling CPU's tick (idle secondary CPUs rely on IPIs).
// Cheap when it is already masked.
void Stop();
}
+55
View File
@@ -0,0 +1,55 @@
/*
* Abi.hpp
* aarch64 user-mode ABI details (implements Arch/Abi.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
// UEFI on AArch64 uses the standard AAPCS64 calling convention.
#define ARCH_EFIAPI
namespace Arch {
constexpr uint16_t ElfMachine = 183; // EM_AARCH64
constexpr const char* ElfMachineName = "aarch64";
inline RelocationKind ClassifyRelocation(uint32_t type) {
switch (type) {
case 0: return RelocationKind::None;
case 1027: return RelocationKind::Relative; // R_AARCH64_RELATIVE
case 257: // R_AARCH64_ABS64
case 1025: // R_AARCH64_GLOB_DAT
case 1026: return RelocationKind::Symbol; // R_AARCH64_JUMP_SLOT
default: return RelocationKind::Unsupported;
}
}
// Variant I: TP points at a 16-byte TCB and the TLS block follows it,
// starting at the next p_align boundary.
inline TlsLayout ComputeTlsLayout(uint64_t memSize, uint64_t pAlign) {
uint64_t align = pAlign ? pAlign : 1;
uint64_t templateOffset = (16 + align - 1) & ~(align - 1);
return {templateOffset, 0, templateOffset + memSize};
}
// System calls: number in x8, arguments in x0-x5, `svc #0`.
inline void WriteExitStub(uint8_t* page) {
uint32_t* code = (uint32_t*)page;
code[0] = 0xD2800000; // mov x0, #0 (exit code 0)
code[1] = 0xD2800008; // mov x8, #0 (SYS_EXIT = 0)
code[2] = 0xD4000001; // svc #0
}
// The return address travels in x30, not on the stack: EnterUserMode
// points x30 at the exit stub. The stack only needs 16-byte alignment.
inline uint64_t SetupUserEntryStack(uint8_t*, uint64_t stackTop, uint64_t) {
return stackTop & ~0xFULL;
}
inline uint64_t ThreadEntryStackPointer(uint64_t stackTop) {
return stackTop & ~0xFULL;
}
}
+26
View File
@@ -0,0 +1,26 @@
/*
* AcpiSleep.cpp
* aarch64: no ACPI S3 (implements ACPI/AcpiSleep.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <ACPI/AcpiSleep.hpp>
#include <Common/StatusCodes.hpp>
// Suspend on ARM goes through PSCI (SYSTEM_SUSPEND), not the FACS waking
// vector; until that exists, S3 is reported as unavailable.
namespace Hal::AcpiSleep {
void Initialize(ACPI::CommonSDTHeader*) {
}
bool IsS3Available() {
return false;
}
int Suspend() {
return -1;
}
}
+144
View File
@@ -0,0 +1,144 @@
/*
Context.S
aarch64 context switch and first entry to EL0 (see Arch/Context.hpp)
Copyright (c) 2026 Daniel Hammer
*/
/* The kernel is built -mgeneral-regs-only; this file alone touches
the FP/SIMD registers, to save and restore user state. */
.arch_extension fp
.arch_extension simd
.text
/*
* void ArchContextSwitch(uint64_t* oldSp, uint64_t newSp, uint64_t newRoot,
* uint8_t* oldFpu, uint8_t* newFpu)
* x0 = where to save the old SP x1 = SP to resume
* x2 = TTBR0 root for the new side x3/x4 = FP/SIMD save areas (may be 0)
*
* Frame on the kernel stack (Context.cpp's PrepareThreadStack builds the
* same shape): x19..x28, x29, x30 -- 96 bytes. The kernel is built with
* -mgeneral-regs-only, so q0-q31/FPCR/FPSR only ever hold user state.
*/
.global ArchContextSwitch
.type ArchContextSwitch, %function
ArchContextSwitch:
cbz x3, 1f
stp q0, q1, [x3, #0]
stp q2, q3, [x3, #32]
stp q4, q5, [x3, #64]
stp q6, q7, [x3, #96]
stp q8, q9, [x3, #128]
stp q10, q11, [x3, #160]
stp q12, q13, [x3, #192]
stp q14, q15, [x3, #224]
stp q16, q17, [x3, #256]
stp q18, q19, [x3, #288]
stp q20, q21, [x3, #320]
stp q22, q23, [x3, #352]
stp q24, q25, [x3, #384]
stp q26, q27, [x3, #416]
stp q28, q29, [x3, #448]
stp q30, q31, [x3, #480]
mrs x9, fpcr
mrs x10, fpsr
add x11, x3, #512
stp x9, x10, [x11]
1:
sub sp, sp, #96
stp x19, x20, [sp, #0]
stp x21, x22, [sp, #16]
stp x23, x24, [sp, #32]
stp x25, x26, [sp, #48]
stp x27, x28, [sp, #64]
stp x29, x30, [sp, #80]
mov x9, sp
str x9, [x0]
mov sp, x1
/* Switch the process half only when it differs (no ASIDs yet, so a
switch invalidates this CPU's TLB). */
mrs x9, ttbr0_el1
and x9, x9, #0x0000fffffffffffe
cmp x9, x2
b.eq 2f
dsb ishst
msr ttbr0_el1, x2
isb
tlbi vmalle1
dsb nsh
isb
2:
ldp x19, x20, [sp, #0]
ldp x21, x22, [sp, #16]
ldp x23, x24, [sp, #32]
ldp x25, x26, [sp, #48]
ldp x27, x28, [sp, #64]
ldp x29, x30, [sp, #80]
add sp, sp, #96
cbz x4, 3f
ldp q0, q1, [x4, #0]
ldp q2, q3, [x4, #32]
ldp q4, q5, [x4, #64]
ldp q6, q7, [x4, #96]
ldp q8, q9, [x4, #128]
ldp q10, q11, [x4, #160]
ldp q12, q13, [x4, #192]
ldp q14, q15, [x4, #224]
ldp q16, q17, [x4, #256]
ldp q18, q19, [x4, #288]
ldp q20, q21, [x4, #320]
ldp q22, q23, [x4, #352]
ldp q24, q25, [x4, #384]
ldp q26, q27, [x4, #416]
ldp q28, q29, [x4, #448]
ldp q30, q31, [x4, #480]
add x11, x4, #512
ldp x9, x10, [x11]
msr fpcr, x9
msr fpsr, x10
3:
ret
.size ArchContextSwitch, . - ArchContextSwitch
/*
* void ArchEnterUserMode(uint64_t entry, uint64_t userSp, uint64_t arg,
* uint64_t returnAddress, uint64_t kernelStackTop)
*
* Drops to EL0t with interrupts unmasked. The kernel stack is reset to its
* top: SP_EL1 is what the next exception from EL0 runs on.
*/
.global ArchEnterUserMode
.type ArchEnterUserMode, %function
ArchEnterUserMode:
msr daifset, #0xf
mov sp, x4
msr elr_el1, x0
msr sp_el0, x1
msr spsr_el1, xzr /* EL0t, DAIF clear */
mov x0, x2
mov x30, x3
mov x1, xzr
mov x2, xzr
mov x3, xzr
mov x4, xzr
mov x5, xzr
mov x6, xzr
mov x7, xzr
mov x8, xzr
mov x9, xzr
mov x10, xzr
mov x11, xzr
mov x12, xzr
mov x13, xzr
mov x14, xzr
mov x15, xzr
mov x16, xzr
mov x17, xzr
mov x18, xzr
mov x29, xzr
eret
.size ArchEnterUserMode, . - ArchEnterUserMode
+53
View File
@@ -0,0 +1,53 @@
/*
* Context.cpp
* aarch64 thread context (implements Arch/Context.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Context.hpp>
#include <Libraries/Memory.hpp>
#include <Sched/Scheduler.hpp>
// Context.S
extern "C" void ArchContextSwitch(uint64_t* oldSp, uint64_t newSp, uint64_t newRoot,
uint8_t* oldFpu, uint8_t* newFpu);
extern "C" [[noreturn]] void ArchEnterUserMode(uint64_t entry, uint64_t userSp, uint64_t arg,
uint64_t returnAddress, uint64_t kernelStackTop);
namespace Arch {
void InitFpuState(uint8_t* area) {
// All-zero is the architectural reset state: round-to-nearest, no
// exceptions trapped, no flags raised.
memset(area, 0, FpuStateSize);
}
uint64_t PrepareThreadStack(uint64_t kernelStackTop, void (*entry)()) {
// The frame Context.S pops: x19..x28, x29, x30 (= return address).
uint64_t* sp = (uint64_t*)kernelStackTop - 12;
for (int i = 0; i < 11; i++) {
sp[i] = 0;
}
sp[11] = (uint64_t)entry;
return (uint64_t)sp;
}
void SwitchContext(uint64_t* oldSp, uint64_t newSp, uint64_t newRoot,
uint8_t* oldFpu, uint8_t* newFpu) {
ArchContextSwitch(oldSp, newSp, newRoot, oldFpu, newFpu);
}
void ActivateThread(Smp::CpuData* cpu, uint64_t kernelStackTop,
uint64_t threadPointer) {
cpu->entry.kernelStackTop = kernelStackTop;
asm volatile("msr tpidr_el0, %0" :: "r"(threadPointer));
}
void EnterUserMode(uint64_t entry, uint64_t userSp, uint64_t arg) {
// Returning from the entry function lands in the exit stub (see
// SetupUserEntryStack in Abi.hpp).
ArchEnterUserMode(entry, userSp, arg, Sched::ExitStubAddr,
Smp::GetCurrentCpuData()->entry.kernelStackTop);
}
}
+14
View File
@@ -0,0 +1,14 @@
/*
* Context.hpp
* aarch64 thread context constants (see Arch/Context.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Arch {
// q0-q31 followed by FPCR and FPSR (Context.S).
constexpr uint64_t FpuStateSize = 32 * 16 + 16;
constexpr uint64_t FpuStateAlign = 16;
}
+120
View File
@@ -0,0 +1,120 @@
/*
* Cpu.hpp
* aarch64 implementation of the Arch/Cpu.hpp primitives
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Arch {
// PSTATE.I in the DAIF view
static constexpr uint64_t DaifIrqMask = 1ULL << 7;
inline void Pause() {
asm volatile("yield" ::: "memory");
}
// Device-nGnRE / Normal-NC (write-combining) stores complete in order
// once a DSB has been issued.
inline void StoreFence() {
asm volatile("dsb st" ::: "memory");
}
inline void MemoryFence() {
asm volatile("dsb sy" ::: "memory");
}
inline void EnableInterrupts() {
asm volatile("msr daifclr, #2" ::: "memory");
}
inline void DisableInterrupts() {
asm volatile("msr daifset, #2" ::: "memory");
}
inline bool InterruptsEnabled() {
uint64_t daif;
asm volatile("mrs %0, daif" : "=r"(daif));
return (daif & DaifIrqMask) == 0;
}
inline IrqFlags SaveAndDisableInterrupts() {
uint64_t daif;
asm volatile("mrs %0, daif\n\tmsr daifset, #2" : "=r"(daif) :: "memory");
return daif;
}
inline void RestoreInterrupts(IrqFlags flags) {
asm volatile("msr daif, %0" :: "r"(flags) : "memory");
}
inline void Halt() {
asm volatile("wfi" ::: "memory");
}
// WFI wakes on a pending interrupt even while PSTATE.I masks it, so
// checking for work and then waiting with IRQs masked cannot lose a
// wakeup. Unmask briefly afterwards so the pending IRQ is taken.
inline void HaltWithInterruptsDisabled() {
asm volatile("wfi\n\tmsr daifclr, #2\n\tisb\n\tmsr daifset, #2" ::: "memory");
}
[[noreturn]] inline void HaltForever() {
for (;;) {
asm volatile("msr daifset, #0xf\n\twfi" ::: "memory");
}
}
inline uint64_t ReadTimestamp() {
uint64_t value;
asm volatile("isb\n\tmrs %0, cntvct_el0" : "=r"(value) :: "memory");
return value;
}
// The generic timer runs from boot at CNTFRQ_EL0 Hz, so it times short
// delays without calibration.
inline void IoDelay() {
uint64_t frequency;
asm volatile("mrs %0, cntfrq_el0" : "=r"(frequency));
uint64_t ticks = frequency / 1000000 + 1;
uint64_t start = ReadTimestamp();
while (ReadTimestamp() - start < ticks) {
Pause();
}
}
// ARMv8 has no "clean the whole cache" instruction: set/way maintenance
// is only safe from firmware, which PSCI performs on power transitions.
// Order outstanding accesses so the firmware call sees them.
inline void WritebackInvalidateCaches() {
asm volatile("dsb sy\n\tisb" ::: "memory");
}
inline void FlushTlbPage(uint64_t virtualAddress) {
asm volatile("dsb ishst\n\ttlbi vaae1is, %0\n\tdsb ish\n\tisb"
:: "r"(virtualAddress >> 12) : "memory");
}
inline void FlushTlbAll() {
asm volatile("dsb ishst\n\ttlbi vmalle1is\n\tdsb ish\n\tisb" ::: "memory");
}
// The process (lower) half of the address space is translated through
// TTBR0_EL1; the kernel half through TTBR1_EL1, which never changes.
static constexpr uint64_t TtbrAddressMask = 0x0000FFFFFFFFFFFEULL;
inline uint64_t ReadPageTableRoot() {
uint64_t ttbr;
asm volatile("mrs %0, ttbr0_el1" : "=r"(ttbr));
return ttbr & TtbrAddressMask;
}
// No ASIDs yet: every switch invalidates the old process's entries.
inline void WritePageTableRoot(uint64_t rootPhys) {
asm volatile("dsb ishst\n\tmsr ttbr0_el1, %0\n\tisb\n\ttlbi vmalle1\n\tdsb nsh\n\tisb"
:: "r"(rootPhys) : "memory");
}
}
+26
View File
@@ -0,0 +1,26 @@
/*
* CpuPower.cpp
* aarch64 CPU power management (implements Arch/CpuPower.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/CpuPower.hpp>
// No frequency scaling or thermal governor yet (on ARM these come from
// ACPI CPPC / SCMI rather than CPU registers).
namespace Arch::CpuPower {
void InitializeBsp() {}
void InitializeAp() {}
void ApplyPolicyIfChanged() {}
void ThermalTick(uint64_t) {}
void ServiceDeferredDiagnostics() {}
void ReapplyAfterWake() {}
bool GetSnapshot(Snapshot& out) {
out = {};
return false;
}
}
@@ -0,0 +1,22 @@
/*
* InterruptFrame.hpp
* aarch64 exception frame
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Arch {
// What the exception vectors save before calling into C++.
struct PanicFrame {
uint64_t x[31]; // x0-x30
uint64_t sp; // interrupted SP (SP_EL0 for EL0, else SP_EL1)
uint64_t elr; // ELR_EL1: faulting/preferred-return PC
uint64_t spsr; // SPSR_EL1
uint64_t esr; // ESR_EL1: exception syndrome
uint64_t far; // FAR_EL1: faulting address
};
}
+52
View File
@@ -0,0 +1,52 @@
/*
* Interrupts.cpp
* aarch64 IRQ slot dispatch (implements Arch/Interrupts.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Interrupts.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
// IRQ slots are delivered by the interrupt controller driver (GICv2/v3,
// not written yet), which maps INTIDs onto slots and calls ArchIrqDispatch
// from the IRQ exception vector after acknowledging the interrupt.
namespace Arch {
static IrqHandler g_irqHandlers[IRQ_COUNT] = {};
void RegisterIrqHandler(uint8_t irq, IrqHandler handler) {
if (irq >= IRQ_COUNT) return;
g_irqHandlers[irq] = handler;
Kt::KernelLogStream(Kt::DEBUG, "IRQ") << "Registered handler for IRQ "
<< base::dec << (uint64_t)irq;
}
// TODO(aarch64): GIC SPI routing (ACPI MADT GICD / interrupt overrides).
void RouteLegacyIrq(uint8_t irq) {
Kt::KernelLogStream(Kt::WARNING, "IRQ") << "aarch64: cannot route IRQ "
<< base::dec << (uint64_t)irq << " (no GIC driver)";
}
void EnableLegacyIrq(uint8_t irq) {
RouteLegacyIrq(irq);
}
// TODO(aarch64): GICv3 ITS / GICv2m doorbell.
bool ComposeMsi(uint8_t, MsiMessage&) {
return false;
}
// TODO(aarch64): GIC software-generated interrupts.
bool SendIpi(int, uint8_t) {
return false;
}
}
extern "C" void ArchIrqDispatch(uint64_t irq, bool fromUser) {
if (irq < Arch::IRQ_COUNT && Arch::g_irqHandlers[irq] != nullptr) {
Arch::g_irqHandlers[irq]((uint8_t)irq, fromUser);
}
}
+28
View File
@@ -0,0 +1,28 @@
/*
* IoPort.hpp
* aarch64 port I/O (implements Io/IoPort.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Arch/Cpu.hpp>
// ARM has no I/O address space. Until a PCI host bridge driver maps its I/O
// aperture, port accesses behave like accesses to an absent device.
namespace Io {
static constexpr bool HasPortIo = false;
inline void Out8(uint8_t, uint16_t) {}
inline void Out16(uint16_t, uint16_t) {}
inline void Out32(uint32_t, uint16_t) {}
inline uint8_t In8(uint16_t) { return 0xFF; }
inline uint16_t In16(uint16_t) { return 0xFFFF; }
inline uint32_t In32(uint16_t) { return 0xFFFFFFFF; }
inline void IoPortWait() {
Arch::IoDelay();
}
};
+110
View File
@@ -0,0 +1,110 @@
/*
* Paging.cpp
* aarch64 virtual memory (implements Memory/Paging.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Memory/Paging.hpp>
#include <Arch/Cpu.hpp>
#include <Common/Panic.hpp>
#include <CppLib/Spinlock.hpp>
// TODO(aarch64): 4 KiB-granule, 48-bit VA translation tables. The kernel
// half lives in TTBR1_EL1 and is never switched; "address space roots"
// handed to the generic kernel are TTBR0_EL1 tables, so
// CreateUserAddressSpace has no kernel half to copy. MAIR_EL1 needs
// Normal-WB, Normal-NC (MapWC) and Device-nGnRE (MapMMIO) indices.
namespace Memory::VMM {
Paging* g_paging = nullptr;
static kcp::Mutex pagingLock;
void LockUserPaging() { pagingLock.Acquire(); }
void UnlockUserPaging() { pagingLock.Release(); }
[[noreturn]] static void NotImplemented(const char* what) {
Panic(what, nullptr);
}
void ActivateKernelAddressSpace() {
Arch::WritePageTableRoot(g_paging->KernelRoot());
}
void FlushTLB() {
Arch::FlushTlbAll();
}
Paging::Paging() {
NotImplemented("aarch64: virtual memory not implemented");
}
void Paging::Init(std::uint64_t, std::uint64_t, const montauk::boot::MemoryMap&,
const montauk::boot::Framebuffer&) {
NotImplemented("aarch64: Paging::Init not implemented");
}
std::uint64_t Paging::HhdmLimit() { return 0; }
void Paging::Map(std::uint64_t, std::uint64_t) {
NotImplemented("aarch64: Paging::Map not implemented");
}
void Paging::MapMMIO(std::uint64_t, std::uint64_t) {
NotImplemented("aarch64: Paging::MapMMIO not implemented");
}
void Paging::MapWC(std::uint64_t, std::uint64_t) {
NotImplemented("aarch64: Paging::MapWC not implemented");
}
void Paging::MapUser(std::uint64_t, std::uint64_t) {
NotImplemented("aarch64: Paging::MapUser not implemented");
}
std::uint64_t Paging::GetPhysAddr(std::uint64_t, std::uint64_t) {
NotImplemented("aarch64: Paging::GetPhysAddr not implemented");
}
std::uint64_t Paging::GetPhysAddr(std::uint64_t virtualAddress) {
return GetPhysAddr(rootPhys, virtualAddress);
}
std::uint64_t Paging::CreateUserAddressSpace() {
NotImplemented("aarch64: Paging::CreateUserAddressSpace not implemented");
}
bool Paging::MapUserIn(std::uint64_t, std::uint64_t, std::uint64_t) {
NotImplemented("aarch64: Paging::MapUserIn not implemented");
}
bool Paging::MapUserInPermissions(std::uint64_t, std::uint64_t, std::uint64_t, bool, bool) {
NotImplemented("aarch64: Paging::MapUserInPermissions not implemented");
}
bool Paging::ProtectUserIn(std::uint64_t, std::uint64_t, bool, bool) {
NotImplemented("aarch64: Paging::ProtectUserIn not implemented");
}
bool Paging::MapUserInWC(std::uint64_t, std::uint64_t, std::uint64_t) {
NotImplemented("aarch64: Paging::MapUserInWC not implemented");
}
void Paging::UnmapUserIn(std::uint64_t, std::uint64_t) {
NotImplemented("aarch64: Paging::UnmapUserIn not implemented");
}
void Paging::FreeUserHalf(std::uint64_t) {
NotImplemented("aarch64: Paging::FreeUserHalf not implemented");
}
bool Paging::IsUserRangeAccessible(std::uint64_t, std::uint64_t, std::uint64_t, bool) {
NotImplemented("aarch64: Paging::IsUserRangeAccessible not implemented");
}
void Paging::MapEfiRuntime(const montauk::boot::EfiInfo&) {
NotImplemented("aarch64: Paging::MapEfiRuntime not implemented");
}
}
+28
View File
@@ -0,0 +1,28 @@
/*
* PanicFrame.cpp
* aarch64 CPU state for the panic screen
* Copyright (c) 2026 Daniel Hammer
*/
#include <Common/Panic.hpp>
#include <Arch/aarch64/InterruptFrame.hpp>
#include <CppLib/BoxUI.hpp>
static constexpr int BoxWidth = PanicBoxWidth;
namespace Arch {
void PrintPanicFrame(PanicFrame* frame) {
PrintBoxedSeparator(kerr, BoxWidth);
PrintBoxedLine(kerr, "CPU State:", BoxWidth, true);
PrintBoxedHex(kerr, "Exception Class (ESR)", frame->esr >> 26, BoxWidth);
PrintBoxedHex(kerr, "Syndrome (ESR)", frame->esr, BoxWidth);
PrintBoxedHex(kerr, "Fault Address (FAR)", frame->far, BoxWidth);
PrintBoxedSeparator(kerr, BoxWidth);
PrintBoxedLine(kerr, "Registers:", BoxWidth, true);
PrintBoxedHex(kerr, "Program Counter (ELR)", frame->elr, BoxWidth);
PrintBoxedHex(kerr, "PSTATE (SPSR)", frame->spsr, BoxWidth);
PrintBoxedHex(kerr, "Stack Pointer", frame->sp, BoxWidth);
PrintBoxedHex(kerr, "Link Register (x30)", frame->x[30], BoxWidth);
}
}
+34
View File
@@ -0,0 +1,34 @@
/*
* PerCpu.hpp
* aarch64 per-CPU state (see Arch/Smp.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Arch {
// Read by the exception vectors through TPIDR_EL1.
struct CpuEntryState {
uint64_t kernelStackTop; // SP_EL1 to use on entry from EL0
uint64_t scratch; // vector-private scratch slot
};
struct CpuState {
uint64_t mpidr; // MPIDR_EL1 affinity of this CPU
};
// TPIDR_EL1 holds the CpuData pointer (zeroed in EarlyInitialize until
// Smp::InitBsp installs it).
inline void* CurrentCpuLocal() {
void* ptr;
asm volatile("mrs %0, tpidr_el1" : "=r"(ptr));
return ptr;
}
inline void* TryCurrentCpuLocal() {
return CurrentCpuLocal();
}
}
+64
View File
@@ -0,0 +1,64 @@
/*
* Platform.cpp
* aarch64 bring-up hooks (implements Arch/Platform.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Platform.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/Smp.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
using namespace Kt;
namespace Arch {
void EarlyInitialize() {
// No per-CPU data yet: Smp::TryGetCurrentCpuData must see null.
asm volatile("msr tpidr_el1, xzr");
// CPACR_EL1.FPEN = 0b11: FP/SIMD usable at EL0 and EL1 (the kernel
// itself is built -mgeneral-regs-only; Context.S saves user state).
uint64_t cpacr;
asm volatile("mrs %0, cpacr_el1" : "=r"(cpacr));
cpacr |= 3ULL << 20;
asm volatile("msr cpacr_el1, %0\n\tisb" :: "r"(cpacr));
}
// TODO(aarch64): install the VBAR_EL1 vector table (EL1/EL0 synchronous
// exceptions -> panic / SyscallDispatch, IRQ -> GIC -> ArchIrqDispatch).
void InitializeTraps() {
KernelLogStream(WARNING, "Arch") << "aarch64: exception vectors not implemented";
}
// MAIR_EL1 attribute indices are chosen together with the page-table
// format (Paging.cpp), so there is nothing to program separately.
void InitializeMemoryAttributes() {
}
// TODO(aarch64): GIC distributor/redistributor from the ACPI MADT.
void InitializeInterrupts(Hal::ACPI::CommonSDTHeader*) {
Smp::InitBsp();
KernelLogStream(WARNING, "Arch") << "aarch64: no interrupt controller driver";
}
void InitializePlatformDevices() {
}
// EL0 entry needs no global state beyond Arch::ActivateThread.
void PrepareUserMode() {
}
void LateInitialize() {
}
void ListPlatformDevices(PlatformDeviceSink, void*) {
}
// TODO(aarch64): PSCI SYSTEM_RESET through the conduit the FADT names.
void Reboot() {
HaltForever();
}
}
+60
View File
@@ -0,0 +1,60 @@
/*
* Smp.cpp
* aarch64 per-CPU data (implements the arch half of Arch/Smp.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Smp.hpp>
#include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
using namespace Kt;
namespace Smp {
static CpuData g_cpus[MaxCPUs];
static int g_cpuCount = 0;
static int g_detectedCpuCount = 1;
CpuData* GetCpuData(int index) {
if (index < 0 || index >= g_cpuCount) return nullptr;
return &g_cpus[index];
}
int GetCpuCount() {
return g_cpuCount;
}
int GetDetectedCpuCount() {
return g_detectedCpuCount;
}
void InitBsp() {
memset(g_cpus, 0, sizeof(g_cpus));
CpuData& bsp = g_cpus[0];
bsp.selfPtr = (uint64_t)&bsp;
bsp.cpuIndex = 0;
bsp.currentSlot = -1;
bsp.started = true;
asm volatile("mrs %0, mpidr_el1" : "=r"(bsp.arch.mpidr));
asm volatile("msr tpidr_el1, %0" :: "r"(&bsp));
g_cpuCount = 1;
KernelLogStream(OK, "SMP") << "BSP initialized (MPIDR " << base::hex
<< bsp.arch.mpidr << ")";
}
void BootAPs(const montauk::boot::SmpInfo& smp) {
if (smp.cpuCount > 1) {
g_detectedCpuCount = (int)smp.cpuCount;
}
// TODO(aarch64): start secondaries through smp.startCpu once the GIC
// (reschedule/TLB-shootdown SGIs) and the per-CPU generic timer work.
KernelLogStream(WARNING, "SMP") << "aarch64: secondary CPUs not started ("
<< base::dec << smp.cpuCount << " reported)";
}
}
+16
View File
@@ -0,0 +1,16 @@
/*
* Syscall.cpp
* aarch64 system-call entry (implements Arch/Syscall.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Syscall.hpp>
namespace Arch {
// `svc #0` arrives through the EL0 synchronous vector installed by
// InitializeTraps; there is no per-CPU register to program.
void InitializeSyscallEntry() {
}
}
+21
View File
@@ -0,0 +1,21 @@
/*
* SyscallFrame.hpp
* aarch64 system-call frame (see Arch/Syscall.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Arch {
// Saved by the EL0 synchronous-exception vector for `svc #0`:
// x0-x5 carry the arguments and x8 the system-call number.
struct SyscallFrame {
uint64_t arg1, arg2, arg3, arg4, arg5, arg6; // x0-x5
uint64_t syscall_nr; // x8
uint64_t elr, spsr, sp_el0; // user PC, PSTATE, SP
uint64_t x19_x30[12]; // callee-saved + LR
};
}
+24
View File
@@ -0,0 +1,24 @@
/*
* Timer.cpp
* aarch64 tick timer (implements Arch/Timer.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Timer.hpp>
// TODO(aarch64): drive the EL1 virtual timer (CNTV_TVAL/CNTV_CTL) from
// CNTFRQ_EL0, re-arming on each tick, once the GIC delivers its PPI.
namespace Arch::Timer {
bool InitializeBsp(uint32_t) {
return false;
}
void StartPeriodic(uint32_t) {
}
void Stop() {
}
}
+62
View File
@@ -0,0 +1,62 @@
/*
* Abi.hpp
* x86_64 user-mode ABI details (implements Arch/Abi.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
// UEFI uses the Microsoft x64 calling convention.
#define ARCH_EFIAPI __attribute__((__ms_abi__))
namespace Arch {
constexpr uint16_t ElfMachine = 62; // EM_X86_64
constexpr const char* ElfMachineName = "x86_64";
inline RelocationKind ClassifyRelocation(uint32_t type) {
switch (type) {
case 0: return RelocationKind::None;
case 8: return RelocationKind::Relative; // R_X86_64_RELATIVE
case 1: // R_X86_64_64
case 6: // R_X86_64_GLOB_DAT
case 7: return RelocationKind::Symbol; // R_X86_64_JUMP_SLOT
default: return RelocationKind::Unsupported;
}
}
// Variant II. The linker addresses TLS variables at
// TP - align_up(memSize, p_align) + offset, so the template must start
// exactly that far below TP; rounding p_align up first (as an older
// version did) shifts every variable away from its initial value.
// TP itself is kept at least 16-byte aligned, which the gap between
// base and the template absorbs. The 16 bytes above TP are the TCB.
inline TlsLayout ComputeTlsLayout(uint64_t memSize, uint64_t pAlign) {
uint64_t align = pAlign ? pAlign : 1;
uint64_t block = (memSize + align - 1) & ~(align - 1);
uint64_t tpAlign = align < 16 ? 16 : align;
uint64_t tp = (block + tpAlign - 1) & ~(tpAlign - 1);
return {tp - block, tp, tp + 16};
}
inline void WriteExitStub(uint8_t* stub) {
stub[0] = 0x31; stub[1] = 0xFF; // xor edi, edi (exit code 0)
stub[2] = 0x31; stub[3] = 0xC0; // xor eax, eax (SYS_EXIT = 0)
stub[4] = 0x0F; stub[5] = 0x05; // syscall
}
// The entry function is entered as if CALLed: the return address sits
// at [RSP], with RSP = 16n - 8.
inline uint64_t SetupUserEntryStack(uint8_t* topPage, uint64_t stackTop,
uint64_t returnAddress) {
*(uint64_t*)(topPage + 0xFF8) = returnAddress;
return stackTop - 8;
}
// SystemV alignment as if CALLed (16n - 8); the slot is never read.
inline uint64_t ThreadEntryStackPointer(uint64_t stackTop) {
return (stackTop & ~0xFULL) - 8;
}
}
@@ -4,8 +4,10 @@
* Copyright (c) 2026 Daniel Hammer
*/
#include "AcpiSleep.hpp"
#include "AcpiEvents.hpp"
#include <ACPI/AcpiSleep.hpp>
#include <ACPI/AcpiEvents.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/x86_64/Acpi/S3State.hpp>
#include <ACPI/FADT.hpp>
#include <ACPI/AML/AmlParser.hpp>
#include <ACPI/AML/AmlInterpreter.hpp>
@@ -14,19 +16,19 @@
#include <CppLib/Stream.hpp>
#include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp>
#include <Hal/GDT.hpp>
#include <Hal/Apic/Apic.hpp>
#include <Hal/Apic/IoApic.hpp>
#include <Hal/Apic/Pic.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Hal/CpuPower.hpp>
#include <Arch/x86_64/GDT.hpp>
#include <Arch/x86_64/Apic/Apic.hpp>
#include <Arch/x86_64/Apic/IoApic.hpp>
#include <Arch/x86_64/Apic/Pic.hpp>
#include <Timekeeping/Timer.hpp>
#include <Arch/CpuPower.hpp>
#include <Drivers/Graphics/IntelGPU.hpp>
#include <Drivers/Graphics/DgpuPower.hpp>
#include <Drivers/PS2/PS2Controller.hpp>
#include <Graphics/Framebuffer.hpp>
#include <Libraries/Memory.hpp>
#include <Hal/MSR.hpp>
#include <Hal/SmpBoot.hpp>
#include <Arch/x86_64/MSR.hpp>
#include <Arch/Smp.hpp>
#include <Api/Syscall.hpp>
using namespace Kt;
@@ -186,7 +188,7 @@ namespace Hal {
<< base::hex << (uint64_t)g_s3SlpTypA
<< " SLP_TYPb=" << base::hex << (uint64_t)g_s3SlpTypB << ")";
KernelLogStream(INFO, "S3") << "Kernel PML4 phys = " << base::hex
<< (uint64_t)Memory::VMM::g_paging->PML4;
<< Memory::VMM::g_paging->KernelRoot();
KernelLogStream(INFO, "S3") << "FACS at phys " << base::hex
<< (uint64_t)fadt.FacsAddress << " version=" << base::dec
<< (uint64_t)g_facs->Version << " flags=" << base::hex
@@ -314,7 +316,7 @@ namespace Hal {
// shadow copy of the PML4 at a fixed low-memory address.
// Only the PML4 page itself needs to be below 4GB — its entries
// contain full 52-bit physical addresses for sub-tables.
uint64_t kernelPml4Phys = (uint64_t)Memory::VMM::g_paging->PML4;
uint64_t kernelPml4Phys = Memory::VMM::g_paging->KernelRoot();
uint8_t* kernelPml4Virt = (uint8_t*)Memory::HHDM(kernelPml4Phys);
uint8_t* shadowPml4Virt = (uint8_t*)Memory::HHDM((uint64_t)SHADOW_PML4_PHYS);
memcpy(shadowPml4Virt, kernelPml4Virt, 4096);
@@ -439,7 +441,7 @@ namespace Hal {
Hal::InitializePAT();
// HWP enable and POWER_CTL reset across S3; re-apply them and
// bump the policy epoch so APs re-apply on their next tick.
Hal::CpuPower::ReapplyAfterWake();
Arch::CpuPower::ReapplyAfterWake();
reinitProgress(0x05); // PIC
Hal::DisableLegacyPic();
reinitProgress(0x06); // Local APIC
@@ -447,7 +449,7 @@ namespace Hal {
reinitProgress(0x07); // IO APIC
Hal::IoApic::Reinitialize();
reinitProgress(0x08); // APIC Timer
Timekeeping::ApicTimerReinitialize();
Timekeeping::TimerReinitialize();
reinitProgress(0x09); // PM1 clear
ClearPM1Status();
reinitProgress(0x0A); // _WAK
@@ -463,7 +465,7 @@ namespace Hal {
reinitProgress(0x0D); // discrete GPU back off
Drivers::Graphics::DgpuPower::Resume();
reinitProgress(0x0E); // sti
asm volatile("sti");
Arch::EnableInterrupts();
KernelLogStream(OK, "S3") << "Resumed from S3 suspend";
@@ -517,7 +519,7 @@ namespace Hal {
Io::Out8(0x00, 0x71);
// 3. Disable interrupts
asm volatile("cli");
Arch::DisableInterrupts();
// 4. Identity-map the trampoline page so the CPU can execute it
// after paging is re-enabled with our CR3 during wake.
@@ -525,7 +527,7 @@ namespace Hal {
Memory::VMM::g_paging->Map(TRAMPOLINE_PHYS, TRAMPOLINE_PHYS);
// 5. Flush all caches
asm volatile("wbinvd");
Arch::WritebackInvalidateCaches();
// 6. Set the waking vector in FACS
SetWakingVector();
@@ -548,7 +550,7 @@ namespace Hal {
}
// 10. Flush caches again
asm volatile("wbinvd");
Arch::WritebackInvalidateCaches();
// 11. Assert SLP_EN — this triggers S3 entry
Io::Out16(pm1a | PM1_SLP_EN, (uint16_t)g_pm1aControlBlock);
@@ -563,7 +565,7 @@ namespace Hal {
// on QEMU/KVM the vCPU must reach HLT before QEMU finalizes S3.
// On resume, firmware jumps to our waking vector.
for (;;) {
asm volatile("hlt");
Arch::Halt();
// If we wake from HLT (spurious or real wake), check WAK_STS
if (g_pm1aEventBlock != 0) {
uint16_t sts = Io::In16((uint16_t)g_pm1aEventBlock);
@@ -574,7 +576,7 @@ namespace Hal {
// If we get here, either S3 failed or we woke immediately.
// The normal wake path goes through AcpiResumeLongMode instead.
// Re-enable interrupts as a safety measure.
asm volatile("sti");
Arch::EnableInterrupts();
KernelLogStream(WARNING, "S3") << "S3 entry may have failed (fell through)";
return 0;
@@ -5,10 +5,11 @@
*/
#include "CpuIdle.hpp"
#include <Arch/Cpu.hpp>
#include <ACPI/FADT.hpp>
#include <ACPI/AML/AmlInterpreter.hpp>
#include <Hal/Cpu.hpp>
#include <Hal/SmpBoot.hpp>
#include <Arch/x86_64/CpuFeatures.hpp>
#include <Arch/Smp.hpp>
#include <Io/IoPort.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
@@ -480,7 +481,7 @@ namespace Hal {
if (hasMwait) {
Hal::IdleWaitWithInterruptsDisabled(addr, CurrentIdleHint());
} else {
Hal::HaltWithInterruptsDisabled();
Arch::HaltWithInterruptsDisabled();
}
}
File renamed without changes.
+30
View File
@@ -0,0 +1,30 @@
/*
* S3State.hpp
* CPU state saved across S3 by S3Wake.asm
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Hal {
namespace AcpiSleep {
// ============================================================================
// Layout must match S3Wake.asm offsets exactly.
// No FPU/SSE state — the kernel is compiled with -mno-sse.
struct CpuState {
uint64_t Rax, Rbx, Rcx, Rdx; // 0x00 - 0x18
uint64_t Rsi, Rdi, Rbp, Rsp; // 0x20 - 0x38
uint64_t R8, R9, R10, R11; // 0x40 - 0x58
uint64_t R12, R13, R14, R15; // 0x60 - 0x78
uint64_t Rflags; // 0x80
uint64_t Rip; // 0x88
uint64_t Cr3; // 0x90
uint64_t Cr0, Cr4; // 0x98, 0xA0
uint8_t GdtPtr[10]; // 0xA8
uint8_t IdtPtr[10]; // 0xB2
} __attribute__((aligned(16)));
};
};
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -10,7 +10,7 @@
#include "IoApic.hpp"
#include "Interrupts.hpp"
#include <ACPI/MADT.hpp>
#include <Hal/IDT.hpp>
#include <Arch/x86_64/IDT.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Memory/Paging.hpp>
@@ -67,8 +67,8 @@ namespace Hal {
// Step 7: Route keyboard (IRQ1) and mouse (IRQ12) to BSP
uint8_t bspApicId = (uint8_t)LocalApic::GetId();
IoApic::RouteIrq(IRQ_KEYBOARD, IRQ_VECTOR_BASE + IRQ_KEYBOARD, bspApicId);
IoApic::RouteIrq(IRQ_MOUSE, IRQ_VECTOR_BASE + IRQ_MOUSE, bspApicId);
IoApic::RouteIrq(Arch::IRQ_KEYBOARD, IRQ_VECTOR_BASE + Arch::IRQ_KEYBOARD, bspApicId);
IoApic::RouteIrq(Arch::IRQ_MOUSE, IRQ_VECTOR_BASE + Arch::IRQ_MOUSE, bspApicId);
// Note: sti is NOT called here. The caller (Main.cpp) enables
// interrupts after setting up per-CPU GS base for SMP/SWAPGS.
File renamed without changes.
@@ -6,7 +6,9 @@
#include "Interrupts.hpp"
#include "Apic.hpp"
#include <Hal/IDT.hpp>
#include "IoApic.hpp"
#include <Arch/x86_64/IDT.hpp>
#include <Arch/Smp.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
@@ -14,19 +16,15 @@
extern "C" void* IrqStubTable[Hal::IRQ_COUNT];
extern "C" void IrqStubSpurious();
// Local APIC message address range; destination APIC ID in bits 19:12.
static constexpr uint64_t MsiAddressBase = 0xFEE00000;
namespace Hal {
// Dispatch table: one handler per IRQ
static IrqHandler g_irqHandlers[IRQ_COUNT] = {};
void RegisterIrqHandler(uint8_t irq, IrqHandler handler) {
if (irq >= IRQ_COUNT) return;
g_irqHandlers[irq] = handler;
Kt::KernelLogStream(Kt::DEBUG, "IRQ") << "Registered handler for IRQ " << base::dec << (uint64_t)irq
<< " (vector " << (uint64_t)(IRQ_VECTOR_BASE + irq) << ")";
}
static Arch::IrqHandler g_irqHandlers[IRQ_COUNT] = {};
void InitializeIrqHandlers() {
// Install IRQ stubs into IDT vectors 32..55
// Install IRQ stubs into IDT vectors 32..79
for (int i = 0; i < IRQ_COUNT; i++) {
IDTEncodeInterrupt(IRQ_VECTOR_BASE + i, IrqStubTable[i], 0x8E);
}
@@ -41,6 +39,41 @@ namespace Hal {
};
namespace Arch {
void RegisterIrqHandler(uint8_t irq, IrqHandler handler) {
if (irq >= IRQ_COUNT) return;
Hal::g_irqHandlers[irq] = handler;
Kt::KernelLogStream(Kt::DEBUG, "IRQ") << "Registered handler for IRQ " << base::dec << (uint64_t)irq
<< " (vector " << (uint64_t)(Hal::IRQ_VECTOR_BASE + irq) << ")";
}
void RouteLegacyIrq(uint8_t irq) {
// RouteIrq applies the MADT's Interrupt Source Overrides (polarity,
// trigger mode, GSI remapping).
Hal::IoApic::RouteIrq(irq, Hal::IRQ_VECTOR_BASE + irq,
(uint8_t)Hal::LocalApic::GetId());
}
void EnableLegacyIrq(uint8_t irq) {
Hal::IoApic::UnmaskIrq(Hal::IoApic::GetGsiForIrq(irq));
}
bool ComposeMsi(uint8_t irq, MsiMessage& out) {
// Physical destination mode, fixed delivery, edge triggered, to the
// BSP (APIC ID 0 -- the destination field is left zero, as every
// driver has always programmed it).
out = MsiMessage{ MsiAddressBase, (uint32_t)(Hal::IRQ_VECTOR_BASE + irq) };
return true;
}
bool SendIpi(int cpuIndex, uint8_t irq) {
auto* cpu = Smp::GetCpuData(cpuIndex);
if (cpu == nullptr) return false;
return Hal::LocalApic::SendFixedIpi(cpu->arch.lapicId,
Hal::IRQ_VECTOR_BASE + irq);
}
};
// C linkage dispatch function called from assembly stubs
extern "C" void HalIrqDispatch(uint64_t irqNumber, uint64_t savedCs) {
// Send EOI BEFORE calling the handler. The handler may context-switch
@@ -0,0 +1,20 @@
/*
* Interrupts.hpp
* x86_64 IRQ slot <-> IDT vector mapping
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Arch/Interrupts.hpp>
namespace Hal {
using Arch::IRQ_COUNT;
// IRQ vector base: IRQ slot N is delivered on IDT vector 32 + N.
constexpr uint8_t IRQ_VECTOR_BASE = 32;
// Install IRQ stubs into the IDT and set up the dispatch table
void InitializeIrqHandlers();
};
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+133
View File
@@ -0,0 +1,133 @@
/*
* ApicTimer.cpp
* Local APIC timer, PIT-calibrated (implements Arch/Timer.hpp)
* Copyright (c) 2025 Daniel Hammer
*/
#include <Arch/Timer.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/x86_64/Apic/Apic.hpp>
#include <Arch/x86_64/Apic/Interrupts.hpp>
#include <Io/IoPort.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
using namespace Kt;
namespace Arch::Timer {
// PIT constants
static constexpr uint32_t PIT_FREQUENCY = 1193182; // PIT oscillator frequency in Hz
static constexpr uint16_t PIT_CHANNEL2_DATA = 0x42;
static constexpr uint16_t PIT_COMMAND = 0x43;
static constexpr uint16_t PIT_GATE_PORT = 0x61;
// APIC timer LVT mode bits
static constexpr uint32_t LVT_MASKED = (1 << 16);
static constexpr uint32_t LVT_PERIODIC = (1 << 17);
// APIC timer divide configuration values
static constexpr uint32_t DIVIDE_BY_16 = 0x03;
static uint32_t g_ticksPerMs = 0;
static uint32_t CountForIntervalMs(uint32_t intervalMs) {
return g_ticksPerMs * intervalMs;
}
static void ProgramTimer(bool periodic, uint32_t intervalMs) {
uint32_t lvt = (Hal::IRQ_VECTOR_BASE + Arch::IRQ_TIMER);
if (periodic) {
lvt |= LVT_PERIODIC;
}
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_DIVIDE, DIVIDE_BY_16);
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_LVT, lvt);
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_INITIAL,
CountForIntervalMs(intervalMs));
}
// Use PIT channel 2 to create a precise delay for calibration.
// Returns the number of APIC timer ticks that elapsed during ~10ms.
static uint32_t CalibratePit() {
// We want to measure ~10ms worth of APIC timer ticks.
// PIT count for 10ms: PIT_FREQUENCY / 100 = 11931
uint16_t pitCount = PIT_FREQUENCY / 100;
// Set up PIT channel 2 in one-shot mode (mode 0), lobyte/hibyte
// Command: channel 2 (bits 7-6 = 10), lobyte/hibyte (bits 5-4 = 11),
// mode 0 (bits 3-1 = 000), binary (bit 0 = 0) => 0xB0
Io::Out8(0xB0, PIT_COMMAND);
// Disable PIT channel 2 gate to prepare
uint8_t gate = Io::In8(PIT_GATE_PORT);
gate &= ~0x01; // Disable gate (bit 0 = 0)
gate &= ~0x02; // Disable speaker (bit 1 = 0)
Io::Out8(gate, PIT_GATE_PORT);
// Load the count value (low byte first, then high byte)
Io::Out8((uint8_t)(pitCount & 0xFF), PIT_CHANNEL2_DATA);
Io::IoPortWait();
Io::Out8((uint8_t)(pitCount >> 8), PIT_CHANNEL2_DATA);
// Set APIC timer to max count with divide-by-16, one-shot (masked so no IRQ)
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_DIVIDE, DIVIDE_BY_16);
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_LVT, LVT_MASKED);
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_INITIAL, 0xFFFFFFFF);
// Enable PIT channel 2 gate to start counting
gate = Io::In8(PIT_GATE_PORT);
gate |= 0x01; // Enable gate (bit 0 = 1)
Io::Out8(gate, PIT_GATE_PORT);
// Wait for PIT channel 2 output to go high (bit 5 of port 0x61)
while (!(Io::In8(PIT_GATE_PORT) & 0x20)) {
Arch::Pause();
}
// Read how many APIC timer ticks elapsed
uint32_t currentCount = Hal::LocalApic::ReadRegister(Hal::LocalApic::REG_TIMER_CURRENT);
uint32_t elapsed = 0xFFFFFFFF - currentCount;
// Stop the APIC timer
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_INITIAL, 0);
return elapsed;
}
bool InitializeBsp(uint32_t periodMs) {
KernelLogStream(INFO, "Timer") << "Calibrating APIC timer using PIT channel 2";
// Calibrate: measure APIC ticks over ~10ms
uint32_t ticksIn10ms = CalibratePit();
g_ticksPerMs = ticksIn10ms / 10;
if (g_ticksPerMs == 0) {
KernelLogStream(ERROR, "Timer") << "APIC timer calibration failed (0 ticks/ms)";
return false;
}
uint64_t timerFreqHz = (uint64_t)g_ticksPerMs * 1000;
KernelLogStream(OK, "Timer") << "APIC timer: " << base::dec << (uint64_t)g_ticksPerMs
<< " ticks/ms (" << timerFreqHz << " Hz, divide-by-16)";
// Periodic mode on vector IRQ_VECTOR_BASE + IRQ_TIMER
ProgramTimer(true, periodMs);
KernelLogStream(OK, "Timer") << "APIC timer started: initial count="
<< base::dec << (uint64_t)CountForIntervalMs(periodMs);
return true;
}
void StartPeriodic(uint32_t periodMs) {
ProgramTimer(true, periodMs);
}
void Stop() {
uint32_t lvt = Hal::LocalApic::ReadRegister(Hal::LocalApic::REG_TIMER_LVT);
if ((lvt & LVT_MASKED) == 0) {
Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_LVT,
lvt | LVT_MASKED);
}
}
};
File renamed without changes.
+70
View File
@@ -0,0 +1,70 @@
/*
* Context.cpp
* x86_64 thread context (implements Arch/Context.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Context.hpp>
#include <Arch/x86_64/MSR.hpp>
#include <Libraries/Memory.hpp>
// Context.asm: save callee-saved registers + RSP + FPU, switch CR3, restore.
extern "C" void SchedContextSwitch(uint64_t* oldRsp, uint64_t newRsp, uint64_t newCR3,
uint8_t* oldFpuArea, uint8_t* newFpuArea);
// SyscallEntry.asm: jump to user mode via IRETQ.
// `arg` is delivered as the user-mode RDI (SystemV first argument).
extern "C" [[noreturn]] void JumpToUserMode(uint64_t rip, uint64_t rsp, uint64_t arg);
namespace Arch {
static constexpr uint32_t IA32_FS_BASE = 0xC0000100;
void InitFpuState(uint8_t* area) {
memset(area, 0, FpuStateSize);
*(uint16_t*)&area[0] = 0x037F; // FCW: default x87 control word
*(uint32_t*)&area[24] = 0x1F80; // MXCSR: default SSE control/status
}
uint64_t PrepareThreadStack(uint64_t kernelStackTop, void (*entry)()) {
// The frame Context.asm pops: r15, r14, r13, r12, rbx, rbp, then the
// return address it RETs to.
uint64_t* sp = (uint64_t*)kernelStackTop;
*(--sp) = (uint64_t)entry; // return addr
*(--sp) = 0; // rbp
*(--sp) = 0; // rbx
*(--sp) = 0; // r12
*(--sp) = 0; // r13
*(--sp) = 0; // r14
*(--sp) = 0; // r15
return (uint64_t)sp;
}
void SwitchContext(uint64_t* oldSp, uint64_t newSp, uint64_t newRoot,
uint8_t* oldFpu, uint8_t* newFpu) {
SchedContextSwitch(oldSp, newSp, newRoot, oldFpu, newFpu);
}
void ActivateThread(Smp::CpuData* cpu, uint64_t kernelStackTop,
uint64_t threadPointer) {
// Kernel RSP for SYSCALL entry
cpu->entry.kernelRsp = kernelStackTop;
// TSS RSP0 for hardware interrupts from ring 3
cpu->arch.tss->rsp0 = kernelStackTop;
// The TLS thread pointer is IA32_FS_BASE. Cached per CPU: the MSR
// write is skipped when unchanged, so TLS-free processes (pointer 0)
// cost one write per CPU ever. Userspace cannot move FS base itself
// (CR4.FSGSBASE is off), so the cache cannot go stale.
if (cpu->arch.currentFsBase != threadPointer) {
cpu->arch.currentFsBase = threadPointer;
Hal::WriteMSR(IA32_FS_BASE, threadPointer);
}
}
void EnterUserMode(uint64_t entry, uint64_t userSp, uint64_t arg) {
JumpToUserMode(entry, userSp, arg);
}
}
+14
View File
@@ -0,0 +1,14 @@
/*
* Context.hpp
* x86_64 thread context constants (see Arch/Context.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Arch {
// FXSAVE image.
constexpr uint64_t FpuStateSize = 512;
constexpr uint64_t FpuStateAlign = 16;
}
+100
View File
@@ -0,0 +1,100 @@
/*
* Cpu.hpp
* x86_64 implementation of the Arch/Cpu.hpp primitives
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Arch {
// RFLAGS.IF
static constexpr uint64_t RflagsInterruptEnable = 1ULL << 9;
inline void Pause() {
asm volatile("pause" ::: "memory");
}
inline void StoreFence() {
asm volatile("sfence" ::: "memory");
}
inline void MemoryFence() {
asm volatile("mfence" ::: "memory");
}
inline void EnableInterrupts() {
asm volatile("sti" ::: "memory");
}
inline void DisableInterrupts() {
asm volatile("cli" ::: "memory");
}
inline bool InterruptsEnabled() {
uint64_t flags;
asm volatile("pushfq; pop %0" : "=r"(flags));
return (flags & RflagsInterruptEnable) != 0;
}
inline IrqFlags SaveAndDisableInterrupts() {
uint64_t flags;
asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory");
return flags;
}
inline void RestoreInterrupts(IrqFlags flags) {
asm volatile("push %0; popfq" :: "r"(flags) : "memory", "cc");
}
inline void Halt() {
asm volatile("hlt" ::: "memory");
}
// STI's one-instruction interrupt shadow makes STI;HLT atomic.
inline void HaltWithInterruptsDisabled() {
asm volatile("sti\n\thlt\n\tcli" ::: "memory");
}
[[noreturn]] inline void HaltForever() {
for (;;) {
asm volatile("cli; hlt" ::: "memory");
}
}
inline uint64_t ReadTimestamp() {
uint32_t lo, hi;
asm volatile("rdtsc" : "=a"(lo), "=d"(hi));
return ((uint64_t)hi << 32) | lo;
}
// A write to the POST diagnostic port 0x80 is an ISA-bus cycle of roughly
// a microsecond on every chipset since the 486, and never shorter.
inline void IoDelay() {
asm volatile("outb %%al, $0x80" :: "a"(0) : "memory");
}
inline void WritebackInvalidateCaches() {
asm volatile("wbinvd" ::: "memory");
}
inline void FlushTlbPage(uint64_t virtualAddress) {
asm volatile("invlpg (%0)" :: "r"(virtualAddress) : "memory");
}
inline void FlushTlbAll() {
asm volatile("mov %%cr3, %%rax; mov %%rax, %%cr3" ::: "rax", "memory");
}
inline uint64_t ReadPageTableRoot() {
uint64_t cr3;
asm volatile("mov %%cr3, %0" : "=r"(cr3));
return cr3;
}
inline void WritePageTableRoot(uint64_t rootPhys) {
asm volatile("mov %0, %%cr3" :: "r"(rootPhys) : "memory");
}
}
@@ -1,6 +1,6 @@
/*
* Cpu.hpp
* CPU feature enablement helpers
* CpuFeatures.hpp
* x86_64 CPU feature enablement and MWAIT idle helpers
* Copyright (c) 2025 Daniel Hammer
*/
@@ -32,14 +32,11 @@ namespace Hal {
asm volatile("mwait" :: "a"(hint), "c"(0));
}
// Atomic idle entry for paths that have already disabled interrupts.
// Keeping STI immediately adjacent to HLT/MWAIT avoids the classic race
// where an IRQ is handled between enabling interrupts and entering idle,
// after which the CPU sleeps until the next timer deadline.
inline void HaltWithInterruptsDisabled() {
asm volatile("sti\n\thlt\n\tcli" ::: "memory");
}
// Atomic idle entry for paths that have already disabled interrupts
// (the MWAIT counterpart of Arch::HaltWithInterruptsDisabled). Keeping
// STI immediately adjacent to MWAIT avoids the classic race where an IRQ
// is handled between enabling interrupts and entering idle, after which
// the CPU sleeps until the next timer deadline.
inline void IdleWaitWithInterruptsDisabled(volatile uint64_t* monitorAddr,
uint32_t hint = 0) {
asm volatile("monitor" :: "a"(monitorAddr), "c"(0), "d"(0) : "memory");
@@ -30,16 +30,19 @@
* Copyright (c) 2026 Daniel Hammer
*/
#include "CpuPower.hpp"
#include "MSR.hpp"
#include "SmpBoot.hpp"
#include <Arch/CpuPower.hpp>
#include <Arch/Smp.hpp>
#include <Arch/x86_64/MSR.hpp>
#include <Arch/x86_64/Acpi/CpuIdle.hpp>
#include <Terminal/Terminal.hpp>
#include <atomic>
using namespace Kt;
namespace Hal {
namespace Arch {
namespace CpuPower {
using Hal::ReadMSR;
using Hal::WriteMSR;
// ============================================================
// MSR addresses (power/thermal; used only in this module)
@@ -399,6 +402,7 @@ namespace Hal {
out.BaseMHz = g_baseMHz;
out.MaxMHz = g_maxMHz;
out.EffMHz = g_effMHz.load(std::memory_order_relaxed);
out.ApIdleHint = Hal::CpuIdle::GetApIdleHint();
return true;
}
File renamed without changes.
@@ -5,7 +5,7 @@
*/
#include "GDT.hpp"
#include "../Terminal/Terminal.hpp"
#include <Terminal/Terminal.hpp>
#include <Libraries/Memory.hpp>
namespace Hal {
File renamed without changes.
@@ -9,14 +9,14 @@
#include <Memory/Heap.hpp>
#include <Common/Panic.hpp>
#include <Common/TerminationCodes.hpp>
#include <Platform/Registers.hpp>
#include <Arch/x86_64/InterruptFrame.hpp>
#include <CppLib/Stream.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Sched/Scheduler.hpp>
#include <Sched/CrashReport.hpp>
#include <Api/Heap.hpp>
#include <Hal/SmpBoot.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Arch/Smp.hpp>
#include <Timekeeping/Timer.hpp>
namespace Hal {
constexpr auto InterruptGate = 0x8E;
@@ -59,6 +59,20 @@ namespace Hal {
"Reserved"
};
// Termination code for a user-mode CPU exception. The exact vector is
// kept in the crash report; this is only the category.
static TerminationCodes TerminationFromVector(uint8_t vector) {
switch (vector) {
case 0x06: return TerminatedIllegalInstruction; // #UD
case 0x01: // #DB
case 0x03: return TerminatedBreakpoint; // #BP
case 0x00: // #DE
case 0x10: // #MF
case 0x13: return TerminatedArithmeticError; // #XM
default: return TerminatedAccessViolation; // #PF, #GP, #SS, #NP, #AC, #OF, #BR, #CP, ...
}
}
// Exceptions that push a hardware error code before IP/CS/FLAGS/SP/SS
static bool ExceptionHasErrorCode(uint8_t vector) {
return vector == 8 || (vector >= 10 && vector <= 14)
@@ -67,16 +81,16 @@ namespace Hal {
// Extract CS from the interrupt frame. For error-code exceptions, the
// error code sits at offset 0, shifting IP to +8 and CS to +16.
static uint64_t GetExceptionCS(uint8_t vector, System::PanicFrame* frame) {
static uint64_t GetExceptionCS(uint8_t vector, Arch::PanicFrame* frame) {
if (ExceptionHasErrorCode(vector)) {
return *(uint64_t*)((uint8_t*)frame + 16);
}
return frame->CS;
}
static System::PanicFrame* GetExceptionRegs(uint8_t vector, System::PanicFrame* frame) {
static Arch::PanicFrame* GetExceptionRegs(uint8_t vector, Arch::PanicFrame* frame) {
if (ExceptionHasErrorCode(vector)) {
return (System::PanicFrame*)((uint8_t*)frame + sizeof(uint64_t));
return (Arch::PanicFrame*)((uint8_t*)frame + sizeof(uint64_t));
}
return frame;
}
@@ -84,7 +98,7 @@ namespace Hal {
// Shared fatal-exception path: kill the faulting user process (with a
// crash report) or panic the kernel. `frame` is the RAW interrupt frame
// (error code at offset 0 for vectors that push one). Never returns.
static void HandleFatalException(uint8_t i, System::PanicFrame* frame)
static void HandleFatalException(uint8_t i, Arch::PanicFrame* frame)
{
uint64_t cs = GetExceptionCS(i, frame);
bool fromUser = (cs & 3) == 3;
@@ -136,7 +150,7 @@ namespace Hal {
constexpr uint64_t snapshotBytes =
CrashReport::StackSnapshotWords * sizeof(uint64_t);
if (Memory::VMM::Paging::IsUserRangeAccessible(
proc->pml4Phys, regs->SP, snapshotBytes, false)) {
proc->pageTableRoot, regs->SP, snapshotBytes, false)) {
auto* userStack = (const volatile uint64_t*)regs->SP;
for (int word = 0; word < CrashReport::StackSnapshotWords; word++)
rep.stackWords[word] = userStack[word];
@@ -146,7 +160,7 @@ namespace Hal {
if (i == 0x0E) {
// Page fault: read CR2 for faulting address
asm volatile("mov %%cr2, %0" : "=r"(rep.faultingAddress));
auto* pf = (System::PageFaultPanicFrame*)frame;
auto* pf = (Arch::PageFaultPanicFrame*)frame;
rep.pfPresent = pf->PageFaultError.Present;
rep.pfWrite = pf->PageFaultError.Write;
rep.pfUser = pf->PageFaultError.User;
@@ -201,7 +215,7 @@ namespace Hal {
}
template<size_t i>
__attribute__((interrupt)) void ExceptionHandler(System::PanicFrame* frame)
__attribute__((interrupt)) void ExceptionHandler(Arch::PanicFrame* frame)
{
HandleFatalException(i, frame);
}
@@ -212,7 +226,7 @@ namespace Hal {
// user stack growth region maps a fresh zeroed page and retries the
// faulting instruction. With the two-argument form, `frame` points past
// the error code, directly at the saved IP.
__attribute__((interrupt)) void PageFaultHandler(System::PanicFrame* frame, uint64_t errorCode)
__attribute__((interrupt)) void PageFaultHandler(Arch::PanicFrame* frame, uint64_t errorCode)
{
bool fromUser = (frame->CS & 3) == 3;
if (fromUser) asm volatile("swapgs");
@@ -226,7 +240,10 @@ namespace Hal {
// user stack buffers passed into syscalls.
if ((errorCode & 1) == 0 && cpu != nullptr && cpu->currentSlot >= 0
&& Sched::GetCurrentPid() >= 0) {
if (montauk::abi::TryHandleAnonymousPageFault(cr2, errorCode) ||
// Error code bit 1 = write, bit 4 = instruction fetch.
Memory::VMM::PageFault fault{false, (errorCode & (1ULL << 1)) != 0,
(errorCode & (1ULL << 4)) != 0};
if (montauk::abi::TryHandleAnonymousPageFault(cr2, fault) ||
Sched::TryGrowUserStack(cr2)) {
if (fromUser) asm volatile("swapgs");
return;
@@ -236,7 +253,7 @@ namespace Hal {
// Not a growable fault. Hand the RAW frame (error code at offset 0)
// to the fatal path, which re-derives fromUser and swaps GS itself.
if (fromUser) asm volatile("swapgs");
HandleFatalException(0x0E, (System::PanicFrame*)((uint8_t*)frame - 8));
HandleFatalException(0x0E, (Arch::PanicFrame*)((uint8_t*)frame - 8));
}
void LoadIDT(IDTRStruct& idtr) {
File renamed without changes.
@@ -1,10 +1,15 @@
/*
* InterruptFrame.hpp
* x86_64 exception frames as pushed by the CPU
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include <cstdint>
using namespace std;
#if defined (__x86_64__)
namespace System {
namespace Arch {
struct PanicFrame {
uint64_t IP;
uint64_t CS;
@@ -51,4 +56,3 @@ namespace System {
}__attribute__((packed));
};
#endif
+49
View File
@@ -0,0 +1,49 @@
/*
* IoPort.hpp
* x86_64 port I/O (implements Io/IoPort.hpp)
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Io {
static constexpr bool HasPortIo = true;
// OSDev website has the parameters for out(b/w/l) in the opposite order in their example,
// (port, then value), but the manpage on GNU ('man outb') has it in this order.
inline void Out8(uint8_t value, uint16_t port) {
asm ("outb %0, %1" : : "a"(value), "Nd"(port) : "memory");
}
inline void Out16(uint16_t value, uint16_t port) {
asm ("outw %0, %1" : : "a"(value), "Nd"(port) : "memory");
}
inline void Out32(uint32_t value, uint16_t port) {
asm ("outl %0, %1" : : "a"(value), "Nd"(port) : "memory");
}
inline uint8_t In8(uint16_t port) {
uint8_t result;
asm volatile("inb %1, %0" : "=a"(result) : "Nd"(port) : "memory");
return result;
}
inline uint16_t In16(uint16_t port) {
uint16_t result;
asm volatile("inw %1, %0" : "=a"(result) : "Nd"(port) : "memory");
return result;
}
inline uint32_t In32(uint16_t port) {
uint32_t result;
asm volatile("inl %1, %0" : "=a"(result) : "Nd"(port) : "memory");
return result;
}
inline void IoPortWait() {
Out8(0, 0x80);
}
};
File renamed without changes.
+87
View File
@@ -0,0 +1,87 @@
/*
* PageTable.hpp
* x86_64 4-level page-table structures
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <stddef.h>
namespace Memory::VMM {
struct PageTableEntry {
std::uint8_t Present : 1;
std::uint8_t Writable : 1;
std::uint8_t Supervisor : 1;
std::uint8_t WriteThrough : 1;
std::uint8_t CacheDisabled : 1;
std::uint8_t Accessed : 1;
std::uint8_t Ignore : 1;
std::uint8_t LargerPages : 1;
std::uint8_t PageSize : 1;
std::uint8_t Available : 3;
std::uint64_t Address : 52;
};
struct PageTableEntry40Bit {
std::uint8_t Present : 1;
std::uint8_t Writable : 1;
std::uint8_t Supervisor : 1;
std::uint8_t WriteThrough : 1;
std::uint8_t CacheDisabled : 1;
std::uint8_t Accessed : 1;
std::uint8_t Ignore : 1;
std::uint8_t LargerPages : 1;
std::uint8_t PageSize : 1;
std::uint8_t Available : 3;
std::uint64_t Address : 40;
std::uint8_t AvailableHigh : 7;
std::uint8_t PK : 4;
std::uint8_t NX : 1;
};
struct PageTable {
PageTableEntry entries[512];
} __attribute__((packed)) __attribute__((aligned(0x1000)));
struct VirtualAddress {
std::uint64_t address;
// Page-table index extraction works for any byte address. Callers that
// truly require page alignment validate that explicitly before mapping.
VirtualAddress(std::uint64_t newAddress) : address(newAddress) {}
uint64_t GetL4Index() {
return (address >> 39) & 0x1ff;
}
uint64_t GetL3Index() {
return (address >> 30) & 0x1ff;
}
uint64_t GetL2Index() {
return (address >> 21) & 0x1ff;
}
uint64_t GetPageIndex() {
return (address >> 12) & 0x1ff;
}
uint64_t GetIndex(size_t level) {
if (level == 4)
return GetL4Index();
else if (level == 3)
return GetL3Index();
else if (level == 2)
return GetL2Index();
else if (level == 1)
return GetPageIndex();
return 0;
}
};
};
@@ -1,4 +1,12 @@
#include "Paging.hpp"
/*
* Paging.cpp
* x86_64 4-level paging (implements Memory/Paging.hpp)
* Copyright (c) 2025 Daniel Hammer
*/
#include <Memory/Paging.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/x86_64/PageTable.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Common/Panic.hpp>
#include <Memory/HHDM.hpp>
@@ -51,12 +59,22 @@ namespace Memory::VMM {
extern "C" uint64_t KernelStartSymbol;
static std::uint64_t GetPhysAddrImpl(std::uint64_t pml4, std::uint64_t virtualAddress, bool use40BitL1);
std::uint64_t GetPhysKernelAddress(std::uint64_t virtualAddress) {
return Paging::GetPhysAddr(GetCR3(), (std::uint64_t)virtualAddress, true);
return GetPhysAddrImpl(Arch::ReadPageTableRoot(), (std::uint64_t)virtualAddress, true);
}
void ActivateKernelAddressSpace() {
Arch::WritePageTableRoot(g_paging->KernelRoot());
}
void FlushTLB() {
Arch::FlushTlbAll();
}
Paging::Paging() {
PML4 = (PageTable*)SubHHDM((PageTable*)Memory::g_pfa->AllocateZeroed());
rootPhys = SubHHDM(Memory::g_pfa->AllocateZeroed());
}
void Paging::Init(std::uint64_t kernelBaseVirt, std::uint64_t kernelSize, const montauk::boot::MemoryMap& memMap,
@@ -120,11 +138,11 @@ namespace Memory::VMM {
}
}
LoadCR3(PML4);
Arch::WritePageTableRoot(rootPhys);
Kt::KernelLogStream(Kt::OK, "VMM") << "Switched CR3";
}
PageTable* Paging::HandleLevel(VirtualAddress virtualAddress, PageTable* table, const size_t level) {
static PageTable* HandleLevel(VirtualAddress virtualAddress, PageTable* table, const size_t level) {
PageTableEntry* entry = (PageTableEntry*)Memory::HHDM(&table->entries[virtualAddress.GetIndex(level)]);
if (!entry->Present) {
@@ -144,7 +162,7 @@ namespace Memory::VMM {
}
}
PageTable* Paging::HandleLevelUser(VirtualAddress virtualAddress, PageTable* table, const size_t level) {
static PageTable* HandleLevelUser(VirtualAddress virtualAddress, PageTable* table, const size_t level) {
PageTableEntry* entry = (PageTableEntry*)Memory::HHDM(&table->entries[virtualAddress.GetIndex(level)]);
if (!entry->Present) {
@@ -174,7 +192,7 @@ namespace Memory::VMM {
VirtualAddress virtualAddressObj(virtualAddress);
auto PML3 = HandleLevel(virtualAddressObj, PML4, 4);
auto PML3 = HandleLevel(virtualAddressObj, (PageTable*)rootPhys, 4);
auto PML2 = HandleLevel(virtualAddressObj, PML3, 3);
auto PML1 = HandleLevel(virtualAddressObj, PML2, 2);
@@ -190,7 +208,7 @@ namespace Memory::VMM {
VirtualAddress virtualAddressObj(virtualAddress);
auto PML3 = HandleLevel(virtualAddressObj, PML4, 4);
auto PML3 = HandleLevel(virtualAddressObj, (PageTable*)rootPhys, 4);
auto PML2 = HandleLevel(virtualAddressObj, PML3, 3);
auto PML1 = HandleLevel(virtualAddressObj, PML2, 2);
@@ -206,7 +224,7 @@ namespace Memory::VMM {
VirtualAddress virtualAddressObj(virtualAddress);
auto PML3 = HandleLevel(virtualAddressObj, PML4, 4);
auto PML3 = HandleLevel(virtualAddressObj, (PageTable*)rootPhys, 4);
auto PML2 = HandleLevel(virtualAddressObj, PML3, 3);
auto PML1 = HandleLevel(virtualAddressObj, PML2, 2);
@@ -216,7 +234,7 @@ namespace Memory::VMM {
// The page may have been mapped before with other cache attributes
// (an HHDM page remapped uncached): drop any stale translation.
asm volatile("invlpg (%0)" :: "r"(virtualAddress) : "memory");
Arch::FlushTlbPage(virtualAddress);
}
void Paging::MapUser(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
@@ -226,7 +244,7 @@ namespace Memory::VMM {
VirtualAddress virtualAddressObj(virtualAddress);
auto PML3 = HandleLevelUser(virtualAddressObj, PML4, 4);
auto PML3 = HandleLevelUser(virtualAddressObj, (PageTable*)rootPhys, 4);
auto PML2 = HandleLevelUser(virtualAddressObj, PML3, 3);
auto PML1 = HandleLevelUser(virtualAddressObj, PML2, 2);
@@ -235,14 +253,14 @@ namespace Memory::VMM {
SetLeafPte(pageEntry, physicalAddress, true, false, false);
}
std::uint64_t Paging::CreateUserPML4() {
std::uint64_t Paging::CreateUserAddressSpace() {
// Allocate a new PML4
void* newPage = Memory::g_pfa->AllocateZeroed();
uint64_t newPml4Phys = Memory::SubHHDM((uint64_t)newPage);
PageTable* newPml4 = (PageTable*)newPage; // HHDM virtual address
// Copy kernel-half entries (256-511) from the global PML4
PageTable* kernelPml4 = (PageTable*)Memory::HHDM((uint64_t)g_paging->PML4);
PageTable* kernelPml4 = (PageTable*)Memory::HHDM(g_paging->KernelRoot());
for (int i = 256; i < 512; i++) {
newPml4->entries[i] = kernelPml4->entries[i];
}
@@ -250,11 +268,11 @@ namespace Memory::VMM {
return newPml4Phys;
}
bool Paging::MapUserIn(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
return MapUserInPermissions(pml4Phys, physicalAddress, virtualAddress, true, true);
bool Paging::MapUserIn(std::uint64_t pageTableRoot, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
return MapUserInPermissions(pageTableRoot, physicalAddress, virtualAddress, true, true);
}
bool Paging::MapUserInPermissions(std::uint64_t pml4Phys, std::uint64_t physicalAddress,
bool Paging::MapUserInPermissions(std::uint64_t pageTableRoot, std::uint64_t physicalAddress,
std::uint64_t virtualAddress, bool writable,
bool executable) {
pagingLock.Acquire();
@@ -284,7 +302,7 @@ namespace Memory::VMM {
}
};
PageTable* pml4 = (PageTable*)pml4Phys;
PageTable* pml4 = (PageTable*)pageTableRoot;
auto pml3 = walkLevel(pml4, va.GetL4Index());
if (!pml3) { pagingLock.Release(); return false; }
auto pml2 = walkLevel(pml3, va.GetL3Index());
@@ -298,7 +316,7 @@ namespace Memory::VMM {
return true;
}
bool Paging::ProtectUserIn(std::uint64_t pml4Phys, std::uint64_t virtualAddress,
bool Paging::ProtectUserIn(std::uint64_t pageTableRoot, std::uint64_t virtualAddress,
bool writable, bool executable) {
if ((virtualAddress & 0xFFFULL) != 0) return false;
pagingLock.Acquire();
@@ -310,7 +328,7 @@ namespace Memory::VMM {
return (PageTable*)(entry->Address << 12);
};
PageTable* pml4 = (PageTable*)pml4Phys;
PageTable* pml4 = (PageTable*)pageTableRoot;
auto pml3 = walkRead(pml4, va.GetL4Index());
if (!pml3) { pagingLock.Release(); return false; }
auto pml2 = walkRead(pml3, va.GetL3Index());
@@ -329,12 +347,12 @@ namespace Memory::VMM {
if (executable) raw &= ~LeafNoExecute;
else raw |= LeafNoExecute;
*(volatile uint64_t*)pte = raw;
asm volatile("invlpg (%0)" :: "r"(virtualAddress) : "memory");
Arch::FlushTlbPage(virtualAddress);
pagingLock.Release();
return true;
}
bool Paging::MapUserInWC(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
bool Paging::MapUserInWC(std::uint64_t pageTableRoot, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
pagingLock.Acquire();
if (virtualAddress % 0x1000 != 0 || physicalAddress % 0x1000 != 0) {
pagingLock.Release();
@@ -360,7 +378,7 @@ namespace Memory::VMM {
}
};
PageTable* pml4 = (PageTable*)pml4Phys;
PageTable* pml4 = (PageTable*)pageTableRoot;
auto pml3 = walkLevel(pml4, va.GetL4Index());
if (!pml3) { pagingLock.Release(); return false; }
auto pml2 = walkLevel(pml3, va.GetL3Index());
@@ -374,7 +392,7 @@ namespace Memory::VMM {
return true;
}
void Paging::UnmapUserIn(std::uint64_t pml4Phys, std::uint64_t virtualAddress) {
void Paging::UnmapUserIn(std::uint64_t pageTableRoot, std::uint64_t virtualAddress) {
pagingLock.Acquire();
VirtualAddress va(virtualAddress);
@@ -385,7 +403,7 @@ namespace Memory::VMM {
return (PageTable*)(entry->Address << 12);
};
PageTable* pml4 = (PageTable*)pml4Phys;
PageTable* pml4 = (PageTable*)pageTableRoot;
auto pml3 = walkRead(pml4, va.GetL4Index());
if (!pml3) { pagingLock.Release(); return; }
auto pml2 = walkRead(pml3, va.GetL3Index());
@@ -400,14 +418,14 @@ namespace Memory::VMM {
*(uint64_t*)pageEntry = 0;
// Invalidate TLB for this virtual address
asm volatile("invlpg (%0)" :: "r"(virtualAddress) : "memory");
Arch::FlushTlbPage(virtualAddress);
pagingLock.Release();
}
void Paging::FreeUserHalf(std::uint64_t pml4Phys) {
void Paging::FreeUserHalf(std::uint64_t pageTableRoot) {
// PageTable* pointers store PHYSICAL addresses (same convention as MapUserIn/UnmapUserIn).
// Each individual entry access goes through HHDM() to get a valid virtual pointer.
PageTable* pml4 = (PageTable*)pml4Phys;
PageTable* pml4 = (PageTable*)pageTableRoot;
// Walk user-half entries (0-255); kernel-half (256-511) is shared and must not be touched.
for (int i4 = 0; i4 < 256; i4++) {
@@ -459,7 +477,7 @@ namespace Memory::VMM {
}
}
bool Paging::IsUserRangeAccessible(std::uint64_t pml4Phys, std::uint64_t virtualAddress,
bool Paging::IsUserRangeAccessible(std::uint64_t pageTableRoot, std::uint64_t virtualAddress,
std::uint64_t size, bool requireWrite) {
static constexpr uint64_t USER_SPACE_END = 0x0000800000000000ULL;
@@ -470,7 +488,7 @@ namespace Memory::VMM {
auto pageAccessible = [&](uint64_t pageVa) -> bool {
VirtualAddress va(pageVa);
PageTable* pml4 = (PageTable*)pml4Phys;
PageTable* pml4 = (PageTable*)pageTableRoot;
PageTableEntry* pml4e = (PageTableEntry*)Memory::HHDM(&pml4->entries[va.GetL4Index()]);
if (!pml4e->Present || !pml4e->Supervisor) return false;
@@ -506,7 +524,13 @@ namespace Memory::VMM {
return true;
}
std::uint64_t Paging::GetPhysAddr(std::uint64_t pml4, std::uint64_t virtualAddress, bool use40BitL1) {
std::uint64_t Paging::GetPhysAddr(std::uint64_t root, std::uint64_t virtualAddress) {
return GetPhysAddrImpl(root, virtualAddress, false);
}
// use40BitL1 reads the leaf through the narrower historical layout; the
// boot-time kernel-image walk over the bootloader's tables relies on it.
static std::uint64_t GetPhysAddrImpl(std::uint64_t pml4, std::uint64_t virtualAddress, bool use40BitL1) {
VirtualAddress virtualAddressObj(virtualAddress);
PageTable* pml4Virt = (PageTable*)HHDM(pml4);
@@ -548,7 +572,7 @@ namespace Memory::VMM {
}
std::uint64_t Paging::GetPhysAddr(std::uint64_t virtualAddress) {
return GetPhysAddr((std::uint64_t)PML4, virtualAddress, false);
return GetPhysAddrImpl(rootPhys, virtualAddress, false);
}
void Paging::MapEfiRuntime(const montauk::boot::EfiInfo& efi) {
+74
View File
@@ -0,0 +1,74 @@
/*
* PanicFrame.cpp
* x86_64 CPU state for the panic screen
* Copyright (c) 2025 Daniel Hammer
*/
#include <Common/Panic.hpp>
#include <Arch/x86_64/InterruptFrame.hpp>
#include <CppLib/BoxUI.hpp>
static constexpr int BoxWidth = PanicBoxWidth;
static void PrintPageFaultInfo(Arch::PanicFrame*& frame) {
auto* pf = (Arch::PageFaultPanicFrame*)frame;
frame = (Arch::PanicFrame*)&pf->IP;
uint64_t cr2;
asm volatile("mov %%cr2, %0" : "=r"(cr2));
PrintBoxedHex(kerr, "Faulting Address (CR2)", cr2, BoxWidth);
PrintBoxedLine(kerr, "Page Fault Error:", BoxWidth, true);
PrintBoxedDec(kerr, "Present", pf->PageFaultError.Present, BoxWidth);
PrintBoxedDec(kerr, "Write", pf->PageFaultError.Write, BoxWidth);
PrintBoxedDec(kerr, "User", pf->PageFaultError.User, BoxWidth);
PrintBoxedDec(kerr, "Reserved Write", pf->PageFaultError.ReservedWrite, BoxWidth);
PrintBoxedDec(kerr, "Instruction Fetch",pf->PageFaultError.InstructionFetch, BoxWidth);
PrintBoxedDec(kerr, "Protection Key", pf->PageFaultError.ProtectionKey, BoxWidth);
PrintBoxedDec(kerr, "Shadow Stack", pf->PageFaultError.ShadowStack, BoxWidth);
PrintBoxedDec(kerr, "SGX", pf->PageFaultError.SGX, BoxWidth);
}
static void PrintGPFInfo(Arch::PanicFrame*& frame) {
auto* gpf = (Arch::GPFPanicFrame*)frame;
frame = (Arch::PanicFrame*)&gpf->IP;
PrintBoxedLine(kerr, "General Protection Fault:", BoxWidth, true);
PrintBoxedDec(kerr, "Error Code", gpf->GeneralProtectionFaultError, BoxWidth);
}
// Link-time base of the kernel image (see linker-x86_64.ld).
static constexpr uint64_t KernelBaseVirt = 0xFFFFFFFF80000000ULL;
static void PrintRegisters(Arch::PanicFrame* frame) {
PrintBoxedSeparator(kerr, BoxWidth);
PrintBoxedLine(kerr, "Registers:", BoxWidth, true);
PrintBoxedHex(kerr, "Instruction Pointer", frame->IP, BoxWidth);
// The absolute address means nothing without the link map. Its offset from
// the kernel's load base is what `nm bin-x86_64/kernel` can be searched
// for directly, so print that next to it.
if (frame->IP >= KernelBaseVirt)
PrintBoxedHex(kerr, " kernel+", frame->IP - KernelBaseVirt, BoxWidth);
PrintBoxedHex(kerr, "Code Segment", frame->CS, BoxWidth);
PrintBoxedHex(kerr, "Flags", frame->Flags, BoxWidth);
PrintBoxedHex(kerr, "Stack Pointer", frame->SP, BoxWidth);
PrintBoxedHex(kerr, "Stack Segment", frame->SS, BoxWidth);
}
namespace Arch {
void PrintPanicFrame(PanicFrame* frame) {
PrintBoxedSeparator(kerr, BoxWidth);
PrintBoxedLine(kerr, "CPU State:", BoxWidth, true);
PrintBoxedHex(kerr, "Interrupt Vector", frame->InterruptVector, BoxWidth);
if (frame->InterruptVector == 0xE)
PrintPageFaultInfo(frame);
else if (frame->InterruptVector == 0xD)
PrintGPFInfo(frame);
PrintRegisters(frame);
}
}
+57
View File
@@ -0,0 +1,57 @@
/*
* PerCpu.hpp
* x86_64 per-CPU state (see Arch/Smp.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Arch/x86_64/GDT.hpp>
// ====================================================================
// Assembly-visible offsets into Smp::CpuData
// These MUST match the struct layout. Verified by static_assert in
// SmpBoot.cpp; SyscallEntry.asm repeats them.
// ====================================================================
#define CPUDATA_SELF_PTR 0
#define CPUDATA_KERNEL_RSP 8
#define CPUDATA_USER_RSP 16
namespace Arch {
// Read by SyscallEntry.asm through GS.
struct CpuEntryState {
uint64_t kernelRsp; // kernel stack top for SYSCALL
uint64_t userRspScratch; // scratch for saving user RSP
};
struct CpuState {
uint32_t lapicId; // local APIC ID
bool hasMwait; // CPU supports MONITOR/MWAIT
uint64_t currentFsBase = ~0ULL; // last IA32_FS_BASE written on this CPU
Hal::TSS64* tss; // this CPU's TSS (BSP: the global one)
// Per-CPU GDT and TSS (APs use these; BSP uses globals)
Hal::BasicGDT cpuGdt __attribute__((aligned(16)));
Hal::TSS64 cpuTss __attribute__((aligned(16)));
};
// GS base holds the CpuData pointer in kernel mode; gs:0 is its selfPtr.
inline void* CurrentCpuLocal() {
void* ptr;
asm volatile("mov %%gs:0, %0" : "=r"(ptr));
return ptr;
}
// Read IA32_GS_BASE directly rather than gs:0, which would fault before
// InitBsp() has installed a GS base and turn the original, diagnosable
// exception into a triple fault.
inline void* TryCurrentCpuLocal() {
uint32_t low;
uint32_t high;
asm volatile("rdmsr" : "=a"(low), "=d"(high) : "c"(0xC0000101u));
return (void*)(((uint64_t)high << 32) | low);
}
}
+80
View File
@@ -0,0 +1,80 @@
/*
* Platform.cpp
* x86_64 bring-up hooks (implements Arch/Platform.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Platform.hpp>
#include <Arch/CpuPower.hpp>
#include <Arch/Smp.hpp>
#include <Arch/x86_64/GDT.hpp>
#include <Arch/x86_64/IDT.hpp>
#include <Arch/x86_64/MSR.hpp>
#include <Arch/x86_64/CpuFeatures.hpp>
#include <Arch/x86_64/Apic/ApicInit.hpp>
#include <Arch/x86_64/Acpi/CpuIdle.hpp>
#include <Drivers/PS2/PS2Controller.hpp>
#include <Drivers/PS2/Keyboard.hpp>
#include <Drivers/PS2/Mouse.hpp>
namespace Arch {
void EarlyInitialize() {
Hal::PrepareGDT();
Hal::BridgeLoadGDT();
Hal::EnableSSE();
}
void InitializeTraps() {
Hal::IDTInitialize();
}
void InitializeMemoryAttributes() {
Hal::InitializePAT();
}
void InitializeInterrupts(Hal::ACPI::CommonSDTHeader* xsdt) {
Hal::CpuIdle::Initialize(xsdt);
Hal::ApicInitialize(xsdt);
Smp::InitBsp();
CpuPower::InitializeBsp();
}
void InitializePlatformDevices() {
Drivers::PS2::Initialize();
Drivers::PS2::Keyboard::Initialize();
Drivers::PS2::Mouse::Initialize();
}
void PrepareUserMode() {
Hal::LoadTSS();
}
void LateInitialize() {
Drivers::PS2::Mouse::FlushState();
}
void ListPlatformDevices(PlatformDeviceSink sink, void* context) {
sink(context, 1, "Local APIC", "");
sink(context, 1, "I/O APIC", "");
sink(context, 2, "LAPIC Timer", "Local APIC periodic timer");
sink(context, 3, "PS/2 Keyboard", "IRQ 1");
if (Drivers::PS2::IsDualChannel()) {
sink(context, 3, "PS/2 Mouse", "IRQ 12");
}
}
void Reboot() {
// Triple fault: load an empty IDT and raise an exception.
struct [[gnu::packed]] { uint16_t limit; uint64_t base; } nullIdt = {0, 0};
asm volatile("lidt %0; int $0x03" :: "m"(nullIdt));
__builtin_unreachable();
}
}
@@ -4,30 +4,30 @@
* Copyright (c) 2026 Daniel Hammer
*/
#include "SmpBoot.hpp"
#include <ACPI/CpuIdle.hpp>
#include <Hal/Apic/Apic.hpp>
#include <Hal/Apic/Interrupts.hpp>
#include <Hal/IDT.hpp>
#include <Hal/MSR.hpp>
#include <Hal/Cpu.hpp>
#include <Hal/CpuPower.hpp>
#include <Arch/Smp.hpp>
#include <Arch/x86_64/Acpi/CpuIdle.hpp>
#include <Arch/x86_64/Apic/Apic.hpp>
#include <Arch/x86_64/Apic/ApicInit.hpp>
#include <Arch/x86_64/Apic/Interrupts.hpp>
#include <Arch/x86_64/IDT.hpp>
#include <Arch/x86_64/MSR.hpp>
#include <Arch/x86_64/CpuFeatures.hpp>
#include <Arch/CpuPower.hpp>
#include <Memory/Paging.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/Syscall.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Memory/HHDM.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Timekeeping/Timer.hpp>
#include <Boot/BootInfo.hpp>
#include <Libraries/Memory.hpp>
// Verify assembly offsets match struct layout
static_assert(__builtin_offsetof(Smp::CpuData, selfPtr) == CPUDATA_SELF_PTR, "CpuData offset mismatch: selfPtr");
static_assert(__builtin_offsetof(Smp::CpuData, kernelRsp) == CPUDATA_KERNEL_RSP, "CpuData offset mismatch: kernelRsp");
static_assert(__builtin_offsetof(Smp::CpuData, userRspScratch)== CPUDATA_USER_RSP, "CpuData offset mismatch: userRspScratch");
static_assert(__builtin_offsetof(Smp::CpuData, currentSlot) == CPUDATA_CURRENT_SLOT, "CpuData offset mismatch: currentSlot");
extern "C" void SyscallEntry();
static_assert(__builtin_offsetof(Smp::CpuData, entry.kernelRsp) == CPUDATA_KERNEL_RSP, "CpuData offset mismatch: kernelRsp");
static_assert(__builtin_offsetof(Smp::CpuData, entry.userRspScratch) == CPUDATA_USER_RSP, "CpuData offset mismatch: userRspScratch");
// Assembly helpers (GDT.asm)
extern "C" void LoadGDT(Hal::GDTPointer* ptr);
@@ -50,25 +50,29 @@ namespace Smp {
return g_cpuCount;
}
int GetDetectedCpuCount() {
return Hal::GetDetectedCpuCount();
}
// ====================================================================
// Per-CPU GDT/TSS setup
// ====================================================================
static void SetupPerCpuGdtTss(CpuData& cpu) {
// Zero the TSS
memset(&cpu.cpuTss, 0, sizeof(Hal::TSS64));
cpu.cpuTss.iopbOffset = sizeof(Hal::TSS64);
memset(&cpu.arch.cpuTss, 0, sizeof(Hal::TSS64));
cpu.arch.cpuTss.iopbOffset = sizeof(Hal::TSS64);
// Allocate a 4KB IST1 stack for Double Fault and NMI.
// These exceptions need a known-good stack to avoid triple
// faults when the normal kernel stack overflows.
void* istPage = Memory::g_pfa->AllocateZeroed();
if (istPage) {
cpu.cpuTss.ist1 = (uint64_t)istPage + 0x1000; // top of stack
cpu.arch.cpuTss.ist1 = (uint64_t)istPage + 0x1000; // top of stack
}
// Copy the standard GDT layout
cpu.cpuGdt = {
cpu.arch.cpuGdt = {
{0xFFFF, 0, 0, 0x00, 0x00, 0}, // 0x00 Null
{0xFFFF, 0, 0, 0x9A, 0xA0, 0}, // 0x08 KernelCode
{0xFFFF, 0, 0, 0x92, 0xA0, 0}, // 0x10 KernelData
@@ -79,27 +83,27 @@ namespace Smp {
};
// Encode the 16-byte TSS descriptor
uint64_t base = (uint64_t)&cpu.cpuTss;
uint64_t base = (uint64_t)&cpu.arch.cpuTss;
uint32_t limit = sizeof(Hal::TSS64) - 1;
// Low 8 bytes
cpu.cpuGdt.TSS.LimitLow = limit & 0xFFFF;
cpu.cpuGdt.TSS.BaseLow = base & 0xFFFF;
cpu.cpuGdt.TSS.BaseMiddle = (base >> 16) & 0xFF;
cpu.cpuGdt.TSS.AccessByte = 0x89; // Present, 64-bit TSS Available
cpu.cpuGdt.TSS.GranularityByte = (limit >> 16) & 0x0F;
cpu.cpuGdt.TSS.BaseHigh = (base >> 24) & 0xFF;
cpu.arch.cpuGdt.TSS.LimitLow = limit & 0xFFFF;
cpu.arch.cpuGdt.TSS.BaseLow = base & 0xFFFF;
cpu.arch.cpuGdt.TSS.BaseMiddle = (base >> 16) & 0xFF;
cpu.arch.cpuGdt.TSS.AccessByte = 0x89; // Present, 64-bit TSS Available
cpu.arch.cpuGdt.TSS.GranularityByte = (limit >> 16) & 0x0F;
cpu.arch.cpuGdt.TSS.BaseHigh = (base >> 24) & 0xFF;
// High 8 bytes (base[63:32])
uint32_t baseUpper = (uint32_t)(base >> 32);
cpu.cpuGdt.TSSHigh.LimitLow = baseUpper & 0xFFFF;
cpu.cpuGdt.TSSHigh.BaseLow = (baseUpper >> 16) & 0xFFFF;
cpu.cpuGdt.TSSHigh.BaseMiddle = 0;
cpu.cpuGdt.TSSHigh.AccessByte = 0;
cpu.cpuGdt.TSSHigh.GranularityByte = 0;
cpu.cpuGdt.TSSHigh.BaseHigh = 0;
cpu.arch.cpuGdt.TSSHigh.LimitLow = baseUpper & 0xFFFF;
cpu.arch.cpuGdt.TSSHigh.BaseLow = (baseUpper >> 16) & 0xFFFF;
cpu.arch.cpuGdt.TSSHigh.BaseMiddle = 0;
cpu.arch.cpuGdt.TSSHigh.AccessByte = 0;
cpu.arch.cpuGdt.TSSHigh.GranularityByte = 0;
cpu.arch.cpuGdt.TSSHigh.BaseHigh = 0;
cpu.tss = &cpu.cpuTss;
cpu.arch.tss = &cpu.arch.cpuTss;
}
// ====================================================================
@@ -126,13 +130,13 @@ namespace Smp {
CpuData& bsp = g_cpus[0];
bsp.selfPtr = (uint64_t)&bsp;
bsp.cpuIndex = 0;
bsp.lapicId = Hal::LocalApic::GetId();
bsp.arch.lapicId = Hal::LocalApic::GetId();
bsp.currentSlot = -1;
bsp.started = true;
bsp.hasMwait = Hal::HasMwait();
bsp.arch.hasMwait = Hal::HasMwait();
// BSP uses the global TSS (already set up in PrepareGDT)
bsp.tss = &Hal::g_tss;
bsp.arch.tss = &Hal::g_tss;
// Allocate IST1 stack for the BSP (for Double Fault / NMI)
void* bspIstPage = Memory::g_pfa->AllocateZeroed();
@@ -145,7 +149,7 @@ namespace Smp {
g_cpuCount = 1;
KernelLogStream(OK, "SMP") << "BSP initialized (LAPIC ID " << (uint64_t)bsp.lapicId << ")";
KernelLogStream(OK, "SMP") << "BSP initialized (LAPIC ID " << (uint64_t)bsp.arch.lapicId << ")";
}
// ====================================================================
@@ -163,7 +167,7 @@ namespace Smp {
// --- Load per-CPU GDT ---
Hal::GDTPointer gdtPtr {
.Size = sizeof(Hal::BasicGDT) - 1,
.GDTAddress = (uint64_t)&cpu->cpuGdt,
.GDTAddress = (uint64_t)&cpu->arch.cpuGdt,
};
LoadGDT(&gdtPtr);
ReloadSegments();
@@ -175,7 +179,7 @@ namespace Smp {
Hal::IDTReload();
// --- Switch to kernel page tables ---
Memory::VMM::LoadCR3(Memory::VMM::g_paging->PML4);
Memory::VMM::ActivateKernelAddressSpace();
// --- Enable SSE ---
Hal::EnableSSE();
@@ -187,45 +191,38 @@ namespace Smp {
Hal::LocalApic::InitializeAP();
// --- Program SYSCALL MSRs ---
uint64_t efer = Hal::ReadMSR(Hal::IA32_EFER);
efer |= 1 | (1ULL << 11); // SCE | NXE
Hal::WriteMSR(Hal::IA32_EFER, efer);
uint64_t star = (0x0010ULL << 48) | (0x0008ULL << 32);
Hal::WriteMSR(Hal::IA32_STAR, star);
Hal::WriteMSR(Hal::IA32_LSTAR, (uint64_t)SyscallEntry);
Hal::WriteMSR(Hal::IA32_FMASK, 0x200);
Arch::InitializeSyscallEntry();
// --- Program PAT (entry 1 = WC) ---
Hal::InitializePAT();
// --- Calibrate and start APIC timer ---
Timekeeping::ApicTimerInitializeAP();
Timekeeping::TimerInitializeAP();
// --- Check MWAIT support ---
cpu->hasMwait = Hal::HasMwait();
cpu->arch.hasMwait = Hal::HasMwait();
// --- Per-CPU power setup (C1E promotion, HWP request) ---
Hal::CpuPower::InitializeAp();
Arch::CpuPower::InitializeAp();
// --- Signal that we are online ---
cpu->started = true;
// --- Enable interrupts and enter idle loop ---
asm volatile("sti");
Arch::EnableInterrupts();
static volatile uint64_t s_idleMonitor = 0;
for (;;) {
// Pick up thermal-governor frequency changes decided by the BSP.
Hal::CpuPower::ApplyPolicyIfChanged();
Arch::CpuPower::ApplyPolicyIfChanged();
// Runnable work sends this AP a reschedule IPI. Keep its periodic
// scheduler tick masked for the entire idle-context pass so long
// firmware waits do not keep generating useless timer interrupts.
Timekeeping::ApicTimerEnterApIdle();
Timekeeping::TimerEnterApIdle();
// Any idle core may run bounded USB/NIC bottom halves. Preserve
// the AP's ACPI/MWAIT idle selection after servicing them.
Timekeeping::ServiceDeferredWork();
Hal::CpuIdle::Wait(10, cpu->hasMwait, &s_idleMonitor);
Hal::CpuIdle::Wait(10, cpu->arch.hasMwait, &s_idleMonitor);
}
}
@@ -242,7 +239,7 @@ namespace Smp {
uint64_t cpuCount = smp.cpuCount;
KernelLogStream(INFO, "SMP") << "Bootloader reports " << cpuCount << " CPU(s), BSP LAPIC ID "
<< (uint64_t)smp.bspLapicId;
<< smp.bspHardwareId;
if (cpuCount <= 1) {
KernelLogStream(INFO, "SMP") << "Single CPU system - no APs to boot";
@@ -262,13 +259,13 @@ namespace Smp {
for (uint64_t i = 0; i < cpuCount; i++) {
montauk::boot::BootCpu& info = smp.cpus[i];
if (info.lapicId == smp.bspLapicId) continue;
if (info.hardwareId == smp.bspHardwareId) continue;
if (apIndex >= MaxCPUs) break;
CpuData& ap = g_cpus[apIndex];
ap.selfPtr = (uint64_t)&ap;
ap.cpuIndex = apIndex;
ap.lapicId = info.lapicId;
ap.arch.lapicId = (uint32_t)info.hardwareId;
ap.currentSlot = -1;
ap.started = false;
@@ -290,13 +287,13 @@ namespace Smp {
volatile bool* flag = &g_cpus[i].started;
uint64_t timeout = 100000000;
while (!*flag && timeout > 0) {
asm volatile("pause");
Arch::Pause();
timeout--;
}
if (!*flag) {
KernelLogStream(ERROR, "SMP") << "AP " << i << " (LAPIC "
<< (uint64_t)g_cpus[i].lapicId << ") failed to start";
<< (uint64_t)g_cpus[i].arch.lapicId << ") failed to start";
}
}
+34
View File
@@ -0,0 +1,34 @@
/*
* Syscall.cpp
* x86_64 SYSCALL/SYSRET setup (implements Arch/Syscall.hpp)
* Copyright (c) 2025 Daniel Hammer
*/
#include <Arch/Syscall.hpp>
#include <Arch/x86_64/MSR.hpp>
// SyscallEntry.asm
extern "C" void SyscallEntry();
namespace Arch {
void InitializeSyscallEntry() {
// Enable SYSCALL/SYSRET and no-execute page permissions in EFER.
uint64_t efer = Hal::ReadMSR(Hal::IA32_EFER);
efer |= 1 | (1ULL << 11); // SCE | NXE
Hal::WriteMSR(Hal::IA32_EFER, efer);
// STAR: kernel CS in [47:32], sysret base in [63:48]
// SYSCALL: CS=0x08, SS=0x10
// SYSRET: CS=0x10+16=0x20|RPL3=0x23, SS=0x10+8=0x18|RPL3=0x1B
uint64_t star = (0x0010ULL << 48) | (0x0008ULL << 32);
Hal::WriteMSR(Hal::IA32_STAR, star);
// LSTAR: SYSCALL entry point
Hal::WriteMSR(Hal::IA32_LSTAR, (uint64_t)SyscallEntry);
// FMASK: mask IF on SYSCALL entry (bit 9 = IF)
Hal::WriteMSR(Hal::IA32_FMASK, 0x200);
}
}
File renamed without changes.
+20
View File
@@ -0,0 +1,20 @@
/*
* SyscallFrame.hpp
* x86_64 system-call frame (see Arch/Syscall.hpp)
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Arch {
// Stack frame pushed by SyscallEntry.asm
struct SyscallFrame {
uint64_t r15, r14, r13, r12, rbp, rbx; // callee-saved
uint64_t arg6, arg5, arg4, arg3, arg2, arg1;
uint64_t syscall_nr;
uint64_t user_rflags, user_rip, user_rsp;
};
}
+7 -5
View File
@@ -187,17 +187,18 @@ namespace montauk::boot {
// to bring up, then calls SmpInfo::startCpu. The bootloader resumes
// the parked AP such that it begins executing `entry(cpu)` on a stack
// the bootloader provides, with the same handoff machine state the BSP
// received (long mode, paging on, kernel mapped). `entry` recovers its
// received (64-bit mode, paging on, kernel mapped). `entry` recovers its
// per-CPU context from `cpu->extraArgument`.
struct BootCpu;
struct SmpInfo {
uint64_t cpuCount; // total CPUs reported (including the BSP)
uint32_t bspLapicId; // local-APIC ID of the bootstrap processor
uint64_t bspHardwareId; // hardware ID of the bootstrap processor
BootCpu* cpus; // HHDM/direct-mapped virtual; `cpuCount` entries.
// The entry whose lapicId == bspLapicId is the BSP
// and must NOT be started (it is already running).
// The entry whose hardwareId == bspHardwareId is
// the BSP and must NOT be started (it is already
// running).
// Wake `cpu`, causing it to begin executing `entry(cpu)`. Returns
// true if the start request was accepted. `entry` runs on a
@@ -207,7 +208,8 @@ namespace montauk::boot {
};
struct BootCpu {
uint32_t lapicId; // local-APIC ID of this CPU
uint64_t hardwareId; // architectural CPU ID: x86 local-APIC ID,
// ARM MPIDR
uint64_t extraArgument; // free for the kernel: stash a per-CPU
// pointer here before calling startCpu; it is
// handed back to `entry` via `cpu->extraArgument`.
+17 -67
View File
@@ -10,40 +10,31 @@
#include <cstdint>
#include <cstddef>
#include <Boot/Boot.hpp>
#include <Hal/GDT.hpp>
#include <Terminal/Terminal.hpp>
#include <Efi/UEFI.hpp>
#include <Common/Panic.hpp>
#include <Memory/Memmap.hpp>
#include <Memory/Heap.hpp>
#include <Memory/HHDM.hpp>
#include <Platform/Util.hpp>
#include <Hal/IDT.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Memory/Paging.hpp>
#include <ACPI/ACPI.hpp>
#include <ACPI/CpuIdle.hpp>
#include <ACPI/AcpiShutdown.hpp>
#include <ACPI/AcpiEvents.hpp>
#include <ACPI/EmbeddedController.hpp>
#include <ACPI/Battery.hpp>
#include <ACPI/AML/AmlParser.hpp>
#include <Hal/Apic/ApicInit.hpp>
#include <Pci/Pci.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Drivers/PS2/PS2Controller.hpp>
#include <Drivers/PS2/Keyboard.hpp>
#include <Drivers/PS2/Mouse.hpp>
#include <Timekeeping/Timer.hpp>
#include <Drivers/Init.hpp>
#include <Graphics/Framebuffer.hpp>
#include <Hal/MSR.hpp>
#include <Hal/Cpu.hpp>
#include <Hal/CpuPower.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/Platform.hpp>
#include <Fs/Boot.hpp>
#include <Sched/Scheduler.hpp>
#include <Ipc/Ipc.hpp>
#include <Api/Syscall.hpp>
#include <Hal/SmpBoot.hpp>
#include <Arch/Smp.hpp>
using namespace Kt;
namespace Memory {
@@ -69,7 +60,7 @@ extern "C" void kmain() {
}
if (!montauk::boot::Initialize()) {
Hal::Halt();
Arch::HaltForever();
}
const montauk::boot::BootInfo& boot = montauk::boot::Info();
const montauk::boot::Framebuffer& framebuffer = boot.framebuffer;
@@ -88,12 +79,7 @@ extern "C" void kmain() {
);
#if defined (__x86_64__)
Hal::PrepareGDT();
Hal::BridgeLoadGDT();
Hal::EnableSSE();
#endif
Arch::EarlyInitialize();
Memory::HHDMBase = boot.hhdmBase;
@@ -112,26 +98,19 @@ extern "C" void kmain() {
heap.Walk();
#if defined (__x86_64__)
Hal::IDTInitialize();
Arch::InitializeTraps();
Memory::VMM::Paging g_paging{};
Memory::VMM::g_paging = &g_paging;
g_paging.Init((uint64_t)&KernelStartSymbol, ((uint64_t)&KernelEndSymbol - (uint64_t)&KernelStartSymbol), boot.memoryMap, framebuffer);
Hal::InitializePAT();
#endif
Arch::InitializeMemoryAttributes();
Graphics::Framebuffer::Initialize(framebuffer);
#if defined (__x86_64__)
Graphics::Framebuffer::MapWriteCombining();
#endif
Hal::ACPI g_acpi((Hal::ACPI::XSDP*)Memory::HHDM(boot.rsdpPhysical));
#if defined (__x86_64__)
if (g_acpi.GetXSDT() != nullptr) {
Hal::AcpiShutdown::Initialize(g_acpi.GetXSDT());
@@ -140,15 +119,11 @@ extern "C" void kmain() {
Hal::EmbeddedController::Initialize(g_acpi.GetXSDT());
Hal::Battery::Initialize();
Hal::CpuIdle::Initialize(g_acpi.GetXSDT());
// Interrupt controllers, this CPU's per-CPU data, idle and power
// management.
Arch::InitializeInterrupts(g_acpi.GetXSDT());
Hal::ApicInitialize(g_acpi.GetXSDT());
Smp::InitBsp();
Hal::CpuPower::InitializeBsp();
asm volatile("sti");
Arch::EnableInterrupts();
Hal::AcpiEvents::Initialize(g_acpi.GetXSDT());
@@ -163,11 +138,9 @@ extern "C" void kmain() {
Drivers::ProbeEarly();
Drivers::InitializeGraphics();
Timekeeping::ApicTimerInitialize();
Timekeeping::TimerInitialize();
Drivers::PS2::Initialize();
Drivers::PS2::Keyboard::Initialize();
Drivers::PS2::Mouse::Initialize();
Arch::InitializePlatformDevices();
// Hybrid graphics: switch off a discrete GPU nothing drives before
// the normal probe can claim its USB-C/audio functions.
@@ -178,7 +151,6 @@ extern "C" void kmain() {
Drivers::InitializeStorage();
Drivers::InitializeAudio();
}
#endif
// UEFI runtime services are optional (absent on legacy-BIOS boots).
if (boot.has(montauk::boot::FeatureEfiSystemTable)) {
@@ -188,9 +160,7 @@ extern "C" void kmain() {
Fs::InitializeBootFilesystems(boot.modules);
#if defined (__x86_64__)
Hal::LoadTSS();
#endif
Arch::PrepareUserMode();
montauk::abi::InitializeSyscalls();
@@ -199,36 +169,16 @@ extern "C" void kmain() {
Fs::LogProtectedPaths();
Ipc::Initialize();
#if defined (__x86_64__)
Smp::BootAPs(boot.smp);
#endif
#if defined (__x86_64__)
Drivers::PS2::Mouse::FlushState();
#endif
Arch::LateInitialize();
Kt::SuppressKernelLog();
Sched::Spawn("0:/os/init.elf");
Timekeeping::EnableSchedulerTick();
#if defined (__x86_64__)
// Use MWAIT for deeper C-states if available, otherwise HLT.
auto* bspCpu = Smp::GetCpuData(0);
if (bspCpu && bspCpu->hasMwait) {
static volatile uint64_t s_bspIdleMonitor = 0;
for (;;) {
Timekeeping::IdleOnce(true, &s_bspIdleMonitor);
Timekeeping::IdleOnce();
}
} else {
for (;;) {
Timekeeping::IdleOnce(false);
}
}
#else
for (;;) {
Timekeeping::IdleOnce(false);
}
#endif
}
+10 -2
View File
@@ -280,13 +280,21 @@ namespace montauk::boot {
if (count > kMaxCpus) count = kMaxCpus;
for (uint64_t i = 0; i < count; i++) {
limine_mp_info* info = resp->cpus[i];
g_cpus[i].lapicId = info->lapic_id;
#if defined(__x86_64__)
g_cpus[i].hardwareId = info->lapic_id;
#elif defined(__aarch64__)
g_cpus[i].hardwareId = info->mpidr;
#endif
g_cpus[i].extraArgument = 0;
g_cpus[i].entry = nullptr;
g_cpus[i].native = info;
}
out.smp.cpuCount = count;
out.smp.bspLapicId = resp->bsp_lapic_id;
#if defined(__x86_64__)
out.smp.bspHardwareId = resp->bsp_lapic_id;
#elif defined(__aarch64__)
out.smp.bspHardwareId = resp->bsp_mpidr;
#endif
out.smp.cpus = g_cpus;
out.smp.startCpu = &LimineStartCpu;
out.features |= FeatureSmp;
+9 -82
View File
@@ -4,10 +4,11 @@
*/
#include "Panic.hpp"
#include "../CppLib/BoxUI.hpp"
#include "../Drivers/Graphics/IntelGPU.hpp"
#include <Arch/Cpu.hpp>
#include <CppLib/BoxUI.hpp>
#include <Drivers/Graphics/IntelGPU.hpp>
static constexpr int BoxWidth = 72;
static constexpr int BoxWidth = PanicBoxWidth;
static void PrintHorizontalEdge(const char* left, const char* right) {
kerr << left;
@@ -15,68 +16,7 @@ static void PrintHorizontalEdge(const char* left, const char* right) {
kerr << right << "\n";
}
static void PrintPageFaultInfo(System::PanicFrame*& frame) {
auto* pf = (System::PageFaultPanicFrame*)frame;
frame = (System::PanicFrame*)&pf->IP;
uint64_t cr2;
asm volatile("mov %%cr2, %0" : "=r"(cr2));
PrintBoxedHex(kerr, "Faulting Address (CR2)", cr2, BoxWidth);
PrintBoxedLine(kerr, "Page Fault Error:", BoxWidth, true);
PrintBoxedDec(kerr, "Present", pf->PageFaultError.Present, BoxWidth);
PrintBoxedDec(kerr, "Write", pf->PageFaultError.Write, BoxWidth);
PrintBoxedDec(kerr, "User", pf->PageFaultError.User, BoxWidth);
PrintBoxedDec(kerr, "Reserved Write", pf->PageFaultError.ReservedWrite, BoxWidth);
PrintBoxedDec(kerr, "Instruction Fetch",pf->PageFaultError.InstructionFetch, BoxWidth);
PrintBoxedDec(kerr, "Protection Key", pf->PageFaultError.ProtectionKey, BoxWidth);
PrintBoxedDec(kerr, "Shadow Stack", pf->PageFaultError.ShadowStack, BoxWidth);
PrintBoxedDec(kerr, "SGX", pf->PageFaultError.SGX, BoxWidth);
}
static void PrintGPFInfo(System::PanicFrame*& frame) {
auto* gpf = (System::GPFPanicFrame*)frame;
frame = (System::PanicFrame*)&gpf->IP;
PrintBoxedLine(kerr, "General Protection Fault:", BoxWidth, true);
PrintBoxedDec(kerr, "Error Code", gpf->GeneralProtectionFaultError, BoxWidth);
}
// Link-time base of the kernel image (see linker-x86_64.ld).
static constexpr uint64_t KernelBaseVirt = 0xFFFFFFFF80000000ULL;
static void PrintRegisters(System::PanicFrame* frame) {
PrintBoxedSeparator(kerr, BoxWidth);
PrintBoxedLine(kerr, "Registers:", BoxWidth, true);
PrintBoxedHex(kerr, "Instruction Pointer", frame->IP, BoxWidth);
// The absolute address means nothing without the link map. Its offset from
// the kernel's load base is what `nm bin-x86_64/kernel` can be searched
// for directly, so print that next to it.
if (frame->IP >= KernelBaseVirt)
PrintBoxedHex(kerr, " kernel+", frame->IP - KernelBaseVirt, BoxWidth);
PrintBoxedHex(kerr, "Code Segment", frame->CS, BoxWidth);
PrintBoxedHex(kerr, "Flags", frame->Flags, BoxWidth);
PrintBoxedHex(kerr, "Stack Pointer", frame->SP, BoxWidth);
PrintBoxedHex(kerr, "Stack Segment", frame->SS, BoxWidth);
}
[[noreturn]] static void Halt() {
while (true) {
#if defined (__x86_64__)
asm ("cli");
asm ("hlt");
#elif defined (__aarch64__) || defined (__riscv)
asm ("wfi");
#elif defined (__loongarch64)
asm ("idle 0");
#endif
}
}
void Panic(const char *meditationString, System::PanicFrame* frame) {
void Panic(const char *meditationString, Arch::PanicFrame* frame) {
// Bring scanout back to buffer 0 first: if a page-flipped client was
// showing the other buffer, the panic box would land on an invisible
// surface. Safe no-op when the GPU driver is inactive.
@@ -96,32 +36,19 @@ void Panic(const char *meditationString, System::PanicFrame* frame) {
PrintBoxedLine(kerr, meditationString, BoxWidth);
PrintBoxedLine(kerr, "", BoxWidth);
// CPU state (x86_64 only)
#if defined (__x86_64__)
// CPU state at the faulting exception
if (frame != nullptr) {
PrintBoxedSeparator(kerr, BoxWidth);
PrintBoxedLine(kerr, "CPU State:", BoxWidth, true);
PrintBoxedHex(kerr, "Interrupt Vector", frame->InterruptVector, BoxWidth);
if (frame->InterruptVector == 0xE)
PrintPageFaultInfo(frame);
else if (frame->InterruptVector == 0xD)
PrintGPFInfo(frame);
PrintRegisters(frame);
Arch::PrintPanicFrame(frame);
}
#endif
// Footer
PrintBoxedLine(kerr, "", BoxWidth);
PrintHorizontalEdge(BOXUI_BL, BOXUI_BR);
kerr << BOXUI_ANSI_RESET;
#if defined (__x86_64__)
// The framebuffer is write-combining. Drain the final panic text before
// halting all forward progress so it cannot remain in a CPU write buffer.
asm volatile("sfence" ::: "memory");
#endif
Arch::StoreFence();
Halt();
Arch::HaltForever();
}
+15 -2
View File
@@ -1,5 +1,18 @@
#pragma once
#include <Platform/Registers.hpp>
#include <Terminal/Terminal.hpp>
void Panic(const char *meditationString, System::PanicFrame* frame);
namespace Arch {
// The CPU state an exception handler captured; its layout belongs to
// the architecture (Arch/<arch>/), generic code only passes it along.
struct PanicFrame;
// Print the frame's CPU state into the panic box. Implemented by the
// architecture.
void PrintPanicFrame(PanicFrame* frame);
}
// Width of the panic screen's box, shared with Arch::PrintPanicFrame.
static constexpr int PanicBoxWidth = 72;
// Print the panic screen and halt this CPU. `frame` may be nullptr.
[[noreturn]] void Panic(const char *meditationString, Arch::PanicFrame* frame);
Loaded 100 of 164 files, more files were not shown because too many files have changed in this diff. Show more