Compare commits

..
37 Commits
Author SHA1 Message Date
daniel 51a2b3a932 feat: add Broadcom FullMAC Wi-Fi driver 2026-10-27 07:47:57 +01:00
daniel 8b4a938164 fix: ship Pi images with MontaukOS UEFI boot entry 2026-10-26 16:31:25 +01:00
daniel be84506fdf feat: make Files app list view columns draggable horizontally 2026-10-26 16:17:06 +01:00
daniel 559c3e2875 fix: bump max file name size for Files list view to 128 bytes 2026-10-26 16:05:21 +01:00
daniel 36291cd1cc fix: make Settings app scrollbar draggable 2026-10-26 16:03:33 +01:00
daniel b781cf8c97 feat: add MontaukOS Imager tool for Raspberry Pi 2026-10-26 16:01:38 +01:00
daniel 7e036c94e7 fix: NTP syncs as soon as DHCP has a lease, random origin cookie instead of the unsynced local clock, TLS validates certificates against UTC 2026-10-26 08:19:48 +01:00
daniel 1d00c1c016 feat: add GENET v5 Ethernet driver 2026-10-26 08:10:20 +01:00
daniel 3310b3d6de fix: Pi 4 Bluetooth - hold RTS deasserted across BT_ON power-on, require Read Local Version to sync, mini UART no flow control first 2026-10-26 07:46:49 +01:00
daniel 93d8becce3 feat: wait up to 10s for a late MONTAUKROOT, retry USB enumeration after a port reset, recover halted EP0/bulk endpoints, MSC card reader settle 2026-10-25 17:57:20 +01:00
daniel ffee9002a6 feat: Raspberry Pi 4/400 Bluetooth over mini UART (H4, Broadcom patchram, firmware mailbox BT_ON); move BT stack to Drivers/Bluetooth 2026-10-25 15:59:33 +01:00
daniel 808e1d66fd feat: aarch64 port - Raspberry Pi 4/400 SD boot (SDHCI, ext2 root + first-boot grow), USB hubs, PCI INTx routing, SMP, DMA fences 2026-10-25 13:48:26 +01:00
daniel 8c09ccef66 feat: add support for aarch64 timer/interrupts, port MontaukOS userspace 2026-10-25 08:27:09 +01:00
daniel 8b6379fb7e feat: aarch64 port - fix build system obstacles, add exception handling, page tables 2026-10-25 07:43:46 +01:00
daniel aae4db01ad fix: split search results in Settings by category 2026-10-24 21:32:22 +02:00
daniel 1bae77ec84 feat: make kernel architecture-agnostic; primitive work on arm port 2026-10-24 21:25:18 +02:00
daniel dab7d5427b fix: fix Bluetooth audio delay 2026-10-24 20:38:35 +02:00
daniel 0a20385e12 feat: add flac support to Music app 2026-10-24 14:09:04 +02:00
daniel 8ae19258b1 fix: fix Files double-click bug 2026-10-24 13:34:36 +02:00
daniel 8de98f3ab3 feat: move Settings out of Files into its own searchable app with categories 2026-10-24 13:26:47 +02:00
daniel d3e49a925d feat: move Files to its own process; add window server messaging (multi-format clipboard, drag and drop), undo/redo, sidebar, breadcrumbs, thumbnails, file type icons, New menu and proper text fields 2026-10-24 12:44:20 +02:00
daniel 07191b1286 feat: Files app - add Trash 2026-10-24 08:20:18 +02:00
daniel e047c1e0c0 feat: expand Files features; add file associations 2026-10-24 07:26:50 +02:00
daniel 23cc56b933 refactor: capitalize boot entry name 2026-10-17 12:22:24 +02:00
daniel 3c2eefe7f4 refactor: move wallpaper.cache to 0:/state 2026-10-17 10:53:36 +02:00
daniel 14c7099e3b feat: remove remaining use of session.toml 2026-10-17 10:36:07 +02:00
daniel e87bd358a6 feat: Rust std target + ruststd tests; SYS_EXIT_GROUP; fix mkdir/create under missing parent 2026-10-17 10:30:24 +02:00
daniel 9e66c3b249 fix: change copyright notice to use 2025-2026 range 2026-10-14 14:59:31 +02:00
daniel 31edff3da4 docs - simplify release notes 2026-10-14 07:56:00 +02:00
daniel f3d475a0d5 feat: release - 0.2.0 2026-10-14 07:46:40 +02:00
daniel 278b208258 feat: add process termination codes and SYS_ABORT; whole-process abort() from any thread in libc and montauk-rt 2026-10-14 07:32:15 +02:00
daniel f8b8f9610f feat: add Rust userspace support; fix TLS layout for p_align < 16; kill whole process on sibling-thread faults 2026-10-14 07:16:42 +02:00
daniel a81298fb24 fix: fix faulty syscall result/status code compares 2026-10-13 07:15:05 +02:00
daniel abfd9de8f6 feat: add SYS_FUTEX_WAIT and SYS_FUTEX_WAKE syscalls; revamp desktop application categories 2026-10-13 07:11:39 +02:00
daniel 60ca9c78c7 fix: remove stray newline in dhcp log 2026-10-03 12:49:06 +02:00
daniel 2aa6360b3d feat: switch text to UTF-8; add Unicode key events, dead keys & Czech layout; fix numpad navigation; add NumLock support 2026-10-03 10:11:49 +02:00
daniel 32660ddd1a feat: add ext2 block cache 2026-10-02 15:19:41 +02:00
547 changed files with 39389 additions and 6761 deletions

No files matched your search

+9
View File
@@ -6,12 +6,21 @@
archives/isos/ archives/isos/
montaukos.org/downloads/*.iso montaukos.org/downloads/*.iso
*.hdd *.hdd
*.img
rpi4-firmware/
qemu.log qemu.log
toolchain/local/ toolchain/local/
toolchain/build/ toolchain/build/
toolchain/src/ toolchain/src/
toolchain/sysroot/ toolchain/sysroot/
toolchain/sysroot-*/
programs/bin/ programs/bin/
# Non-x86 userspace builds (programs/mk/arch.mk)
programs/bin-*/
programs/obj-*/
programs/src/*/obj-*/
programs/lib/*/obj-*/
programs/lib/*/build-*/
programs/obj/ programs/obj/
programs/src/*/obj/ programs/src/*/obj/
programs/libs/*/obj/ programs/libs/*/obj/
+124 -20
View File
@@ -10,6 +10,32 @@ QEMUFLAGS := -smp 4 -m 2G -d int -D qemu.log -no-reboot
override IMAGE_NAME := montauk-$(ARCH) override IMAGE_NAME := montauk-$(ARCH)
# KERNEL_ONLY=1 builds a ramdisk holding just the boot files (no
# userspace), for bringing up a kernel on a new architecture.
KERNEL_ONLY ?= 0
# programs/ builds x86_64 into bin/, other architectures into bin-<arch>/.
ifeq ($(ARCH),x86_64)
PROGRAMS_BIN := programs/bin
else
PROGRAMS_BIN := programs/bin-$(ARCH)
endif
override EFI_LOADER_x86_64 := BOOTX64.EFI
override EFI_LOADER_aarch64 := BOOTAA64.EFI
override EFI_LOADER_riscv64 := BOOTRISCV64.EFI
override EFI_LOADER_loongarch64 := BOOTLOONGARCH64.EFI
# The host strip handles x86_64 images; other targets use their toolchain's.
ifeq ($(ARCH),x86_64)
STRIP := strip
else
STRIP := toolchain/local/bin/$(ARCH)-montauk-strip
endif
# AArch64 UEFI firmware for QEMU (Debian/Ubuntu: qemu-efi-aarch64).
AARCH64_EFI_FIRMWARE ?= /usr/share/qemu-efi-aarch64/QEMU_EFI.fd
# Toolchain for building the 'limine' executable for the host. # Toolchain for building the 'limine' executable for the host.
HOST_CC := cc HOST_CC := cc
HOST_CFLAGS := -g -O2 -pipe HOST_CFLAGS := -g -O2 -pipe
@@ -51,29 +77,72 @@ run-hdd-x86_64: $(IMAGE_NAME).hdd
$(QEMUFLAGS) $(QEMUFLAGS)
.PHONY: run-aarch64 .PHONY: run-aarch64
# The machine mirrors the Raspberry Pi 4/400: Cortex-A72 cores, a GICv2, and
# keyboard and mouse behind a hub (every USB 2 device on the Pi is: the
# VL805's one USB 2 port carries an on-board hub). QEMU's hub is full-speed,
# so the transaction-translator path is only exercised on the real board.
# The kernel console is mirrored to the PL011 (-serial stdio).
run-aarch64: $(IMAGE_NAME).iso run-aarch64: $(IMAGE_NAME).iso
qemu-system-$(ARCH) \ qemu-system-$(ARCH) \
-M virt \ -M virt,gic-version=2 \
-cpu cortex-a72 \ -cpu cortex-a72 \
-device ramfb \ -device ramfb \
-device qemu-xhci,id=xhci \ -device qemu-xhci,id=xhci \
-device usb-kbd,bus=xhci.0 \ -device usb-hub,bus=xhci.0,port=1 \
-device usb-mouse,bus=xhci.0 \ -device usb-kbd,bus=xhci.0,port=1.1 \
-bios /usr/share/ovmf/OVMF.fd \ -device usb-mouse,bus=xhci.0,port=1.2 \
-bios $(AARCH64_EFI_FIRMWARE) \
-cdrom $(IMAGE_NAME).iso \ -cdrom $(IMAGE_NAME).iso \
-serial stdio \
$(QEMUFLAGS) $(QEMUFLAGS)
.PHONY: run-hdd-aarch64 .PHONY: run-hdd-aarch64
run-hdd-aarch64: $(IMAGE_NAME).hdd run-hdd-aarch64: $(IMAGE_NAME).hdd
qemu-system-$(ARCH) \ qemu-system-$(ARCH) \
-M virt \ -M virt,gic-version=2 \
-cpu cortex-a72 \ -cpu cortex-a72 \
-device ramfb \ -device ramfb \
-device qemu-xhci,id=xhci \ -device qemu-xhci,id=xhci \
-device usb-kbd,bus=xhci.0 \ -device usb-hub,bus=xhci.0,port=1 \
-device usb-mouse,bus=xhci.0 \ -device usb-kbd,bus=xhci.0,port=1.1 \
-bios /usr/share/ovmf/OVMF.fd \ -device usb-mouse,bus=xhci.0,port=1.2 \
-hda $(IMAGE_NAME).hdd \ -bios $(AARCH64_EFI_FIRMWARE) \
-drive if=none,id=hd0,format=raw,file=$(IMAGE_NAME).hdd \
-device virtio-blk-pci,drive=hd0 \
-serial stdio \
$(QEMUFLAGS)
# The SD card image on QEMU, with the root read through the SDHCI driver:
# the card goes on sdhci-pci as a padded copy (QEMU wants a power-of-two card;
# RPI4_QEMU_CARD_MIB, default 4 GiB, leaves the first-boot grow room to work
# with; every run starts from a fresh copy, so every run is a first boot),
# and since QEMU's UEFI has no SD driver, the card's ESP is also
# attached alone (the card's first partition, as its own disk) over virtio
# for the firmware to boot Limine from.
RPI4_QEMU_CARD_MIB ?= 4096
.PHONY: run-rpi4-sd
run-rpi4-sd: montauk-rpi4.img
cp --sparse=always montauk-rpi4.img rpi4-sd-qemu.img
size=$$(stat -c %s rpi4-sd-qemu.img); p=$$(($(RPI4_QEMU_CARD_MIB) * 1048576)); \
while [ $$p -lt $$size ]; do p=$$((p * 2)); done; truncate -s $$p rpi4-sd-qemu.img
end=$$(PATH=$$PATH:/usr/sbin:/sbin sfdisk -d montauk-rpi4.img | awk -F'[=,]' '/montauk-rpi4.img1/ {print $$2 + $$4}'); \
dd if=montauk-rpi4.img of=rpi4-esp-qemu.img bs=512 count=$$end status=none
PATH=$$PATH:/usr/sbin:/sbin sfdisk --quiet --delete rpi4-esp-qemu.img 2 2>/dev/null || true
qemu-system-aarch64 \
-M virt,gic-version=2 \
-cpu cortex-a72 \
-device ramfb \
-device qemu-xhci,id=xhci \
-device usb-hub,bus=xhci.0,port=1 \
-device usb-kbd,bus=xhci.0,port=1.1 \
-device usb-mouse,bus=xhci.0,port=1.2 \
-bios $(AARCH64_EFI_FIRMWARE) \
-drive if=none,id=esp,format=raw,file=rpi4-esp-qemu.img \
-device virtio-blk-pci,drive=esp \
-device sdhci-pci \
-drive if=none,id=sd,format=raw,file=rpi4-sd-qemu.img \
-device sd-card,drive=sd \
-serial stdio \
$(QEMUFLAGS) $(QEMUFLAGS)
.PHONY: run-riscv64 .PHONY: run-riscv64
@@ -196,13 +265,23 @@ ports: programs
fi fi
.PHONY: ramdisk .PHONY: ramdisk
ifeq ($(KERNEL_ONLY),1)
ramdisk: limine/limine kernel
rm -rf .kernel-only-root
mkdir -p .kernel-only-root/boot/limine
cp -v limine/$(EFI_LOADER_$(ARCH)) limine.conf .kernel-only-root/boot/limine/
cp -v kernel/bin-$(ARCH)/kernel .kernel-only-root/boot/
./scripts/mkramdisk.sh .kernel-only-root ramdisk.tar
rm -rf .kernel-only-root
else
ramdisk: limine/limine kernel programs ramdisk: limine/limine kernel programs
mkdir -p programs/bin/boot/limine mkdir -p $(PROGRAMS_BIN)/boot/limine
cp -v limine/BOOTX64.EFI programs/bin/boot/limine/ cp -v limine/$(EFI_LOADER_$(ARCH)) $(PROGRAMS_BIN)/boot/limine/
cp -v limine.conf programs/bin/boot/limine/ cp -v limine.conf $(PROGRAMS_BIN)/boot/limine/
cp -v kernel/bin-$(ARCH)/kernel programs/bin/boot/ cp -v kernel/bin-$(ARCH)/kernel $(PROGRAMS_BIN)/boot/
rm -f programs/bin/boot/ramdisk.tar rm -f $(PROGRAMS_BIN)/boot/ramdisk.tar
./scripts/mkramdisk.sh programs/bin ramdisk.tar ./scripts/mkramdisk.sh $(PROGRAMS_BIN) ramdisk.tar
endif
$(IMAGE_NAME).iso: limine/limine kernel ramdisk $(IMAGE_NAME).iso: limine/limine kernel ramdisk
rm -rf iso_root rm -rf iso_root
@@ -252,13 +331,17 @@ ifeq ($(ARCH),loongarch64)
endif endif
rm -rf iso_root rm -rf iso_root
# Example and regression-test programs that are built into 0:/os for
# development but have no place on a release image.
RELEASE_EXCLUDE_PROGRAMS := rusthello.elf futextest.elf memtest.elf
.PHONY: release .PHONY: release
release: limine/limine kernel ramdisk release: limine/limine kernel ramdisk
rm -rf iso_root .release-tmp rm -rf iso_root .release-tmp
mkdir -p iso_root/boot mkdir -p iso_root/boot
cp kernel/bin-$(ARCH)/kernel iso_root/boot/ cp kernel/bin-$(ARCH)/kernel iso_root/boot/
strip iso_root/boot/kernel $(STRIP) iso_root/boot/kernel
cp -r programs/bin .release-tmp cp -r $(PROGRAMS_BIN) .release-tmp
# A release ISO ships only in-tree apps, so drop any out-of-tree port bundles # A release ISO ships only in-tree apps, so drop any out-of-tree port bundles
# that happen to be staged in programs/bin/apps (each carries a .port marker). # that happen to be staged in programs/bin/apps (each carries a .port marker).
# Pass RELEASE_WITH_PORTS=1 to deliberately cut a ports-included image. # Pass RELEASE_WITH_PORTS=1 to deliberately cut a ports-included image.
@@ -281,11 +364,12 @@ ifeq ($(RELEASE_WITHOUT_SDK),1)
rm -rf .release-tmp/sdk/bin .release-tmp/sdk/lib .release-tmp/sdk/include \ rm -rf .release-tmp/sdk/bin .release-tmp/sdk/lib .release-tmp/sdk/include \
.release-tmp/sdk/libexec .release-tmp/sdk/x86_64-montauk .release-tmp/sdk/libexec .release-tmp/sdk/x86_64-montauk
endif endif
find .release-tmp -name '*.elf' -exec strip {} + rm -f $(addprefix .release-tmp/os/,$(RELEASE_EXCLUDE_PROGRAMS))
find .release-tmp -name '*.elf' -exec $(STRIP) {} +
# The ramdisk carries its own copy of the kernel at 0:/boot/kernel -- that is # The ramdisk carries its own copy of the kernel at 0:/boot/kernel -- that is
# the image the Installer lays down on the target disk -- and it is not named # the image the Installer lays down on the target disk -- and it is not named
# *.elf, so the find above misses it. Unstripped it is ~6 MB against ~0.7 MB. # *.elf, so the find above misses it. Unstripped it is ~6 MB against ~0.7 MB.
strip .release-tmp/boot/kernel $(STRIP) .release-tmp/boot/kernel
./scripts/mkramdisk.sh .release-tmp iso_root/boot/ramdisk.tar ./scripts/mkramdisk.sh .release-tmp iso_root/boot/ramdisk.tar
rm -rf .release-tmp rm -rf .release-tmp
mkdir -p iso_root/boot/limine mkdir -p iso_root/boot/limine
@@ -331,6 +415,26 @@ ifeq ($(ARCH),loongarch64)
endif endif
rm -rf iso_root rm -rf iso_root
# Raspberry Pi 4/400 SD card image (UEFI firmware + Limine + MontaukOS).
# ROOTFS=ext2 (default) puts the system on an ext2 partition read through the
# kernel's SD driver; ROOTFS=ramdisk has Limine load ramdisk.tar instead.
# Phony so switching ROOTFS always rebuilds the image.
ROOTFS ?= ext2
.PHONY: rpi4-sd montauk-rpi4.img
rpi4-sd: montauk-rpi4.img
montauk-rpi4.img: limine/limine kernel ramdisk
ifneq ($(ARCH),aarch64)
$(error rpi4-sd needs ARCH=aarch64)
endif
./scripts/mk-rpi4-sd.sh kernel/bin-$(ARCH)/kernel ramdisk.tar $@ $(ROOTFS) $(PROGRAMS_BIN)-rpi
# Graphical imager: write montauk-rpi4.img to an SD card, or update the
# installation on one in place (scripts/montauk-imager.py, docs/raspberry-pi.md).
.PHONY: rpi4-imager
rpi4-imager:
./scripts/montauk-imager.py
$(IMAGE_NAME).hdd: limine/limine kernel ramdisk $(IMAGE_NAME).hdd: limine/limine kernel ramdisk
rm -f $(IMAGE_NAME).hdd rm -f $(IMAGE_NAME).hdd
dd if=/dev/zero bs=1M count=0 seek=64 of=$(IMAGE_NAME).hdd dd if=/dev/zero bs=1M count=0 seek=64 of=$(IMAGE_NAME).hdd
@@ -362,7 +466,7 @@ endif
clean: clean:
$(MAKE) -C kernel clean $(MAKE) -C kernel clean
# $(MAKE) -C programs clean # $(MAKE) -C programs clean
rm -rf iso_root .release-tmp $(IMAGE_NAME).iso $(IMAGE_NAME).hdd ramdisk.tar rm -rf iso_root .release-tmp $(IMAGE_NAME).iso $(IMAGE_NAME).hdd montauk-rpi4.img rpi4-sd-qemu.img rpi4-esp-qemu.img ramdisk.tar
.PHONY: distclean .PHONY: distclean
distclean: distclean:
+75
View File
@@ -1,3 +1,78 @@
MontaukOS 0.2.0 (October 14, 2026)
(278b20825815ec04a22fd1c2fcfedbd38e116e6d)
New
====
* Capability (application permissions) model added
* Consent prompts for missing app permissions
* New Capabilities app for reviewing and managing app permissions
* Unified system status codes; the shell and Files now print a readable message on failure
* Text is now UTF-8 throughout, with Unicode key events and dead keys
* Keyboard layout support, with English, Norwegian, Czech, Swedish, and Danish layouts
* NumLock support
* Battery percentage reporting via ACPI, with a battery indicator in the desktop panel
* Display brightness control, including on hybrid graphics laptops, with a slider in the desktop panel
* Discrete GPUs are powered down automatically on hybrid graphics laptops
* ext2 block cache with readahead, speeding up disk-heavy work such as on-OS compilation
* Userspace system logging: klog is now syslog, logs identify the user and program, and system services log through montauk::service_log
* The system log is written to disk (0:/state/logs) during shutdown
* Desktop sessions now own the processes started in them, and those processes are ended on logout
* Rust userspace support (no_std)
* Futex syscalls (SYS_FUTEX_WAIT/SYS_FUTEX_WAKE) as groundwork for Rust std and pthreads
* Process termination codes; abort() now ends the whole process from any thread
* isatty() now reports whether a terminal is actually attached
* Settings panel and a Pure Black theme in Terminal (the default for Console sessions)
* Syntax highlighting for C++, HTML, CSS, and JavaScript in edit and Text Editor
* Expanded SVG renderer
* New vol command for listing mounted volumes
* Revamped desktop application categories
* Fonts, icons, and manual pages moved to 0:/os; persistent state now lives in 0:/state
* RTL-SDR driver moved from the kernel to userspace
Fixed
=====
* Opening a directory as a file is now rejected on ext2 and FAT32, fixing on-OS gcc on installed systems
* Fixed several syscall result and status code comparisons
* Fixed a TLS layout bug for binaries whose TLS segment has an alignment below 16
* A fault in one thread now ends the whole process instead of leaving its sibling threads running
* Fixed an ACPI hardware ID endianness bug
* Fixed a race in Wi-Fi event processing and other Wi-Fi bugs
* DHCP no longer runs when no network device has an active link
* Reduced idle CPU wakeups and deferred-work polling
* Redirected consoles are now inherited by processes spawned with capabilities
* Fixed numpad navigation
* Fixed a timezone formatting bug
* Fixed the login wallpaper startup delay
* Fixed text overlap in file dialogs
* Persisted kernel logs are no longer double-spaced
* Files lists now scroll with the arrow keys, and volume labels are consistent
* mbrtowc added to libc
Included in ISO
================
Two x86_64 images are published for this release.
montauk-0.2.0-sdk-x86_64.iso additionally carries the
Montauk SDK (gcc, binutils, etc.)
* MontaukOS SMP kernel
* Userspace bootstrap (0:/os/init.elf) and other essential system utilities
* MontaukOS desktop environment and graphical apps
* NetSurf web browser (0:/apps/netsurf)
* Manual pages (0:/os/man), readable via man command
* Service and system configuration (0:/config: init.toml, capabilities.toml, ssh.toml, ntp.toml)
* System CA certificates (0:/os/certs/ca-certificates.crt)
* Intel Bluetooth controller and Wi-Fi (iwlwifi) firmware (0:/os/firmware/intel)
* Third-party license texts and notices (0:/os/licenses)
* Tiny C Compiler (tcc) with MontaukOS headers and libraries (0:/sdk/tcc)
* Lua 5.4 interpreter (lua) with headers and static library (0:/sdk/lua)
* Montauk SDK - native GCC/G++ and binutils (0:/sdk) - SDK image only
* Flat Remix icon pack
* JetBrains Mono, Noto Serif, Roboto, and C059 Roman (open Century Schoolbook) TrueType fonts
* DOOM engine with SDL_mixer audio support (game data not included)
* Experimental HTTP server (httpd) and default page (0:/www/index.html)
* Default wallpaper - blossoms by Nikhil Kumar, Unsplash (0:/os/wallpapers)
MontaukOS 0.1.9 (August 13, 2026) MontaukOS 0.1.9 (August 13, 2026)
(a28b8ad0b2932203b812e967f35f029f060fd6b5) (a28b8ad0b2932203b812e967f35f029f060fd6b5)
+10 -4
View File
@@ -70,10 +70,16 @@ sudo apt install build-essential nasm git curl xorriso mtools gdisk qemu-system-
### Cross-Architecture Builds ### 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 ```bash
make ARCH=aarch64 make kernel ARCH=aarch64 # kernel/bin-aarch64/kernel
make ARCH=riscv64 make kernel ARCH=aarch64 KERNEL_LD=/path/to/ld.lld
make ARCH=loongarch64 ./scripts/check-arch-neutral.sh # boundary check + aarch64 compile
``` ```
+207
View File
@@ -0,0 +1,207 @@
# 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/ the port in progress (see below)
```
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 port
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, and:
- Exception vectors (`Vectors.S`, installed in `EarlyInitialize`). Every
exception saves one 288-byte frame that is both `Arch::PanicFrame` and
`Arch::SyscallFrame`. EL0 `svc` goes to `SyscallDispatch`; other
synchronous exceptions go to `ArchSyncException` (`Exceptions.cpp`), which
grows user stacks and lazy anonymous memory, kills faulting processes with
a crash report, and panics on kernel faults.
- Translation tables (`Paging.cpp`): 4 KiB granule, 48-bit VAs. The kernel
half lives in its own TTBR1 table; `KernelRoot()` is the TTBR0 table of
kernel contexts (EFI identity mappings only, so null pointers fault). The
HHDM maps only the memory map's RAM and firmware regions, never the holes
between them (device space on ARM). MAIR: 0 Normal WB, 1 Normal NC
(`MapWC`), 2 Device-nGnRE (`MapMMIO`).
- GICv2 (`Interrupts.cpp`, from the MADT): IRQ slots map onto INTIDs --
`IRQ_TIMER` onto the generic timer's PPI, the MSI slots (24-45) onto the
SPIs of a GICv2m MSI frame (`ComposeMsi`), the IPI slots onto SGIs 0/1,
and the platform lines (1-23) onto the SPIs `AssignPlatformLine` gives
them (see "Interrupt lines" below).
Interrupts are completed (EOI) before the handler runs, as on x86; a
level-triggered source drops its line first in an ack hook.
- Generic timer tick (`Timer.cpp`): the EL1 virtual timer at
`CNTFRQ_EL0`, its PPI from the GTDT, re-armed from the ack hook.
- PSCI `SYSTEM_RESET` / `SYSTEM_OFF` (`Arch::Reboot` / `Arch::PowerOff`)
through the conduit the FADT's ARM_BOOT_ARCH names. Hardware-reduced
ACPI has no PM1 block, so `AcpiShutdown` leaves power-off to the port.
- `Arch::SyncInstructionCache` after the ELF loader writes code: ARM
instruction fetch does not snoop data writes (QEMU hides this; real
cores do not). EL0 may maintain caches itself (`SCTLR_EL1.UCI`).
- SMP (`Smp.cpp`): Limine wakes each secondary CPU (PSCI `CPU_ON`
underneath) on the bootloader's tables; it programs its own system
registers and the kernel translation regime (`Bringup.hpp`), its banked
GIC CPU interface and timer PPI, then idles until a reschedule SGI.
TLB maintenance is broadcast (`tlbi ...is`).
The generic kernel is compiled `-fsigned-char` for AArch64: it was written
for x86's signed plain `char`.
Building it needs `clang` and `ld.lld`; without a system `ld.lld` the
kernel makefile uses the one rustup ships
(`~/.rustup/toolchains/*/lib/rustlib/*/bin/gcc-ld/ld.lld`), and `KERNEL_LD=`
overrides both. `make run ARCH=aarch64` boots the full image (desktop, apps,
USB keyboard and mouse) on QEMU `virt` with a GICv2 and Cortex-A72 cores, the
Raspberry Pi 4/400's configuration; it needs `qemu-system-aarch64` and
`qemu-efi-aarch64`, and the userspace compiler
(`./toolchain/build-montauk-toolchain.sh aarch64`). `KERNEL_ONLY=1` builds an
image without userspace.
The kernel console is mirrored to the serial port the ACPI SPCR table names
(PL011, SBSA UART, 16550 or the BCM2835 mini UART; `Drivers/Serial/Uart.cpp`),
including the log written before the port was found. On QEMU that is
`-serial stdio`.
## Interrupt lines
Drivers for PCI functions without MSI read the Interrupt Line register and
pass it to `Arch::EnableLegacyIrq`. On x86 the firmware fills that register
with the function's I/O APIC input (`Arch::PlatformLinesAreFixed()`). On
other platforms `Pci::AssignInterruptLines` (`Pci/PciAcpi.cpp`) fills it
instead: it finds each pin's global system interrupt in the root bridge's
`_PRT` (swizzling through PCI-to-PCI bridges, resolving PNP0C0F link
devices through `_CRS`), binds the GSI to a platform line with
`Hal::GsiRouting::LineForGsi` and writes the line number into the register.
Devices that ACPI describes directly (platform devices) get their line from
`LineForGsi` too.
## DMA
`Memory/Dma.hpp` serves devices whose DMA does not snoop the CPU caches
(ACPI `_CCA = 0`): pages for rings and descriptors come uncached, and
`SyncForDevice` / `SyncForCpu` clean and invalidate streaming buffers
around a transfer (`Arch::CleanDataCache` / `InvalidateDataCache`). For
coherent devices every call is the plain allocation or a no-op. The xHCI
driver uses it; it also orders its TRB writes with `Arch::DmaWriteFence`
(the cycle bit last, a control TD's Setup TRB after its Data and Status
TRBs) and its event reads with `Arch::DmaReadFence`. Those fences cost
nothing on x86 and are required on ARM, where stores to normal memory and
device registers may otherwise be observed out of order. The other PCI
drivers (AHCI, NVMe, e1000, HDA) do not have them yet: they work on QEMU,
which does not reorder, but need them before they meet real ARM hardware.
## Raspberry Pi 4 / 400
See `docs/raspberry-pi.md`: `make ARCH=aarch64 rpi4-sd`, the firmware it
expects and how the Pi's hardware is reached.
## Userspace
The programs are architecture-neutral C/C++ built by `<arch>-montauk-gcc`
(see `toolchain/README.md`). `programs/mk/arch.mk` holds everything
architecture-specific in the build: the toolchain prefix, FP/SIMD flag sets
(`ARCH_FP_FLAGS` for GUI apps, `ARCH_NOFP_FLAGS` for system programs) and the
output layout. x86_64 keeps `bin/`, `obj/` and libraries next to their
sources; other architectures build beside them into `bin-<arch>/`,
`obj-<arch>/` and `lib/<name>/build-<arch>/`, so both can be built from one
tree. The few architecture-specific sources are the raw system-call
wrappers (`#if defined(__x86_64__)` / `__aarch64__` blocks in
`montauk/syscall.h`, `libc/montauk.h`, `lib/libc/libc.c` and
`lib/libc/crt/crt1.c`), `lib/libc/arch/setjmp-<arch>.S` and the scanout
store fence in `gui/framebuffer.hpp`.
Not built for AArch64: tcc (it generates x86 code), the on-OS SDK and the
Rust programs (their `x86_64-unknown-montauk` target).
## 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.
+344
View File
@@ -0,0 +1,344 @@
# Raspberry Pi 4 / 400
MontaukOS runs on the Raspberry Pi 4 and 400 through the Pi Firmware Task
Force's UEFI firmware ([pftf/RPi4](https://github.com/pftf/RPi4), an EDK2
build) in ACPI mode, booting Limine like any other UEFI machine.
## Building and writing the SD card
```bash
./toolchain/build-montauk-toolchain.sh aarch64 # once
make ARCH=aarch64 rpi4-sd # -> montauk-rpi4.img
sudo dd if=montauk-rpi4.img of=/dev/sdX bs=4M conv=fsync
```
Or use the imager, which also updates a card in place:
```bash
make rpi4-imager # = ./scripts/montauk-imager.py
```
### The imager
`scripts/montauk-imager.py` is a Tk window (`python3-tk`) that lists the SD
cards and USB drives attached (never the host's own disks) and what is on
them, and either writes a fresh image or updates the MontaukOS on a card. It
runs as the user and does the writing by starting itself again through
`pkexec` (it asks for the password each time). The same operations work from
a terminal: `list`, and as root `flash`, `update` and `expand`
(`--help` for the options). Images can be raw or `.gz`, `.xz` or `.zip`
compressed; **Build** runs `make ARCH=aarch64 rpi4-sd`.
- **Write a fresh image** erases the card, writes the image, clears the last
MiB of the card (a backup GPT from an earlier OS) and reads the card back to
verify it. *Expand* grows the root to fill the card right away instead of
leaving it to `firstboot.elf`.
- **Update** leaves the partition table alone and finds both partitions by
label, so a root that has grown to fill the card (or was resized by hand)
stays as it is. On the ESP every file is written except `RPI_EFI.fd`, which
holds the firmware's own settings (*Update the UEFI firmware* replaces it);
a changed `config.txt` is kept as `config.txt.bak`. On the root, `os/`,
`apps/`, `sdk/`, `boot/` get the image's new and changed files; `config/`
and `www/` only get files the card lacks, `users/` and `state/` are not
touched. `init.toml` becomes the image's, keeping each service's `enabled`
from the card and appending services only the card has; `capabilities.toml`
becomes the image's plus grants only the card has (old copies: `.old`).
`os/firstboot.elf` is only written if the card still has it, so a grown
card does not grow again. *Remove system files the new image no longer
ships* prunes `os/`, `apps/`, `sdk/` and `boot/` (keeping `os/btkeys.bin`,
the Bluetooth bonds). Unchanged files are skipped and each file is
replaced by a rename; the volume is checked with `e2fsck` before and after,
and its features against what `Fs/Ext2.cpp` mounts.
- **Expand** (a host-side first boot) grows the root partition to the end of
the card, at most 1 TiB, and the volume with `resize2fs`; `firstboot.elf`
then finds nothing to do and deletes itself.
While it works, a runtime udev rule keeps the desktop from automounting the
card's partitions (Linux writes to an ext2 volume as soon as it mounts it).
`scripts/mk-rpi4-sd.sh` downloads the firmware release once into
`rpi4-firmware/` (`PFTF_VERSION=v1.53` by default) and builds an MBR image:
| Partition | Type | Contents |
|---|---|---|
| 1 | 0xEF, FAT32 | the firmware files, Limine (`EFI/BOOT/BOOTAA64.EFI`), `boot/kernel` |
| 2 | 0x83, ext2, label `MONTAUKROOT` | the system tree (the contents of `ramdisk.tar`) |
Limine loads only the kernel; the `limine.conf` on the card has no ramdisk
module. Finding no ramdisk, the kernel mounts the partition labelled
`MONTAUKROOT` as `0:/` (`Fs/Boot.cpp`), read through the SD driver
(`Drivers/Storage/Sdhci`), and the ESP after it. Files load as they are
used instead of the firmware reading the whole 80 MB tree before boot.
The card can also boot from a USB card reader (or the image from a USB
stick): the root then comes through `Drivers/USB/MassStorage.cpp`. A USB
root can register after the boot scan (a SuperSpeed link still training, a
reader still bringing its card up), so a boot that finds no `MONTAUKROOT`
waits up to 10 s for one, servicing USB hot-plug meanwhile.
Unlike the ramdisk, the root is persistent: changes to `0:/` survive a
reboot, so shut down from the desktop rather than pulling power.
The image is sized to its contents (`ROOT_FREE_MIB`, default 64, sets the
free space on the root) and grows to fill the card on first boot.
### First boot: growing the root
Only the SD image ships `0:/os/firstboot.elf` (`programs/src/firstboot`,
built into `programs/bin-aarch64-rpi/`, which the script lays over the
system tree), and the script adds its init service, ahead of all others,
and its capability grant (`storage_admin`, `system_image`) to the image's
`init.toml` and `capabilities.toml`. On first boot it shows a status card
in the login screen's style while it calls `SYS_FS_GROW` on drive 0 in a
loop:
1. The kernel grows the MBR entry of the root partition as far as the
filesystem can use (to the end of the card, or 1 TiB), on a 1 MiB
boundary (`Gpt::ResizeMbrPartition`).
2. The ext2 driver grows the mounted volume (`Fs/Ext2.cpp`, "Online
grow"): each call writes the bitmaps and zeroed inode tables of a few
new block groups past the old end, then the last call commits the
bitmaps, descriptor table and superblock. The descriptor table grows
into the blocks `mke2fs -O resize_inode -E resize=2^28` reserved; the
leftover reservation is freed and `resize_inode` cleared, so the volume
grows once.
The new groups' inode tables are the bulk of the writes (0.1% of the card,
one inode per 256 KiB), written as long multi-block runs. Once the volume has grown, `firstboot.elf` deletes
itself and init skips its (optional) service from then on. If it fails it
stays and the next boot tries again; an interrupted grow is safe, since the
new groups are only referenced once the superblock is written, and a
partition larger than its filesystem is valid.
`make ARCH=aarch64 rpi4-sd ROOTFS=ramdisk` builds the old layout instead:
one FAT32 partition with `boot/ramdisk.tar`, which Limine loads through the
firmware. It boots slowly but does not depend on the kernel's SD driver.
`make ARCH=aarch64 run-rpi4-sd` boots the ext2 image on QEMU with the card
on `sdhci-pci`, so the root comes through the same driver. QEMU's UEFI has
no SD driver, so the card's ESP is also attached on its own over virtio for
the firmware to load Limine from. The card is padded to
`RPI4_QEMU_CARD_MIB` (default 4096), and every run starts from a fresh
copy, so every run is a first boot.
The image's `RPI_EFI.fd` comes with a **MontaukOS** boot entry first in the
firmware's boot order (`scripts/rpi4-efi-bootorder.py` writes it into the
release's empty variable store), so the card boots on its own instead of
falling behind the PXE/HTTP entries the firmware creates. The entry names the
ESP by the image's MBR disk ID and partition number, so it works from the SD
slot or a USB drive. A card whose `RPI_EFI.fd` predates it keeps its old
boot order until it is updated with *Update the UEFI firmware*
(`--firmware`), which also copies the image's disk ID to the card.
The firmware defaults are what MontaukOS needs; leave them:
- **System Table Mode: ACPI.** The kernel reads ACPI only.
- **Limit RAM to 3 GB: enabled.** The BCM2711's PCIe can only reach the
first 3 GB (its tables say so in `_DMA`). The kernel warns at boot when
RAM extends past a device's `_DMA` range.
## Serial console
The kernel console (the whole boot log, including what was printed before
the port was found) is mirrored to the UART the firmware's SPCR table names.
The firmware's `config.txt` applies the `miniuart-bt` overlay, which puts the
PL011 on GPIO 14/15: header pins 8 (TX) and 10 (RX), ground on pin 6, 115200
8N1, 3.3 V levels. On the Pi 400 the header is on the back.
## How the hardware is reached
| Part | Source | Driver |
|---|---|---|
| 4x Cortex-A72 | MADT, PSCI (TF-A, SMC) | `Arch/aarch64` SMP via Limine MP |
| GIC-400 (GICv2) | MADT | `Arch/aarch64/Interrupts.cpp` |
| Generic timer | GTDT | `Arch/aarch64/Timer.cpp` |
| Display | UEFI GOP (VideoCore) | Limine framebuffer |
| Keyboard (built in), USB ports | VL805 xHCI as ACPI `\_SB.SCB0.XHC0` (`PNP0D10`) | `Drivers/USB/Xhci.cpp` platform probe |
| USB 2 devices (built-in keyboard too) | VIA hub on the VL805's USB 2 root port | `Drivers/USB/Hub.cpp` |
| Reset, power-off | PSCI | `Arch::Reboot`, `Arch::PowerOff` |
| Bluetooth (CYW43455 on the 4B, CYW43456 on the 400) | `BCM2EA6` (`\_SB.GDV0.BTH0`) on the mini UART (`BCM2836`) | `Drivers/Bluetooth/Broadcom.cpp`, `Drivers/Serial/BtUart.cpp` |
| VideoCore firmware mailbox (radio power, clocks, board MAC) | `BCM2849` (`RPIQ`) | `Drivers/Platform/RpiFirmware.cpp` |
| GPIO pin functions | `BCM2845` (`GPI0`) | `Drivers/Platform/RpiGpio.cpp` |
| Ethernet (GENET v5 MAC, BCM54213PE PHY) | `BCM6E4E` (`\_SB.ETH0`) | `Drivers/Net/Genet.cpp` |
| Wi-Fi (the same CYW4345x, SDIO) | Arasan SD controller `BCM2847`, GPIO 34-39 | `Drivers/Net/Wifi/Brcmf*.cpp` |
The VL805 sits behind the BCM2711's PCIe, whose config space is not ECAM
(the root bridge's `_DSD` says `linux-ecam-quirk-id = "bcm2711"`), so the
kernel does not enumerate PCI there. The firmware has already brought up the
link and the VL805 and describes the controller as a platform device: a
register block at `0x600000000`, GSI 175, and `_CCA = 0`, i.e. its DMA does
not snoop the CPU caches. `XHC0._INI` (run by the AML interpreter) enables
the controller's PCI memory decoding and bus mastering. The xHCI driver then
keeps its rings and contexts in uncached memory, bounces control-transfer
data through an uncached page, and cleans/invalidates the caller buffers of
bulk transfers (`Memory/Dma.hpp`). It also watches the event ring from the
timer tick, so input keeps working if the INTx line never fires.
The VL805 has one USB 2 root port (port 1) and four USB 3 ones. An on-board
VIA hub (`2109:3431`) sits on the USB 2 port, and the USB 2 lines of every
socket, and the Pi 400's internal keyboard (`04d9:0007`), hang off it: a
keyboard or mouse is always a low/full-speed device behind a high-speed hub,
reached through that hub's transaction translator.
## Bluetooth
The radio's Bluetooth half speaks HCI over a UART (H4) on GPIO 30-33 (CTS,
RTS, TXD, RXD); its power switch, `BT_ON`, is line 0 of the VideoCore
firmware's GPIO expander, reached through the mailbox. The firmware tables
describe it as `BTH0`, whose `UARTSerialBus` names the UART it uses: with
the `miniuart-bt` overlay the PL011 is the console, so Bluetooth has the mini
UART. The kernel refuses to take a UART that is the SPCR console.
At boot the driver probe routes the pins (`ALT5` for the mini UART, `ALT3`
for the PL011), starts the UART (interrupt driven, 16 KiB receive and
transmit rings) and the H4 transport under the shared HCI layer. Once the
root is mounted the idle loop finishes, as for USB adapters:
1. power-cycle the controller through `BT_ON`, HCI Reset at 115200 baud;
2. Read Local Version: the ROM's LMP subversion picks the patch file,
`0:/os/firmware/brcm/BCM4345C0.hcd` (0x6119, 4B) or `BCM4345C5.hcd`
(0x6606, 400), shipped unmodified from Raspberry Pi OS's bluez-firmware;
3. raise both ends to the operating speed (vendor `0xFC18`);
4. Download Minidriver (`0xFC2E`), then every record of the `.hcd` file
(Write RAM, then Launch RAM), as Linux's `btbcm` does;
5. the patched firmware restarts at 115200: reset, raise the speed again.
The mini UART's FIFO is 8 bytes deep. Its hardware RTS is tried first; if
the controller answers with it on, the link runs at 921600 baud, otherwise
at 460800 without flow control (what Raspberry Pi OS uses), and at 115200
if the UART has no interrupt. The baud rate derives from the VPU core
clock, so `mk-rpi4-sd.sh` pins it (`core_freq=500`, `core_freq_min=500` in
`config.txt`), as the overlay's documentation requires. A failed fast
bring-up is retried entirely at 115200.
The CYW43456 reports a chip-wide default address (`43:45:C5:00:1F:AC`);
the driver replaces it with the board's Ethernet MAC from the firmware.
`0:/config/bluetooth.toml` still overrides both (Broadcom `0xFC01`).
## Ethernet
The BCM2711's own Gigabit MAC, GENET v5 (a UniMAC behind a DMA engine),
talks RGMII to a BCM54213PE PHY at MDIO address 1. The firmware describes
it as `BCM6E4E`: a 64 KiB register block, two interrupts (the driver uses
the first, which carries the default queue), `_CCA = 0`, and a `_DSD` with
`phy-mode` (`rgmii-rxid`: the PHY delays the receive clock, the MAC the
transmit clock) and `brcm,max-dma-burst-size`.
The driver uses only the default queue (ring 16), with all 256 descriptors
of each direction. The descriptors live in the MAC's own SRAM, so only the
2 KiB packet buffers need cache maintenance. Received frames start two
bytes into their buffer (`RBUF_ALIGN_2B`); neither direction uses the
64-byte status blocks, so there is no checksum offload. The UniMAC accepts
broadcast and its own address only (the MDF filter).
The interrupt only acknowledges and masks; frames are dispatched from the
idle bottom half, which also checks the ring itself, so receive works if
the interrupt never arrives. The PHY is polled once a second (its interrupt
pin is not described); a link change reprograms the RGMII block and the
UniMAC's speed and duplex. A link the firmware already negotiated is kept,
otherwise the PHY is reset and advertises 10/100 and 1000 full duplex.
The MAC address is the board's, read from OTP through the firmware mailbox
(the one the Bluetooth driver uses too); the address the firmware's own
driver programmed is the fallback. `dhcp.elf` waits up to 10 s for a wired
link, since autonegotiation takes about 3 s after the PHY reset.
## Wi-Fi
The radio's WLAN half is an SDIO card on the BCM2711's older SD controller,
the Arasan-derived EMMC (`BCM2847`, `0x7E300000`); the card slot is on
EMMC2, so the two never share a bus. Its pins are GPIO 34-39 (ALT3; clock
without a pull, the rest pulled up, as Linux's `sdio_pins`) and its power
switch, `WL_ON`, is line 1 of the firmware's GPIO expander. The probe
claims the controller only while the slot is on EMMC2 (`BRCME88C` present);
if the tables do not describe the Arasan, its address is taken as EMMC2's
less 256 KiB.
It is a FullMAC radio: the firmware does the 802.11 MLME itself and the host
sees Ethernet frames. The driver follows Linux's brcmfmac (ISC license,
Broadcom; its notice is in `NOTICES.txt`):
| File | Part |
|---|---|
| `BrcmfSdio.cpp` | the Arasan as an SDIO host: CMD5/3/7, 4-bit at 25 MHz, CMD52/CMD53, polled PIO, 32-bit register access |
| `BrcmfChip.cpp` | backplane window (function 1), clocks, the enumeration ROM's core list, firmware + NVRAM download, function 2, the firmware console |
| `BrcmfProto.cpp` | SDPCM framing and transmit credit, CDC ioctls, BDC data, events, the receive pump, the transmit queue |
| `BrcmfConnect.cpp` | preinit and CLM, escan, join, keys, link state |
Once the root is mounted the idle loop:
1. routes the pins, switches the EMMC clock on, pulses `WL_ON`, enumerates
the card (function 1 blocks of 64 bytes, function 2 of 512);
2. requests the ALP clock, reads the chip ID: `0x4345` revision 9 is the
CYW43456 (Pi 400), any other revision the CYW43455 (Pi 4B), which picks
`0:/os/firmware/brcm/brcmfmac43456-sdio.*` or `brcmfmac43455-sdio.*`
(shipped unmodified from Raspberry Pi OS's firmware-nonfree);
3. halts the ARM Cortex-R4, holds the 802.11 core in reset, sizes the RAM
from the CR4's banks, writes the firmware at `0x198000` and the NVRAM
(stripped to `key=value\0` pairs and a length token) at the top of RAM,
reading both back to verify, puts the reset vector at address 0 and
releases the CPU;
4. waits for the HT clock, sends the SDPCM protocol version through the
mailbox, enables function 2, the F2 watermark and MES busy signalling of
Linux's CYW43455 settings, save/restore if the chip has it, and the card
interrupt;
5. reads `cur_etheraddr` (the NVRAM's shared placeholder address
`b8:27:eb:74:f2:6c` is replaced with the board address + 1, as on
Raspberry Pi OS), loads the CLM blob (`clmload`, 1400-byte chunks: these
firmware builds carry no regulatory data of their own), turns off glom,
roaming, ARP/ND offload and power save, enables the events it handles and
brings the interface up (`WLC_UP`).
Nothing uses the controller's interrupt: the bus is swept from the idle
loop (and from the timer tick when the cores never idle), every 10 ms while
a scan or a connection is active and every 250 ms otherwise, and at once when
the controller shows the card interrupt. Every sweep reads the SDIO core's
interrupt status, then every frame waiting in function 2 (a 4-byte header
read, then the rest). Ioctls wait for transmit credit and their answer by
sweeping the bus themselves. Frames are delivered with the bus unlocked, and
transmitting never waits for the bus: data frames go into a 32-frame queue
that whoever holds the bus, or the next sweep, sends.
Scans are escans (all channels, active, firmware dwell times); each result
event carries a `brcmf_bss_info_le` whose information elements go into the
same scan table the Intel driver fills from beacons. A join sets `wpaie`
(the RSN element our supplicant builds), `wpa_auth`, `auth` and `wsec`, then
`join` with the BSSID and the chanspec escan reported. The 4-way handshake
runs in the host's supplicant (`Wpa.cpp`) as for the Intel adapter: EAPOL
frames are ordinary Ethernet frames here, and the keys go to the firmware
with `wsec_key` once message 4 has left the transmit queue. The radio does
AES-CCMP only: GCMP networks are refused, as are WPA3-only (SAE), WEP and
TKIP ones.
Unanswered ioctls (a leaky bucket of three), a halt reported through
the mailbox, or repeated bus errors mark the radio failed: the firmware's
console and any trap record are printed to the kernel log (`WiFi-FW`), and
the facade's recovery powers the radio off and repeats the bring-up (at
most three times).
`tests/wifi/run.sh` drives `BrcmfProto.cpp` and `BrcmfConnect.cpp` with the
real supplicant against a simulated firmware: preinit and the CLM chunks,
escan parsing, open and WPA2 joins (an independent authenticator checks
messages 2 and 4 and the installed keys), the wrong passphrase, refusals,
join timeouts and a firmware that stops answering.
## Not supported yet
- SD high-speed mode: the driver transfers by polled PIO at 25 MHz on a
4-bit bus (multi-block CMD18/CMD25 with Auto CMD12), at most 12.5 MB/s;
switching the card to 50 MHz (CMD6) would double that.
- Wi-Fi beyond 802.11 station mode on one interface: no access point mode,
no SDIO high speed (50 MHz), no glom, no firmware supplicant.
- Audio (HDMI via VideoCore, the 3.5 mm jack via PWM).
## Status
Bluetooth works on a Pi 400 (CYW43456), including A2DP music to
headphones. Ethernet has not run yet: QEMU has no GENET model. Wi-Fi has
not run on hardware yet either (QEMU has no model of the radio); only its
protocol and connection layers are tested, in the host harness. It is untested on a Pi 4B (CYW43455), and QEMU has no model of
the radio.
Everything else above is verified on QEMU only, including the Pi-specific path:
the firmware's own XHC0 table, retargeted at QEMU's xHCI and loaded with the
`bcm2711` quirk on QEMU's root bridge, boots to the desktop with non-coherent
DMA. Real hardware has not been tried yet. The ext2 root on the SD card boots on QEMU (`run-rpi4-sd`); the
first-boot grow has been tested only against the kernel's ext2 code in a
host harness (images grown, then checked with `e2fsck`).
+53 -2
View File
@@ -27,6 +27,20 @@ ifeq ($(ARCH),x86_64)
endif endif
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. # User controllable C compiler command.
CC ?= cc CC ?= cc
@@ -139,8 +153,10 @@ ifeq ($(ARCH),aarch64)
override CXX += \ override CXX += \
-target aarch64-unknown-none -target aarch64-unknown-none
endif endif
# Plain char is signed on x86; the generic kernel is written for that.
override SHARED_FLAGS += \ override SHARED_FLAGS += \
-mgeneral-regs-only -mgeneral-regs-only \
-fsigned-char
override LDFLAGS += \ override LDFLAGS += \
-Wl,-m,aarch64elf -Wl,-m,aarch64elf
endif endif
@@ -197,8 +213,10 @@ override CXXFLAGS += \
-std=gnu++20 \ -std=gnu++20 \
-fno-rtti \ -fno-rtti \
-fno-exceptions \ -fno-exceptions \
-fmodules-ts \
$(SHARED_FLAGS) $(SHARED_FLAGS)
ifneq ($(CXX_IS_CLANG),1)
override CXXFLAGS += -fmodules-ts
endif
# Internal linker flags that should not be changed by the user. # Internal linker flags that should not be changed by the user.
override LDFLAGS += \ override LDFLAGS += \
@@ -212,6 +230,9 @@ override LDFLAGS += \
# Use "find" to glob all *.c, *.cpp, *.S, and *.asm files in the tree and obtain the # Use "find" to glob all *.c, *.cpp, *.S, and *.asm files in the tree and obtain the
# object and header dependency file names. # object and header dependency file names.
override SRCFILES := $(shell cd src && find -L * -type f | LC_ALL=C sort) 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 CFILES := $(filter %.c,$(SRCFILES))
override CXXFILES := $(filter %.cpp,$(SRCFILES)) override CXXFILES := $(filter %.cpp,$(SRCFILES))
override OBJCFILES := $(filter %.mm,$(SRCFILES)) override OBJCFILES := $(filter %.mm,$(SRCFILES))
@@ -253,6 +274,11 @@ $(BUILDNO_HDR): $(filter-out $(BUILDNO_OBJ),$(OBJ)) $(PROG_SRC)
# on a clean build (where no .d dependency exists yet). # on a clean build (where no .d dependency exists yet).
$(BUILDNO_OBJ): $(BUILDNO_HDR) $(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. # Link rules for building the C compiler runtime.
cc-runtime-$(ARCH)/cc-runtime.a: GNUmakefile cc-runtime/* cc-runtime-$(ARCH)/cc-runtime.a: GNUmakefile cc-runtime/*
rm -rf cc-runtime-$(ARCH) rm -rf cc-runtime-$(ARCH)
@@ -263,10 +289,35 @@ cc-runtime-$(ARCH)/cc-runtime.a: GNUmakefile cc-runtime/*
CFLAGS="$(CFLAGS)" \ CFLAGS="$(CFLAGS)" \
CPPFLAGS='-isystem ../freestnd-c-hdrs/$(ARCH)/include -DCC_RUNTIME_NO_FLOAT' 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)
# Without a system lld, fall back to the one rustup ships.
KERNEL_LD ?= $(firstword $(shell command -v ld.lld) \
$(wildcard $(HOME)/.rustup/toolchains/*/lib/rustlib/*/bin/gcc-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. # Link rules for the final executable.
bin-$(ARCH)/$(OUTPUT): GNUmakefile linker-$(ARCH).ld $(OBJ) cc-runtime-$(ARCH)/cc-runtime.a bin-$(ARCH)/$(OUTPUT): GNUmakefile linker-$(ARCH).ld $(OBJ) cc-runtime-$(ARCH)/cc-runtime.a
mkdir -p "$$(dirname $@)" mkdir -p "$$(dirname $@)"
ifeq ($(KERNEL_LD),)
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJ) cc-runtime-$(ARCH)/cc-runtime.a -o $@ $(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. # Compilation rules for *.c files.
obj-$(ARCH)/%.c.o: src/%.c GNUmakefile 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 */ /* Any address in this region will do, but often 0xffffffff80000000 is chosen as */
/* that is the beginning of the region. */ /* that is the beginning of the region. */
. = 0xffffffff80000000; . = 0xffffffff80000000;
KernelStartSymbol = .;
/* Define a section to contain the Limine requests and assign it to its own PHDR */ /* Define a section to contain the Limine requests and assign it to its own PHDR */
.limine_requests : { .limine_requests : {
@@ -74,4 +75,6 @@ SECTIONS
*(.eh_frame*) *(.eh_frame*)
*(.note .note.*) *(.note .note.*)
} }
KernelEndSymbol = .;
} }
+1 -1
View File
@@ -16,7 +16,7 @@
#include <Memory/Paging.hpp> #include <Memory/Paging.hpp>
#include <Memory/Heap.hpp> #include <Memory/Heap.hpp>
#include <Libraries/Memory.hpp> #include <Libraries/Memory.hpp>
#include <Timekeeping/ApicTimer.hpp> #include <Timekeeping/Timer.hpp>
using namespace Kt; using namespace Kt;
+7 -12
View File
@@ -7,9 +7,7 @@
#include "AcpiEvents.hpp" #include "AcpiEvents.hpp"
#include <ACPI/AcpiSleep.hpp> #include <ACPI/AcpiSleep.hpp>
#include <Io/IoPort.hpp> #include <Io/IoPort.hpp>
#include <Hal/Apic/Apic.hpp> #include <Arch/Interrupts.hpp>
#include <Hal/Apic/IoApic.hpp>
#include <Hal/Apic/Interrupts.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
@@ -191,24 +189,21 @@ namespace Hal {
// routed. Clear stale status only after the enables are down. // routed. Clear stale status only after the enables are down.
DisableAndClearGpes(); 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 // active-low (ACPI spec requirement). The MADT may have an
// Interrupt Source Override for this IRQ that already sets // Interrupt Source Override for this IRQ that already sets
// these flags; RouteIrq applies overrides automatically. // these flags; RouteLegacyIrq applies overrides automatically.
uint8_t sciVector = IRQ_VECTOR_BASE + (uint8_t)g_sciIrq;
uint8_t bspApicId = (uint8_t)LocalApic::GetId();
// Register the handler before routing to avoid missing events // 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) // Route to the boot CPU (applies firmware polarity/trigger overrides)
IoApic::RouteIrq((uint8_t)g_sciIrq, sciVector, bspApicId); Arch::RouteLegacyIrq((uint8_t)g_sciIrq);
g_initialized = true; g_initialized = true;
KernelLogStream(OK, "ACPI") << "ACPI events initialized (SCI IRQ " KernelLogStream(OK, "ACPI") << "ACPI events initialized (SCI IRQ "
<< base::dec << (uint64_t)g_sciIrq << ", vector " << base::dec << (uint64_t)g_sciIrq << ")";
<< (uint64_t)sciVector << ")";
} }
void Reinitialize() { void Reinitialize() {
+21 -21
View File
@@ -5,6 +5,7 @@
*/ */
#include "AcpiShutdown.hpp" #include "AcpiShutdown.hpp"
#include <Arch/Cpu.hpp>
#include "AcpiSleep.hpp" #include "AcpiSleep.hpp"
#include "AcpiEvents.hpp" #include "AcpiEvents.hpp"
#include <ACPI/FADT.hpp> #include <ACPI/FADT.hpp>
@@ -37,8 +38,24 @@ namespace Hal {
return; return;
} }
// Map and parse the DSDT to find \_S5_
auto* dsdt = (void*)Memory::HHDM(fadt.DsdtAddress); auto* dsdt = (void*)Memory::HHDM(fadt.DsdtAddress);
AML::InitializeInterpreter(dsdt, xsdt);
if (AML::GetInterpreter().IsInitialized()) {
AcpiDevices::DeviceList devices{};
AcpiDevices::EnumerateAll(devices);
}
AcpiSleep::Initialize(xsdt);
// Hardware-reduced ACPI (ARM) has no PM1 registers: the
// architecture powers off instead (Arch::PowerOff).
if ((fadt.Flags & FADT::FlagHardwareReduced) || fadt.PM1aControlBlock == 0) {
KernelLogStream(INFO, "ACPI") << "No PM1 control block - ACPI S5 shutdown unavailable";
return;
}
AML::S5Object s5 = AML::FindS5(dsdt); AML::S5Object s5 = AML::FindS5(dsdt);
if (!s5.Valid) { if (!s5.Valid) {
@@ -54,23 +71,6 @@ namespace Hal {
KernelLogStream(OK, "ACPI") << "ACPI shutdown initialized (PM1a=" << base::hex KernelLogStream(OK, "ACPI") << "ACPI shutdown initialized (PM1a=" << base::hex
<< (uint64_t)g_pm1aControlBlock << " SLP_TYPa=" << (uint64_t)g_slpTypA << ")"; << (uint64_t)g_pm1aControlBlock << " SLP_TYPa=" << (uint64_t)g_slpTypA << ")";
// Initialize the full AML interpreter with the DSDT.
// This loads the entire DSDT namespace, enabling device enumeration,
// method evaluation, and field I/O for the rest of the kernel.
AML::InitializeInterpreter(dsdt, xsdt);
// Enumerate ACPI devices now that the namespace is loaded
if (AML::GetInterpreter().IsInitialized()) {
AcpiDevices::DeviceList devices{};
AcpiDevices::EnumerateAll(devices);
}
// Initialize S3 suspend support (reads FACS and \_S3_ from namespace)
AcpiSleep::Initialize(xsdt);
// Note: ACPI event handling (SCI, power button) is initialized
// from Main.cpp after the APIC subsystem is ready.
} }
bool IsAvailable() { bool IsAvailable() {
@@ -95,7 +95,7 @@ namespace Hal {
} }
} }
asm volatile("cli"); Arch::DisableInterrupts();
// Phase 1: Write SLP_TYP to PM1x_CNT without SLP_EN, // Phase 1: Write SLP_TYP to PM1x_CNT without SLP_EN,
// preserving existing register bits // preserving existing register bits
@@ -110,7 +110,7 @@ namespace Hal {
} }
// Flush all caches before entering sleep state // Flush all caches before entering sleep state
asm volatile("wbinvd"); Arch::WritebackInvalidateCaches();
// Phase 2: Assert SLP_EN to trigger the transition // Phase 2: Assert SLP_EN to trigger the transition
Io::Out16(pm1a | SLP_EN, (uint16_t)g_pm1aControlBlock); Io::Out16(pm1a | SLP_EN, (uint16_t)g_pm1aControlBlock);
@@ -122,7 +122,7 @@ namespace Hal {
// Halt and wait for the hardware to power off // Halt and wait for the hardware to power off
for (;;) { for (;;) {
asm volatile("hlt"); Arch::Halt();
} }
} }
}; };
+4 -15
View File
@@ -75,21 +75,6 @@ namespace Hal {
// CPU State (saved across S3) // 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. // Returns 0 on successful resume, or -1 if S3 is unavailable.
int Suspend(); 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 "AcpiDevices.hpp"
#include <ACPI/AML/AmlInterpreter.hpp> #include <ACPI/AML/AmlInterpreter.hpp>
#include <CppLib/Spinlock.hpp> #include <CppLib/Spinlock.hpp>
#include <Timekeeping/ApicTimer.hpp> #include <Timekeeping/Timer.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
+2
View File
@@ -107,6 +107,8 @@ namespace Hal {
result.AcpiEnable = fadt->AcpiEnable; result.AcpiEnable = fadt->AcpiEnable;
result.AcpiDisable = fadt->AcpiDisable; result.AcpiDisable = fadt->AcpiDisable;
result.Flags = fadt->Flags; result.Flags = fadt->Flags;
result.ArmBootArchFlags = fadt->Header.Length >= offsetof(Table, ArmBootArchFlags) + 2
? fadt->ArmBootArchFlags : 0;
result.Valid = true; result.Valid = true;
KernelLogStream(INFO, "FADT") << "DSDT at " << base::hex << result.DsdtAddress; KernelLogStream(INFO, "FADT") << "DSDT at " << base::hex << result.DsdtAddress;
+8
View File
@@ -69,6 +69,13 @@ namespace Hal {
uint8_t X_GPE1Block[12]; uint8_t X_GPE1Block[12];
} __attribute__((packed)); } __attribute__((packed));
// FADT Flags
constexpr uint32_t FlagHardwareReduced = 1u << 20;
// ARM_BOOT_ARCH flags (ACPI 5.1+)
constexpr uint16_t ArmBootPsciCompliant = 1u << 0;
constexpr uint16_t ArmBootPsciUseHvc = 1u << 1;
struct ParsedFADT { struct ParsedFADT {
uint64_t DsdtAddress; uint64_t DsdtAddress;
uint64_t FacsAddress; uint64_t FacsAddress;
@@ -91,6 +98,7 @@ namespace Hal {
uint8_t AcpiEnable; uint8_t AcpiEnable;
uint8_t AcpiDisable; uint8_t AcpiDisable;
uint32_t Flags; uint32_t Flags;
uint16_t ArmBootArchFlags; // 0 when the table predates the field
bool Valid; bool Valid;
}; };
+41
View File
@@ -0,0 +1,41 @@
/*
* GsiRouting.cpp
* Platform interrupt lines for ACPI global system interrupts
* Copyright (c) 2026 Daniel Hammer
*/
#include "GsiRouting.hpp"
#include <Arch/Interrupts.hpp>
#include <CppLib/Spinlock.hpp>
namespace Hal::GsiRouting {
static constexpr uint8_t FirstLine = 1;
static constexpr uint8_t LineCount = 24;
static uint32_t g_lineGsi[LineCount] = {};
static uint8_t g_nextLine = FirstLine;
static kcp::Mutex g_lock;
uint8_t LineForGsi(uint32_t gsi, bool levelTriggered, bool activeLow) {
if (Arch::PlatformLinesAreFixed()) return 0;
g_lock.Acquire();
for (uint8_t line = FirstLine; line < g_nextLine; line++) {
if (g_lineGsi[line] == gsi) {
g_lock.Release();
return line;
}
}
uint8_t line = 0;
if (g_nextLine < LineCount &&
Arch::AssignPlatformLine(g_nextLine, gsi, levelTriggered, activeLow)) {
line = g_nextLine++;
g_lineGsi[line] = gsi;
}
g_lock.Release();
return line;
}
}
+19
View File
@@ -0,0 +1,19 @@
/*
* GsiRouting.hpp
* Platform interrupt lines for ACPI global system interrupts
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Hal::GsiRouting {
// The platform line (1-23) that delivers global system interrupt `gsi`,
// assigning one on first use (Arch::AssignPlatformLine); every device on
// one GSI shares its line. Drivers register their handler on the line
// and call Arch::EnableLegacyIrq. Returns 0 where the platform's lines
// are wired by the hardware (x86) or when none is left.
uint8_t LineForGsi(uint32_t gsi, bool levelTriggered, bool activeLow);
}
+6 -6
View File
@@ -11,8 +11,8 @@
#include <Drivers/Audio/IntelHda.hpp> #include <Drivers/Audio/IntelHda.hpp>
#include <Drivers/Audio/Mixer.hpp> #include <Drivers/Audio/Mixer.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <Drivers/USB/Bluetooth/Bluetooth.hpp> #include <Drivers/Bluetooth/Bluetooth.hpp>
#include <Drivers/USB/Bluetooth/A2dp.hpp> #include <Drivers/Bluetooth/A2dp.hpp>
#include <Sched/Scheduler.hpp> #include <Sched/Scheduler.hpp>
#include "Syscall.hpp" #include "Syscall.hpp"
@@ -55,10 +55,10 @@ namespace montauk::abi {
return Drivers::Audio::Mixer::SetOutput( return Drivers::Audio::Mixer::SetOutput(
(Drivers::Audio::Mixer::Output)value); (Drivers::Audio::Mixer::Output)value);
if (cmd == AUDIO_CTL_BT_STATUS) { if (cmd == AUDIO_CTL_BT_STATUS) {
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0; if (!Drivers::Bluetooth::IsInitialized()) return 0;
if (Drivers::USB::Bluetooth::A2dp::IsReady()) return 2; if (Drivers::Bluetooth::A2dp::IsReady()) return 2;
return Drivers::USB::Bluetooth::A2dp::GetState() return Drivers::Bluetooth::A2dp::GetState()
== Drivers::USB::Bluetooth::A2dp::State::Idle == Drivers::Bluetooth::A2dp::State::Idle
? 0 : 1; ? 0 : 1;
} }
+22 -22
View File
@@ -6,8 +6,8 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <Drivers/USB/Bluetooth/Bluetooth.hpp> #include <Drivers/Bluetooth/Bluetooth.hpp>
#include <Drivers/USB/Bluetooth/Hci.hpp> #include <Drivers/Bluetooth/Hci.hpp>
#include <Libraries/Memory.hpp> #include <Libraries/Memory.hpp>
#include <Sched/Scheduler.hpp> #include <Sched/Scheduler.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
@@ -19,14 +19,14 @@ namespace montauk::abi {
static int64_t Sys_BtScan(BtScanResult* buf, int maxCount, uint32_t timeoutMs) { static int64_t Sys_BtScan(BtScanResult* buf, int maxCount, uint32_t timeoutMs) {
if (!buf || maxCount <= 0) return InvalidArgument; if (!buf || maxCount <= 0) return InvalidArgument;
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice; if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
// Use a stack buffer for kernel-side results (max 16) // Use a stack buffer for kernel-side results (max 16)
Drivers::USB::Bluetooth::Hci::InquiryDevice tmpBuf[16]; Drivers::Bluetooth::Hci::InquiryDevice tmpBuf[16];
int kMax = maxCount; int kMax = maxCount;
if (kMax > 16) kMax = 16; if (kMax > 16) kMax = 16;
int found = Drivers::USB::Bluetooth::Scan(tmpBuf, kMax, timeoutMs); int found = Drivers::Bluetooth::Scan(tmpBuf, kMax, timeoutMs);
if (found < 0) return found; if (found < 0) return found;
// Convert kernel InquiryDevice to userspace BtScanResult // Convert kernel InquiryDevice to userspace BtScanResult
@@ -47,38 +47,38 @@ namespace montauk::abi {
static int64_t Sys_BtConnect(const uint8_t* bdAddr) { static int64_t Sys_BtConnect(const uint8_t* bdAddr) {
if (!bdAddr) return InvalidArgument; if (!bdAddr) return InvalidArgument;
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice; if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
return (int64_t)Drivers::USB::Bluetooth::Connect(bdAddr); return (int64_t)Drivers::Bluetooth::Connect(bdAddr);
} }
static int64_t Sys_BtDisconnect(const uint8_t* bdAddr) { static int64_t Sys_BtDisconnect(const uint8_t* bdAddr) {
if (!bdAddr) return InvalidArgument; if (!bdAddr) return InvalidArgument;
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice; if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec
<< (uint64_t)Sched::GetCurrentPid() << (uint64_t)Sched::GetCurrentPid()
<< " entered Bluetooth disconnect stage"; << " entered Bluetooth disconnect stage";
return (int64_t)Drivers::USB::Bluetooth::Disconnect(bdAddr); return (int64_t)Drivers::Bluetooth::Disconnect(bdAddr);
} }
static int64_t Sys_BtSetAddr(const uint8_t* bdAddr) { static int64_t Sys_BtSetAddr(const uint8_t* bdAddr) {
if (!bdAddr) return InvalidArgument; if (!bdAddr) return InvalidArgument;
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice; if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
// bdAddr is a 6-byte buffer, addr[0] = least-significant octet. // bdAddr is a 6-byte buffer, addr[0] = least-significant octet.
return Drivers::USB::Bluetooth::SetAddress(bdAddr) ? Success : GenericFailure; return Drivers::Bluetooth::SetAddress(bdAddr) ? Success : GenericFailure;
} }
static int64_t Sys_BtBonds(BtBondInfo* buf, int maxCount) { static int64_t Sys_BtBonds(BtBondInfo* buf, int maxCount) {
if (!buf || maxCount <= 0) return 0; if (!buf || maxCount <= 0) return 0;
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0; if (!Drivers::Bluetooth::IsInitialized()) return 0;
Drivers::USB::Bluetooth::Hci::BondInfo tmpBuf[8]; Drivers::Bluetooth::Hci::BondInfo tmpBuf[8];
int kMax = maxCount; int kMax = maxCount;
if (kMax > 8) kMax = 8; if (kMax > 8) kMax = 8;
int count = Drivers::USB::Bluetooth::ListBonded(tmpBuf, kMax); int count = Drivers::Bluetooth::ListBonded(tmpBuf, kMax);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
memcpy(buf[i].bdAddr, tmpBuf[i].Addr, 6); memcpy(buf[i].bdAddr, tmpBuf[i].Addr, 6);
@@ -91,20 +91,20 @@ namespace montauk::abi {
static int64_t Sys_BtForget(const uint8_t* bdAddr) { static int64_t Sys_BtForget(const uint8_t* bdAddr) {
if (!bdAddr) return InvalidArgument; if (!bdAddr) return InvalidArgument;
if (!Drivers::USB::Bluetooth::IsInitialized()) return NoDevice; if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
return (int64_t)Drivers::USB::Bluetooth::ForgetDevice(bdAddr); return (int64_t)Drivers::Bluetooth::ForgetDevice(bdAddr);
} }
static int64_t Sys_BtList(BtDevInfo* buf, int maxCount) { static int64_t Sys_BtList(BtDevInfo* buf, int maxCount) {
if (!buf || maxCount <= 0) return 0; if (!buf || maxCount <= 0) return 0;
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0; if (!Drivers::Bluetooth::IsInitialized()) return 0;
Drivers::USB::Bluetooth::Hci::ConnectionInfo tmpBuf[4]; Drivers::Bluetooth::Hci::ConnectionInfo tmpBuf[4];
int kMax = maxCount; int kMax = maxCount;
if (kMax > 4) kMax = 4; if (kMax > 4) kMax = 4;
int count = Drivers::USB::Bluetooth::ListConnected(tmpBuf, kMax); int count = Drivers::Bluetooth::ListConnected(tmpBuf, kMax);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
memcpy(buf[i].bdAddr, tmpBuf[i].BdAddr, 6); memcpy(buf[i].bdAddr, tmpBuf[i].BdAddr, 6);
@@ -123,14 +123,14 @@ namespace montauk::abi {
memset(buf, 0, sizeof(BtAdapterInfo)); memset(buf, 0, sizeof(BtAdapterInfo));
if (!Drivers::USB::Bluetooth::IsInitialized()) { if (!Drivers::Bluetooth::IsInitialized()) {
buf->initialized = 0; buf->initialized = 0;
return NoDevice; return NoDevice;
} }
buf->initialized = 1; buf->initialized = 1;
memcpy(buf->bdAddr, Drivers::USB::Bluetooth::GetBdAddr(), 6); memcpy(buf->bdAddr, Drivers::Bluetooth::GetBdAddr(), 6);
buf->scanning = Drivers::USB::Bluetooth::Hci::IsInquiryActive() ? 1 : 0; buf->scanning = Drivers::Bluetooth::Hci::IsInquiryActive() ? 1 : 0;
// Copy adapter name // Copy adapter name
const char* name = "MontaukOS"; const char* name = "MontaukOS";
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once #pragma once
#define MONTAUK_BUILD_NUMBER 288 #define MONTAUK_BUILD_NUMBER 123
+94 -71
View File
@@ -1,6 +1,6 @@
/* /*
* Clipboard.hpp * Clipboard.hpp
* SYS_CLIPBOARD_* plain-text clipboard syscalls * SYS_CLIPBOARD_* syscalls: per-user multi-format clipboard
* Copyright (c) 2026 Daniel Hammer * Copyright (c) 2026 Daniel Hammer
*/ */
@@ -11,24 +11,25 @@
#include <CppLib/Spinlock.hpp> #include <CppLib/Spinlock.hpp>
#include "Syscall.hpp" #include "Syscall.hpp"
#include "DataTransfer.hpp"
namespace montauk::abi { namespace montauk::abi {
static constexpr int ClipboardMaxSlots = 32; static constexpr int ClipboardMaxSlots = 32;
// One clipboard per user. It holds a multi-format transfer; the text
// syscalls read and replace its TRANSFER_MIME_TEXT format, so text-only
// apps and multi-format apps share the same clipboard.
struct ClipboardSlot { struct ClipboardSlot {
bool inUse; bool inUse;
char user[32]; char user[32];
char* text; DataTransfer::Transfer transfer;
uint32_t len;
uint64_t serial; uint64_t serial;
}; };
static ClipboardSlot g_clipboardSlots[ClipboardMaxSlots]; static ClipboardSlot g_clipboardSlots[ClipboardMaxSlots];
static kcp::Mutex g_clipboardLock; static kcp::Mutex g_clipboardLock;
static int Sys_ClipboardClear();
static const char* ClipboardCurrentUser() { static const char* ClipboardCurrentUser() {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return nullptr; if (proc == nullptr) return nullptr;
@@ -81,57 +82,73 @@ namespace montauk::abi {
freeSlot->inUse = true; freeSlot->inUse = true;
ClipboardCopyUser(freeSlot->user, user); ClipboardCopyUser(freeSlot->user, user);
freeSlot->text = nullptr; freeSlot->transfer.count = 0;
freeSlot->len = 0;
freeSlot->serial = 0; freeSlot->serial = 0;
return freeSlot; return freeSlot;
} }
// Swap `incoming` into the caller's clipboard (it becomes empty) and free
// what was there. An empty transfer clears the clipboard.
static int ClipboardReplace(DataTransfer::Transfer& incoming) {
const char* currentUser = ClipboardCurrentUser();
if (currentUser == nullptr) {
DataTransfer::Free(incoming);
return GenericFailure;
}
char user[32];
ClipboardCopyUser(user, currentUser);
DataTransfer::Transfer old;
old.count = 0;
g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardEnsureSlotLocked(user);
if (slot == nullptr) {
g_clipboardLock.Release();
DataTransfer::Free(incoming);
return Busy;
}
old = slot->transfer;
slot->transfer = incoming;
slot->serial = ClipboardNextSerial(slot->serial);
g_clipboardLock.Release();
incoming.count = 0;
DataTransfer::Free(old);
return Success;
}
static int Sys_ClipboardClear() {
DataTransfer::Transfer empty;
empty.count = 0;
return ClipboardReplace(empty);
}
static int Sys_ClipboardSetText(const char* data, uint32_t len) { static int Sys_ClipboardSetText(const char* data, uint32_t len) {
if (len == 0) if (len == 0)
return Sys_ClipboardClear(); return Sys_ClipboardClear();
if (data == nullptr || len > CLIPBOARD_MAX_TEXT_BYTES) if (data == nullptr || len > CLIPBOARD_MAX_TEXT_BYTES)
return InvalidArgument; return InvalidArgument;
if (Memory::g_heap == nullptr)
return OutOfMemory;
for (uint32_t i = 0; i < len; i++) { for (uint32_t i = 0; i < len; i++) {
if (data[i] == '\0') if (data[i] == '\0')
return InvalidArgument; return InvalidArgument;
} }
const char* currentUser = ClipboardCurrentUser(); DataTransfer::Transfer transfer;
if (currentUser == nullptr) return GenericFailure; int rc = DataTransfer::BuildText(data, len, transfer);
if (rc != Success) return rc;
return ClipboardReplace(transfer);
}
char user[32]; static int Sys_ClipboardSetData(const TransferItem* items, uint32_t count) {
ClipboardCopyUser(user, currentUser); if (count == 0) return Sys_ClipboardClear();
DataTransfer::Transfer transfer;
char* newText = (char*)Memory::g_heap->Request(len + 1); int rc = DataTransfer::BuildFromUser(items, count, transfer);
if (newText == nullptr) return OutOfMemory; if (rc != Success) return rc;
for (uint32_t i = 0; i < len; i++) return ClipboardReplace(transfer);
newText[i] = data[i];
newText[len] = '\0';
char* oldText = nullptr;
g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardEnsureSlotLocked(user);
if (slot == nullptr) {
g_clipboardLock.Release();
Memory::g_heap->Free(newText);
return -1;
}
oldText = slot->text;
slot->text = newText;
slot->len = len;
slot->serial = ClipboardNextSerial(slot->serial);
g_clipboardLock.Release();
if (oldText != nullptr)
Memory::g_heap->Free(oldText);
return Success;
} }
static int Sys_ClipboardGetInfo(ClipboardInfo* out) { static int Sys_ClipboardGetInfo(ClipboardInfo* out) {
@@ -150,7 +167,8 @@ namespace montauk::abi {
g_clipboardLock.Acquire(); g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardFindSlotLocked(user); ClipboardSlot* slot = ClipboardFindSlotLocked(user);
if (slot != nullptr) { if (slot != nullptr) {
out->textBytes = slot->len; const DataTransfer::Format* text = DataTransfer::Find(slot->transfer, TRANSFER_MIME_TEXT);
out->textBytes = text ? text->len : 0;
out->serial = slot->serial; out->serial = slot->serial;
} }
g_clipboardLock.Release(); g_clipboardLock.Release();
@@ -171,52 +189,57 @@ namespace montauk::abi {
g_clipboardLock.Acquire(); g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardFindSlotLocked(user); ClipboardSlot* slot = ClipboardFindSlotLocked(user);
if (slot == nullptr || slot->len == 0) { if (slot != nullptr && outSerial != nullptr)
if (slot != nullptr && outSerial != nullptr) *outSerial = slot->serial;
*outSerial = slot->serial; const DataTransfer::Format* text =
slot ? DataTransfer::Find(slot->transfer, TRANSFER_MIME_TEXT) : nullptr;
if (text == nullptr || text->len == 0) {
g_clipboardLock.Release(); g_clipboardLock.Release();
return Success; return Success;
} }
*outLen = slot->len; int rc = DataTransfer::CopyOut(*text, buffer, bufferLen, outLen);
if (outSerial != nullptr) *outSerial = slot->serial;
if (buffer == nullptr || bufferLen < slot->len) {
g_clipboardLock.Release();
return BufferTooSmall;
}
for (uint32_t i = 0; i < slot->len; i++)
buffer[i] = slot->text[i];
g_clipboardLock.Release(); g_clipboardLock.Release();
return Success; return rc;
} }
static int Sys_ClipboardClear() { static int Sys_ClipboardFormats(TransferFormatInfo* out, uint32_t max, uint64_t* outSerial) {
if (outSerial != nullptr) *outSerial = 0;
const char* currentUser = ClipboardCurrentUser(); const char* currentUser = ClipboardCurrentUser();
if (currentUser == nullptr) return GenericFailure; if (currentUser == nullptr) return GenericFailure;
char user[32]; char user[32];
ClipboardCopyUser(user, currentUser); ClipboardCopyUser(user, currentUser);
char* oldText = nullptr; g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
uint32_t n = 0;
if (slot != nullptr) {
if (outSerial != nullptr) *outSerial = slot->serial;
n = out ? DataTransfer::ListFormats(slot->transfer, out, max) : 0;
if (out == nullptr || max == 0) n = slot->transfer.count;
}
g_clipboardLock.Release();
return (int)n;
}
static int Sys_ClipboardGetData(const char* mime, void* buffer, uint32_t bufferLen, uint32_t* outLen) {
if (outLen == nullptr) return InvalidArgument;
*outLen = 0;
const char* currentUser = ClipboardCurrentUser();
if (currentUser == nullptr) return GenericFailure;
char user[32];
ClipboardCopyUser(user, currentUser);
g_clipboardLock.Acquire(); g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardEnsureSlotLocked(user); ClipboardSlot* slot = ClipboardFindSlotLocked(user);
if (slot == nullptr) { const DataTransfer::Format* format = slot ? DataTransfer::Find(slot->transfer, mime) : nullptr;
g_clipboardLock.Release(); int rc = format ? DataTransfer::CopyOut(*format, buffer, bufferLen, outLen) : NotFound;
return -1;
}
oldText = slot->text;
slot->text = nullptr;
slot->len = 0;
slot->serial = ClipboardNextSerial(slot->serial);
g_clipboardLock.Release(); g_clipboardLock.Release();
return rc;
if (oldText != nullptr)
Memory::g_heap->Free(oldText);
return Success;
} }
} }
+156
View File
@@ -0,0 +1,156 @@
/*
* DataTransfer.cpp
* Multi-format payloads shared by the clipboard and drag and drop
* Copyright (c) 2026 Daniel Hammer
*/
#include "DataTransfer.hpp"
#include "UserMemory.hpp"
#include <Memory/Heap.hpp>
namespace DataTransfer {
using namespace montauk::abi;
static bool MimeEquals(const char* a, const char* b) {
int i = 0;
for (; a[i] && b[i]; i++) {
char ca = (a[i] >= 'A' && a[i] <= 'Z') ? a[i] + 32 : a[i];
char cb = (b[i] >= 'A' && b[i] <= 'Z') ? b[i] + 32 : b[i];
if (ca != cb) return false;
}
return a[i] == b[i];
}
// Length of the "type/subtype" part, before any ";parameters".
static int MimeBaseLen(const char* mime) {
int n = 0;
while (mime[n] && mime[n] != ';') n++;
while (n > 0 && mime[n - 1] == ' ') n--;
return n;
}
static bool MimeBaseEquals(const char* a, const char* b) {
int la = MimeBaseLen(a);
int lb = MimeBaseLen(b);
if (la != lb) return false;
for (int i = 0; i < la; i++) {
char ca = (a[i] >= 'A' && a[i] <= 'Z') ? a[i] + 32 : a[i];
char cb = (b[i] >= 'A' && b[i] <= 'Z') ? b[i] + 32 : b[i];
if (ca != cb) return false;
}
return true;
}
bool ValidMime(const char* mime) {
if (mime == nullptr) return false;
bool slash = false;
uint32_t i = 0;
for (; mime[i]; i++) {
if (i + 1 >= TRANSFER_MIME_MAX) return false;
char c = mime[i];
if (c < 0x20 || c > 0x7E) return false;
if (c == '/') slash = true;
}
return i > 0 && slash;
}
void Free(Transfer& transfer) {
for (uint32_t i = 0; i < transfer.count; i++) {
if (transfer.formats[i].data != nullptr) Memory::g_heap->Free(transfer.formats[i].data);
transfer.formats[i].data = nullptr;
transfer.formats[i].len = 0;
}
transfer.count = 0;
}
static int AddFormat(Transfer& transfer, const char* mime, const uint8_t* data, uint32_t len) {
if (transfer.count >= TRANSFER_MAX_FORMATS) return InvalidArgument;
Format& f = transfer.formats[transfer.count];
uint32_t m = 0;
for (; mime[m] && m + 1 < TRANSFER_MIME_MAX; m++) f.mime[m] = mime[m];
f.mime[m] = '\0';
// Never hand out a null pointer for an empty format; one byte keeps
// "present but empty" distinct from "absent".
f.data = (uint8_t*)Memory::g_heap->Request(len > 0 ? len : 1);
if (f.data == nullptr) return OutOfMemory;
for (uint32_t i = 0; i < len; i++) f.data[i] = data[i];
f.len = len;
transfer.count++;
return Success;
}
int BuildFromUser(const TransferItem* items, uint32_t count, Transfer& out) {
out.count = 0;
if (items == nullptr || count == 0 || count > TRANSFER_MAX_FORMATS) return InvalidArgument;
if (Memory::g_heap == nullptr) return OutOfMemory;
// Validate everything before allocating anything.
uint64_t total = 0;
for (uint32_t i = 0; i < count; i++) {
if (!UserMemory::String(items[i].mime, TRANSFER_MIME_MAX)) return BadPointer;
if (!ValidMime((const char*)items[i].mime)) return InvalidArgument;
if (items[i].len > 0 && !UserMemory::Range(items[i].data, items[i].len, false)) return BadPointer;
total += items[i].len;
for (uint32_t j = 0; j < i; j++) {
if (MimeEquals((const char*)items[i].mime, (const char*)items[j].mime))
return InvalidArgument;
}
}
if (total > TRANSFER_MAX_BYTES) return InvalidArgument;
for (uint32_t i = 0; i < count; i++) {
int rc = AddFormat(out, (const char*)items[i].mime,
(const uint8_t*)items[i].data, items[i].len);
if (rc != Success) {
Free(out);
return rc;
}
}
return Success;
}
int BuildText(const char* text, uint32_t len, Transfer& out) {
out.count = 0;
if (Memory::g_heap == nullptr) return OutOfMemory;
int rc = AddFormat(out, TRANSFER_MIME_TEXT, (const uint8_t*)text, len);
if (rc != Success) Free(out);
return rc;
}
const Format* Find(const Transfer& transfer, const char* mime) {
if (mime == nullptr) return nullptr;
for (uint32_t i = 0; i < transfer.count; i++) {
if (MimeEquals(transfer.formats[i].mime, mime)) return &transfer.formats[i];
}
bool has_params = false;
for (int i = 0; mime[i]; i++) {
if (mime[i] == ';') { has_params = true; break; }
}
if (has_params) return nullptr;
for (uint32_t i = 0; i < transfer.count; i++) {
if (MimeBaseEquals(transfer.formats[i].mime, mime)) return &transfer.formats[i];
}
return nullptr;
}
uint32_t ListFormats(const Transfer& transfer, TransferFormatInfo* out, uint32_t max) {
uint32_t n = 0;
for (; n < transfer.count && n < max; n++) {
for (uint32_t c = 0; c < TRANSFER_MIME_MAX; c++) out[n].mime[c] = transfer.formats[n].mime[c];
out[n].len = transfer.formats[n].len;
out[n]._reserved = 0;
}
return n;
}
int CopyOut(const Format& format, void* buffer, uint32_t bufferLen, uint32_t* outLen) {
if (outLen != nullptr) *outLen = format.len;
if (format.len == 0) return Success;
if (buffer == nullptr || bufferLen < format.len) return BufferTooSmall;
uint8_t* dst = (uint8_t*)buffer;
for (uint32_t i = 0; i < format.len; i++) dst[i] = format.data[i];
return Success;
}
}
+52
View File
@@ -0,0 +1,52 @@
/*
* DataTransfer.hpp
* Multi-format payloads shared by the clipboard and drag and drop
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include "Syscall.hpp"
// A transfer is a small set of formats, each one MIME type plus bytes, all
// owned by the kernel heap. Producers offer every representation they can
// (a file list and its paths as text, say) and each consumer reads the one it
// understands. The clipboard holds one transfer per user; the window server
// holds the active drag's.
namespace DataTransfer {
struct Format {
char mime[montauk::abi::TRANSFER_MIME_MAX];
uint8_t* data;
uint32_t len;
};
struct Transfer {
Format formats[montauk::abi::TRANSFER_MAX_FORMATS];
uint32_t count;
};
// Copy `count` formats in from user memory, validating every pointer the
// items carry (the item array itself must already be validated). On
// failure `out` is left empty. Returns a StatusCodes value.
int BuildFromUser(const montauk::abi::TransferItem* items, uint32_t count, Transfer& out);
// A transfer holding just `text` as TRANSFER_MIME_TEXT.
int BuildText(const char* text, uint32_t len, Transfer& out);
void Free(Transfer& transfer);
// Exact match first; a request without parameters ("text/plain") also
// matches a stored format that has them ("text/plain;charset=utf-8").
const Format* Find(const Transfer& transfer, const char* mime);
uint32_t ListFormats(const Transfer& transfer, montauk::abi::TransferFormatInfo* out, uint32_t max);
// Copy a format to a (validated) user buffer. *outLen always receives the
// full length; BufferTooSmall when it does not fit, nothing copied then.
int CopyOut(const Format& format, void* buffer, uint32_t bufferLen, uint32_t* outLen);
// MIME string checks shared by every entry point: "type/subtype" with
// optional parameters, printable ASCII, shorter than TRANSFER_MIME_MAX.
bool ValidMime(const char* mime);
}
+30 -18
View File
@@ -6,18 +6,20 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <Hal/Apic/ApicInit.hpp>
#include <Drivers/PS2/PS2Controller.hpp>
#include <Drivers/USB/Xhci.hpp> #include <Drivers/USB/Xhci.hpp>
#include <Drivers/USB/UsbDevice.hpp> #include <Drivers/USB/UsbDevice.hpp>
#include <Drivers/USB/Bluetooth/Bluetooth.hpp> #include <Drivers/Bluetooth/Bluetooth.hpp>
#include <Drivers/Net/E1000.hpp> #include <Drivers/Net/E1000.hpp>
#include <Drivers/Net/E1000E.hpp> #include <Drivers/Net/E1000E.hpp>
#include <Drivers/Net/Genet.hpp>
#include <Drivers/Net/Wifi/Wifi.hpp> #include <Drivers/Net/Wifi/Wifi.hpp>
#include <Drivers/Graphics/IntelGPU.hpp> #include <Drivers/Graphics/IntelGPU.hpp>
#include <Drivers/Storage/Ahci.hpp> #include <Drivers/Storage/Ahci.hpp>
#include <Drivers/Audio/IntelHda.hpp> #include <Drivers/Audio/IntelHda.hpp>
#include <Pci/Pci.hpp> #include <Pci/Pci.hpp>
#include <Arch/Arch.hpp>
#include <Arch/Platform.hpp>
#include <Arch/Smp.hpp>
#include "Syscall.hpp" #include "Syscall.hpp"
#include <Drivers/Storage/BlockDevice.hpp> #include <Drivers/Storage/BlockDevice.hpp>
@@ -69,28 +71,21 @@ namespace montauk::abi {
}; };
// CPU cores (category 0) // CPU cores (category 0)
int cpuCount = Hal::GetDetectedCpuCount(); int cpuCount = Smp::GetDetectedCpuCount();
if (cpuCount > 0) { if (cpuCount > 0) {
char detail[48]; char detail[48];
int p = 0; 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_dec(detail, p, cpuCount, 48);
p = dl_append(detail, p, " core(s)", 48); p = dl_append(detail, p, " core(s)", 48);
add(0, "Processor", detail); add(0, "Processor", detail);
} }
// Interrupt controllers (category 1) // Interrupt controllers (category 1), timer (2), platform input (3)
add(1, "Local APIC", ""); Arch::ListPlatformDevices([](void* ctx, uint8_t category,
add(1, "I/O APIC", ""); const char* name, const char* detail) {
(*(decltype(add)*)ctx)(category, name, detail);
// Timer (category 2) }, &add);
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");
}
// USB devices (category 4) // USB devices (category 4)
if (Drivers::USB::Xhci::IsInitialized()) { if (Drivers::USB::Xhci::IsInitialized()) {
@@ -128,11 +123,24 @@ namespace montauk::abi {
if (Drivers::Net::E1000E::IsInitialized()) { if (Drivers::Net::E1000E::IsInitialized()) {
add(5, "Intel E1000E", "Gigabit Ethernet (82574L)"); add(5, "Intel E1000E", "Gigabit Ethernet (82574L)");
} }
if (Drivers::Net::Genet::IsInitialized()) {
uint32_t speed = Drivers::Net::Genet::GetLinkSpeed();
add(5, "Broadcom GENET", speed == 1000 ? "Gigabit Ethernet (1000 Mbit/s)"
: speed == 100 ? "Gigabit Ethernet (100 Mbit/s)"
: speed == 10 ? "Gigabit Ethernet (10 Mbit/s)"
: "Gigabit Ethernet (no link)");
}
if (Drivers::Net::Wifi::IsPresent()) { if (Drivers::Net::Wifi::IsPresent()) {
add(5, "Intel Wi-Fi", Drivers::Net::Wifi::IsLinkUp() add(5, "Intel Wi-Fi", Drivers::Net::Wifi::IsLinkUp()
? "802.11 wireless (connected)" ? "802.11 wireless (connected)"
: "802.11 wireless"); : "802.11 wireless");
} }
// An on-board UART Bluetooth controller (Raspberry Pi) is no USB
// device, so it is listed here; USB adapters appear above.
if (Drivers::Bluetooth::IsInitialized() &&
Drivers::Bluetooth::Hci::GetTransport() == Drivers::Bluetooth::Hci::Transport::Uart) {
add(5, "Bluetooth Adapter", "Broadcom, HCI over UART");
}
// Display (category 6) // Display (category 6)
if (Drivers::Graphics::IntelGPU::IsInitialized()) { if (Drivers::Graphics::IntelGPU::IsInitialized()) {
@@ -154,7 +162,7 @@ namespace montauk::abi {
add(9, "Intel HDA", detail); add(9, "Intel HDA", detail);
} }
if (Drivers::USB::Bluetooth::IsInitialized()) { if (Drivers::Bluetooth::IsInitialized()) {
add(9, "Bluetooth Audio", "A2DP (SBC)"); add(9, "Bluetooth Audio", "A2DP (SBC)");
} }
@@ -254,6 +262,10 @@ namespace montauk::abi {
case Drivers::Storage::BLOCK_KIND_USB_MSC: case Drivers::Storage::BLOCK_KIND_USB_MSC:
buf->type = 4; buf->type = 4;
break; break;
case Drivers::Storage::BLOCK_KIND_SD:
buf->type = 5;
buf->rpm = 1;
break;
default: default:
break; break;
} }
+21 -2
View File
@@ -27,6 +27,23 @@ namespace montauk::abi {
return required == 0 || Sched::HasCapability(required); return required == 0 || Sched::HasCapability(required);
} }
// Drivers fail a create under a missing parent with a bare -1; report it
// as NoSuchPath so callers can tell it apart (Rust's create_dir_all only
// creates the missing ancestors when mkdir says NotFound).
static bool ParentDirExists(const char* resolved) {
char parent[256];
int len = Lib::strlen(resolved);
if (len >= (int)sizeof(parent)) return false;
memcpy(parent, resolved, len + 1);
while (len > 0 && parent[len - 1] == '/') len--;
while (len > 0 && parent[len - 1] != '/') len--;
while (len > 0 && parent[len - 1] == '/') len--;
if (len == 0 || parent[len - 1] == ':') return true; // drive root
parent[len] = '\0';
Fs::Vfs::StatInfo info;
return Fs::Vfs::VfsStat(parent, info) == 0 && info.isDir;
}
static int Sys_Open(const char* path) { static int Sys_Open(const char* path) {
char resolved[256]; char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
@@ -77,14 +94,14 @@ namespace montauk::abi {
proc->readdirCursor = (slot + 1) % Sched::UserReadDirSlots; proc->readdirCursor = (slot + 1) % Sched::UserReadDirSlots;
uint64_t userVa = Sched::UserReadDirBase + (uint64_t)slot * 0x1000ULL; 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; uint8_t* pageBuf = nullptr;
if (physAddr == 0) { if (physAddr == 0) {
void* page = Memory::g_pfa->AllocateZeroed(); void* page = Memory::g_pfa->AllocateZeroed();
if (page == nullptr) return OutOfMemory; if (page == nullptr) return OutOfMemory;
physAddr = Memory::SubHHDM((uint64_t)page); 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); Memory::g_pfa->Free(page);
return OutOfMemory; return OutOfMemory;
} }
@@ -117,6 +134,7 @@ namespace montauk::abi {
char resolved[256]; char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
if (!CanModifyFilePath(resolved)) return SystemIntegrityError; if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
if (!ParentDirExists(resolved)) return NoSuchPath;
return Ipc::CreateFileHandle(resolved); return Ipc::CreateFileHandle(resolved);
} }
@@ -171,6 +189,7 @@ namespace montauk::abi {
char resolved[256]; char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
if (!CanModifyFilePath(resolved)) return SystemIntegrityError; if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
if (!ParentDirExists(resolved)) return NoSuchPath;
return Fs::Vfs::VfsMkdir(resolved); return Fs::Vfs::VfsMkdir(resolved);
} }
+2 -2
View File
@@ -83,7 +83,7 @@ namespace montauk::abi {
for (uint64_t i = 0; i < numPages; i++) { for (uint64_t i = 0; i < numPages; i++) {
if (!Memory::VMM::Paging::MapUserInWC( if (!Memory::VMM::Paging::MapUserInWC(
proc->pml4Phys, proc->pageTableRoot,
fbPhys + i * 0x1000, fbPhys + i * 0x1000,
userVa + i * 0x1000 userVa + i * 0x1000
)) { )) {
@@ -98,7 +98,7 @@ namespace montauk::abi {
uint64_t buf1Phys = Drivers::Graphics::IntelGPU::GetBufferPhys(1); uint64_t buf1Phys = Drivers::Graphics::IntelGPU::GetBufferPhys(1);
for (uint64_t i = 0; i < numPages; i++) { for (uint64_t i = 0; i < numPages; i++) {
if (!Memory::VMM::Paging::MapUserInWC( if (!Memory::VMM::Paging::MapUserInWC(
proc->pml4Phys, proc->pageTableRoot,
buf1Phys + i * 0x1000, buf1Phys + i * 0x1000,
userVa + (numPages + i) * 0x1000 userVa + (numPages + i) * 0x1000
)) { )) {
+15 -15
View File
@@ -107,7 +107,7 @@ namespace montauk::abi {
if (page == nullptr) break; if (page == nullptr) break;
uint64_t phys = Memory::SubHHDM((uint64_t)page); uint64_t phys = Memory::SubHHDM((uint64_t)page);
if (!Memory::VMM::Paging::MapUserInPermissions( if (!Memory::VMM::Paging::MapUserInPermissions(
proc->pml4Phys, phys, pageVa, writable, executable)) { proc->pageTableRoot, phys, pageVa, writable, executable)) {
Memory::g_pfa->Free(page); Memory::g_pfa->Free(page);
break; break;
} }
@@ -131,7 +131,7 @@ namespace montauk::abi {
// Reset heap allocation tracking for a process slot. // Reset heap allocation tracking for a process slot.
// The actual physical pages are freed by Paging::FreeUserHalf() during process cleanup. // 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; if (slot < 0 || slot >= Sched::MaxProcesses) return;
g_heapLocks[slot].Acquire(); g_heapLocks[slot].Acquire();
HeapAlloc* area = g_heapAllocs[slot]; HeapAlloc* area = g_heapAllocs[slot];
@@ -182,7 +182,7 @@ namespace montauk::abi {
} }
for (uint64_t i = 0; i < area->numPages; i++) for (uint64_t i = 0; i < area->numPages; i++)
if (Memory::VMM::Paging::GetPhysAddr( if (Memory::VMM::Paging::GetPhysAddr(
proc->pml4Phys, area->va + i * 0x1000ULL) != 0) proc->pageTableRoot, area->va + i * 0x1000ULL) != 0)
resident++; resident++;
*link = area->next; *link = area->next;
area->next = released; area->next = released;
@@ -194,7 +194,7 @@ namespace montauk::abi {
// user TLB entry can otherwise corrupt the frame's next owner. // user TLB entry can otherwise corrupt the frame's next owner.
while (released != nullptr) { while (released != nullptr) {
HeapAlloc* next = released->next; HeapAlloc* next = released->next;
Memory::UnmapAndFreeUserRange(proc->pml4Phys, released->va, Memory::UnmapAndFreeUserRange(proc->pageTableRoot, released->va,
released->numPages); released->numPages);
Sched::ReleaseUserHeapRange(slot, released->va, Sched::ReleaseUserHeapRange(slot, released->va,
released->numPages * 0x1000ULL); released->numPages * 0x1000ULL);
@@ -253,11 +253,11 @@ namespace montauk::abi {
uint64_t resident = 0; uint64_t resident = 0;
for (uint64_t i = 0; i < pages; i++) 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++; resident++;
g_heapLocks[slot].Release(); g_heapLocks[slot].Release();
Memory::UnmapAndFreeUserRange(proc->pml4Phys, addr, pages); Memory::UnmapAndFreeUserRange(proc->pageTableRoot, addr, pages);
Sched::ReleaseUserHeapRange(slot, addr, size); Sched::ReleaseUserHeapRange(slot, addr, size);
g_heapLocks[slot].Acquire(); g_heapLocks[slot].Acquire();
Sched::g_allocatedPages[slot] -= resident; Sched::g_allocatedPages[slot] -= resident;
@@ -336,10 +336,10 @@ namespace montauk::abi {
bool writable = (prot & VmProtWrite) != 0; bool writable = (prot & VmProtWrite) != 0;
bool executable = (prot & VmProtExec) != 0; bool executable = (prot & VmProtExec) != 0;
for (uint64_t i = 0; i < pages; i++) { 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) addr + i * 0x1000ULL) == 0)
continue; continue;
if (!Memory::VMM::Paging::ProtectUserIn(proc->pml4Phys, if (!Memory::VMM::Paging::ProtectUserIn(proc->pageTableRoot,
addr + i * 0x1000ULL, addr + i * 0x1000ULL,
writable, executable)) { writable, executable)) {
g_heapLocks[slot].Release(); g_heapLocks[slot].Release();
@@ -347,17 +347,17 @@ namespace montauk::abi {
} }
} }
g_heapLocks[slot].Release(); g_heapLocks[slot].Release();
Memory::ShootdownUserRange(proc->pml4Phys, addr, (uint32_t)pages); Memory::ShootdownUserRange(proc->pageTableRoot, addr, (uint32_t)pages);
return Success; return Success;
} }
// Resolve a non-present access inside an anonymous VMA. This is used for // Resolve a non-present access inside an anonymous VMA. This is used for
// both ring-3 faults and kernel copies to untouched userspace buffers. // 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(); auto* proc = Sched::GetCurrentProcessPtr();
int slot = GetCurrentSlot(); int slot = GetCurrentSlot();
if (proc == nullptr || slot < 0 || faultAddr < Sched::UserHeapBase || 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; uint64_t pageVa = faultAddr & ~0xFFFULL;
g_heapLocks[slot].Acquire(); g_heapLocks[slot].Acquire();
@@ -370,14 +370,14 @@ namespace montauk::abi {
} }
} }
if (found == nullptr || if (found == nullptr ||
((errorCode & (1ULL << 1)) != 0 && (found->prot & VmProtWrite) == 0) || (fault.write && (found->prot & VmProtWrite) == 0) ||
((errorCode & (1ULL << 4)) != 0 && (found->prot & VmProtExec) == 0)) { (fault.execute && (found->prot & VmProtExec) == 0)) {
g_heapLocks[slot].Release(); g_heapLocks[slot].Release();
return false; return false;
} }
// A sibling may have resolved the same fault while this CPU waited. // 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(); g_heapLocks[slot].Release();
return true; return true;
} }
@@ -389,7 +389,7 @@ namespace montauk::abi {
} }
uint64_t phys = Memory::SubHHDM((uint64_t)page); uint64_t phys = Memory::SubHHDM((uint64_t)page);
bool mapped = Memory::VMM::Paging::MapUserInPermissions( bool mapped = Memory::VMM::Paging::MapUserInPermissions(
proc->pml4Phys, phys, pageVa, proc->pageTableRoot, phys, pageVa,
(found->prot & VmProtWrite) != 0, (found->prot & VmProtExec) != 0); (found->prot & VmProtWrite) != 0, (found->prot & VmProtExec) != 0);
if (!mapped) Memory::g_pfa->Free(page); if (!mapped) Memory::g_pfa->Free(page);
else Sched::g_allocatedPages[slot]++; else Sched::g_allocatedPages[slot]++;
+2 -2
View File
@@ -17,13 +17,13 @@ namespace montauk::abi {
// Copy strings into fixed-size arrays (user-accessible) // Copy strings into fixed-size arrays (user-accessible)
const char* name = "MontaukOS"; const char* name = "MontaukOS";
const char* ver = "0.2.0"; const char* ver = "0.2.1";
for (int i = 0; name[i]; i++) outInfo->osName[i] = name[i]; for (int i = 0; name[i]; i++) outInfo->osName[i] = name[i];
outInfo->osName[9] = '\0'; outInfo->osName[9] = '\0';
for (int i = 0; ver[i]; i++) outInfo->osVersion[i] = ver[i]; for (int i = 0; ver[i]; i++) outInfo->osVersion[i] = ver[i];
outInfo->osVersion[5] = '\0'; outInfo->osVersion[5] = '\0';
outInfo->apiVersion = 11; outInfo->apiVersion = 12;
outInfo->maxProcesses = Sched::MaxProcesses; outInfo->maxProcesses = Sched::MaxProcesses;
outInfo->buildNumber = MONTAUK_BUILD_NUMBER; outInfo->buildNumber = MONTAUK_BUILD_NUMBER;
} }
+20 -3
View File
@@ -122,8 +122,9 @@ namespace montauk::abi {
return WriteAllToStream(stream, (const uint8_t*)data, len); return WriteAllToStream(stream, (const uint8_t*)data, len);
} }
static int Sys_ChildIoWriteKey(int childPid, const KeyEvent* key) { // Queue a key event (raw bytes, `len` of them) on a redirected child's key
if (key == nullptr) return InvalidArgument; // mailbox. Readers accept both the 9-byte legacy and the full format.
static int ChildIoQueueKey(int childPid, const void* key, uint16_t len) {
auto* child = Sched::GetProcessByPid(childPid); auto* child = Sched::GetProcessByPid(childPid);
if (child == nullptr || child->parentPid != Sched::GetCurrentPid()) if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
return PermissionDenied; return PermissionDenied;
@@ -133,7 +134,7 @@ namespace montauk::abi {
for (;;) { for (;;) {
uint64_t observedWake = Sched::ObserveObjectWake(mailbox); uint64_t observedWake = Sched::ObserveObjectWake(mailbox);
int rc = Ipc::MailboxSend(mailbox, 0, key, sizeof(KeyEvent)); int rc = Ipc::MailboxSend(mailbox, 0, key, len);
if (rc < 0) return GenericFailure; if (rc < 0) return GenericFailure;
if (rc == 0) { if (rc == 0) {
Sched::BlockOnObjectSince(mailbox, 0, observedWake); Sched::BlockOnObjectSince(mailbox, 0, observedWake);
@@ -143,6 +144,22 @@ namespace montauk::abi {
} }
} }
static int Sys_ChildIoWriteKey(int childPid, const KeyEventV1* key) {
if (key == nullptr) return InvalidArgument;
KeyEventV1 copy = *key;
return ChildIoQueueKey(childPid, &copy, sizeof(copy));
}
// Size-versioned: copies min(size, sizeof(KeyEvent)) bytes (validated
// readable by the dispatcher) and zero-fills the rest.
static int Sys_ChildIoWriteKeyEx(int childPid, const void* key, uint64_t size) {
if (key == nullptr || size < sizeof(KeyEventV1)) return InvalidArgument;
KeyEvent ev;
memset(&ev, 0, sizeof(ev));
memcpy(&ev, key, size < sizeof(ev) ? size : sizeof(ev));
return ChildIoQueueKey(childPid, &ev, sizeof(ev));
}
static int Sys_ChildIoSetTermsz(int childPid, int cols, int rows) { static int Sys_ChildIoSetTermsz(int childPid, int cols, int rows) {
auto* child = Sched::GetProcessByPid(childPid); auto* child = Sched::GetProcessByPid(childPid);
if (child == nullptr || child->parentPid != Sched::GetCurrentPid()) if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
+137 -22
View File
@@ -12,6 +12,107 @@
#include "Common.hpp" #include "Common.hpp"
namespace montauk::abi { namespace montauk::abi {
// ---- Key text ----------------------------------------------------------
// Keyboard layouts are applied in userspace (desktop, terminal), which send
// KeyEvents carrying the Unicode codepoint. Binaries built before that send
// 9-byte KeyEventV1s whose `ascii` is a Windows-1252 byte; both kinds share
// a redirected child's key mailbox, told apart by message length.
static uint32_t KeyCp1252ToUnicode(uint8_t b) {
static constexpr uint16_t high[32] = {
0x20AC, 0x81, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021,
0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x8D, 0x017D, 0x8F,
0x90, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x9D, 0x017E, 0x0178,
};
return (b >= 0x80 && b <= 0x9F) ? high[b - 0x80] : b;
}
// The character a key event types, or 0 for control/navigation keys.
static uint32_t KeyTextCodepoint(const KeyEvent& ev) {
if (ev.codepoint) return ev.codepoint;
uint8_t b = (uint8_t)ev.ascii;
if (b < 0x20 || b == 0x7F) return 0;
return KeyCp1252ToUnicode(b);
}
static int KeyEncodeUtf8(uint32_t cp, uint8_t* out) {
if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) cp = 0xFFFD;
if (cp < 0x80) { out[0] = (uint8_t)cp; return 1; }
if (cp < 0x800) {
out[0] = (uint8_t)(0xC0 | (cp >> 6));
out[1] = (uint8_t)(0x80 | (cp & 0x3F));
return 2;
}
if (cp < 0x10000) {
out[0] = (uint8_t)(0xE0 | (cp >> 12));
out[1] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
out[2] = (uint8_t)(0x80 | (cp & 0x3F));
return 3;
}
out[0] = (uint8_t)(0xF0 | (cp >> 18));
out[1] = (uint8_t)(0x80 | ((cp >> 12) & 0x3F));
out[2] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
out[3] = (uint8_t)(0x80 | (cp & 0x3F));
return 4;
}
static void KeyFromDriver(const Drivers::PS2::Keyboard::KeyEvent& k, KeyEvent* out) {
memset(out, 0, sizeof(KeyEvent));
out->scancode = k.Scancode;
out->ascii = k.Ascii;
out->pressed = k.Pressed;
out->shift = k.Shift;
out->ctrl = k.Ctrl;
out->alt = k.Alt;
out->capslock = k.CapsLock;
out->altgr = k.AltGr;
out->extended = k.Extended;
// The kernel's own scancode table is US ASCII.
uint8_t a = (uint8_t)k.Ascii;
out->codepoint = (a >= 0x20 && a < 0x7F) ? a : 0;
}
// Receive one key from a redirected child's mailbox as a full KeyEvent,
// upgrading legacy 9-byte messages. Returns MailboxRecv's result.
static int KeyMailboxRecv(Ipc::Mailbox* mailbox, KeyEvent* out) {
memset(out, 0, sizeof(KeyEvent));
uint16_t len = sizeof(KeyEvent);
int rc = Ipc::MailboxRecv(mailbox, nullptr, out, &len, true);
if (rc > 0 && len < sizeof(KeyEvent)) {
memset((uint8_t*)out + len, 0, sizeof(KeyEvent) - len);
out->codepoint = KeyTextCodepoint(*out);
}
return rc;
}
// Turn a pressed key into the next getchar byte. Characters above ASCII are
// returned as UTF-8, one byte per call; the rest is parked in the process.
static bool KeyTakeChar(Sched::Process* proc, const KeyEvent& ev, char* out) {
if (!ev.pressed) return false;
uint32_t cp = KeyTextCodepoint(ev);
if (cp >= 0x80 && proc != nullptr) {
int n = KeyEncodeUtf8(cp, proc->getcharUtf8);
proc->getcharUtf8Len = (uint8_t)n;
proc->getcharUtf8Pos = 1;
*out = (char)proc->getcharUtf8[0];
return true;
}
if (ev.ascii != 0 && ev.ascii != KEY_ASCII_SUBSTITUTE) {
*out = ev.ascii;
return true;
}
return false;
}
static bool KeyPendingChar(Sched::Process* proc, char* out) {
if (proc == nullptr || proc->getcharUtf8Pos >= proc->getcharUtf8Len) return false;
*out = (char)proc->getcharUtf8[proc->getcharUtf8Pos++];
return true;
}
// ---- Syscalls ------------------------------------------------------------
static bool Sys_IsKeyAvailable() { static bool Sys_IsKeyAvailable() {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
if (proc && proc->redirected) { if (proc && proc->redirected) {
@@ -27,8 +128,8 @@ namespace montauk::abi {
return Drivers::PS2::Keyboard::IsKeyAvailable(); return Drivers::PS2::Keyboard::IsKeyAvailable();
} }
static void Sys_GetKey(KeyEvent* outEvent) { // Blocking read of the next key event, in the full format.
if (outEvent == nullptr) return; static void ReadKeyEvent(KeyEvent* outEvent) {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
if (proc && proc->redirected) { if (proc && proc->redirected) {
Ipc::HandleSnapshot snapshot; Ipc::HandleSnapshot snapshot;
@@ -36,8 +137,7 @@ namespace montauk::abi {
if (mailbox != nullptr) { if (mailbox != nullptr) {
for (;;) { for (;;) {
uint64_t observedWake = Sched::ObserveObjectWake(mailbox); uint64_t observedWake = Sched::ObserveObjectWake(mailbox);
uint16_t len = sizeof(KeyEvent); int rc = KeyMailboxRecv(mailbox, outEvent);
int rc = Ipc::MailboxRecv(mailbox, nullptr, outEvent, &len, true);
if (rc > 0) return; if (rc > 0) return;
if (rc < 0) { if (rc < 0) {
memset(outEvent, 0, sizeof(KeyEvent)); memset(outEvent, 0, sizeof(KeyEvent));
@@ -47,20 +147,33 @@ namespace montauk::abi {
} }
} }
} }
auto k = Drivers::PS2::Keyboard::GetKey(); KeyFromDriver(Drivers::PS2::Keyboard::GetKey(), outEvent);
outEvent->scancode = k.Scancode; }
outEvent->ascii = k.Ascii;
outEvent->pressed = k.Pressed; // SYS_GETKEY: legacy 9-byte event. A new-format event's first 9 bytes are
outEvent->shift = k.Shift; // already a valid KeyEventV1 (ascii holds the 1252 byte or SUB).
outEvent->ctrl = k.Ctrl; static void Sys_GetKey(KeyEventV1* outEvent) {
outEvent->alt = k.Alt; if (outEvent == nullptr) return;
outEvent->capslock = k.CapsLock; KeyEvent ev;
outEvent->altgr = k.AltGr; ReadKeyEvent(&ev);
outEvent->extended = k.Extended; memcpy(outEvent, &ev, sizeof(KeyEventV1));
}
// SYS_GETKEY_EX: size-versioned. The caller's size was validated by the
// dispatcher (>= sizeof(KeyEventV1), writable).
static int Sys_GetKeyEx(void* outEvent, uint64_t size) {
KeyEvent ev;
ReadKeyEvent(&ev);
uint64_t n = size < sizeof(KeyEvent) ? size : sizeof(KeyEvent);
memcpy(outEvent, &ev, n);
if (size > n) memset((uint8_t*)outEvent + n, 0, size - n);
return Success;
} }
static char Sys_GetChar() { static char Sys_GetChar() {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
char pending;
if (KeyPendingChar(proc, &pending)) return pending;
if (proc && proc->redirected) { if (proc && proc->redirected) {
Ipc::HandleSnapshot inputSnapshot; Ipc::HandleSnapshot inputSnapshot;
Ipc::HandleSnapshot mailboxSnapshot; Ipc::HandleSnapshot mailboxSnapshot;
@@ -78,11 +191,11 @@ namespace montauk::abi {
} }
if (mailbox != nullptr) { if (mailbox != nullptr) {
KeyEvent ev{}; KeyEvent ev;
uint16_t len = sizeof(ev); int rc = KeyMailboxRecv(mailbox, &ev);
int rc = Ipc::MailboxRecv(mailbox, nullptr, &ev, &len, true);
if (rc > 0) { if (rc > 0) {
if (ev.pressed && ev.ascii != 0) return ev.ascii; char c;
if (KeyTakeChar(proc, ev, &c)) return c;
continue; continue;
} }
if (rc < 0 && input == nullptr) return 0; if (rc < 0 && input == nullptr) return 0;
@@ -115,6 +228,8 @@ namespace montauk::abi {
*/ */
static char Sys_GetCharNb() { static char Sys_GetCharNb() {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
char pending;
if (KeyPendingChar(proc, &pending)) return pending;
if (proc && proc->redirected) { if (proc && proc->redirected) {
Ipc::HandleSnapshot inputSnapshot; Ipc::HandleSnapshot inputSnapshot;
Ipc::HandleSnapshot mailboxSnapshot; Ipc::HandleSnapshot mailboxSnapshot;
@@ -127,10 +242,10 @@ namespace montauk::abi {
} }
while (mailbox != nullptr) { while (mailbox != nullptr) {
KeyEvent ev{}; KeyEvent ev;
uint16_t len = sizeof(ev); if (KeyMailboxRecv(mailbox, &ev) <= 0) break;
if (Ipc::MailboxRecv(mailbox, nullptr, &ev, &len, true) <= 0) break; char c;
if (ev.pressed && ev.ascii != 0) return ev.ascii; if (KeyTakeChar(proc, ev, &c)) return c;
} }
return 0; return 0;
} }
+3 -3
View File
@@ -91,7 +91,7 @@ namespace montauk::abi {
} }
// Load the library into the process's address space // 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) { if (loadBias == 0) {
Kt::KernelLogStream(Kt::ERROR, "Lib") << "Failed to load library: " << path; Kt::KernelLogStream(Kt::ERROR, "Lib") << "Failed to load library: " << path;
g_libLocks[slot].Release(); g_libLocks[slot].Release();
@@ -134,7 +134,7 @@ namespace montauk::abi {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
if (proc != nullptr) { if (proc != nullptr) {
Memory::UnmapAndFreeUserRange(proc->pml4Phys, libBase, Memory::UnmapAndFreeUserRange(proc->pageTableRoot, libBase,
(libEnd - libBase) / 0x1000ULL); (libEnd - libBase) / 0x1000ULL);
} }
@@ -203,7 +203,7 @@ namespace montauk::abi {
uint64_t libBase = GetLibSlotBase(i); uint64_t libBase = GetLibSlotBase(i);
uint64_t libEnd = libBase + Sched::LIB_MAX_SIZE; uint64_t libEnd = libBase + Sched::LIB_MAX_SIZE;
Memory::UnmapAndFreeUserRange(proc->pml4Phys, libBase, Memory::UnmapAndFreeUserRange(proc->pageTableRoot, libBase,
(libEnd - libBase) / 0x1000ULL); (libEnd - libBase) / 0x1000ULL);
g_libTable[slot][i].inUse = false; g_libTable[slot][i].inUse = false;
+6 -1
View File
@@ -9,7 +9,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <Sched/Scheduler.hpp> #include <Sched/Scheduler.hpp>
#include <Timekeeping/ApicTimer.hpp> #include <Timekeeping/Timer.hpp>
#include <Net/Icmp.hpp> #include <Net/Icmp.hpp>
#include <Net/Dns.hpp> #include <Net/Dns.hpp>
#include <Net/Ipv4.hpp> #include <Net/Ipv4.hpp>
@@ -18,6 +18,7 @@
#include <Net/NetIf.hpp> #include <Net/NetIf.hpp>
#include <Drivers/Net/E1000.hpp> #include <Drivers/Net/E1000.hpp>
#include <Drivers/Net/E1000E.hpp> #include <Drivers/Net/E1000E.hpp>
#include <Drivers/Net/Genet.hpp>
#include <Drivers/Net/Wifi/Wifi.hpp> #include <Drivers/Net/Wifi/Wifi.hpp>
#include "Syscall.hpp" #include "Syscall.hpp"
@@ -157,6 +158,10 @@ namespace montauk::abi {
out->rxPackets = Drivers::Net::E1000E::GetRxPacketCount(); out->rxPackets = Drivers::Net::E1000E::GetRxPacketCount();
out->txPackets = Drivers::Net::E1000E::GetTxPacketCount(); out->txPackets = Drivers::Net::E1000E::GetTxPacketCount();
CopyNetStatusDriver(out->driver, "Intel e1000e"); CopyNetStatusDriver(out->driver, "Intel e1000e");
} else if (Drivers::Net::Genet::IsInitialized()) {
out->rxPackets = Drivers::Net::Genet::GetRxPacketCount();
out->txPackets = Drivers::Net::Genet::GetTxPacketCount();
CopyNetStatusDriver(out->driver, "Broadcom GENET");
} }
} }
+15 -15
View File
@@ -8,13 +8,13 @@
#include <cstdint> #include <cstdint>
#include <Efi/UEFI.hpp> #include <Efi/UEFI.hpp>
#include <Memory/Paging.hpp> #include <Memory/Paging.hpp>
#include <Io/IoPort.hpp>
#include <Graphics/Framebuffer.hpp> #include <Graphics/Framebuffer.hpp>
#include <ACPI/AcpiShutdown.hpp> #include <ACPI/AcpiShutdown.hpp>
#include <ACPI/AcpiSleep.hpp> #include <ACPI/AcpiSleep.hpp>
#include <ACPI/CpuIdle.hpp>
#include <ACPI/Battery.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 <Sched/Scheduler.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
@@ -56,15 +56,13 @@ namespace montauk::abi {
Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec
<< (uint64_t)Sched::GetCurrentPid() << " entered final reboot stage"; << (uint64_t)Sched::GetCurrentPid() << " entered final reboot stage";
if (Efi::g_ResetSystem) { if (Efi::g_ResetSystem) {
/* Switch to kernel PML4 which has identity-mapped UEFI runtime regions */ /* Switch to the kernel address space, which has identity-mapped UEFI runtime regions */
Memory::VMM::LoadCR3(Memory::VMM::g_paging->PML4); Memory::VMM::ActivateKernelAddressSpace();
Efi::g_ResetSystem(Efi::EfiResetCold, 0, 0, nullptr); Efi::g_ResetSystem(Efi::EfiResetCold, 0, 0, nullptr);
} }
/* Fallback: triple fault via null IDT */ /* Fallback: architecture-specific reset */
struct [[gnu::packed]] { uint16_t limit; uint64_t base; } nullIdt = {0, 0}; Arch::Reboot();
asm volatile("lidt %0; int $0x03" :: "m"(nullIdt));
__builtin_unreachable();
} }
static void Sys_Shutdown() { static void Sys_Shutdown() {
@@ -75,15 +73,17 @@ namespace montauk::abi {
Hal::AcpiShutdown::Shutdown(); Hal::AcpiShutdown::Shutdown();
} }
/* Hardware-reduced platforms (PSCI on ARM) */
Arch::PowerOff();
/* Fallback: EFI ResetSystem */ /* Fallback: EFI ResetSystem */
if (Efi::g_ResetSystem) { if (Efi::g_ResetSystem) {
Memory::VMM::LoadCR3(Memory::VMM::g_paging->PML4); Memory::VMM::ActivateKernelAddressSpace();
Efi::g_ResetSystem(Efi::EfiResetShutdown, 0, 0, nullptr); Efi::g_ResetSystem(Efi::EfiResetShutdown, 0, 0, nullptr);
} }
/* Last resort: halt the CPU */ /* Last resort: halt the CPU */
asm volatile("cli; hlt"); Arch::HaltForever();
__builtin_unreachable();
} }
static int64_t Sys_Suspend() { static int64_t Sys_Suspend() {
@@ -96,8 +96,8 @@ namespace montauk::abi {
// SYS_POWERINFO. Fills a CPU power/thermal snapshot. Returns -1 when the // SYS_POWERINFO. Fills a CPU power/thermal snapshot. Returns -1 when the
// CPU exposes no power management features (e.g. under QEMU). // CPU exposes no power management features (e.g. under QEMU).
static int64_t Sys_PowerInfo(PowerInfo* out) { static int64_t Sys_PowerInfo(PowerInfo* out) {
Hal::CpuPower::Snapshot s{}; Arch::CpuPower::Snapshot s{};
if (!Hal::CpuPower::GetSnapshot(s)) { if (!Arch::CpuPower::GetSnapshot(s)) {
return NoDevice; return NoDevice;
} }
@@ -112,7 +112,7 @@ namespace montauk::abi {
out->baseMHz = s.BaseMHz; out->baseMHz = s.BaseMHz;
out->maxMHz = s.MaxMHz; out->maxMHz = s.MaxMHz;
out->effMHz = s.EffMHz; out->effMHz = s.EffMHz;
out->apIdleHint = Hal::CpuIdle::GetApIdleHint(); out->apIdleHint = s.ApIdleHint;
return Success; return Success;
} }
+28 -1
View File
@@ -7,7 +7,7 @@
#pragma once #pragma once
#include <Sched/Scheduler.hpp> #include <Sched/Scheduler.hpp>
#include <Timekeeping/ApicTimer.hpp> #include <Timekeeping/Timer.hpp>
#include <Memory/Paging.hpp> #include <Memory/Paging.hpp>
#include <Memory/PageFrameAllocator.hpp> #include <Memory/PageFrameAllocator.hpp>
#include <Memory/HHDM.hpp> #include <Memory/HHDM.hpp>
@@ -220,7 +220,26 @@ namespace montauk::abi {
return count; return count;
} }
// Flag every thread of the caller's process with TerminatedAborted, then
// exit this one. A sibling's ExitProcess ends only itself; the main
// thread and the other siblings follow via killPending.
static void Sys_Abort() {
Sched::KillProcess(Sched::GetCurrentPid(), TerminatedAborted);
Sched::ExitProcess();
}
// Like Sys_Abort, but a normal exit with the caller's code: every thread
// goes, and waiters see exitCode & 0xFF, the same as SYS_EXIT on the main
// thread.
static void Sys_ExitGroup(int exitCode) {
Sched::KillProcess(Sched::GetCurrentPid(), exitCode & 0xFF);
Sched::ExitProcess();
}
static int Sys_Kill(int pid) { static int Sys_Kill(int pid) {
// A process may always kill itself (kill(getpid(), ...)); waiters
// see TerminatedKilled. abort() uses SYS_ABORT instead.
if (pid == Sched::GetCurrentPid()) return Sched::KillProcess(pid);
if (!Sched::HasCapability(CAP_PROCESS_ADMIN)) { if (!Sched::HasCapability(CAP_PROCESS_ADMIN)) {
int ancestor = pid; int ancestor = pid;
bool descendant = false; bool descendant = false;
@@ -302,6 +321,14 @@ namespace montauk::abi {
return Sched::GetCurrentTid(); return Sched::GetCurrentTid();
} }
static int Sys_FutexWait(uint64_t addr, uint32_t expected, uint64_t timeoutNs) {
return Sched::FutexWait(addr, expected, timeoutNs);
}
static int Sys_FutexWake(uint64_t addr, uint32_t count) {
return Sched::FutexWake(addr, count);
}
static int Sys_Chdir(const char* path) { static int Sys_Chdir(const char* path) {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr || path == nullptr) return InvalidArgument; if (proc == nullptr || path == nullptr) return InvalidArgument;
+8 -8
View File
@@ -6,23 +6,23 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <Arch/Cpu.hpp>
namespace montauk::abi { namespace montauk::abi {
// ---- Random number generation ---- // ---- Random number generation ----
// Uses RDTSC mixed with xorshift64* PRNG for entropy. // Uses the CPU timestamp counter mixed with xorshift64* PRNG for entropy.
// RDRAND is intentionally avoided: some firmware disables the RDRAND // RDRAND is intentionally avoided on x86: some firmware disables the
// hardware unit while CPUID still advertises support (bit 30 of ECX), // RDRAND hardware unit while CPUID still advertises support (bit 30 of
// causing #UD on real hardware. RDTSC-based entropy is sufficient for // ECX), causing #UD on real hardware. Timestamp-based entropy is
// seeding BearSSL's PRNG for TLS session keys. // sufficient for seeding BearSSL's PRNG for TLS session keys.
static int64_t Sys_GetRandom(uint8_t* buf, uint64_t len) { static int64_t Sys_GetRandom(uint8_t* buf, uint64_t len) {
uint64_t tsc; uint64_t tsc = Arch::ReadTimestamp();
asm volatile("rdtsc; shl $32, %%rdx; or %%rdx, %%rax" : "=a"(tsc) :: "rdx");
uint64_t state = tsc; uint64_t state = tsc;
for (uint64_t i = 0; i < len; i += 8) { 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 ^= tsc;
state ^= state >> 12; state ^= state >> 12;
state ^= state << 25; state ^= state << 25;
+39
View File
@@ -163,6 +163,45 @@ namespace montauk::abi {
return (int64_t)Fs::FsProbe::SyncAndUnmountAll(); return (int64_t)Fs::FsProbe::SyncAndUnmountAll();
} }
// Grow a mounted volume to fill the free space after its partition: the
// MBR entry first, only as far as the filesystem can use, then the
// filesystem a slice per call. Interrupted grows resume from the next
// call (or boot), since a partition larger than its filesystem is valid.
static int64_t Sys_FsGrow(int drive, FsGrowStatus* out, bool apply) {
int partIndex = Fs::FsProbe::GetPartitionForDrive(drive);
auto* part = Drivers::Storage::Gpt::GetPartition(partIndex);
if (part == nullptr) return UnsupportedOp; // ramdisk, or no such drive
uint64_t maxSectors = Drivers::Storage::Gpt::GetMbrPartitionMaxSectors(partIndex);
if (maxSectors < part->SectorCount) maxSectors = part->SectorCount;
Fs::Vfs::GrowStatus gs{};
int rc = Fs::Vfs::VfsGrow(drive, maxSectors, false, &gs);
if (rc == -2) return UnsupportedOp;
if (rc < 0) return IoFailure;
// Leave whatever the filesystem cannot use unpartitioned, with the end
// on a 1 MiB boundary like the partitions mk-rpi4-sd.sh makes.
uint64_t wanted = (part->StartLba + (gs.targetBytes + 511) / 512 + 2047) / 2048 * 2048
- part->StartLba;
if (wanted > maxSectors) wanted = maxSectors;
if (apply && wanted > part->SectorCount) {
if (Drivers::Storage::Gpt::ResizeMbrPartition(partIndex, wanted) != 0) return IoFailure;
}
rc = apply ? Fs::Vfs::VfsGrow(drive, part->SectorCount, true, &gs) : 0;
if (rc == -2) return UnsupportedOp;
if (rc < 0) return IoFailure;
out->fsBytes = gs.fsBytes;
out->partBytes = part->SectorCount * 512;
out->targetBytes = gs.targetBytes;
out->stepsDone = gs.stepsDone;
out->stepsTotal = gs.stepsTotal;
return rc;
}
// Format a partition with a filesystem. Returns 0 on success, GenericFailure on error. // Format a partition with a filesystem. Returns 0 on success, GenericFailure on error.
static int64_t Sys_FsFormat(const FsFormatParams* params) { static int64_t Sys_FsFormat(const FsFormatParams* params) {
if (params == nullptr) return InvalidArgument; if (params == nullptr) return InvalidArgument;
+83 -32
View File
@@ -6,8 +6,6 @@
#include "Syscall.hpp" #include "Syscall.hpp"
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <Hal/MSR.hpp>
#include <Hal/GDT.hpp>
/* For common functions used by multiple syscall implementations*/ /* For common functions used by multiple syscall implementations*/
#include "Common.hpp" #include "Common.hpp"
@@ -18,7 +16,7 @@
#include "Terminal.hpp" // SYS_PRINT, SYS_PUTCHAR #include "Terminal.hpp" // SYS_PRINT, SYS_PUTCHAR
#include "Filesystem.hpp" // SYS_OPEN, SYS_READ, SYS_GETSIZE, SYS_CLOSE, SYS_READDIR, SYS_FWRITE, SYS_FCREATE #include "Filesystem.hpp" // SYS_OPEN, SYS_READ, SYS_GETSIZE, SYS_CLOSE, SYS_READDIR, SYS_FWRITE, SYS_FCREATE
#include "Heap.hpp" // SYS_ALLOC, SYS_FREE #include "Heap.hpp" // SYS_ALLOC, SYS_FREE
#include "Time.hpp" // SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME #include "Time.hpp" // SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME, SYS_GETUNIXTIME
#include "Keyboard.hpp" // SYS_ISKEYAVAILABLE, SYS_GETKEY, SYS_GETCHAR #include "Keyboard.hpp" // SYS_ISKEYAVAILABLE, SYS_GETKEY, SYS_GETCHAR
#include "Info.hpp" // SYS_GETINFO #include "Info.hpp" // SYS_GETINFO
#include "Graphics.hpp" // SYS_FBINFO, SYS_FBMAP, SYS_TERMSIZE, SYS_TERMSCALE #include "Graphics.hpp" // SYS_FBINFO, SYS_FBMAP, SYS_TERMSIZE, SYS_TERMSCALE
@@ -31,7 +29,7 @@
#include "Device.hpp" // SYS_DEVLIST, SYS_DISKINFO #include "Device.hpp" // SYS_DEVLIST, SYS_DISKINFO
#include "Input.hpp" // SYS_INPUT_WAIT #include "Input.hpp" // SYS_INPUT_WAIT
#include "Storage.hpp" // SYS_PARTLIST, SYS_VOLINFO, SYS_DISKREAD, SYS_DISKWRITE #include "Storage.hpp" // SYS_PARTLIST, SYS_VOLINFO, SYS_DISKREAD, SYS_DISKWRITE
#include "Window.hpp" // SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL, SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE, SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE #include "Window.hpp" // SYS_WIN_REQUEST*, SYS_WIN_DRAG_*, SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL, SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE, SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE
#include "Audio.hpp" // SYS_AUDIOOPEN, SYS_AUDIOCLOSE, SYS_AUDIOWRITE, SYS_AUDIOCTL #include "Audio.hpp" // SYS_AUDIOOPEN, SYS_AUDIOCLOSE, SYS_AUDIOWRITE, SYS_AUDIOCTL
#include "BluetoothSyscall.hpp" // SYS_BTSCAN, SYS_BTCONNECT, SYS_BTDISCONNECT, SYS_BTLIST, SYS_BTINFO #include "BluetoothSyscall.hpp" // SYS_BTSCAN, SYS_BTCONNECT, SYS_BTDISCONNECT, SYS_BTLIST, SYS_BTINFO
#include "Usb.hpp" // generic process-owned USB interface access #include "Usb.hpp" // generic process-owned USB interface access
@@ -39,12 +37,9 @@
#include "IpcSyscall.hpp" // SYS_DUPHANDLE, SYS_WAIT_HANDLE, SYS_STREAM_CREATE, SYS_STREAM_READ, SYS_STREAM_WRITE, SYS_MAILBOX_CREATE, SYS_MAILBOX_SEND, SYS_MAILBOX_RECV, SYS_WAITSET_CREATE, SYS_WAITSET_ADD, SYS_WAITSET_REMOVE, SYS_WAITSET_WAIT, SYS_PROC_OPEN, SYS_SURFACE_CREATE, SYS_SURFACE_MAP, SYS_SURFACE_RESIZE #include "IpcSyscall.hpp" // SYS_DUPHANDLE, SYS_WAIT_HANDLE, SYS_STREAM_CREATE, SYS_STREAM_READ, SYS_STREAM_WRITE, SYS_MAILBOX_CREATE, SYS_MAILBOX_SEND, SYS_MAILBOX_RECV, SYS_WAITSET_CREATE, SYS_WAITSET_ADD, SYS_WAITSET_REMOVE, SYS_WAITSET_WAIT, SYS_PROC_OPEN, SYS_SURFACE_CREATE, SYS_SURFACE_MAP, SYS_SURFACE_RESIZE
#include "LibSyscall.hpp" // SYS_LOAD_LIB, SYS_UNLOAD_LIB, SYS_DLSYM #include "LibSyscall.hpp" // SYS_LOAD_LIB, SYS_UNLOAD_LIB, SYS_DLSYM
#include "CrashReportSyscall.hpp" // SYS_CRASH_REPORT #include "CrashReportSyscall.hpp" // SYS_CRASH_REPORT
#include "Clipboard.hpp" // SYS_CLIPBOARD_* #include "Clipboard.hpp" // SYS_CLIPBOARD_* (text and multi-format)
#include "UserMemory.hpp" #include "UserMemory.hpp"
// Assembly entry point
extern "C" void SyscallEntry();
namespace montauk::abi { namespace montauk::abi {
static constexpr uint64_t kMaxPrintableStringBytes = 4096; static constexpr uint64_t kMaxPrintableStringBytes = 4096;
@@ -139,8 +134,8 @@ namespace montauk::abi {
case SYS_ISKEYAVAILABLE: case SYS_ISKEYAVAILABLE:
return (int64_t)Sys_IsKeyAvailable(); return (int64_t)Sys_IsKeyAvailable();
case SYS_GETKEY: case SYS_GETKEY:
if (!UserMemory::Writable<KeyEvent>(frame->arg1)) return BadPointer; if (!UserMemory::Writable<KeyEventV1>(frame->arg1)) return BadPointer;
Sys_GetKey((KeyEvent*)frame->arg1); Sys_GetKey((KeyEventV1*)frame->arg1);
return Success; return Success;
case SYS_GETCHAR: case SYS_GETCHAR:
return (int64_t)Sys_GetChar(); return (int64_t)Sys_GetChar();
@@ -250,6 +245,8 @@ namespace montauk::abi {
if (!UserMemory::Writable<DateTime>(frame->arg1)) return BadPointer; if (!UserMemory::Writable<DateTime>(frame->arg1)) return BadPointer;
Sys_GetTime((DateTime*)frame->arg1); Sys_GetTime((DateTime*)frame->arg1);
return Success; return Success;
case SYS_GETUNIXTIME:
return Sys_GetUnixTime();
case SYS_SETUNIXTIME: case SYS_SETUNIXTIME:
if (!Sched::Consent::Require(CAP_SET_TIME)) return CapabilityDenied; if (!Sched::Consent::Require(CAP_SET_TIME)) return CapabilityDenied;
return Sys_SetUnixTime((int64_t)frame->arg1); return Sys_SetUnixTime((int64_t)frame->arg1);
@@ -365,8 +362,8 @@ namespace montauk::abi {
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, false)) return BadPointer; if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, false)) return BadPointer;
return (int64_t)Sys_ChildIoWrite((int)frame->arg1, (const char*)frame->arg2, (int)frame->arg3); return (int64_t)Sys_ChildIoWrite((int)frame->arg1, (const char*)frame->arg2, (int)frame->arg3);
case SYS_CHILDIO_WRITEKEY: case SYS_CHILDIO_WRITEKEY:
if (!UserMemory::Readable<KeyEvent>(frame->arg2)) return BadPointer; if (!UserMemory::Readable<KeyEventV1>(frame->arg2)) return BadPointer;
return (int64_t)Sys_ChildIoWriteKey((int)frame->arg1, (const KeyEvent*)frame->arg2); return (int64_t)Sys_ChildIoWriteKey((int)frame->arg1, (const KeyEventV1*)frame->arg2);
case SYS_CHILDIO_SETTERMSZ: case SYS_CHILDIO_SETTERMSZ:
return (int64_t)Sys_ChildIoSetTermsz((int)frame->arg1, (int)frame->arg2, (int)frame->arg3); return (int64_t)Sys_ChildIoSetTermsz((int)frame->arg1, (int)frame->arg2, (int)frame->arg3);
case SYS_WINCREATE: case SYS_WINCREATE:
@@ -400,6 +397,12 @@ namespace montauk::abi {
return (int64_t)Sys_ProcList((ProcInfo*)frame->arg1, (int)frame->arg2); return (int64_t)Sys_ProcList((ProcInfo*)frame->arg1, (int)frame->arg2);
case SYS_KILL: case SYS_KILL:
return (int64_t)Sys_Kill((int)frame->arg1); return (int64_t)Sys_Kill((int)frame->arg1);
case SYS_ABORT:
Sys_Abort();
return Success;
case SYS_EXIT_GROUP:
Sys_ExitGroup((int)frame->arg1);
return Success;
case SYS_SETSESSION: case SYS_SETSESSION:
return (int64_t)Sys_SetSession(); return (int64_t)Sys_SetSession();
case SYS_KILLSESSION: case SYS_KILLSESSION:
@@ -453,6 +456,10 @@ namespace montauk::abi {
case SYS_FS_SYNC: case SYS_FS_SYNC:
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied; if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
return Sys_FsSync(); return Sys_FsSync();
case SYS_FS_GROW:
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
if (!UserMemory::Range(frame->arg2, sizeof(FsGrowStatus), true)) return BadPointer;
return Sys_FsGrow((int)frame->arg1, (FsGrowStatus*)frame->arg2, frame->arg3 != 0);
case SYS_FSFORMAT: case SYS_FSFORMAT:
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied; if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
if (!UserMemory::Readable<FsFormatParams>(frame->arg1)) return BadPointer; if (!UserMemory::Readable<FsFormatParams>(frame->arg1)) return BadPointer;
@@ -527,6 +534,14 @@ namespace montauk::abi {
return Sched::Consent::Next((ConsentRequest*)frame->arg1); return Sched::Consent::Next((ConsentRequest*)frame->arg1);
case SYS_CONSENT_REPLY: case SYS_CONSENT_REPLY:
return Sched::Consent::Reply(frame->arg1, (int)frame->arg2); return Sched::Consent::Reply(frame->arg1, (int)frame->arg2);
case SYS_GETKEY_EX:
if (frame->arg2 < sizeof(KeyEventV1) || frame->arg2 > 4096) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
return Sys_GetKeyEx((void*)frame->arg1, frame->arg2);
case SYS_CHILDIO_WRITEKEY_EX:
if (frame->arg3 < sizeof(KeyEventV1) || frame->arg3 > 4096) return InvalidArgument;
if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return BadPointer;
return (int64_t)Sys_ChildIoWriteKeyEx((int)frame->arg1, (const void*)frame->arg2, frame->arg3);
case SYS_THREAD_SPAWN: case SYS_THREAD_SPAWN:
return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3); return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3);
case SYS_THREAD_EXIT: case SYS_THREAD_EXIT:
@@ -537,6 +552,13 @@ namespace montauk::abi {
return Sys_ThreadJoin((int)frame->arg1, (int*)frame->arg2); return Sys_ThreadJoin((int)frame->arg1, (int*)frame->arg2);
case SYS_THREAD_SELF: case SYS_THREAD_SELF:
return Sys_ThreadSelf(); return Sys_ThreadSelf();
case SYS_FUTEX_WAIT:
// Validation demand-commits the page so the scheduler can
// read the word with its lock held without faulting.
if (!UserMemory::Readable<uint32_t>(frame->arg1)) return BadPointer;
return Sys_FutexWait(frame->arg1, (uint32_t)frame->arg2, frame->arg3);
case SYS_FUTEX_WAKE:
return Sys_FutexWake(frame->arg1, (uint32_t)frame->arg2);
case SYS_BTSCAN: case SYS_BTSCAN:
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied; if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied;
if ((int64_t)frame->arg2 < 0) return InvalidArgument; if ((int64_t)frame->arg2 < 0) return InvalidArgument;
@@ -705,6 +727,51 @@ namespace montauk::abi {
UserMemory::IsUserPtr(frame->arg4) ? (uint64_t*)frame->arg4 : nullptr); UserMemory::IsUserPtr(frame->arg4) ? (uint64_t*)frame->arg4 : nullptr);
case SYS_CLIPBOARD_CLEAR: case SYS_CLIPBOARD_CLEAR:
return Sys_ClipboardClear(); return Sys_ClipboardClear();
case SYS_CLIPBOARD_SET_DATA:
if (frame->arg2 > TRANSFER_MAX_FORMATS) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, frame->arg2 * sizeof(TransferItem), false)) return BadPointer;
return Sys_ClipboardSetData((const TransferItem*)frame->arg1, (uint32_t)frame->arg2);
case SYS_CLIPBOARD_FORMATS: {
uint64_t max = frame->arg2 > TRANSFER_MAX_FORMATS ? TRANSFER_MAX_FORMATS : frame->arg2;
if (!UserMemory::OptionalRange(frame->arg1, max * sizeof(TransferFormatInfo), true)) return BadPointer;
if (!UserMemory::OptionalRange(frame->arg3, sizeof(uint64_t), true)) return BadPointer;
return Sys_ClipboardFormats(frame->arg1 ? (TransferFormatInfo*)frame->arg1 : nullptr,
(uint32_t)max,
frame->arg3 ? (uint64_t*)frame->arg3 : nullptr);
}
case SYS_CLIPBOARD_GET_DATA:
if (!UserMemory::String(frame->arg1, TRANSFER_MIME_MAX)) return BadPointer;
if (frame->arg3 > 0 && !UserMemory::Range(frame->arg2, frame->arg3, true)) return BadPointer;
if (!UserMemory::Writable<uint32_t>(frame->arg4)) return BadPointer;
return Sys_ClipboardGetData((const char*)frame->arg1,
frame->arg3 > 0 ? (void*)frame->arg2 : nullptr,
(uint32_t)frame->arg3, (uint32_t*)frame->arg4);
case SYS_WIN_REQUEST:
if (!UserMemory::Readable<WinRequest>(frame->arg2)) return BadPointer;
return Sys_WinRequest((int)frame->arg1, (const WinRequest*)frame->arg2);
case SYS_WIN_REQUEST_POLL:
if (!UserMemory::Writable<WinRequest>(frame->arg1)) return BadPointer;
return Sys_WinRequestPoll((WinRequest*)frame->arg1);
case SYS_WIN_DRAG_BEGIN:
if (!UserMemory::Readable<WinDragBegin>(frame->arg2)) return BadPointer;
if (frame->arg4 == 0 || frame->arg4 > TRANSFER_MAX_FORMATS) return InvalidArgument;
if (!UserMemory::Range(frame->arg3, frame->arg4 * sizeof(TransferItem), false)) return BadPointer;
return Sys_WinDragBegin((int)frame->arg1, (const WinDragBegin*)frame->arg2,
(const TransferItem*)frame->arg3, (uint32_t)frame->arg4);
case SYS_WIN_DRAG_FORMATS: {
uint64_t max = frame->arg3 > TRANSFER_MAX_FORMATS ? TRANSFER_MAX_FORMATS : frame->arg3;
if (!UserMemory::OptionalRange(frame->arg2, max * sizeof(TransferFormatInfo), true)) return BadPointer;
return Sys_WinDragFormats((uint32_t)frame->arg1,
frame->arg2 ? (TransferFormatInfo*)frame->arg2 : nullptr,
(uint32_t)max);
}
case SYS_WIN_DRAG_GET_DATA:
if (!UserMemory::String(frame->arg2, TRANSFER_MIME_MAX)) return BadPointer;
if (frame->arg4 > 0 && !UserMemory::Range(frame->arg3, frame->arg4, true)) return BadPointer;
if (!UserMemory::Writable<uint32_t>(frame->arg5)) return BadPointer;
return Sys_WinDragGetData((uint32_t)frame->arg1, (const char*)frame->arg2,
frame->arg4 > 0 ? (void*)frame->arg3 : nullptr,
(uint32_t)frame->arg4, (uint32_t*)frame->arg5);
case SYS_INPUT_WAIT: case SYS_INPUT_WAIT:
return (int64_t)Sys_InputWait(frame->arg1, frame->arg2); return (int64_t)Sys_InputWait(frame->arg1, frame->arg2);
case SYS_LOG_WRITE: { case SYS_LOG_WRITE: {
@@ -730,29 +797,13 @@ namespace montauk::abi {
} }
} }
// ---- SYSCALL MSR initialization ---- // ---- Entry point initialization ----
void InitializeSyscalls() { void InitializeSyscalls() {
// Enable SYSCALL/SYSRET and no-execute page permissions in EFER. Arch::InitializeSyscallEntry();
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] Kt::KernelLogStream(Kt::OK, "Syscall") << "System call entry installed ("
// SYSCALL: CS=0x08, SS=0x10 << kcp::dec << (SYS_SPAWN_REDIR_CAPS + 1) << " syscall slots)";
// 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)";
} }
} }
+245 -13
View File
@@ -8,6 +8,7 @@
#include <cstdint> #include <cstdint>
#include <cstddef> #include <cstddef>
#include <Common/StatusCodes.hpp> #include <Common/StatusCodes.hpp>
#include <Arch/Syscall.hpp>
namespace montauk::abi { namespace montauk::abi {
@@ -358,6 +359,55 @@ namespace montauk::abi {
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
/* Unicode key events. Size-versioned: the caller passes sizeof(its
KeyEvent); the kernel copies min(size, its own) bytes and zero-fills the
rest, so the struct can grow without another syscall. */
static constexpr uint64_t SYS_GETKEY_EX = 194; // (KeyEvent*, size) -> 0; blocks like SYS_GETKEY
static constexpr uint64_t SYS_CHILDIO_WRITEKEY_EX = 195; // (childPid, const KeyEvent*, size) -> status
/* Futexes: process-private wait queues keyed by a user address. WAIT
blocks only if *addr still equals `expected` (checked atomically with
enrollment). timeoutNs: FUTEX_WAIT_FOREVER = no deadline, 0 = check
only; rounded up to the 1 ms scheduler tick. WAIT returns 0 when woken,
WouldBlock on value mismatch, TimedOut on deadline. WAKE returns the
number of threads woken (count = FUTEX_WAKE_ALL wakes every waiter). */
static constexpr uint64_t SYS_FUTEX_WAIT = 196; // (uint32_t* addr, expected, timeoutNs) -> status
static constexpr uint64_t SYS_FUTEX_WAKE = 197; // (uint32_t* addr, count) -> woken, or status
static constexpr uint64_t FUTEX_WAIT_FOREVER = ~0ULL;
static constexpr uint32_t FUTEX_WAKE_ALL = 0xFFFFFFFFu;
/* Abnormal termination from any thread: ends the whole process (every
thread) with exit code TerminatedAborted (256+SIGABRT). SYS_EXIT from a
sibling thread ends only that thread, so abort() needs its own call. */
static constexpr uint64_t SYS_ABORT = 198; // () -> does not return
/* Normal termination of the whole process from any thread, with an exit
code (0..255) instead of SYS_ABORT's fixed TerminatedAborted. libc
exit() and Rust std::process::exit() use it, so exiting from a sibling
thread ends the process rather than only that thread. */
static constexpr uint64_t SYS_EXIT_GROUP = 199; // (exitCode) -> does not return
/* Multi-format clipboard (see "Window server messaging" below). The text
calls above operate on its TRANSFER_MIME_TEXT format. */
static constexpr uint64_t SYS_CLIPBOARD_SET_DATA = 200; // (const TransferItem*, count) -> status
static constexpr uint64_t SYS_CLIPBOARD_FORMATS = 201; // (TransferFormatInfo*, max, uint64_t* serial) -> count
static constexpr uint64_t SYS_CLIPBOARD_GET_DATA = 202; // (mime, buf, max, uint32_t* len) -> status
/* Window server requests and drag and drop. */
static constexpr uint64_t SYS_WIN_REQUEST = 203; // (windowId, const WinRequest*) -> status
static constexpr uint64_t SYS_WIN_REQUEST_POLL = 204; // (WinRequest*) -> 1 / 0; compositor only
static constexpr uint64_t SYS_WIN_DRAG_BEGIN = 205; // (windowId or -1 for the compositor's own surface, const WinDragBegin*, const TransferItem*, count) -> serial
static constexpr uint64_t SYS_WIN_DRAG_FORMATS = 206; // (serial, TransferFormatInfo*, max) -> count
static constexpr uint64_t SYS_WIN_DRAG_GET_DATA = 207; // (serial, mime, buf, max, uint32_t* len) -> status
/* Storage.hpp -- grow a mounted volume (and its MBR partition) to fill the
free space after it. apply=0 only reports; apply=1 does one bounded slice
and returns 1 while more remains, 0 once done. CAP_STORAGE_ADMIN. */
static constexpr uint64_t SYS_FS_GROW = 208; // (drive, FsGrowStatus*, apply) -> 1 / 0 / status
/* Time.hpp -- wall clock as UTC unix seconds (SYS_GETTIME is local time). */
static constexpr uint64_t SYS_GETUNIXTIME = 209; // () -> seconds
/* Kernel-owned process capabilities. User identities may namespace /* Kernel-owned process capabilities. User identities may namespace
per-user resources, but never participate in authorization decisions. */ per-user resources, but never participate in authorization decisions. */
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0; static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
@@ -600,7 +650,10 @@ namespace montauk::abi {
uint64_t txPackets; uint64_t txPackets;
}; };
struct KeyEvent { // Legacy 9-byte key event, read by SYS_GETKEY and written by
// SYS_CHILDIO_WRITEKEY. Kept so binaries built before codepoints still work.
// `ascii` is a Windows-1252 byte.
struct KeyEventV1 {
uint8_t scancode; uint8_t scancode;
char ascii; char ascii;
bool pressed; bool pressed;
@@ -616,6 +669,32 @@ namespace montauk::abi {
bool extended; bool extended;
}; };
// Key event with the Unicode text it types (SYS_GETKEY_EX,
// SYS_CHILDIO_WRITEKEY_EX, WinEvent). Its first 9 bytes ARE a KeyEventV1, so
// a legacy reader sees a valid event. `codepoint` is the character the key
// produces (0 for control/navigation keys and dead keys). `ascii` is its
// Windows-1252 byte, or KEY_ASCII_SUBSTITUTE when 1252 has none.
struct KeyEvent {
uint8_t scancode;
char ascii;
bool pressed;
bool shift;
bool ctrl;
bool alt;
bool capslock;
bool altgr;
bool extended;
uint8_t _reserved[3];
uint32_t codepoint;
};
static_assert(sizeof(KeyEventV1) == 9, "KeyEventV1 is frozen ABI");
static_assert(sizeof(KeyEvent) == 16, "KeyEvent must fit the WinEvent union");
// ASCII SUB: `ascii` of a text key whose character has no Windows-1252 byte.
// A control character, so legacy code that only accepts ascii >= 0x20 skips
// it, while code testing `ascii == 0` for navigation keys is not confused.
static constexpr char KEY_ASCII_SUBSTITUTE = 0x1A;
struct MouseState { struct MouseState {
int32_t x; int32_t x;
int32_t y; int32_t y;
@@ -649,16 +728,55 @@ namespace montauk::abi {
}; };
// Window server shared types // Window server shared types
enum WinEventType : uint8_t {
WIN_EVENT_KEY = 0,
WIN_EVENT_MOUSE = 1,
WIN_EVENT_RESIZE = 2,
WIN_EVENT_CLOSE = 3,
WIN_EVENT_SCALE = 4,
// Drag and drop (WinEvent::drag). A window may read the drag's data
// (SYS_WIN_DRAG_*) once it has received one of these for that serial.
WIN_EVENT_DRAG_ENTER = 5, // pointer entered the window during a drag
WIN_EVENT_DRAG_OVER = 6, // pointer moved; coalesced like mouse moves
WIN_EVENT_DRAG_LEAVE = 7, // pointer left, or the drag was cancelled
WIN_EVENT_DROP = 8, // released here; drag.actions = chosen action
WIN_EVENT_DRAG_END = 9, // to the source only; drag.actions = action
// taken (0 = cancelled / not accepted)
};
// Modifier keys held at the time of a drag event (WinEvent::drag.modifiers).
static constexpr uint8_t WIN_MOD_SHIFT = 1u << 0;
static constexpr uint8_t WIN_MOD_CTRL = 1u << 1;
static constexpr uint8_t WIN_MOD_ALT = 1u << 2;
// Drag actions; a source offers a set, a drop carries exactly one.
static constexpr uint8_t WIN_DRAG_COPY = 1u << 0;
static constexpr uint8_t WIN_DRAG_MOVE = 1u << 1;
static constexpr uint8_t WIN_DRAG_LINK = 1u << 2;
struct WinDragEvent {
int32_t x, y; // window-local; -1,-1 when not over the window
uint32_t serial; // identifies the drag
uint8_t actions; // WIN_DRAG_* (see WinEventType)
uint8_t modifiers; // WIN_MOD_*
uint8_t _pad[2];
};
static_assert(sizeof(WinDragEvent) == 16, "WinDragEvent must fit the WinEvent union");
struct WinEvent { struct WinEvent {
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale uint8_t type; // WinEventType
uint8_t _pad[3]; uint8_t _pad[3];
union { union {
KeyEvent key; KeyEvent key; // 16 bytes: the union was already that size (mouse)
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse; // modifiers (WIN_MOD_*) was padding before; compositors that
// predate it send 0.
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons, modifiers; } mouse;
struct { int32_t w, h; } resize; struct { int32_t w, h; } resize;
struct { int32_t scale; } scale; struct { int32_t scale; } scale;
WinDragEvent drag; // WIN_EVENT_DRAG_* / WIN_EVENT_DROP
}; };
}; };
static_assert(sizeof(WinEvent) == 20, "WinEvent layout is shared with existing binaries");
struct WinInfo { struct WinInfo {
int32_t id; int32_t id;
@@ -679,6 +797,116 @@ namespace montauk::abi {
uint64_t pixelVa; // VA of pixel buffer in caller's address space uint64_t pixelVa; // VA of pixel buffer in caller's address space
}; };
// ---- Window server messaging ----------------------------------------
//
// Three pieces, shared by the compositor (the desktop) and every app:
// * WinEvent types 5-9 below: drag-and-drop events, compositor -> app.
// * WinRequest: typed records an app posts about one of its own windows
// (SYS_WIN_REQUEST); only the compositor can collect them
// (SYS_WIN_REQUEST_POLL). Unknown types are ignored, so new requests
// can be added without breaking older compositors or apps.
// * Transfers: multi-format payloads (one MIME type per format) carried
// by the clipboard (SYS_CLIPBOARD_*_DATA) and by drags
// (SYS_WIN_DRAG_*). The older text clipboard calls read and write the
// TRANSFER_MIME_TEXT format of the same clipboard.
// Transfers -----------------------------------------------------------
static constexpr uint32_t TRANSFER_MAX_FORMATS = 8;
static constexpr uint32_t TRANSFER_MIME_MAX = 64; // incl. NUL
static constexpr uint32_t TRANSFER_MAX_BYTES = 256 * 1024; // all formats together
// Well-known formats. Any other "type/subtype" string is allowed.
static constexpr const char* TRANSFER_MIME_TEXT = "text/plain;charset=utf-8";
// UTF-8 absolute paths ("0:/..."), one per line.
static constexpr const char* TRANSFER_MIME_FILE_LIST = "application/x-montauk-file-list";
// Clipboard companion to a file list: "copy" or "cut".
static constexpr const char* TRANSFER_MIME_FILE_OP = "application/x-montauk-file-op";
// One format to store. Pointers are carried as 64-bit values so the
// layout is the same for every caller.
struct TransferItem {
uint64_t mime; // const char*, NUL-terminated, < TRANSFER_MIME_MAX
uint64_t data; // const void*
uint32_t len;
uint32_t _reserved;
};
static_assert(sizeof(TransferItem) == 24, "TransferItem is ABI");
struct TransferFormatInfo {
char mime[TRANSFER_MIME_MAX];
uint32_t len;
uint32_t _reserved;
};
static_assert(sizeof(TransferFormatInfo) == 72, "TransferFormatInfo is ABI");
// Requests --------------------------------------------------------------
static constexpr uint32_t WIN_REQUEST_PAYLOAD_MAX = 240;
struct WinRequest {
uint16_t type; // WinRequestType
uint16_t size; // payload bytes in use
int32_t windowId; // window the request is about (the sender must own it
// when posting); -1 if it was destroyed since
int32_t senderPid; // filled in by the kernel
uint32_t serial; // filled in by the kernel; increases per request
uint8_t payload[WIN_REQUEST_PAYLOAD_MAX];
};
static_assert(sizeof(WinRequest) == 256, "WinRequest is ABI");
enum WinRequestType : uint16_t {
WIN_REQ_LAUNCH_ITEM = 1, // WinReqLaunchItem
WIN_REQ_SET_PARENT = 2, // WinReqSetParent
WIN_REQ_DRAG_STATUS = 3, // WinReqDragStatus
WIN_REQ_ACTIVATE = 4, // no payload: raise and focus the window
// Types from here on are generated by the kernel; SYS_WIN_REQUEST
// refuses them so an app cannot forge one.
WIN_REQ_KERNEL_BASE = 0x8000,
WIN_REQ_DRAG_BEGIN = 0x8001, // WinReqDragBegin (from SYS_WIN_DRAG_BEGIN)
};
// Open a desktop catalog item (an app manifest id, or a built-in such as
// "desktop-settings"), as if the user had picked it from the launcher, so
// it runs with the authority the desktop would give it. `args` reach only
// apps whose manifest declares files.types (they take a document path);
// anything else starts without them, so a request cannot steer a
// privileged app through its command line.
struct WinReqLaunchItem {
char itemId[48];
char args[192];
};
// Mark the window as a dialog of another window owned by the same
// process: the compositor centres it over the parent.
struct WinReqSetParent {
int32_t parentWindowId;
};
// Answer to DRAG_ENTER/DRAG_OVER: the action this window would perform
// on a drop at the current position (0 = would refuse). Advisory; it
// drives the compositor's drag feedback.
struct WinReqDragStatus {
uint32_t serial;
uint8_t action; // one WIN_DRAG_* or 0
uint8_t _pad[3];
};
// Describes a drag for the compositor's feedback. Supplied by the app to
// SYS_WIN_DRAG_BEGIN and relayed (with the serial) as WIN_REQ_DRAG_BEGIN.
struct WinDragBegin {
uint8_t actions; // WIN_DRAG_* offered
uint8_t _pad[3];
uint32_t itemCount; // for a "3 items" badge; 0 = do not show
char label[64]; // short description, e.g. the file name
};
static_assert(sizeof(WinDragBegin) == 72, "WinDragBegin is ABI");
struct WinReqDragBegin {
uint32_t serial;
WinDragBegin info;
};
struct MemStats { struct MemStats {
uint64_t totalBytes; uint64_t totalBytes;
uint64_t freeBytes; uint64_t freeBytes;
@@ -695,7 +923,7 @@ namespace montauk::abi {
struct DiskInfo { struct DiskInfo {
uint8_t port; // block device index uint8_t port; // block device index
uint8_t type; // 0=none, 1=SATA, 2=SATAPI, 3=NVMe, 4=USB mass storage uint8_t type; // 0=none, 1=SATA, 2=SATAPI, 3=NVMe, 4=USB mass storage, 5=SD card
uint8_t sataGen; // SATA gen (1/2/3) uint8_t sataGen; // SATA gen (1/2/3)
uint8_t _pad0; uint8_t _pad0;
uint64_t sectorCount; // Total user-addressable sectors uint64_t sectorCount; // Total user-addressable sectors
@@ -777,6 +1005,14 @@ namespace montauk::abi {
char label[32]; // volume label char label[32]; // volume label
}; };
struct FsGrowStatus {
uint64_t fsBytes; // filesystem size now
uint64_t partBytes; // partition size now
uint64_t targetBytes; // filesystem size once grown; == fsBytes if it cannot grow
uint32_t stepsDone; // progress of the grow, out of stepsTotal
uint32_t stepsTotal;
};
struct ProcInfo { struct ProcInfo {
int32_t pid; int32_t pid;
int32_t parentPid; int32_t parentPid;
@@ -1010,15 +1246,11 @@ namespace montauk::abi {
uint64_t timestampTick; uint64_t timestampTick;
}; };
// Stack frame pushed by SyscallEntry.asm // Saved user context of a system call; its layout is the
struct SyscallFrame { // architecture's (see Arch/Syscall.hpp).
uint64_t r15, r14, r13, r12, rbp, rbx; // callee-saved using SyscallFrame = Arch::SyscallFrame;
uint64_t arg6, arg5, arg4, arg3, arg2, arg1;
uint64_t syscall_nr;
uint64_t user_rflags, user_rip, user_rsp;
};
// Kernel-only: set up SYSCALL MSRs and initialize dispatch // Kernel-only: install the system-call entry point and initialize dispatch
void InitializeSyscalls(); void InitializeSyscalls();
} }
+6 -2
View File
@@ -1,11 +1,11 @@
/* /*
* Time.hpp * Time.hpp
* SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME syscalls * SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME, SYS_GETUNIXTIME syscalls
* Copyright (c) 2026 Daniel Hammer * Copyright (c) 2026 Daniel Hammer
*/ */
#pragma once #pragma once
#include <Timekeeping/ApicTimer.hpp> #include <Timekeeping/Timer.hpp>
#include <Timekeeping/Time.hpp> #include <Timekeeping/Time.hpp>
#include "Syscall.hpp" #include "Syscall.hpp"
@@ -39,6 +39,10 @@ namespace montauk::abi {
return (int64_t)Timekeeping::GetTZOffset(); return (int64_t)Timekeeping::GetTZOffset();
} }
static int64_t Sys_GetUnixTime() {
return Timekeeping::GetUnixTimestamp();
}
static int64_t Sys_SetUnixTime(int64_t unixSeconds) { static int64_t Sys_SetUnixTime(int64_t unixSeconds) {
return Timekeeping::SetUnixTimestamp(unixSeconds) ? Success : InvalidArgument; return Timekeeping::SetUnixTimestamp(unixSeconds) ? Success : InvalidArgument;
} }
+12 -12
View File
@@ -19,9 +19,9 @@ namespace montauk::abi::UserMemory {
return addr != 0 && addr < USER_SPACE_END; return addr != 0 && addr < USER_SPACE_END;
} }
inline uint64_t CurrentPml4() { inline uint64_t CurrentRoot() {
auto* proc = Sched::GetCurrentProcessPtr(); 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) { 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 < addr) return false;
if (addr + size > USER_SPACE_END) return false; if (addr + size > USER_SPACE_END) return false;
uint64_t pml4Phys = CurrentPml4(); uint64_t pageTableRoot = CurrentRoot();
if (pml4Phys == 0) return false; 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; return true;
// Anonymous VMAs are demand-committed. Validation precedes the actual // Anonymous VMAs are demand-committed. Validation precedes the actual
@@ -42,13 +42,13 @@ namespace montauk::abi::UserMemory {
uint64_t page = addr & ~0xFFFULL; uint64_t page = addr & ~0xFFFULL;
uint64_t last = (addr + size - 1) & ~0xFFFULL; uint64_t last = (addr + size - 1) & ~0xFFFULL;
for (;;) { for (;;) {
if (!Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, page, 1, requireWrite) && if (!Memory::VMM::Paging::IsUserRangeAccessible(pageTableRoot, page, 1, requireWrite) &&
!TryHandleAnonymousPageFault(page, requireWrite ? 2ULL : 0ULL)) !TryHandleAnonymousPageFault(page, {false, requireWrite, false}))
return false; return false;
if (page == last) break; if (page == last) break;
page += 0x1000ULL; 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) { 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) { inline bool String(uint64_t addr, uint64_t maxLen) {
if (!ValidUserPtr(addr) || maxLen == 0) return false; if (!ValidUserPtr(addr) || maxLen == 0) return false;
uint64_t pml4Phys = CurrentPml4(); uint64_t pageTableRoot = CurrentRoot();
if (pml4Phys == 0) return false; if (pageTableRoot == 0) return false;
const char* str = (const char*)addr; const char* str = (const char*)addr;
for (uint64_t i = 0; i < maxLen; i++) { for (uint64_t i = 0; i < maxLen; i++) {
if (!Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, addr + i, 1, false)) { if (!Memory::VMM::Paging::IsUserRangeAccessible(pageTableRoot, addr + i, 1, false)) {
if (!TryHandleAnonymousPageFault(addr + i, 0)) return false; if (!TryHandleAnonymousPageFault(addr + i, {false, false, false})) return false;
} }
if (str[i] == '\0') return true; if (str[i] == '\0') return true;
} }
+208 -16
View File
@@ -54,13 +54,13 @@ namespace WinServer {
Ipc::ReleaseSurface(surface); 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; int result = InvalidArgument;
for (int i = 0; i < MaxRetiredSnapshots; i++) { for (int i = 0; i < MaxRetiredSnapshots; i++) {
if (!g_retiredSnapshots[i].used || g_retiredSnapshots[i].windowId != windowId) continue; if (!g_retiredSnapshots[i].used || g_retiredSnapshots[i].windowId != windowId) continue;
if (g_retiredSnapshots[i].surface != nullptr) { 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; result = Success;
} }
Ipc::ReleaseSurface(g_retiredSnapshots[i].surface); Ipc::ReleaseSurface(g_retiredSnapshots[i].surface);
@@ -73,11 +73,18 @@ namespace WinServer {
return result; return result;
} }
static void OrphanRequestsLocked(int windowId);
static void ReleaseSlotResourcesLocked(WindowSlot& slot, bool unmapOwner) { static void ReleaseSlotResourcesLocked(WindowSlot& slot, bool unmapOwner) {
// Queued requests stay deliverable (a dialog may post "open this
// app" and close at once), but no longer name a window that could be
// handed to someone else.
OrphanRequestsLocked((int)(&slot - g_slots));
if (unmapOwner && slot.liveSurface != nullptr) { if (unmapOwner && slot.liveSurface != nullptr) {
auto* owner = Sched::GetProcessByPid(slot.ownerPid); auto* owner = Sched::GetProcessByPid(slot.ownerPid);
if (owner != nullptr) { if (owner != nullptr) {
Ipc::UnmapSurfaceForPid(slot.liveSurface, slot.ownerPid, owner->pml4Phys); Ipc::UnmapSurfaceForPid(slot.liveSurface, slot.ownerPid, owner->pageTableRoot);
} }
} }
@@ -94,19 +101,63 @@ namespace WinServer {
ResetSlotLocked(slot); ResetSlotLocked(slot);
} }
// ---- Messaging state ----
static constexpr int MaxRequests = 64;
// One app may not hold more than this many queued requests, so a single
// misbehaving process cannot starve the others out of the queue.
static constexpr int MaxRequestsPerSender = 16;
static constexpr int MaxDragReaders = 8;
static montauk::abi::WinRequest g_requests[MaxRequests];
static int g_requestHead = 0; // oldest
static int g_requestCount = 0;
static uint32_t g_requestSerial = 0;
struct DragState {
bool active;
uint32_t serial;
int sourceWindow;
int sourcePid;
DataTransfer::Transfer transfer;
int readers[MaxDragReaders];
int readerCount;
};
static DragState g_drag;
static uint32_t g_dragSerial = 0;
static bool IsDragEvent(uint8_t type) {
return type >= montauk::abi::WIN_EVENT_DRAG_ENTER && type <= montauk::abi::WIN_EVENT_DRAG_END;
}
static void GrantDragReaderLocked(uint32_t serial, int pid) {
if (!g_drag.active || g_drag.serial != serial || pid <= 0) return;
if (pid == g_drag.sourcePid) return;
for (int i = 0; i < g_drag.readerCount; i++) {
if (g_drag.readers[i] == pid) return;
}
if (g_drag.readerCount < MaxDragReaders) g_drag.readers[g_drag.readerCount++] = pid;
}
static int SendEventLocked(int windowId, const montauk::abi::WinEvent* event) { static int SendEventLocked(int windowId, const montauk::abi::WinEvent* event) {
if (windowId < 0 || windowId >= MaxWindows || event == nullptr) return InvalidArgument; if (windowId < 0 || windowId >= MaxWindows || event == nullptr) return InvalidArgument;
WindowSlot& slot = g_slots[windowId]; WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.eventMailbox == nullptr) return InvalidArgument; if (!slot.used || slot.eventMailbox == nullptr) return InvalidArgument;
int rc = (event->type == 1) // Delivering a drag event is what lets the receiving app read the
? Ipc::MailboxSendCoalescedMouse(slot.eventMailbox, 0, event, sizeof(*event)) // drag's data: it needs the formats to decide whether to accept.
if (IsDragEvent(event->type)) GrantDragReaderLocked(event->drag.serial, slot.ownerPid);
bool motion = event->type == montauk::abi::WIN_EVENT_MOUSE ||
event->type == montauk::abi::WIN_EVENT_DRAG_OVER;
int rc = motion
? Ipc::MailboxSendCoalescedMotion(slot.eventMailbox, 0, event, sizeof(*event))
: Ipc::MailboxSend(slot.eventMailbox, 0, event, sizeof(*event)); : Ipc::MailboxSend(slot.eventMailbox, 0, event, sizeof(*event));
if (rc < 0) return GenericFailure; if (rc < 0) return GenericFailure;
return rc > 0 ? Success : Busy; 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) { uint64_t& heapNext, uint64_t& outVa) {
WsGuard guard; WsGuard guard;
@@ -143,7 +194,7 @@ namespace WinServer {
FillSurfaceLocked(snapshot, w, h, InitialWindowPixel); FillSurfaceLocked(snapshot, w, h, InitialWindowPixel);
uint64_t userVa = 0; 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::ReleaseMailbox(mailbox, true, true);
Ipc::ReleaseSurface(snapshot); Ipc::ReleaseSurface(snapshot);
Ipc::ReleaseSurface(live); Ipc::ReleaseSurface(live);
@@ -236,7 +287,7 @@ namespace WinServer {
return count; 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; WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) return 0; if (windowId < 0 || windowId >= MaxWindows) return 0;
@@ -244,23 +295,23 @@ namespace WinServer {
if (!slot.used || slot.snapshotSurface == nullptr) return 0; if (!slot.used || slot.snapshotSurface == nullptr) return 0;
uint64_t outVa = 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 0;
} }
return outVa; return outVa;
} }
int Unmap(int windowId, int callerPid, uint64_t callerPml4) { int Unmap(int windowId, int callerPid, uint64_t callerRoot) {
WsGuard guard; WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) { 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]; WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.snapshotSurface == nullptr) return result; 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; if (current == 0) return Success;
return result; return result;
} }
@@ -270,7 +321,7 @@ namespace WinServer {
return SendEventLocked(windowId, event); 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) { uint64_t& heapNext, uint64_t& outVa) {
WsGuard guard; WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) return InvalidArgument; if (windowId < 0 || windowId >= MaxWindows) return InvalidArgument;
@@ -297,7 +348,7 @@ namespace WinServer {
Ipc::RetainSurface(newSnapshot); Ipc::RetainSurface(newSnapshot);
uint64_t newOwnerVa = 0; 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(newSnapshot);
Ipc::ReleaseSurface(newLive); Ipc::ReleaseSurface(newLive);
return GenericFailure; return GenericFailure;
@@ -315,7 +366,7 @@ namespace WinServer {
} }
if (slot.liveSurface != nullptr) { if (slot.liveSurface != nullptr) {
Ipc::UnmapSurfaceForPid(slot.liveSurface, callerPid, ownerPml4); Ipc::UnmapSurfaceForPid(slot.liveSurface, callerPid, ownerRoot);
Ipc::ReleaseSurface(slot.liveSurface); Ipc::ReleaseSurface(slot.liveSurface);
} }
if (slot.snapshotSurface != nullptr) { if (slot.snapshotSurface != nullptr) {
@@ -376,12 +427,153 @@ namespace WinServer {
return g_uiScale; return g_uiScale;
} }
static void OrphanRequestsLocked(int windowId) {
for (int i = 0; i < g_requestCount; i++) {
montauk::abi::WinRequest& r = g_requests[(g_requestHead + i) % MaxRequests];
if (r.windowId == windowId) r.windowId = -1;
}
}
static void EndDragLocked() {
DataTransfer::Free(g_drag.transfer);
g_drag.active = false;
g_drag.readerCount = 0;
}
void CleanupProcess(int pid) { void CleanupProcess(int pid) {
WsGuard guard; WsGuard guard;
for (int i = 0; i < MaxWindows; i++) { for (int i = 0; i < MaxWindows; i++) {
if (!g_slots[i].used || g_slots[i].ownerPid != pid) continue; if (!g_slots[i].used || g_slots[i].ownerPid != pid) continue;
ReleaseSlotResourcesLocked(g_slots[i], false); ReleaseSlotResourcesLocked(g_slots[i], false);
} }
// Requests the process already posted are still delivered: they are
// messages in flight, and a process may well exit right after its
// last one ("open this file" from a dialog-only Files).
if (g_drag.active && g_drag.sourcePid == pid) EndDragLocked();
}
static int EnqueueRequestLocked(montauk::abi::WinRequest& request) {
if (g_requestCount >= MaxRequests) return WouldBlock;
request.serial = ++g_requestSerial;
if (request.serial == 0) request.serial = ++g_requestSerial;
g_requests[(g_requestHead + g_requestCount) % MaxRequests] = request;
g_requestCount++;
return Success;
}
static bool OwnsWindowLocked(int windowId, int pid) {
return windowId >= 0 && windowId < MaxWindows && g_slots[windowId].used &&
g_slots[windowId].ownerPid == pid;
}
int PostRequest(int windowId, int callerPid, const montauk::abi::WinRequest& request) {
using namespace montauk::abi;
if (request.type == 0 || request.type >= WIN_REQ_KERNEL_BASE) return InvalidArgument;
if (request.size > WIN_REQUEST_PAYLOAD_MAX) return InvalidArgument;
WsGuard guard;
if (!OwnsWindowLocked(windowId, callerPid)) return PermissionDenied;
int mine = 0;
for (int i = 0; i < g_requestCount; i++) {
if (g_requests[(g_requestHead + i) % MaxRequests].senderPid == callerPid) mine++;
}
if (mine >= MaxRequestsPerSender) return WouldBlock;
WinRequest copy = request;
copy.windowId = windowId;
copy.senderPid = callerPid;
// Bytes past `size` are not the sender's business to pass along.
for (uint32_t i = copy.size; i < WIN_REQUEST_PAYLOAD_MAX; i++) copy.payload[i] = 0;
return EnqueueRequestLocked(copy);
}
int PollRequest(montauk::abi::WinRequest* out) {
WsGuard guard;
while (g_requestCount > 0) {
montauk::abi::WinRequest& r = g_requests[g_requestHead];
g_requestHead = (g_requestHead + 1) % MaxRequests;
g_requestCount--;
// A destroyed window's requests arrive with windowId -1 (see
// OrphanRequestsLocked); anything else must still match.
if (r.windowId >= 0 && !OwnsWindowLocked(r.windowId, r.senderPid)) continue;
*out = r;
return 1;
}
return 0;
}
int DragBegin(int windowId, int callerPid, bool compositor,
const montauk::abi::WinDragBegin& info, DataTransfer::Transfer& transfer) {
using namespace montauk::abi;
WsGuard guard;
// The compositor drags from its own surface (desktop icons) with
// windowId -1; it already knows about such a drag, so no request.
bool fromSurface = compositor && windowId == -1;
if (!fromSurface && !OwnsWindowLocked(windowId, callerPid)) {
DataTransfer::Free(transfer);
return PermissionDenied;
}
uint32_t serial = ++g_dragSerial;
if (serial == 0) serial = ++g_dragSerial;
if (!fromSurface) {
WinRequest request = {};
request.type = WIN_REQ_DRAG_BEGIN;
request.size = sizeof(WinReqDragBegin);
request.windowId = windowId;
request.senderPid = callerPid;
WinReqDragBegin* payload = (WinReqDragBegin*)request.payload;
payload->serial = serial;
payload->info = info;
payload->info.label[sizeof(payload->info.label) - 1] = '\0';
int rc = EnqueueRequestLocked(request);
if (rc != Success) {
DataTransfer::Free(transfer);
return rc;
}
}
// A new drag replaces the previous one (only one pointer exists).
if (g_drag.active) EndDragLocked();
g_drag.active = true;
g_drag.serial = serial;
g_drag.sourceWindow = windowId;
g_drag.sourcePid = callerPid;
g_drag.transfer = transfer;
g_drag.readerCount = 0;
transfer.count = 0;
return (int)serial;
}
static bool DragReadableLocked(uint32_t serial, int callerPid, bool compositor) {
if (!g_drag.active || g_drag.serial != serial) return false;
if (compositor || callerPid == g_drag.sourcePid) return true;
for (int i = 0; i < g_drag.readerCount; i++) {
if (g_drag.readers[i] == callerPid) return true;
}
return false;
}
int DragFormats(uint32_t serial, int callerPid, bool compositor,
montauk::abi::TransferFormatInfo* out, uint32_t max) {
WsGuard guard;
if (!g_drag.active || g_drag.serial != serial) return NotFound;
if (!DragReadableLocked(serial, callerPid, compositor)) return PermissionDenied;
if (out == nullptr || max == 0) return (int)g_drag.transfer.count;
return (int)DataTransfer::ListFormats(g_drag.transfer, out, max);
}
int DragGetData(uint32_t serial, int callerPid, bool compositor, const char* mime,
void* buffer, uint32_t bufferLen, uint32_t* outLen) {
WsGuard guard;
if (outLen != nullptr) *outLen = 0;
if (!g_drag.active || g_drag.serial != serial) return NotFound;
if (!DragReadableLocked(serial, callerPid, compositor)) return PermissionDenied;
const DataTransfer::Format* format = DataTransfer::Find(g_drag.transfer, mime);
if (format == nullptr) return NotFound;
return DataTransfer::CopyOut(*format, buffer, bufferLen, outLen);
} }
} }
+27 -4
View File
@@ -8,6 +8,7 @@
#include "Syscall.hpp" #include "Syscall.hpp"
#include <cstdint> #include <cstdint>
#include <Ipc/Ipc.hpp> #include <Ipc/Ipc.hpp>
#include "DataTransfer.hpp"
namespace WinServer { namespace WinServer {
@@ -27,16 +28,16 @@ namespace WinServer {
uint32_t flags; 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); uint64_t& heapNext, uint64_t& outVa);
int Destroy(int windowId, int callerPid); int Destroy(int windowId, int callerPid);
int Present(int windowId, int callerPid); int Present(int windowId, int callerPid);
int Poll(int windowId, int callerPid, montauk::abi::WinEvent* outEvent); int Poll(int windowId, int callerPid, montauk::abi::WinEvent* outEvent);
int Enumerate(montauk::abi::WinInfo* outArray, int maxCount); int Enumerate(montauk::abi::WinInfo* outArray, int maxCount);
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);
int Unmap(int windowId, int callerPid, uint64_t callerPml4); int Unmap(int windowId, int callerPid, uint64_t callerRoot);
int SendEvent(int windowId, const montauk::abi::WinEvent* event); 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); uint64_t& heapNext, uint64_t& outVa);
void CleanupProcess(int pid); void CleanupProcess(int pid);
int SetCursor(int windowId, int callerPid, int cursor); int SetCursor(int windowId, int callerPid, int cursor);
@@ -44,4 +45,26 @@ namespace WinServer {
int SetScale(int scale); int SetScale(int scale);
int GetScale(); int GetScale();
// ---- Messaging (see "Window server messaging" in Api/Syscall.hpp) ----
// Queue an app request about one of the caller's own windows.
int PostRequest(int windowId, int callerPid, const montauk::abi::WinRequest& request);
// Pop the oldest request (compositor only; the caller checks the cap).
// Returns 1 with *out filled, or 0 when the queue is empty.
int PollRequest(montauk::abi::WinRequest* out);
// Start a drag from the caller's window, or (compositor only, windowId
// -1) from the desktop surface itself. Takes ownership of `transfer` (it
// is emptied either way) and returns the new drag serial (> 0) or a
// negative StatusCodes value.
int DragBegin(int windowId, int callerPid, bool compositor,
const montauk::abi::WinDragBegin& info, DataTransfer::Transfer& transfer);
// Read the drag's formats or data. Allowed for the source process, the
// compositor, and processes the compositor has sent a drag event for this
// serial to.
int DragFormats(uint32_t serial, int callerPid, bool compositor,
montauk::abi::TransferFormatInfo* out, uint32_t max);
int DragGetData(uint32_t serial, int callerPid, bool compositor, const char* mime,
void* buffer, uint32_t bufferLen, uint32_t* outLen);
} }
+44 -5
View File
@@ -2,7 +2,9 @@
* Window.hpp * Window.hpp
* SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL, * SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL,
* SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE, * SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE,
* SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE syscalls * SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE syscalls,
* and window server messaging: SYS_WIN_REQUEST, SYS_WIN_REQUEST_POLL,
* SYS_WIN_DRAG_BEGIN, SYS_WIN_DRAG_FORMATS, SYS_WIN_DRAG_GET_DATA
* Copyright (c) 2026 Daniel Hammer * Copyright (c) 2026 Daniel Hammer
*/ */
@@ -20,7 +22,7 @@ namespace montauk::abi {
if (proc == nullptr) return GenericFailure; if (proc == nullptr) return GenericFailure;
uint64_t outVa = 0; 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); proc->heapNext, outVa);
result->id = id; result->id = id;
result->pixelVa = (id >= 0) ? outVa : 0; result->pixelVa = (id >= 0) ? outVa : 0;
@@ -53,13 +55,13 @@ namespace montauk::abi {
if (!Sched::HasCapability(CAP_COMPOSITOR)) return 0; if (!Sched::HasCapability(CAP_COMPOSITOR)) return 0;
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return 0; 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) { static int Sys_WinUnmap(int windowId) {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return GenericFailure; 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) { static int Sys_WinSendEvent(int windowId, const WinEvent* event) {
@@ -72,7 +74,7 @@ namespace montauk::abi {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return 0; if (proc == nullptr) return 0;
uint64_t outVa = 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); proc->heapNext, outVa);
return (r == 0) ? outVa : 0; return (r == 0) ? outVa : 0;
} }
@@ -92,4 +94,41 @@ namespace montauk::abi {
static int Sys_WinGetScale() { static int Sys_WinGetScale() {
return WinServer::GetScale(); return WinServer::GetScale();
} }
// Any app may post requests, but only about its own windows (checked in
// WinServer); only the compositor may read them back.
static int Sys_WinRequest(int windowId, const WinRequest* request) {
if (request == nullptr) return InvalidArgument;
WinRequest copy = *request;
return WinServer::PostRequest(windowId, Sched::GetCurrentPid(), copy);
}
static int Sys_WinRequestPoll(WinRequest* out) {
if (!Sched::HasCapability(CAP_COMPOSITOR)) return CapabilityDenied;
if (out == nullptr) return InvalidArgument;
return WinServer::PollRequest(out);
}
static int Sys_WinDragBegin(int windowId, const WinDragBegin* info,
const TransferItem* items, uint32_t count) {
if (info == nullptr) return InvalidArgument;
WinDragBegin infoCopy = *info;
DataTransfer::Transfer transfer;
int rc = DataTransfer::BuildFromUser(items, count, transfer);
if (rc != Success) return rc;
return WinServer::DragBegin(windowId, Sched::GetCurrentPid(),
Sched::HasCapability(CAP_COMPOSITOR), infoCopy, transfer);
}
static int Sys_WinDragFormats(uint32_t serial, TransferFormatInfo* out, uint32_t max) {
return WinServer::DragFormats(serial, Sched::GetCurrentPid(),
Sched::HasCapability(CAP_COMPOSITOR), out, max);
}
static int Sys_WinDragGetData(uint32_t serial, const char* mime, void* buffer,
uint32_t bufferLen, uint32_t* outLen) {
return WinServer::DragGetData(serial, Sched::GetCurrentPid(),
Sched::HasCapability(CAP_COMPOSITOR), mime,
buffer, bufferLen, outLen);
}
}; };
+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);
}
+121
View File
@@ -0,0 +1,121 @@
/*
* 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();
// Order this CPU's earlier writes to DMA memory (descriptors, buffers)
// before a later write the device acts on: the descriptor's valid/cycle
// bit, or a doorbell register. x86 keeps stores in order already.
inline void DmaWriteFence();
// Order the read of a device-written valid/cycle/status field before the
// reads of the data it guards.
inline void DmaReadFence();
// ---- 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();
// Cache maintenance for devices whose DMA does not snoop the CPU caches
// (ACPI _CCA = 0). Clean: write dirty lines back so the device reads
// what the CPU wrote. Invalidate: write back and discard, so the CPU
// then reads what the device wrote. No-ops where DMA is coherent (x86).
inline void CleanDataCache(const void* address, uint64_t size);
inline void InvalidateDataCache(const void* address, uint64_t size);
// Make instructions just written to [address, address+size) (any
// mapping of them) visible to instruction fetch on every CPU. A no-op
// where instruction fetch snoops data writes (x86).
inline void SyncInstructionCache(const void* address, uint64_t size);
// 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 * CpuPower.hpp
* Intel CPU power management: HWP frequency scaling, C1E promotion, * Architecture-neutral CPU power/thermal management interface
* and the package thermal governor.
* Copyright (c) 2026 Daniel Hammer * Copyright (c) 2026 Daniel Hammer
*/ */
#pragma once #pragma once
#include <cstdint> #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 { namespace CpuPower {
// Snapshot of the power/thermal state for SYS_POWERINFO and logging. // 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 BaseMHz; // CPUID base frequency (0 = unknown)
uint32_t MaxMHz; // CPUID max turbo frequency uint32_t MaxMHz; // CPUID max turbo frequency
uint32_t EffMHz; // measured average active frequency (BSP) 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, // BSP: detect features, enable HWP with a balanced energy preference,
+81
View File
@@ -0,0 +1,81 @@
/*
* 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);
// Whether the platform interrupt lines are wired by the hardware (x86:
// ISA IRQs and I/O APIC pins, as the firmware programs them into PCI
// Interrupt Line registers). Where they are not (ARM), the PCI layer
// assigns lines to PCI INTx pins from ACPI _PRT with AssignPlatformLine.
bool PlatformLinesAreFixed();
// Make platform line `irq` (1-23) deliver global system interrupt `gsi`
// (ACPI numbering) with the given trigger mode and polarity, so that
// EnableLegacyIrq(irq) unmasks it. Only where !PlatformLinesAreFixed().
bool AssignPlatformLine(uint8_t irq, uint32_t gsi, bool levelTriggered, bool activeLow);
// 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);
}
+58
View File
@@ -0,0 +1,58 @@
/*
* 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();
// Switch the machine off where ACPI S5 does not apply (hardware-reduced
// ACPI: ARM's PSCI SYSTEM_OFF). Returns if the platform cannot.
void PowerOff();
}
+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;
}
}
+22
View File
@@ -0,0 +1,22 @@
/*
* Bringup.hpp
* aarch64 per-CPU bring-up shared by the boot CPU and secondaries
* (private to the port)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
namespace Arch {
// System registers every CPU programs for itself: SP_EL1 as the stack,
// the exception vectors, SCTLR_EL1 policy bits, FP/SIMD access.
// Platform.cpp.
void InitializeCpuRegisters();
// Load the kernel's translation regime (MAIR, TCR, TTBR1 kernel half,
// TTBR0 kernel-context table) on the calling CPU. Paging::Init builds
// the tables and calls it on the boot CPU. Paging.cpp.
void ActivateKernelTranslation();
}
+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;
}
+170
View File
@@ -0,0 +1,170 @@
/*
* 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");
}
// Devices are outer-shareable observers.
inline void DmaWriteFence() {
asm volatile("dmb oshst" ::: "memory");
}
inline void DmaReadFence() {
asm volatile("dmb oshld" ::: "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");
}
// The TLBI operand is VA[55:12] in bits [43:0]; the bits above are RES0.
inline uint64_t DataCacheLineSize() {
uint64_t ctr;
asm volatile("mrs %0, ctr_el0" : "=r"(ctr));
return 4ULL << ((ctr >> 16) & 0xF); // DminLine
}
// To the point of coherency: where non-snooping device DMA looks.
inline void CleanDataCache(const void* address, uint64_t size) {
uint64_t line = DataCacheLineSize();
uint64_t end = (uint64_t)address + size;
for (uint64_t p = (uint64_t)address & ~(line - 1); p < end; p += line) {
asm volatile("dc cvac, %0" :: "r"(p) : "memory");
}
asm volatile("dsb sy" ::: "memory");
}
// Clean too: a partial line at either end may hold CPU data next to the
// buffer, which a plain invalidate would throw away.
inline void InvalidateDataCache(const void* address, uint64_t size) {
uint64_t line = DataCacheLineSize();
uint64_t end = (uint64_t)address + size;
for (uint64_t p = (uint64_t)address & ~(line - 1); p < end; p += line) {
asm volatile("dc civac, %0" :: "r"(p) : "memory");
}
asm volatile("dsb sy" ::: "memory");
}
// Clean the data cache to the point of unification, then invalidate
// the whole instruction cache: invalidating by address would only
// reach the alias it names on an aliasing (VIPT) instruction cache.
inline void SyncInstructionCache(const void* address, uint64_t size) {
uint64_t line = DataCacheLineSize();
uint64_t start = (uint64_t)address & ~(line - 1);
uint64_t end = (uint64_t)address + size;
for (uint64_t p = start; p < end; p += line) {
asm volatile("dc cvau, %0" :: "r"(p) : "memory");
}
asm volatile("dsb ish\n\tic ialluis\n\tdsb ish\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) & ((1ULL << 44) - 1)) : "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;
}
}
+184
View File
@@ -0,0 +1,184 @@
/*
* Exceptions.cpp
* aarch64 synchronous exceptions: faults, user crashes, kernel panics
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/aarch64/InterruptFrame.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/Smp.hpp>
#include <Common/Panic.hpp>
#include <Common/TerminationCodes.hpp>
#include <CppLib/Stream.hpp>
#include <Memory/Paging.hpp>
#include <Sched/Scheduler.hpp>
#include <Sched/CrashReport.hpp>
#include <Api/Heap.hpp>
#include <Timekeeping/Timer.hpp>
using namespace Kt;
namespace {
// ESR_EL1.EC exception classes
constexpr uint64_t EcUnknown = 0x00;
constexpr uint64_t EcIllegalState = 0x0E;
constexpr uint64_t EcInstAbortLower = 0x20;
constexpr uint64_t EcInstAbortSame = 0x21;
constexpr uint64_t EcPcAlignment = 0x22;
constexpr uint64_t EcDataAbortLower = 0x24;
constexpr uint64_t EcDataAbortSame = 0x25;
constexpr uint64_t EcSpAlignment = 0x26;
constexpr uint64_t EcFpException = 0x2C;
constexpr uint64_t EcBreakpointLower = 0x30;
constexpr uint64_t EcBrk = 0x3C;
constexpr uint64_t UserSpaceEnd = 0x0000800000000000ULL;
uint64_t ExceptionClass(const Arch::PanicFrame* frame) {
return (frame->esr >> 26) & 0x3F;
}
bool IsAbort(uint64_t ec) {
return ec == EcInstAbortLower || ec == EcInstAbortSame
|| ec == EcDataAbortLower || ec == EcDataAbortSame;
}
// Fault status code (ISS[5:0]) of an abort: translation faults mean
// the page is not mapped, permission faults that it is.
bool IsTranslationFault(uint64_t esr) {
uint64_t fsc = esr & 0x3F;
return fsc >= 0x04 && fsc <= 0x07;
}
bool IsPermissionFault(uint64_t esr) {
uint64_t fsc = esr & 0x3F;
return fsc >= 0x0C && fsc <= 0x0F;
}
const char* ExceptionName(uint64_t ec) {
switch (ec) {
case EcUnknown: return "Undefined Instruction";
case EcIllegalState: return "Illegal Execution State";
case EcInstAbortLower:
case EcInstAbortSame: return "Instruction Abort";
case EcPcAlignment: return "PC Alignment Fault";
case EcDataAbortLower:
case EcDataAbortSame: return "Data Abort";
case EcSpAlignment: return "SP Alignment Fault";
case EcFpException: return "Floating-Point Exception";
case EcBrk: return "Breakpoint (BRK)";
default:
if (ec >= EcBreakpointLower && ec <= 0x35) return "Debug Exception";
return "Synchronous Exception";
}
}
TerminationCodes TerminationFromClass(uint64_t ec) {
if (ec == EcUnknown || ec == EcIllegalState) return TerminatedIllegalInstruction;
if (ec == EcBrk || (ec >= EcBreakpointLower && ec <= 0x35)) return TerminatedBreakpoint;
if (ec == EcFpException) return TerminatedArithmeticError;
return TerminatedAccessViolation;
}
template<size_t N>
void CopyName(char (&dst)[N], const char* src) {
size_t i = 0;
for (; i + 1 < N && src[i]; i++) dst[i] = src[i];
dst[i] = '\0';
}
// Kill the faulting process with a crash report. Never returns.
[[noreturn]] void TerminateUserProcess(Arch::PanicFrame* frame, uint64_t ec) {
// Teardown can wait on devices and other CPUs; keep the clock running.
Arch::EnableInterrupts();
auto* proc = Sched::GetCurrentProcessPtr();
const char* name = ExceptionName(ec);
KernelLogStream(ERROR, "Exception") << name << " in process \"" << proc->name
<< "\" (pid " << proc->pid << ", ESR 0x" << base::hex << frame->esr
<< ", PC 0x" << frame->elr << ") - process terminated";
CrashReport::Report rep{};
rep.valid = true;
rep.pid = proc->pid;
CopyName(rep.processName, proc->name);
rep.exceptionVector = (uint8_t)ec;
CopyName(rep.exceptionName, name);
rep.instructionPointer = frame->elr;
rep.flags = frame->spsr;
rep.stackPointer = frame->sp;
constexpr uint64_t snapshotBytes = CrashReport::StackSnapshotWords * sizeof(uint64_t);
if (Memory::VMM::Paging::IsUserRangeAccessible(proc->pageTableRoot, frame->sp,
snapshotBytes, false)) {
auto* userStack = (const volatile uint64_t*)frame->sp;
for (int word = 0; word < CrashReport::StackSnapshotWords; word++)
rep.stackWords[word] = userStack[word];
rep.stackWordCount = CrashReport::StackSnapshotWords;
}
if (IsAbort(ec)) {
rep.faultingAddress = frame->far;
rep.pfPresent = IsPermissionFault(frame->esr);
rep.pfWrite = ec == EcDataAbortLower && (frame->esr & (1ULL << 6)) != 0;
rep.pfUser = 1;
rep.pfInstructionFetch = ec == EcInstAbortLower;
}
rep.timestampTick = Timekeeping::GetTicks();
CrashReport::AddReport(rep);
Sched::SpawnCrashPad(proc->pid);
// As on x86: a sibling thread's fault must end the whole process.
int code = TerminationFromClass(ec);
if (Sched::GetCurrentThreadPtr() != proc) {
Sched::KillProcess(proc->pid, code);
} else {
Sched::SetProcessExitCode(proc->pid, code);
}
Sched::ExitProcess();
__builtin_unreachable();
}
// A not-present user page: anonymous memory mapped lazily or the user
// stack growing. Also reached from the kernel touching user buffers.
bool TryResolveUserFault(const Arch::PanicFrame* frame, uint64_t ec) {
if (!IsAbort(ec) || !IsTranslationFault(frame->esr)) return false;
if (frame->far >= UserSpaceEnd) return false;
auto* cpu = Smp::TryGetCurrentCpuData();
if (cpu == nullptr || cpu->currentSlot < 0 || Sched::GetCurrentPid() < 0) return false;
bool isData = ec == EcDataAbortLower || ec == EcDataAbortSame;
Memory::VMM::PageFault fault{false, isData && (frame->esr & (1ULL << 6)) != 0, !isData};
return montauk::abi::TryHandleAnonymousPageFault(frame->far, fault)
|| Sched::TryGrowUserStack(frame->far);
}
}
extern "C" void ArchSyncException(Arch::PanicFrame* frame, bool fromUser) {
uint64_t ec = ExceptionClass(frame);
if (TryResolveUserFault(frame, ec)) {
return;
}
if (fromUser && Sched::GetCurrentPid() >= 0) {
TerminateUserProcess(frame, ec);
}
Panic(ExceptionName(ec), frame);
}
// FIQs are never routed, SErrors are asynchronous external aborts, and the
// kernel neither runs on SP_EL0 nor supports AArch32 user code.
extern "C" void ArchUnexpectedException(Arch::PanicFrame* frame, uint64_t kind) {
static const char* const names[] = {
"Exception on SP_EL0", "Unexpected FIQ", "SError (asynchronous abort)",
"AArch32 exception",
};
Panic(kind < 4 ? names[kind] : "Unexpected exception", frame);
}
+40
View File
@@ -0,0 +1,40 @@
/*
* Gic.hpp
* aarch64 GICv2 interrupt controller (private to the port)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <ACPI/ACPI.hpp>
namespace Arch::Gic {
// Bring up the distributor and the calling CPU's interface from the
// ACPI MADT. Returns false when no usable GIC is described.
bool Initialize(Hal::ACPI::CommonSDTHeader* xsdt);
bool IsInitialized();
// The XSDT Initialize was given (for the other ACPI tables the port reads).
Hal::ACPI::CommonSDTHeader* CurrentXsdt();
// Per-CPU part of Initialize, for secondary CPUs.
void InitializeCpuInterface();
// Map a private peripheral interrupt (16-31) onto an IRQ slot and enable
// it on the calling CPU.
void RoutePpi(uint32_t intid, uint8_t irq, bool edgeTriggered);
// Called for an IRQ slot after the interrupt is acknowledged and before
// it is completed (EOI): level-triggered sources deassert here.
using AckHook = void (*)();
void SetAckHook(uint8_t irq, AckHook hook);
// The calling CPU's target bit, as the distributor sees it.
uint8_t CurrentTargetMask();
// Find an ACPI table in the XSDT by signature.
Hal::ACPI::CommonSDTHeader* FindTable(Hal::ACPI::CommonSDTHeader* xsdt, const char* signature);
}
@@ -0,0 +1,24 @@
/*
* InterruptFrame.hpp
* aarch64 exception frame
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Arch {
// What the exception vectors (Vectors.S) 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
};
static_assert(sizeof(PanicFrame) == 288, "must match Vectors.S");
}
+420
View File
@@ -0,0 +1,420 @@
/*
* Interrupts.cpp
* aarch64 GICv2 driver and IRQ slot dispatch (implements Arch/Interrupts.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Interrupts.hpp>
#include <Arch/Smp.hpp>
#include <Arch/aarch64/Gic.hpp>
#include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
// IRQ slots map onto GIC interrupt IDs (INTIDs):
// IRQ_TIMER the generic timer's PPI (Timer.cpp routes it)
// 24-45 (MSI slots) SPIs of the GICv2m MSI frame, one per slot
// IRQ_TLB_SHOOTDOWN,
// IRQ_RESCHEDULE SGIs 0 and 1
// 1-23 (platform lines) SPIs the PCI layer assigns to INTx pins from
// ACPI _PRT (AssignPlatformLine)
using namespace Kt;
namespace Arch {
// Distributor registers
static constexpr uint32_t GICD_CTLR = 0x000;
static constexpr uint32_t GICD_TYPER = 0x004;
static constexpr uint32_t GICD_ISENABLER = 0x100;
static constexpr uint32_t GICD_ICENABLER = 0x180;
static constexpr uint32_t GICD_ICPENDR = 0x280;
static constexpr uint32_t GICD_ICACTIVER = 0x380;
static constexpr uint32_t GICD_IPRIORITYR = 0x400;
static constexpr uint32_t GICD_ITARGETSR = 0x800;
static constexpr uint32_t GICD_ICFGR = 0xC00;
static constexpr uint32_t GICD_SGIR = 0xF00;
// CPU interface registers
static constexpr uint32_t GICC_CTLR = 0x00;
static constexpr uint32_t GICC_PMR = 0x04;
static constexpr uint32_t GICC_BPR = 0x08;
static constexpr uint32_t GICC_IAR = 0x0C;
static constexpr uint32_t GICC_EOIR = 0x10;
// GICv2m MSI frame registers
static constexpr uint32_t V2M_MSI_TYPER = 0x008;
static constexpr uint32_t V2M_MSI_SETSPI_NS = 0x040;
static constexpr uint32_t SpuriousIntid = 1020;
static constexpr uint32_t MaxIntids = 1020;
static constexpr uint8_t DefaultPriority = 0xA0;
static constexpr uint8_t NoSlot = 0xFF;
static constexpr uint8_t FirstMsiSlot = 24;
static constexpr uint8_t LastMsiSlot = 45;
// MADT interrupt-controller structures (ACPI 6.x)
static constexpr uint8_t MadtGicc = 0x0B;
static constexpr uint8_t MadtGicd = 0x0C;
static constexpr uint8_t MadtGicMsiFrame = 0x0D;
struct MadtEntry {
uint8_t Type;
uint8_t Length;
} __attribute__((packed));
struct MadtGiccEntry {
MadtEntry Header;
uint16_t Reserved;
uint32_t CpuInterfaceNumber;
uint32_t AcpiProcessorUid;
uint32_t Flags;
uint32_t ParkingProtocolVersion;
uint32_t PerformanceInterruptGsiv;
uint64_t ParkedAddress;
uint64_t PhysicalBaseAddress; // GICC
uint64_t Gicv;
uint64_t Gich;
uint32_t VgicMaintenanceInterrupt;
uint64_t GicrBaseAddress;
uint64_t Mpidr;
} __attribute__((packed));
struct MadtGicdEntry {
MadtEntry Header;
uint16_t Reserved;
uint32_t GicId;
uint64_t PhysicalBaseAddress;
uint32_t SystemVectorBase;
uint8_t GicVersion;
} __attribute__((packed));
struct MadtMsiFrameEntry {
MadtEntry Header;
uint16_t Reserved;
uint32_t FrameId;
uint64_t PhysicalBaseAddress;
uint32_t Flags; // bit 0: SpiCount/SpiBase override TYPER
uint16_t SpiCount;
uint16_t SpiBase;
} __attribute__((packed));
static constexpr uint32_t MadtHeaderSize = 44; // SDT header + LAPIC address + flags
static IrqHandler g_irqHandlers[IRQ_COUNT] = {};
static Gic::AckHook g_ackHooks[IRQ_COUNT] = {};
static uint8_t g_intidToSlot[MaxIntids];
static uint32_t g_slotToIntid[IRQ_COUNT];
static volatile uint8_t* g_gicd = nullptr;
static volatile uint8_t* g_gicc = nullptr;
static uint32_t g_intidCount = 0;
static Hal::ACPI::CommonSDTHeader* g_xsdt = nullptr;
struct PlatformLine {
uint32_t gsi; // 0: unassigned
bool edgeTriggered;
};
static PlatformLine g_platformLines[24] = {};
static uint64_t g_msiFramePhys = 0;
static uint32_t g_msiSpiBase = 0;
static uint32_t g_msiSpiCount = 0;
static inline uint32_t GicdRead(uint32_t reg) {
return *(volatile uint32_t*)(g_gicd + reg);
}
static inline void GicdWrite(uint32_t reg, uint32_t value) {
*(volatile uint32_t*)(g_gicd + reg) = value;
}
static inline void GicdWrite8(uint32_t reg, uint8_t value) {
*(volatile uint8_t*)(g_gicd + reg) = value;
}
static inline uint8_t GicdRead8(uint32_t reg) {
return *(volatile uint8_t*)(g_gicd + reg);
}
static inline uint32_t GiccRead(uint32_t reg) {
return *(volatile uint32_t*)(g_gicc + reg);
}
static inline void GiccWrite(uint32_t reg, uint32_t value) {
*(volatile uint32_t*)(g_gicc + reg) = value;
}
static volatile uint8_t* MapRegisters(uint64_t phys, uint64_t size) {
for (uint64_t offset = 0; offset < size; offset += 0x1000) {
Memory::VMM::g_paging->MapMMIO(phys + offset, Memory::HHDM(phys + offset));
}
return (volatile uint8_t*)Memory::HHDM(phys);
}
static void SetConfig(uint32_t intid, bool edgeTriggered) {
uint32_t reg = GICD_ICFGR + (intid / 16) * 4;
uint32_t shift = (intid % 16) * 2 + 1;
uint32_t value = GicdRead(reg);
if (edgeTriggered) value |= 1u << shift;
else value &= ~(1u << shift);
GicdWrite(reg, value);
}
static void EnableIntid(uint32_t intid) {
GicdWrite(GICD_ISENABLER + (intid / 32) * 4, 1u << (intid % 32));
}
static void Bind(uint32_t intid, uint8_t irq) {
if (intid >= MaxIntids || irq >= IRQ_COUNT) return;
g_intidToSlot[intid] = irq;
g_slotToIntid[irq] = intid;
}
// Route a shared peripheral interrupt to the boot CPU and unmask it.
static void RouteSpi(uint32_t intid, uint8_t irq, bool edgeTriggered) {
if (intid < 32 || intid >= g_intidCount) return;
Bind(intid, irq);
GicdWrite8(GICD_IPRIORITYR + intid, DefaultPriority);
GicdWrite8(GICD_ITARGETSR + intid, Smp::GetCpuData(0)->arch.gicTargetMask);
SetConfig(intid, edgeTriggered);
EnableIntid(intid);
}
namespace Gic {
Hal::ACPI::CommonSDTHeader* FindTable(Hal::ACPI::CommonSDTHeader* xsdt, const char* signature) {
if (xsdt == nullptr) return nullptr;
uint32_t count = (xsdt->Length - sizeof(Hal::ACPI::CommonSDTHeader)) / 8;
auto* entries = (uint64_t*)((uint64_t)xsdt + sizeof(Hal::ACPI::CommonSDTHeader));
for (uint32_t i = 0; i < count; i++) {
auto* header = (Hal::ACPI::CommonSDTHeader*)Memory::HHDM(entries[i]);
if (header->Signature[0] == signature[0] && header->Signature[1] == signature[1]
&& header->Signature[2] == signature[2] && header->Signature[3] == signature[3]) {
return header;
}
}
return nullptr;
}
// ITARGETSR0-7 are banked: each byte reads back the reading CPU's
// own interface bit.
uint8_t CurrentTargetMask() {
uint32_t value = GicdRead(GICD_ITARGETSR);
return (uint8_t)(value | value >> 8 | value >> 16 | value >> 24);
}
void InitializeCpuInterface() {
// SGIs and PPIs are banked per CPU: disable the PPIs until a
// driver routes one, keep the SGIs (IPIs) enabled.
GicdWrite(GICD_ICENABLER, 0xFFFF0000u);
GicdWrite(GICD_ISENABLER, 0x0000FFFFu);
GicdWrite(GICD_ICPENDR, 0xFFFFFFFFu);
for (uint32_t intid = 0; intid < 32; intid++) {
GicdWrite8(GICD_IPRIORITYR + intid, DefaultPriority);
}
GiccWrite(GICC_PMR, 0xF0);
GiccWrite(GICC_BPR, 0);
GiccWrite(GICC_CTLR, 1);
auto* cpu = Smp::GetCurrentCpuData();
cpu->arch.gicTargetMask = CurrentTargetMask();
}
void RoutePpi(uint32_t intid, uint8_t irq, bool edgeTriggered) {
if (intid < 16 || intid >= 32) return;
Bind(intid, irq);
SetConfig(intid, edgeTriggered);
EnableIntid(intid);
}
void SetAckHook(uint8_t irq, AckHook hook) {
if (irq < IRQ_COUNT) g_ackHooks[irq] = hook;
}
bool IsInitialized() {
return g_gicc != nullptr;
}
Hal::ACPI::CommonSDTHeader* CurrentXsdt() {
return g_xsdt;
}
bool Initialize(Hal::ACPI::CommonSDTHeader* xsdt) {
g_xsdt = xsdt;
auto* madt = FindTable(xsdt, "APIC");
if (madt == nullptr) {
KernelLogStream(ERROR, "GIC") << "No MADT";
return false;
}
uint64_t gicdPhys = 0, giccPhys = 0;
uint8_t version = 0;
auto* cursor = (uint8_t*)madt + MadtHeaderSize;
auto* end = (uint8_t*)madt + madt->Length;
while (cursor + sizeof(MadtEntry) <= end) {
auto* entry = (MadtEntry*)cursor;
if (entry->Length < sizeof(MadtEntry)) break;
if (entry->Type == MadtGicd) {
auto* gicd = (MadtGicdEntry*)entry;
gicdPhys = gicd->PhysicalBaseAddress;
version = gicd->GicVersion;
} else if (entry->Type == MadtGicc && giccPhys == 0) {
giccPhys = ((MadtGiccEntry*)entry)->PhysicalBaseAddress;
} else if (entry->Type == MadtGicMsiFrame && g_msiFramePhys == 0) {
auto* frame = (MadtMsiFrameEntry*)entry;
g_msiFramePhys = frame->PhysicalBaseAddress;
if (frame->Flags & 1) {
g_msiSpiBase = frame->SpiBase;
g_msiSpiCount = frame->SpiCount;
}
}
cursor += entry->Length;
}
// Version 0 means "infer from the hardware"; 1 and 2 share a
// programming model.
if (gicdPhys == 0 || giccPhys == 0 || version > 2) {
KernelLogStream(ERROR, "GIC") << "No GICv2 in the MADT (version "
<< base::dec << (uint64_t)version << ")";
return false;
}
for (uint32_t i = 0; i < MaxIntids; i++) g_intidToSlot[i] = NoSlot;
for (int i = 0; i < IRQ_COUNT; i++) g_slotToIntid[i] = SpuriousIntid;
Bind(0, IRQ_TLB_SHOOTDOWN);
Bind(1, IRQ_RESCHEDULE);
g_gicd = MapRegisters(gicdPhys, 0x1000);
g_gicc = MapRegisters(giccPhys, 0x2000);
g_intidCount = ((GicdRead(GICD_TYPER) & 0x1F) + 1) * 32;
if (g_intidCount > MaxIntids) g_intidCount = MaxIntids;
// Distributor: everything masked, idle, level-triggered and
// aimed at no CPU until RouteSpi claims it.
GicdWrite(GICD_CTLR, 0);
for (uint32_t intid = 32; intid < g_intidCount; intid += 32) {
GicdWrite(GICD_ICENABLER + (intid / 32) * 4, 0xFFFFFFFFu);
GicdWrite(GICD_ICPENDR + (intid / 32) * 4, 0xFFFFFFFFu);
GicdWrite(GICD_ICACTIVER + (intid / 32) * 4, 0xFFFFFFFFu);
}
for (uint32_t intid = 32; intid < g_intidCount; intid++) {
GicdWrite8(GICD_IPRIORITYR + intid, DefaultPriority);
GicdWrite8(GICD_ITARGETSR + intid, 0);
}
for (uint32_t intid = 32; intid < g_intidCount; intid += 16) {
GicdWrite(GICD_ICFGR + (intid / 16) * 4, 0);
}
GicdWrite(GICD_CTLR, 1);
InitializeCpuInterface();
if (g_msiFramePhys != 0) {
volatile uint8_t* frame = MapRegisters(g_msiFramePhys, 0x1000);
if (g_msiSpiCount == 0) {
uint32_t typer = *(volatile uint32_t*)(frame + V2M_MSI_TYPER);
g_msiSpiBase = (typer >> 16) & 0x3FF;
g_msiSpiCount = typer & 0x3FF;
}
}
KernelLogStream(OK, "GIC") << "GICv2: distributor 0x" << base::hex << gicdPhys
<< ", CPU interface 0x" << giccPhys << ", " << base::dec
<< (uint64_t)g_intidCount << " interrupt IDs";
if (g_msiSpiCount != 0) {
KernelLogStream(OK, "GIC") << "GICv2m MSI frame 0x" << base::hex << g_msiFramePhys
<< ": SPIs " << base::dec << (uint64_t)g_msiSpiBase << "-"
<< (uint64_t)(g_msiSpiBase + g_msiSpiCount - 1);
}
return true;
}
}
void RegisterIrqHandler(uint8_t irq, IrqHandler handler) {
if (irq >= IRQ_COUNT) return;
g_irqHandlers[irq] = handler;
KernelLogStream(DEBUG, "IRQ") << "Registered handler for IRQ "
<< base::dec << (uint64_t)irq;
}
bool PlatformLinesAreFixed() {
return false;
}
// ACPI GSIs are GIC INTIDs. The GIC knows only active-high levels and
// rising edges; an active-low line is inverted on its way in.
bool AssignPlatformLine(uint8_t irq, uint32_t gsi, bool levelTriggered, bool) {
if (irq == 0 || irq >= 24 || gsi < 32 || gsi >= g_intidCount) return false;
g_platformLines[irq] = PlatformLine{ gsi, !levelTriggered };
return true;
}
void RouteLegacyIrq(uint8_t irq) {
if (irq >= 24 || g_platformLines[irq].gsi == 0) {
KernelLogStream(WARNING, "IRQ") << "aarch64: platform line "
<< base::dec << (uint64_t)irq << " is not assigned";
return;
}
RouteSpi(g_platformLines[irq].gsi, irq, g_platformLines[irq].edgeTriggered);
}
void EnableLegacyIrq(uint8_t irq) {
RouteLegacyIrq(irq);
}
bool ComposeMsi(uint8_t irq, MsiMessage& out) {
if (g_gicd == nullptr || irq < FirstMsiSlot || irq > LastMsiSlot) return false;
uint32_t index = irq - FirstMsiSlot;
if (index >= g_msiSpiCount) return false;
uint32_t spi = g_msiSpiBase + index;
if (g_slotToIntid[irq] != spi) {
RouteSpi(spi, irq, true);
}
out = MsiMessage{ g_msiFramePhys + V2M_MSI_SETSPI_NS, spi };
return true;
}
bool SendIpi(int cpuIndex, uint8_t irq) {
if (g_gicd == nullptr) return false;
auto* cpu = Smp::GetCpuData(cpuIndex);
if (cpu == nullptr || cpu->arch.gicTargetMask == 0) return false;
uint32_t sgi = g_slotToIntid[irq];
if (sgi >= 16) return false;
// Make the IPI's memory effects visible before it is raised.
asm volatile("dsb ishst" ::: "memory");
GicdWrite(GICD_SGIR, ((uint32_t)cpu->arch.gicTargetMask << 16) | sgi);
return true;
}
}
// IRQ exception vector (Vectors.S). As on x86, the interrupt is completed
// BEFORE the handler runs: the handler may context-switch away and not
// come back here for a long time, and an un-EOId interrupt would hold this
// CPU's running priority up (no further ticks). Level-triggered sources
// get the chance to drop their line first through an ack hook.
extern "C" void ArchIrqException(void*, bool fromUser) {
using namespace Arch;
uint32_t iar = GiccRead(GICC_IAR);
uint32_t intid = iar & 0x3FF;
if (intid >= SpuriousIntid) return;
uint8_t irq = g_intidToSlot[intid];
if (irq != NoSlot && g_ackHooks[irq] != nullptr) {
g_ackHooks[irq]();
}
GiccWrite(GICC_EOIR, iar);
if (irq != NoSlot && g_irqHandlers[irq] != nullptr) {
g_irqHandlers[irq](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();
}
};
+528
View File
@@ -0,0 +1,528 @@
/*
* Paging.cpp
* aarch64 virtual memory (implements Memory/Paging.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Memory/Paging.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Memory/HHDM.hpp>
#include <Arch/Cpu.hpp>
#include <Common/Panic.hpp>
#include <CppLib/Spinlock.hpp>
#include <Arch/aarch64/Bringup.hpp>
// 4 KiB granule, 48-bit virtual addresses, four levels (0-3). The kernel
// half is translated through TTBR1_EL1 (g_kernelHalfRoot), which never
// changes after Init. The "address space roots" the generic kernel switches
// between are TTBR0_EL1 tables: a process's root holds only its own lower
// half, and KernelRoot() is the TTBR0 table of kernel contexts, empty apart
// from the EFI runtime identity mappings (so null pointers fault).
//
// The HHDM covers only what the memory map lists as RAM or firmware data:
// unlike on x86, the holes between are device space on ARM machines, and a
// cacheable mapping of device registers invites speculative accesses.
namespace Memory::VMM {
Paging* g_paging = nullptr;
static std::uint64_t g_hhdmLimit = 0;
static std::uint64_t g_kernelHalfRoot = 0;
std::uint64_t Paging::HhdmLimit() { return g_hhdmLimit; }
// Translation table descriptor bits (VMSAv8-64, stage 1)
static constexpr uint64_t DescValid = 1ULL << 0;
static constexpr uint64_t DescTable = 1ULL << 1; // table (levels 0-2) or page (level 3)
static constexpr uint64_t DescAttrShift = 2; // AttrIndx[2:0], into MAIR_EL1
static constexpr uint64_t DescAttrMask = 7ULL << DescAttrShift;
static constexpr uint64_t DescApUser = 1ULL << 6; // AP[1]: EL0 may access
static constexpr uint64_t DescApReadOnly = 1ULL << 7; // AP[2]
static constexpr uint64_t DescShInner = 3ULL << 8;
static constexpr uint64_t DescAf = 1ULL << 10; // access flag (no AF faults)
static constexpr uint64_t DescNotGlobal = 1ULL << 11;
static constexpr uint64_t DescPxn = 1ULL << 53;
static constexpr uint64_t DescUxn = 1ULL << 54;
static constexpr uint64_t DescAddrMask = 0x0000FFFFFFFFF000ULL;
// MAIR_EL1 attribute indices
static constexpr uint64_t AttrNormal = 0; // write-back, read/write-allocate
static constexpr uint64_t AttrNormalNc = 1; // non-cacheable (write-combining)
static constexpr uint64_t AttrDevice = 2; // Device-nGnRE
static constexpr uint64_t MairValue = 0xFFULL << (8 * AttrNormal)
| 0x44ULL << (8 * AttrNormalNc)
| 0x04ULL << (8 * AttrDevice);
static constexpr uint64_t KernelHalfBase = 0xFFFF000000000000ULL;
static constexpr uint64_t UserSpaceEnd = 0x0000800000000000ULL;
// EFI memory types whose runtime regions are device registers
static constexpr uint32_t EfiMemoryMappedIo = 11;
static constexpr uint32_t EfiMemoryMappedIoPortSpace = 12;
static kcp::Mutex pagingLock;
void LockUserPaging() { pagingLock.Acquire(); }
void UnlockUserPaging() { pagingLock.Release(); }
void ActivateKernelAddressSpace() {
Arch::WritePageTableRoot(g_paging->KernelRoot());
}
void FlushTLB() {
Arch::FlushTlbAll();
}
using Descriptor = uint64_t;
static inline volatile Descriptor* TableAt(uint64_t phys) {
return (volatile Descriptor*)Memory::HHDM(phys);
}
static inline uint64_t Index(uint64_t va, int level) {
return (va >> (39 - 9 * level)) & 0x1FF;
}
// Order descriptor writes before the table walker (and later
// instructions) can observe them.
static inline void PublishTables() {
asm volatile("dsb ishst\n\tisb" ::: "memory");
}
static inline uint64_t ReadTtbr1() {
uint64_t ttbr;
asm volatile("mrs %0, ttbr1_el1" : "=r"(ttbr));
return ttbr & Arch::TtbrAddressMask;
}
// The level-3 descriptor for `va`, creating intermediate tables.
// Returns nullptr when out of memory or when a block mapping covers
// `va` (only the bootloader's tables contain blocks).
static volatile Descriptor* WalkCreate(uint64_t root, uint64_t va) {
uint64_t table = root;
for (int level = 0; level < 3; level++) {
volatile Descriptor* entry = &TableAt(table)[Index(va, level)];
Descriptor desc = *entry;
if ((desc & DescValid) == 0) {
void* page = Memory::g_pfa->AllocateZeroed();
if (page == nullptr) return nullptr;
table = Memory::SubHHDM(page);
// The zeroed table must be visible before the walker can
// reach it.
asm volatile("dsb ishst" ::: "memory");
*entry = table | DescTable | DescValid;
} else if ((desc & DescTable) == 0) {
return nullptr;
} else {
table = desc & DescAddrMask;
}
}
return &TableAt(table)[Index(va, 3)];
}
// The level-3 descriptor for `va`, or nullptr if a table is missing.
static volatile Descriptor* WalkExisting(uint64_t root, uint64_t va) {
uint64_t table = root;
for (int level = 0; level < 3; level++) {
Descriptor desc = TableAt(table)[Index(va, level)];
if ((desc & (DescValid | DescTable)) != (DescValid | DescTable)) return nullptr;
table = desc & DescAddrMask;
}
return &TableAt(table)[Index(va, 3)];
}
// Install a leaf. Replacing a live translation (other address or
// attributes) needs break-before-make: invalidate, flush, then write.
static void SetLeaf(volatile Descriptor* entry, Descriptor desc, uint64_t va) {
Descriptor old = *entry;
if (old == desc) return;
if (old & DescValid) {
*entry = 0;
Arch::FlushTlbPage(va);
}
*entry = desc;
PublishTables();
}
static Descriptor KernelLeaf(uint64_t phys, uint64_t attr) {
Descriptor desc = (phys & DescAddrMask) | DescTable | DescValid | DescAf | DescShInner
| (attr << DescAttrShift) | DescUxn;
// Kernel memory stays executable, as on x86; device memory never is.
if (attr != AttrNormal) desc |= DescPxn;
return desc;
}
static Descriptor UserLeaf(uint64_t phys, uint64_t attr, bool writable, bool executable) {
Descriptor desc = (phys & DescAddrMask) | DescTable | DescValid | DescAf | DescShInner
| DescNotGlobal | (attr << DescAttrShift) | DescApUser | DescPxn;
if (!writable) desc |= DescApReadOnly;
if (!executable) desc |= DescUxn;
return desc;
}
static inline bool IsUserLeaf(Descriptor desc) {
return (desc & (DescValid | DescTable)) == (DescValid | DescTable) && (desc & DescApUser);
}
// Translate through a table tree that may contain block mappings (the
// bootloader's). Returns 0 if unmapped.
static uint64_t Translate(uint64_t root, uint64_t va) {
uint64_t table = root;
for (int level = 0; level <= 3; level++) {
Descriptor desc = TableAt(table)[Index(va, level)];
if ((desc & DescValid) == 0) return 0;
uint64_t span = 1ULL << (39 - 9 * level);
bool leaf = level == 3 || (desc & DescTable) == 0;
if (leaf) {
if (level == 0) return 0; // no level-0 blocks with 4 KiB granules
return ((desc & DescAddrMask) & ~(span - 1)) | (va & (span - 1));
}
table = desc & DescAddrMask;
}
return 0;
}
void Paging::MapEfiRuntime(const montauk::boot::EfiInfo& efi) {
if (efi.memoryMap == nullptr || efi.descriptorSize == 0) return;
struct EfiMemDesc {
uint32_t Type;
uint64_t PhysicalStart;
uint64_t VirtualStart;
uint64_t NumberOfPages;
uint64_t Attribute;
};
static constexpr uint64_t EFI_MEMORY_RUNTIME = 0x8000000000000000ULL;
auto* base = (uint8_t*)efi.memoryMap;
uint64_t count = efi.memoryMapSize / efi.descriptorSize;
for (uint64_t i = 0; i < count; i++) {
auto* desc = (EfiMemDesc*)(base + i * efi.descriptorSize);
if ((desc->Attribute & EFI_MEMORY_RUNTIME) == 0) continue;
bool device = desc->Type == EfiMemoryMappedIo || desc->Type == EfiMemoryMappedIoPortSpace;
for (uint64_t p = 0; p < desc->NumberOfPages; p++) {
uint64_t addr = desc->PhysicalStart + p * 0x1000;
// Firmware code runs at its HHDM alias (Efi::Init calls the
// services there) and reaches its data at physical addresses.
// Runtime regions are reserved memory, outside the HHDM.
if (device) {
MapMMIO(addr, addr);
MapMMIO(addr, Memory::HHDM(addr));
} else {
Map(addr, addr);
Map(addr, Memory::HHDM(addr));
}
}
}
FlushTLB();
}
}
namespace Arch {
// The new tables map the code running here at the same address as the
// bootloader's did, so the switch is a register write followed by a
// full invalidate.
void ActivateKernelTranslation() {
using namespace Memory::VMM;
// 48-bit VAs in both halves, 4 KiB granules, write-back walks of
// inner-shareable tables; the physical size is what the CPU has.
uint64_t mmfr0;
asm volatile("mrs %0, id_aa64mmfr0_el1" : "=r"(mmfr0));
uint64_t ips = mmfr0 & 0xF;
if (ips > 5) ips = 5; // 52-bit output needs LPA2 descriptors
uint64_t tcr = 16ULL // T0SZ
| 1ULL << 8 | 1ULL << 10 // IRGN0, ORGN0: write-back
| 3ULL << 12 // SH0: inner shareable
| 0ULL << 14 // TG0: 4 KiB
| 16ULL << 16 // T1SZ
| 1ULL << 24 | 1ULL << 26 // IRGN1, ORGN1
| 3ULL << 28 // SH1
| 2ULL << 30 // TG1: 4 KiB
| ips << 32; // IPS
asm volatile(
"dsb ish\n\t"
"msr mair_el1, %0\n\t"
"msr tcr_el1, %1\n\t"
"isb\n\t"
"msr ttbr1_el1, %2\n\t"
"msr ttbr0_el1, %3\n\t"
"isb\n\t"
"tlbi vmalle1\n\t"
"dsb nsh\n\t"
"isb"
:: "r"(MairValue), "r"(tcr), "r"(g_kernelHalfRoot), "r"(g_paging->KernelRoot()) : "memory");
}
}
namespace Memory::VMM {
Paging::Paging() {
rootPhys = Memory::SubHHDM(Memory::g_pfa->AllocateZeroed());
g_kernelHalfRoot = Memory::SubHHDM(Memory::g_pfa->AllocateZeroed());
}
// Kernel mappings of lower-half addresses (EFI identity mappings) go to
// the kernel contexts' TTBR0 table.
static uint64_t KernelRootFor(uint64_t kernelContextRoot, uint64_t va) {
return va >= KernelHalfBase ? g_kernelHalfRoot : kernelContextRoot;
}
void Paging::Init(std::uint64_t kernelBaseVirt, std::uint64_t kernelSize, const montauk::boot::MemoryMap& memMap,
const montauk::boot::Framebuffer& framebuffer) {
if (Memory::HHDMBase < KernelHalfBase) {
Panic("aarch64: HHDM outside the 48-bit kernel half", nullptr);
}
uint64_t bootTtbr1 = ReadTtbr1();
uint64_t bootTcr, bootMair;
asm volatile("mrs %0, tcr_el1" : "=r"(bootTcr));
asm volatile("mrs %0, mair_el1" : "=r"(bootMair));
Kt::KernelLogStream(Kt::DEBUG, "VMM") << "Bootloader TCR 0x" << base::hex << bootTcr
<< ", MAIR 0x" << bootMair << ", TTBR1 0x" << bootTtbr1;
// The kernel image, at the physical pages the bootloader loaded it to.
for (uint64_t va = kernelBaseVirt; va < kernelBaseVirt + kernelSize; va += 0x1000) {
uint64_t phys = Translate(bootTtbr1, va);
if (phys == 0) Panic("aarch64: kernel page missing from the bootloader's tables", nullptr);
Map(phys, va);
}
// HHDM: every region that is real backing store (see the top of
// this file). The limit is published for the AML interpreter.
uint64_t maxPhysAddr = 0;
for (size_t i = 0; i < memMap.count; i++) {
const auto& region = memMap.regions[i];
if (region.kind == montauk::boot::MemoryKind::Reserved
|| region.kind == montauk::boot::MemoryKind::Unknown) {
continue;
}
uint64_t start = region.base & ~0xFFFULL;
uint64_t end = (region.base + region.length + 0xFFF) & ~0xFFFULL;
bool framebufferRegion = region.kind == montauk::boot::MemoryKind::Framebuffer;
for (uint64_t phys = start; phys < end; phys += 0x1000) {
if (framebufferRegion) MapWC(phys, Memory::HHDM(phys));
else Map(phys, Memory::HHDM(phys));
}
if (end > maxPhysAddr) maxPhysAddr = end;
}
g_hhdmLimit = maxPhysAddr;
// The framebuffer may not be listed in the memory map at all.
if (framebuffer.address != 0) {
uint64_t fbPhys = Memory::SubHHDM(framebuffer.address) & ~0xFFFULL;
uint64_t fbPages = (framebuffer.height * framebuffer.pitch + 0xFFF) / 0x1000;
for (uint64_t p = 0; p < fbPages; p++) {
uint64_t phys = fbPhys + p * 0x1000;
MapWC(phys, Memory::HHDM(phys));
}
}
// The bootloader wrote the image through the data cache. Push it to
// the point of unification and drop every instruction-cache line
// before the kernel relies on fetching code it has not run yet: a
// line still dirty in L1D is invisible to instruction fetch, which
// then reads stale DRAM (an "unknown reason" exception on a valid
// instruction).
Arch::SyncInstructionCache((const void*)kernelBaseVirt, kernelSize);
Arch::ActivateKernelTranslation();
Kt::KernelLogStream(Kt::OK, "VMM") << "Switched to kernel translation tables (HHDM limit 0x"
<< base::hex << g_hhdmLimit << ")";
}
static void MapKernelPage(uint64_t root, uint64_t phys, uint64_t va, uint64_t attr, const char* what) {
if (va % 0x1000 != 0 || phys % 0x1000 != 0) {
Panic(what, nullptr);
}
volatile Descriptor* entry = WalkCreate(KernelRootFor(root, va), va);
if (entry == nullptr) {
Panic("OOM in Paging (kernel page table allocation)", nullptr);
}
SetLeaf(entry, KernelLeaf(phys, attr), va);
}
void Paging::Map(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
MapKernelPage(rootPhys, physicalAddress, virtualAddress, AttrNormal,
"Value that isn't page-aligned passed as address to Paging::Map!");
}
void Paging::MapMMIO(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
MapKernelPage(rootPhys, physicalAddress, virtualAddress, AttrDevice,
"Value that isn't page-aligned passed as address to Paging::MapMMIO!");
}
void Paging::MapWC(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
MapKernelPage(rootPhys, physicalAddress, virtualAddress, AttrNormalNc,
"Value that isn't page-aligned passed as address to Paging::MapWC!");
}
void Paging::MapUser(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
if (virtualAddress % 0x1000 != 0 || physicalAddress % 0x1000 != 0) {
Panic("Value that isn't page-aligned passed as address to Paging::MapUser!", nullptr);
}
volatile Descriptor* entry = WalkCreate(rootPhys, virtualAddress);
if (entry == nullptr) {
Panic("OOM in Paging::MapUser (page table allocation)", nullptr);
}
SetLeaf(entry, UserLeaf(physicalAddress, AttrNormal, true, true), virtualAddress);
}
std::uint64_t Paging::GetPhysAddr(std::uint64_t root, std::uint64_t virtualAddress) {
if (virtualAddress >= KernelHalfBase) {
root = g_kernelHalfRoot;
}
return Translate(root, virtualAddress);
}
std::uint64_t Paging::GetPhysAddr(std::uint64_t virtualAddress) {
return GetPhysAddr(rootPhys, virtualAddress);
}
// The kernel half lives in TTBR1, so a new address space starts empty.
std::uint64_t Paging::CreateUserAddressSpace() {
void* page = Memory::g_pfa->AllocateZeroed();
if (page == nullptr) return 0;
return Memory::SubHHDM(page);
}
bool Paging::MapUserIn(std::uint64_t root, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
return MapUserInPermissions(root, physicalAddress, virtualAddress, true, true);
}
static bool MapUserLeaf(uint64_t root, uint64_t phys, uint64_t va, Descriptor desc, const char* what) {
pagingLock.Acquire();
if (va % 0x1000 != 0 || phys % 0x1000 != 0) {
pagingLock.Release();
Panic(what, nullptr);
}
volatile Descriptor* entry = WalkCreate(root, va);
if (entry == nullptr) {
pagingLock.Release();
return false;
}
SetLeaf(entry, desc, va);
pagingLock.Release();
return true;
}
bool Paging::MapUserInPermissions(std::uint64_t root, std::uint64_t physicalAddress,
std::uint64_t virtualAddress, bool writable, bool executable) {
return MapUserLeaf(root, physicalAddress, virtualAddress,
UserLeaf(physicalAddress, AttrNormal, writable, executable),
"Non-aligned address in Paging::MapUserIn!");
}
bool Paging::MapUserInWC(std::uint64_t root, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
return MapUserLeaf(root, physicalAddress, virtualAddress,
UserLeaf(physicalAddress, AttrNormalNc, true, false),
"Non-aligned address in Paging::MapUserInWC!");
}
// Permission changes need no break-before-make, only the TLB flush.
bool Paging::ProtectUserIn(std::uint64_t root, std::uint64_t virtualAddress,
bool writable, bool executable) {
if ((virtualAddress & 0xFFFULL) != 0) return false;
pagingLock.Acquire();
volatile Descriptor* entry = WalkExisting(root, virtualAddress);
if (entry == nullptr || !IsUserLeaf(*entry)) {
pagingLock.Release();
return false;
}
Descriptor desc = *entry;
if (writable) desc &= ~DescApReadOnly;
else desc |= DescApReadOnly;
if (executable) desc &= ~DescUxn;
else desc |= DescUxn;
*entry = desc;
Arch::FlushTlbPage(virtualAddress);
pagingLock.Release();
return true;
}
void Paging::UnmapUserIn(std::uint64_t root, std::uint64_t virtualAddress) {
pagingLock.Acquire();
volatile Descriptor* entry = WalkExisting(root, virtualAddress);
if (entry != nullptr && (*entry & DescValid)) {
*entry = 0;
Arch::FlushTlbPage(virtualAddress);
}
pagingLock.Release();
}
void Paging::FreeUserHalf(std::uint64_t root) {
// User space is the lower 128 TiB: level-0 entries 0-255.
for (int i0 = 0; i0 < 256; i0++) {
Descriptor d0 = TableAt(root)[i0];
if ((d0 & DescValid) == 0) continue;
uint64_t l1 = d0 & DescAddrMask;
for (int i1 = 0; i1 < 512; i1++) {
Descriptor d1 = TableAt(l1)[i1];
if ((d1 & DescValid) == 0) continue;
uint64_t l2 = d1 & DescAddrMask;
for (int i2 = 0; i2 < 512; i2++) {
Descriptor d2 = TableAt(l2)[i2];
if ((d2 & DescValid) == 0) continue;
uint64_t l3 = d2 & DescAddrMask;
for (int i3 = 0; i3 < 512; i3++) {
Descriptor leaf = TableAt(l3)[i3];
if ((leaf & DescValid) == 0) continue;
// Write-combining/device pages are not the process's.
if (((leaf & DescAttrMask) >> DescAttrShift) != AttrNormal) continue;
uint64_t pagePhys = leaf & DescAddrMask;
if (pagePhys != 0) {
Memory::g_pfa->Free((void*)Memory::HHDM(pagePhys));
}
}
Memory::g_pfa->Free((void*)Memory::HHDM(l3));
}
Memory::g_pfa->Free((void*)Memory::HHDM(l2));
}
Memory::g_pfa->Free((void*)Memory::HHDM(l1));
}
}
bool Paging::IsUserRangeAccessible(std::uint64_t root, std::uint64_t virtualAddress,
std::uint64_t size, bool requireWrite) {
if (size == 0) return true;
if (virtualAddress >= UserSpaceEnd) return false;
if (virtualAddress + size < virtualAddress) return false;
if (virtualAddress + size > UserSpaceEnd) return false;
uint64_t page = virtualAddress & ~0xFFFULL;
uint64_t lastPage = (virtualAddress + size - 1) & ~0xFFFULL;
for (;;) {
volatile Descriptor* entry = WalkExisting(root, page);
if (entry == nullptr) return false;
Descriptor desc = *entry;
if (!IsUserLeaf(desc)) return false;
if (requireWrite && (desc & DescApReadOnly)) return false;
if (page == lastPage) break;
page += 0x1000;
}
return true;
}
}
+36
View File
@@ -0,0 +1,36 @@
/*
* 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);
// An "unknown reason" exception on a kernel PC: show the word the
// data side sees there. A valid instruction means instruction fetch
// saw something else (cache maintenance); garbage means the code
// itself was overwritten. The fetch succeeded, so the page is mapped.
if ((frame->esr >> 26) == 0 && frame->elr >= 0xFFFF000000000000ULL && (frame->elr & 3) == 0) {
PrintBoxedHex(kerr, "Instruction at PC", *(volatile uint32_t*)frame->elr, BoxWidth);
}
}
}
+36
View File
@@ -0,0 +1,36 @@
/*
* 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
uint8_t gicTargetMask; // GICv2 CPU-interface bit (SGI/SPI targets)
uint64_t timerPeriod; // tick period in counter ticks, 0 = stopped
};
// 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();
}
}
+140
View File
@@ -0,0 +1,140 @@
/*
* 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 <Arch/aarch64/Gic.hpp>
#include <Arch/aarch64/Bringup.hpp>
#include <ACPI/FADT.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
using namespace Kt;
extern "C" char ArchExceptionVectors[]; // Vectors.S
namespace Arch {
// SCTLR_EL1.WXN: writable memory is execute-never
static constexpr uint64_t SctlrWxn = 1ULL << 19;
// SCTLR_EL1.UCT/UCI: EL0 may read CTR_EL0 and clean/invalidate caches
// by address (what __builtin___clear_cache needs for generated code).
static constexpr uint64_t SctlrUct = 1ULL << 15;
static constexpr uint64_t SctlrUci = 1ULL << 26;
// PSCI 0.2+ function IDs (SMC32 calling convention)
static constexpr uint64_t PsciSystemOff = 0x84000008;
static constexpr uint64_t PsciSystemReset = 0x84000009;
// How the firmware takes PSCI calls, from the FADT's ARM_BOOT_ARCH.
enum class PsciConduit { None, Smc, Hvc };
static PsciConduit g_psci = PsciConduit::None;
static uint64_t PsciCall(uint64_t function) {
register uint64_t x0 asm("x0") = function;
register uint64_t x1 asm("x1") = 0;
register uint64_t x2 asm("x2") = 0;
register uint64_t x3 asm("x3") = 0;
if (g_psci == PsciConduit::Hvc) {
asm volatile("hvc #0" : "+r"(x0), "+r"(x1), "+r"(x2), "+r"(x3) :: "memory");
} else if (g_psci == PsciConduit::Smc) {
asm volatile("smc #0" : "+r"(x0), "+r"(x1), "+r"(x2), "+r"(x3) :: "memory");
}
return x0;
}
static void InitializePsci(Hal::ACPI::CommonSDTHeader* xsdt) {
Hal::FADT::ParsedFADT fadt{};
if (!Hal::FADT::Parse(xsdt, fadt) || !fadt.Valid) return;
if (!(fadt.ArmBootArchFlags & Hal::FADT::ArmBootPsciCompliant)) {
KernelLogStream(WARNING, "PSCI") << "Firmware is not PSCI compliant: no reset or power-off";
return;
}
g_psci = (fadt.ArmBootArchFlags & Hal::FADT::ArmBootPsciUseHvc) ? PsciConduit::Hvc : PsciConduit::Smc;
KernelLogStream(OK, "PSCI") << "PSCI through " << (g_psci == PsciConduit::Hvc ? "HVC" : "SMC");
}
void InitializeCpuRegisters() {
// Run on SP_EL1 so exceptions taken from the kernel arrive on the
// stack it is already using (Vectors.S has no SP_EL0 handlers).
asm volatile("mov x9, sp\n\tmsr spsel, #1\n\tmov sp, x9" ::: "x9", "memory");
// The vectors need no allocation, so take over from the
// bootloader's right away.
asm volatile("msr vbar_el1, %0\n\tisb" :: "r"(ArchExceptionVectors) : "memory");
// Kernel mappings are RWX (Paging.cpp), so writable must not
// imply execute-never; user code may maintain its own caches.
uint64_t sctlr;
asm volatile("mrs %0, sctlr_el1" : "=r"(sctlr));
sctlr &= ~SctlrWxn;
sctlr |= SctlrUct | SctlrUci;
asm volatile("msr sctlr_el1, %0\n\tisb" :: "r"(sctlr));
// 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));
}
void EarlyInitialize() {
// No per-CPU data yet: Smp::TryGetCurrentCpuData must see null.
asm volatile("msr tpidr_el1, xzr");
InitializeCpuRegisters();
}
// Installed by EarlyInitialize.
void InitializeTraps() {
KernelLogStream(OK, "Arch") << "Installed exception vectors at 0x" << base::hex
<< (uint64_t)ArchExceptionVectors;
}
// MAIR_EL1 attribute indices are chosen together with the page-table
// format (Paging.cpp), so there is nothing to program separately.
void InitializeMemoryAttributes() {
}
void InitializeInterrupts(Hal::ACPI::CommonSDTHeader* xsdt) {
Smp::InitBsp();
if (!Gic::Initialize(xsdt)) {
KernelLogStream(ERROR, "Arch") << "aarch64: no interrupt controller, running without IRQs";
}
InitializePsci(xsdt);
}
void InitializePlatformDevices() {
}
// EL0 entry needs no global state beyond Arch::ActivateThread.
void PrepareUserMode() {
}
void LateInitialize() {
}
void ListPlatformDevices(PlatformDeviceSink sink, void* context) {
if (Gic::IsInitialized()) {
sink(context, 1, "GICv2", "ARM Generic Interrupt Controller");
sink(context, 2, "Generic Timer", "ARM architected timer (EL1 virtual)");
}
}
void Reboot() {
PsciCall(PsciSystemReset);
HaltForever();
}
void PowerOff() {
if (g_psci != PsciConduit::None) {
PsciCall(PsciSystemOff);
}
}
}
+135
View File
@@ -0,0 +1,135 @@
/*
* Smp.cpp
* aarch64 per-CPU data and secondary CPU start-up (implements the arch half of Arch/Smp.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Smp.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/Syscall.hpp>
#include <Arch/aarch64/Bringup.hpp>
#include <Arch/aarch64/Gic.hpp>
#include <Boot/BootInfo.hpp>
#include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Timekeeping/Timer.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 << ")";
}
// A secondary CPU, woken by the bootloader on its own stack and still
// on the bootloader's translation tables. The kernel stashed this CPU's
// CpuData in extraArgument.
static void ApEntry(montauk::boot::BootCpu* bootCpu) {
CpuData* cpu = (CpuData*)bootCpu->extraArgument;
Arch::InitializeCpuRegisters();
Arch::ActivateKernelTranslation();
asm volatile("msr tpidr_el1, %0" :: "r"(cpu));
Arch::Gic::InitializeCpuInterface();
Arch::InitializeSyscallEntry();
Timekeeping::TimerInitializeAP();
__atomic_store_n(&cpu->started, true, __ATOMIC_RELEASE);
Arch::EnableInterrupts();
// Runnable work arrives as a reschedule IPI; keep the periodic tick
// masked while idle.
for (;;) {
Timekeeping::TimerEnterApIdle();
Timekeeping::ServiceDeferredWork();
Arch::DisableInterrupts();
Arch::HaltWithInterruptsDisabled();
Arch::EnableInterrupts();
}
}
void BootAPs(const montauk::boot::SmpInfo& smp) {
if (smp.cpus == nullptr || smp.startCpu == nullptr || smp.cpuCount <= 1) {
KernelLogStream(INFO, "SMP") << "Single CPU system - no secondary CPUs to start";
return;
}
g_detectedCpuCount = (int)smp.cpuCount;
if (!Arch::Gic::IsInitialized()) {
KernelLogStream(WARNING, "SMP") << "No interrupt controller: secondary CPUs left parked";
return;
}
int apIndex = 1;
for (uint64_t i = 0; i < smp.cpuCount && apIndex < MaxCPUs; i++) {
montauk::boot::BootCpu& info = smp.cpus[i];
if (info.hardwareId == smp.bspHardwareId) continue;
CpuData& ap = g_cpus[apIndex];
ap.selfPtr = (uint64_t)&ap;
ap.cpuIndex = apIndex;
ap.currentSlot = -1;
ap.started = false;
ap.arch.mpidr = info.hardwareId;
info.extraArgument = (uint64_t)&ap;
// Publish the CpuData before the CPU that reads it is woken.
asm volatile("dsb ish" ::: "memory");
smp.startCpu(&info, ApEntry);
apIndex++;
}
g_cpuCount = apIndex;
int online = 1;
for (int i = 1; i < g_cpuCount; i++) {
uint64_t start = Arch::ReadTimestamp();
uint64_t frequency;
asm volatile("mrs %0, cntfrq_el0" : "=r"(frequency));
while (!__atomic_load_n(&g_cpus[i].started, __ATOMIC_ACQUIRE) &&
Arch::ReadTimestamp() - start < frequency) {
Arch::Pause();
}
if (g_cpus[i].started) {
online++;
} else {
KernelLogStream(ERROR, "SMP") << "CPU " << base::dec << (uint64_t)i
<< " (MPIDR " << base::hex << g_cpus[i].arch.mpidr << ") failed to start";
}
}
KernelLogStream(OK, "SMP") << base::dec << (uint64_t)online << " CPU(s) online";
}
}
+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() {
}
}
+25
View File
@@ -0,0 +1,25 @@
/*
* SyscallFrame.hpp
* aarch64 system-call frame (see Arch/Syscall.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Arch {
// The exception frame Vectors.S saves for `svc #0` (the same layout as
// Arch::PanicFrame): x0-x5 carry the arguments and x8 the system-call
// number; the result is written back to x0.
struct SyscallFrame {
uint64_t arg1, arg2, arg3, arg4, arg5, arg6; // x0-x5
uint64_t x6, x7;
uint64_t syscall_nr; // x8
uint64_t x9_x30[22];
uint64_t sp_el0, elr, spsr, esr, far;
};
static_assert(sizeof(SyscallFrame) == 288, "must match Vectors.S");
}
+116
View File
@@ -0,0 +1,116 @@
/*
* Timer.cpp
* aarch64 tick timer: the EL1 virtual generic timer (implements Arch/Timer.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Timer.hpp>
#include <Arch/Interrupts.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/Smp.hpp>
#include <Arch/aarch64/Gic.hpp>
#include <ACPI/ACPI.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
// The generic timer counts at CNTFRQ_EL0 on every CPU, so it needs no
// calibration. Each CPU's virtual timer raises a PPI (the GTDT names it,
// normally INTID 27); the tick is re-armed from the interrupt's ack hook,
// which also drops the level-triggered line before the EOI.
using namespace Kt;
namespace Arch::Timer {
// CNTV_CTL_EL0
static constexpr uint64_t CtlEnable = 1ULL << 0;
static constexpr uint64_t CtlMask = 1ULL << 1;
// GTDT timer flags
static constexpr uint32_t GtdtEdgeTriggered = 1u << 0;
static constexpr uint32_t DefaultVirtualTimerIntid = 27;
struct Gtdt {
Hal::ACPI::CommonSDTHeader Header;
uint64_t CntControlBase;
uint32_t Reserved;
uint32_t SecureEl1Gsiv;
uint32_t SecureEl1Flags;
uint32_t NonSecureEl1Gsiv;
uint32_t NonSecureEl1Flags;
uint32_t VirtualTimerGsiv;
uint32_t VirtualTimerFlags;
} __attribute__((packed));
static uint64_t g_ticksPerMs = 0;
static uint32_t g_intid = DefaultVirtualTimerIntid;
static bool g_edgeTriggered = false;
static inline void WriteCtl(uint64_t value) {
asm volatile("msr cntv_ctl_el0, %0\n\tisb" :: "r"(value) : "memory");
}
static inline uint64_t ReadCval() {
uint64_t value;
asm volatile("mrs %0, cntv_cval_el0" : "=r"(value));
return value;
}
static inline void WriteCval(uint64_t value) {
asm volatile("msr cntv_cval_el0, %0\n\tisb" :: "r"(value) : "memory");
}
// Advance the deadline by one period from the previous one (no drift),
// skipping ticks the CPU missed rather than replaying them.
static void Rearm() {
uint64_t period = Smp::GetCurrentCpuData()->arch.timerPeriod;
if (period == 0) {
WriteCtl(CtlMask);
return;
}
uint64_t now = ReadTimestamp();
uint64_t next = ReadCval() + period;
if ((int64_t)(next - now) <= 0) next = now + period;
WriteCval(next);
}
static void ReadGtdt() {
auto* gtdt = (Gtdt*)Gic::FindTable(Gic::CurrentXsdt(), "GTDT");
if (gtdt == nullptr || gtdt->Header.Length < sizeof(Gtdt)) return;
if (gtdt->VirtualTimerGsiv >= 16 && gtdt->VirtualTimerGsiv < 32) {
g_intid = gtdt->VirtualTimerGsiv;
g_edgeTriggered = (gtdt->VirtualTimerFlags & GtdtEdgeTriggered) != 0;
}
}
bool InitializeBsp(uint32_t periodMs) {
uint64_t frequency;
asm volatile("mrs %0, cntfrq_el0" : "=r"(frequency));
if (frequency < 1000 || !Gic::IsInitialized()) return false;
g_ticksPerMs = frequency / 1000;
ReadGtdt();
Gic::SetAckHook(IRQ_TIMER, Rearm);
StartPeriodic(periodMs);
KernelLogStream(OK, "Timer") << "Generic timer: " << base::dec << frequency
<< " Hz counter, virtual timer INTID " << (uint64_t)g_intid;
return true;
}
// PPIs are banked: each CPU enables its own timer interrupt.
void StartPeriodic(uint32_t periodMs) {
Gic::RoutePpi(g_intid, IRQ_TIMER, g_edgeTriggered);
uint64_t period = g_ticksPerMs * periodMs;
Smp::GetCurrentCpuData()->arch.timerPeriod = period;
WriteCval(ReadTimestamp() + period);
WriteCtl(CtlEnable);
}
void Stop() {
Smp::GetCurrentCpuData()->arch.timerPeriod = 0;
WriteCtl(CtlMask);
}
}
+201
View File
@@ -0,0 +1,201 @@
/*
Vectors.S
aarch64 exception vector table (VBAR_EL1)
Copyright (c) 2026 Daniel Hammer
*/
/*
* Every exception saves the same 288-byte frame on SP_EL1, laid out as
* Arch::PanicFrame (InterruptFrame.hpp) and Arch::SyscallFrame:
*
* 0 x0 .. x30 (x30 at 240)
* 248 interrupted SP (SP_EL0 from EL0, the pre-exception SP_EL1 from EL1)
* 256 ELR_EL1
* 264 SPSR_EL1
* 272 ESR_EL1
* 280 FAR_EL1
*
* The kernel runs on SP_EL1 (SPSel = 1, see Platform.cpp). From EL0,
* SP_EL1 is the top of the thread's kernel stack: ArchEnterUserMode sets
* it before the first ERET and every return to EL0 restores it.
*/
#define FRAME_SIZE 288
#define FRAME_SP 248
#define FRAME_ELR 256
#define FRAME_SPSR 264
#define FRAME_ESR 272
#define FRAME_FAR 280
.text
.macro SAVE_FRAME el
sub sp, sp, #FRAME_SIZE
stp x0, x1, [sp, #0]
stp x2, x3, [sp, #16]
stp x4, x5, [sp, #32]
stp x6, x7, [sp, #48]
stp x8, x9, [sp, #64]
stp x10, x11, [sp, #80]
stp x12, x13, [sp, #96]
stp x14, x15, [sp, #112]
stp x16, x17, [sp, #128]
stp x18, x19, [sp, #144]
stp x20, x21, [sp, #160]
stp x22, x23, [sp, #176]
stp x24, x25, [sp, #192]
stp x26, x27, [sp, #208]
stp x28, x29, [sp, #224]
.if \el == 0
mrs x21, sp_el0
.else
add x21, sp, #FRAME_SIZE
.endif
stp x30, x21, [sp, #240]
mrs x22, elr_el1
mrs x23, spsr_el1
stp x22, x23, [sp, #FRAME_ELR]
mrs x24, esr_el1
mrs x25, far_el1
stp x24, x25, [sp, #FRAME_ESR]
.endm
/* Interrupts are masked again before ELR/SPSR are reloaded: an IRQ taken
in between would overwrite them. */
.macro RESTORE_FRAME el
msr daifset, #2
ldp x22, x23, [sp, #FRAME_ELR]
msr elr_el1, x22
msr spsr_el1, x23
.if \el == 0
ldr x21, [sp, #FRAME_SP]
msr sp_el0, x21
.endif
ldp x0, x1, [sp, #0]
ldp x2, x3, [sp, #16]
ldp x4, x5, [sp, #32]
ldp x6, x7, [sp, #48]
ldp x8, x9, [sp, #64]
ldp x10, x11, [sp, #80]
ldp x12, x13, [sp, #96]
ldp x14, x15, [sp, #112]
ldp x16, x17, [sp, #128]
ldp x18, x19, [sp, #144]
ldp x20, x21, [sp, #160]
ldp x22, x23, [sp, #176]
ldp x24, x25, [sp, #192]
ldp x26, x27, [sp, #208]
ldp x28, x29, [sp, #224]
ldr x30, [sp, #240]
add sp, sp, #FRAME_SIZE
eret
.endm
/* Each vector slot is 128 bytes; the handlers live outside the table. */
.macro VECTOR target
.balign 0x80
b \target
.endm
.balign 0x800
.global ArchExceptionVectors
ArchExceptionVectors:
/* Current EL with SP_EL0: the kernel never runs on SP_EL0. */
VECTOR el1t_unexpected
VECTOR el1t_unexpected
VECTOR el1t_unexpected
VECTOR el1t_unexpected
/* Current EL with SP_ELx */
VECTOR el1h_sync
VECTOR el1h_irq
VECTOR el1h_fiq
VECTOR el1h_serror
/* Lower EL, AArch64 */
VECTOR el0_sync
VECTOR el0_irq
VECTOR el0_fiq
VECTOR el0_serror
/* Lower EL, AArch32: not supported */
VECTOR el0_aarch32
VECTOR el0_aarch32
VECTOR el0_aarch32
VECTOR el0_aarch32
/* ---- Exceptions from the kernel ---- */
el1t_unexpected:
SAVE_FRAME 1
mov x0, sp
mov x1, #0
bl ArchUnexpectedException
el1h_sync:
SAVE_FRAME 1
mov x0, sp
mov x1, #0
bl ArchSyncException
RESTORE_FRAME 1
el1h_irq:
SAVE_FRAME 1
mov x0, sp
mov x1, #0
bl ArchIrqException
RESTORE_FRAME 1
el1h_fiq:
SAVE_FRAME 1
mov x0, sp
mov x1, #1
bl ArchUnexpectedException
el1h_serror:
SAVE_FRAME 1
mov x0, sp
mov x1, #2
bl ArchUnexpectedException
/* ---- Exceptions from user mode ---- */
el0_sync:
SAVE_FRAME 0
lsr x9, x24, #26 /* ESR_EL1.EC */
cmp x9, #0x15 /* SVC from AArch64 */
b.ne 1f
msr daifclr, #2
mov x0, sp
bl SyscallDispatch
str x0, [sp, #0]
bl SchedExitIfKilled
RESTORE_FRAME 0
1: mov x0, sp
mov x1, #1
bl ArchSyncException
RESTORE_FRAME 0
el0_irq:
SAVE_FRAME 0
mov x0, sp
mov x1, #1
bl ArchIrqException
RESTORE_FRAME 0
el0_fiq:
SAVE_FRAME 0
mov x0, sp
mov x1, #1
bl ArchUnexpectedException
el0_serror:
SAVE_FRAME 0
mov x0, sp
mov x1, #2
bl ArchUnexpectedException
el0_aarch32:
SAVE_FRAME 0
mov x0, sp
mov x1, #3
bl ArchUnexpectedException
+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 * Copyright (c) 2026 Daniel Hammer
*/ */
#include "AcpiSleep.hpp" #include <ACPI/AcpiSleep.hpp>
#include "AcpiEvents.hpp" #include <ACPI/AcpiEvents.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/x86_64/Acpi/S3State.hpp>
#include <ACPI/FADT.hpp> #include <ACPI/FADT.hpp>
#include <ACPI/AML/AmlParser.hpp> #include <ACPI/AML/AmlParser.hpp>
#include <ACPI/AML/AmlInterpreter.hpp> #include <ACPI/AML/AmlInterpreter.hpp>
@@ -14,19 +16,19 @@
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
#include <Memory/HHDM.hpp> #include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp> #include <Memory/Paging.hpp>
#include <Hal/GDT.hpp> #include <Arch/x86_64/GDT.hpp>
#include <Hal/Apic/Apic.hpp> #include <Arch/x86_64/Apic/Apic.hpp>
#include <Hal/Apic/IoApic.hpp> #include <Arch/x86_64/Apic/IoApic.hpp>
#include <Hal/Apic/Pic.hpp> #include <Arch/x86_64/Apic/Pic.hpp>
#include <Timekeeping/ApicTimer.hpp> #include <Timekeeping/Timer.hpp>
#include <Hal/CpuPower.hpp> #include <Arch/CpuPower.hpp>
#include <Drivers/Graphics/IntelGPU.hpp> #include <Drivers/Graphics/IntelGPU.hpp>
#include <Drivers/Graphics/DgpuPower.hpp> #include <Drivers/Graphics/DgpuPower.hpp>
#include <Drivers/PS2/PS2Controller.hpp> #include <Drivers/PS2/PS2Controller.hpp>
#include <Graphics/Framebuffer.hpp> #include <Graphics/Framebuffer.hpp>
#include <Libraries/Memory.hpp> #include <Libraries/Memory.hpp>
#include <Hal/MSR.hpp> #include <Arch/x86_64/MSR.hpp>
#include <Hal/SmpBoot.hpp> #include <Arch/Smp.hpp>
#include <Api/Syscall.hpp> #include <Api/Syscall.hpp>
using namespace Kt; using namespace Kt;
@@ -186,7 +188,7 @@ namespace Hal {
<< base::hex << (uint64_t)g_s3SlpTypA << base::hex << (uint64_t)g_s3SlpTypA
<< " SLP_TYPb=" << base::hex << (uint64_t)g_s3SlpTypB << ")"; << " SLP_TYPb=" << base::hex << (uint64_t)g_s3SlpTypB << ")";
KernelLogStream(INFO, "S3") << "Kernel PML4 phys = " << base::hex 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 KernelLogStream(INFO, "S3") << "FACS at phys " << base::hex
<< (uint64_t)fadt.FacsAddress << " version=" << base::dec << (uint64_t)fadt.FacsAddress << " version=" << base::dec
<< (uint64_t)g_facs->Version << " flags=" << base::hex << (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. // shadow copy of the PML4 at a fixed low-memory address.
// Only the PML4 page itself needs to be below 4GB — its entries // Only the PML4 page itself needs to be below 4GB — its entries
// contain full 52-bit physical addresses for sub-tables. // 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* kernelPml4Virt = (uint8_t*)Memory::HHDM(kernelPml4Phys);
uint8_t* shadowPml4Virt = (uint8_t*)Memory::HHDM((uint64_t)SHADOW_PML4_PHYS); uint8_t* shadowPml4Virt = (uint8_t*)Memory::HHDM((uint64_t)SHADOW_PML4_PHYS);
memcpy(shadowPml4Virt, kernelPml4Virt, 4096); memcpy(shadowPml4Virt, kernelPml4Virt, 4096);
@@ -439,7 +441,7 @@ namespace Hal {
Hal::InitializePAT(); Hal::InitializePAT();
// HWP enable and POWER_CTL reset across S3; re-apply them and // HWP enable and POWER_CTL reset across S3; re-apply them and
// bump the policy epoch so APs re-apply on their next tick. // bump the policy epoch so APs re-apply on their next tick.
Hal::CpuPower::ReapplyAfterWake(); Arch::CpuPower::ReapplyAfterWake();
reinitProgress(0x05); // PIC reinitProgress(0x05); // PIC
Hal::DisableLegacyPic(); Hal::DisableLegacyPic();
reinitProgress(0x06); // Local APIC reinitProgress(0x06); // Local APIC
@@ -447,7 +449,7 @@ namespace Hal {
reinitProgress(0x07); // IO APIC reinitProgress(0x07); // IO APIC
Hal::IoApic::Reinitialize(); Hal::IoApic::Reinitialize();
reinitProgress(0x08); // APIC Timer reinitProgress(0x08); // APIC Timer
Timekeeping::ApicTimerReinitialize(); Timekeeping::TimerReinitialize();
reinitProgress(0x09); // PM1 clear reinitProgress(0x09); // PM1 clear
ClearPM1Status(); ClearPM1Status();
reinitProgress(0x0A); // _WAK reinitProgress(0x0A); // _WAK
@@ -463,7 +465,7 @@ namespace Hal {
reinitProgress(0x0D); // discrete GPU back off reinitProgress(0x0D); // discrete GPU back off
Drivers::Graphics::DgpuPower::Resume(); Drivers::Graphics::DgpuPower::Resume();
reinitProgress(0x0E); // sti reinitProgress(0x0E); // sti
asm volatile("sti"); Arch::EnableInterrupts();
KernelLogStream(OK, "S3") << "Resumed from S3 suspend"; KernelLogStream(OK, "S3") << "Resumed from S3 suspend";
@@ -517,7 +519,7 @@ namespace Hal {
Io::Out8(0x00, 0x71); Io::Out8(0x00, 0x71);
// 3. Disable interrupts // 3. Disable interrupts
asm volatile("cli"); Arch::DisableInterrupts();
// 4. Identity-map the trampoline page so the CPU can execute it // 4. Identity-map the trampoline page so the CPU can execute it
// after paging is re-enabled with our CR3 during wake. // 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); Memory::VMM::g_paging->Map(TRAMPOLINE_PHYS, TRAMPOLINE_PHYS);
// 5. Flush all caches // 5. Flush all caches
asm volatile("wbinvd"); Arch::WritebackInvalidateCaches();
// 6. Set the waking vector in FACS // 6. Set the waking vector in FACS
SetWakingVector(); SetWakingVector();
@@ -548,7 +550,7 @@ namespace Hal {
} }
// 10. Flush caches again // 10. Flush caches again
asm volatile("wbinvd"); Arch::WritebackInvalidateCaches();
// 11. Assert SLP_EN — this triggers S3 entry // 11. Assert SLP_EN — this triggers S3 entry
Io::Out16(pm1a | PM1_SLP_EN, (uint16_t)g_pm1aControlBlock); 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 QEMU/KVM the vCPU must reach HLT before QEMU finalizes S3.
// On resume, firmware jumps to our waking vector. // On resume, firmware jumps to our waking vector.
for (;;) { for (;;) {
asm volatile("hlt"); Arch::Halt();
// If we wake from HLT (spurious or real wake), check WAK_STS // If we wake from HLT (spurious or real wake), check WAK_STS
if (g_pm1aEventBlock != 0) { if (g_pm1aEventBlock != 0) {
uint16_t sts = Io::In16((uint16_t)g_pm1aEventBlock); 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. // If we get here, either S3 failed or we woke immediately.
// The normal wake path goes through AcpiResumeLongMode instead. // The normal wake path goes through AcpiResumeLongMode instead.
// Re-enable interrupts as a safety measure. // Re-enable interrupts as a safety measure.
asm volatile("sti"); Arch::EnableInterrupts();
KernelLogStream(WARNING, "S3") << "S3 entry may have failed (fell through)"; KernelLogStream(WARNING, "S3") << "S3 entry may have failed (fell through)";
return 0; return 0;
@@ -5,10 +5,11 @@
*/ */
#include "CpuIdle.hpp" #include "CpuIdle.hpp"
#include <Arch/Cpu.hpp>
#include <ACPI/FADT.hpp> #include <ACPI/FADT.hpp>
#include <ACPI/AML/AmlInterpreter.hpp> #include <ACPI/AML/AmlInterpreter.hpp>
#include <Hal/Cpu.hpp> #include <Arch/x86_64/CpuFeatures.hpp>
#include <Hal/SmpBoot.hpp> #include <Arch/Smp.hpp>
#include <Io/IoPort.hpp> #include <Io/IoPort.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
@@ -480,7 +481,7 @@ namespace Hal {
if (hasMwait) { if (hasMwait) {
Hal::IdleWaitWithInterruptsDisabled(addr, CurrentIdleHint()); Hal::IdleWaitWithInterruptsDisabled(addr, CurrentIdleHint());
} else { } 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 "IoApic.hpp"
#include "Interrupts.hpp" #include "Interrupts.hpp"
#include <ACPI/MADT.hpp> #include <ACPI/MADT.hpp>
#include <Hal/IDT.hpp> #include <Arch/x86_64/IDT.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
#include <Memory/Paging.hpp> #include <Memory/Paging.hpp>
@@ -67,8 +67,8 @@ namespace Hal {
// Step 7: Route keyboard (IRQ1) and mouse (IRQ12) to BSP // Step 7: Route keyboard (IRQ1) and mouse (IRQ12) to BSP
uint8_t bspApicId = (uint8_t)LocalApic::GetId(); uint8_t bspApicId = (uint8_t)LocalApic::GetId();
IoApic::RouteIrq(IRQ_KEYBOARD, IRQ_VECTOR_BASE + IRQ_KEYBOARD, bspApicId); IoApic::RouteIrq(Arch::IRQ_KEYBOARD, IRQ_VECTOR_BASE + Arch::IRQ_KEYBOARD, bspApicId);
IoApic::RouteIrq(IRQ_MOUSE, IRQ_VECTOR_BASE + IRQ_MOUSE, bspApicId); IoApic::RouteIrq(Arch::IRQ_MOUSE, IRQ_VECTOR_BASE + Arch::IRQ_MOUSE, bspApicId);
// Note: sti is NOT called here. The caller (Main.cpp) enables // Note: sti is NOT called here. The caller (Main.cpp) enables
// interrupts after setting up per-CPU GS base for SMP/SWAPGS. // interrupts after setting up per-CPU GS base for SMP/SWAPGS.
File renamed without changes.
@@ -6,7 +6,9 @@
#include "Interrupts.hpp" #include "Interrupts.hpp"
#include "Apic.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 <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
@@ -14,19 +16,15 @@
extern "C" void* IrqStubTable[Hal::IRQ_COUNT]; extern "C" void* IrqStubTable[Hal::IRQ_COUNT];
extern "C" void IrqStubSpurious(); extern "C" void IrqStubSpurious();
// Local APIC message address range; destination APIC ID in bits 19:12.
static constexpr uint64_t MsiAddressBase = 0xFEE00000;
namespace Hal { namespace Hal {
// Dispatch table: one handler per IRQ // Dispatch table: one handler per IRQ
static IrqHandler g_irqHandlers[IRQ_COUNT] = {}; static Arch::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) << ")";
}
void InitializeIrqHandlers() { 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++) { for (int i = 0; i < IRQ_COUNT; i++) {
IDTEncodeInterrupt(IRQ_VECTOR_BASE + i, IrqStubTable[i], 0x8E); IDTEncodeInterrupt(IRQ_VECTOR_BASE + i, IrqStubTable[i], 0x8E);
} }
@@ -41,6 +39,49 @@ 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 PlatformLinesAreFixed() {
return true;
}
bool AssignPlatformLine(uint8_t, uint32_t, bool, bool) {
return false;
}
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 // C linkage dispatch function called from assembly stubs
extern "C" void HalIrqDispatch(uint64_t irqNumber, uint64_t savedCs) { extern "C" void HalIrqDispatch(uint64_t irqNumber, uint64_t savedCs) {
// Send EOI BEFORE calling the handler. The handler may context-switch // 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;
}
+118
View File
@@ -0,0 +1,118 @@
/*
* 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");
}
// Stores are not reordered with other stores, nor loads with loads.
inline void DmaWriteFence() {
asm volatile("" ::: "memory");
}
inline void DmaReadFence() {
asm volatile("" ::: "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 SyncInstructionCache(const void*, uint64_t) {
}
inline void CleanDataCache(const void*, uint64_t) {
}
inline void InvalidateDataCache(const void*, uint64_t) {
}
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 * CpuFeatures.hpp
* CPU feature enablement helpers * x86_64 CPU feature enablement and MWAIT idle helpers
* Copyright (c) 2025 Daniel Hammer * Copyright (c) 2025 Daniel Hammer
*/ */
@@ -32,14 +32,11 @@ namespace Hal {
asm volatile("mwait" :: "a"(hint), "c"(0)); asm volatile("mwait" :: "a"(hint), "c"(0));
} }
// Atomic idle entry for paths that have already disabled interrupts. // Atomic idle entry for paths that have already disabled interrupts
// Keeping STI immediately adjacent to HLT/MWAIT avoids the classic race // (the MWAIT counterpart of Arch::HaltWithInterruptsDisabled). Keeping
// where an IRQ is handled between enabling interrupts and entering idle, // STI immediately adjacent to MWAIT avoids the classic race where an IRQ
// after which the CPU sleeps until the next timer deadline. // is handled between enabling interrupts and entering idle, after which
inline void HaltWithInterruptsDisabled() { // the CPU sleeps until the next timer deadline.
asm volatile("sti\n\thlt\n\tcli" ::: "memory");
}
inline void IdleWaitWithInterruptsDisabled(volatile uint64_t* monitorAddr, inline void IdleWaitWithInterruptsDisabled(volatile uint64_t* monitorAddr,
uint32_t hint = 0) { uint32_t hint = 0) {
asm volatile("monitor" :: "a"(monitorAddr), "c"(0), "d"(0) : "memory"); asm volatile("monitor" :: "a"(monitorAddr), "c"(0), "d"(0) : "memory");
@@ -30,16 +30,19 @@
* Copyright (c) 2026 Daniel Hammer * Copyright (c) 2026 Daniel Hammer
*/ */
#include "CpuPower.hpp" #include <Arch/CpuPower.hpp>
#include "MSR.hpp" #include <Arch/Smp.hpp>
#include "SmpBoot.hpp" #include <Arch/x86_64/MSR.hpp>
#include <Arch/x86_64/Acpi/CpuIdle.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <atomic> #include <atomic>
using namespace Kt; using namespace Kt;
namespace Hal { namespace Arch {
namespace CpuPower { namespace CpuPower {
using Hal::ReadMSR;
using Hal::WriteMSR;
// ============================================================ // ============================================================
// MSR addresses (power/thermal; used only in this module) // MSR addresses (power/thermal; used only in this module)
@@ -399,6 +402,7 @@ namespace Hal {
out.BaseMHz = g_baseMHz; out.BaseMHz = g_baseMHz;
out.MaxMHz = g_maxMHz; out.MaxMHz = g_maxMHz;
out.EffMHz = g_effMHz.load(std::memory_order_relaxed); out.EffMHz = g_effMHz.load(std::memory_order_relaxed);
out.ApIdleHint = Hal::CpuIdle::GetApIdleHint();
return true; return true;
} }
Loaded 100 of 547 files, more files were not shown because too many files have changed in this diff. Show more